FP ์—ฐ์Šต ์˜ˆ์ œ

๊ทธ๋Ÿด ๋•Œ๊ฐ€ ์žˆ์ž–์Šต๋‹ˆ๊นŒ?

๋‚˜๋ฆ„ OCP ๋ฅผ ์ค€์ˆ˜ํ•˜๋ ค๊ณ  ๋…ธ๋ ฅํ–ˆ๋Š”๋ฐ, ์ธํ„ฐํŽ˜์ด์Šค ์ž์ฒด๊ฐ€ ๋ฐ”๋€Œ์—ฌ์•ผ ํ•˜๋Š” ์ƒํ™ฉ.

INameValidator
{ 
   // 2์ฃผ ์ „
   // bool Validate(string name);   

   // 1์ฃผ ์ „
   // Task<bool> ValidateAsync(string name);
   
   // ์ด๋ฒˆ ์ฃผ
   Task<ValidationResult> ValidateAsync(string name); 
}

๋˜ ๊ทธ๋Ÿด ๋•Œ๋„ ์žˆ์ž–์Šต๋‹ˆ๊นŒ?

๋ฉ”์„œ๋“œ ํ•˜๋‚˜๋งŒ ๋„ฃ์œผ๋ฉด ๋˜๋Š”๋ฐ, ๋ณด์ผ๋Ÿฌ ํ”Œ๋ ˆ์ดํŠธ ์ฝ”๋“œ๊ฐ€ ๋ญ”๊ฐ€ ๋งŽ์•„ ๋ณด์ผ ๋•Œ.

class NullValidator : INameValidator { }
class KoreanNameValidator : INameValidator { }
class LengthValidator : INameValidator { }
// ...

๋ˆ„๊ตฐ๊ฐ€, ์ด๋Ÿฐ ๊ฒฝ์šฐ์—, ํ•จ์ˆ˜ํ˜• ๊ธฐ๋ฒ•์ด ์ข‹๋‹ค๊ณ  ํ•˜๋”๋ผ๊ตฌ์š”.

๊ทธ๋ž˜์„œ, ํ•จ ๋งŒ๋“ค์–ด ๋ดค์Šต๋‹ˆ๋‹ค.

BigSquareHasNoEdge/Transcritor: Transcribes a language into another phonetically.

์•„์ง์€ ์ต์ˆ™ํ•˜์ง€ ์•Š์•„์„œ ๊ทธ๋Ÿฐ๊ฐ€ ๋ญ๊ฐ€ ์ข‹๋‹ค๊ณ  ๋”ฑ ์ž˜๋ผ ๋งํ•  ์ˆ˜ ์—†๋Š”๋ฐ, ํ•œ ๊ฐ€์ง€ ์žฅ์ ์„ ๊ผฝ์œผ๋ผ๋ฉด, ๋ฐ์ดํ„ฐ๋ฅผ ์‚ฌ์šฉํ•˜๋Š” ์‹œ์ ์—, ์šฉ๋„์— ๋งž๋Š” ๋ฉ”์„œ๋“œ(ํ•จ์ˆ˜)๋ฅผ ์ถ”๊ฐ€ํ•˜๋Š” ๊ฒƒ์ด ๋งค์šฐ ์ž์œ ๋กญ์Šต๋‹ˆ๋‹ค. (๊ธฐ์กด ์ฝ”๋“œ ์ˆ˜์ •ํ•˜์ง€ ์•Š์•„๋„ ๋ฉ๋‹ˆ๋‹ค.)

5๊ฐœ์˜ ์ข‹์•„์š”

์ธํ„ฐํŽ˜์ด์Šค๋ฅผ ๋ฐ”๊พธ๋Š” ๊ฒฝ์šฐ๋ผ๋ฉด ๋ชจ๋˜ C#์—์„œ๋Š” ๊ธฐ๋ณธ ์ธํ„ฐํŽ˜์ด์Šค ๋ฉ”์„œ๋“œ๋ฅผ ์‚ฌ์šฉํ•  ์ˆ˜๋„ ์žˆ์Šต๋‹ˆ๋‹ค. :sweat_smile:

2๊ฐœ์˜ ์ข‹์•„์š”

๋•๋ถ„์— ์ธํ„ฐํŽ˜์ด์Šค ๊ธฐ๋ณธ ๊ตฌํ˜„์— ๋Œ€ํ•œ ์ ์ ˆํ•œ ์šฉ๋„๋ฅผ ๋” ์ž˜ ์•Œ๊ฒŒ ๋œ ๊ฒƒ ๊ฐ™์Šต๋‹ˆ๋‹ค.

๋‹ค๋งŒ, ๋งํฌ์—์„œ ์†Œ๊ฐœ๋œ ๋‚ด์šฉ์€ ์ธํ„ฐํŽ˜์ด์Šค ๊ตฌํ˜„ ์ฝ”๋“œ, ์†Œ๋น„ ์ฝ”๋“œ๋ฅผ ์ž์œ ๋กญ๊ฒŒ ์žฌ์ž‘์„ฑ/๋นŒ๋“œ๊ฐ€ ๊ฐ€๋Šฅํ•œ ๊ฒฝ์šฐ๋งŒ ์œ ํšจํ•  ๊ฒƒ ๊ฐ™์Šต๋‹ˆ๋‹ค.

๋ฌผ๋ก  ๋Œ€๋ถ€๋ถ„ ์—ฌ๊ธฐ์— ํ•ด๋‹น๋˜์ง€๋งŒ, ๊ทธ ๋ชจ๋“ˆ์ด ์™ธ๋ถ€ ๋ชจ๋“ˆ(๋‹ค๋ฅธ ์—…์ฒด๊ฐ€ ์ œ๊ณตํ•œ ํŒจํ‚ค์ง€)์ด๋ผ๋ฉด, ์ˆ˜์ • ์š”์ฒญ์— ๋Œ€ํ•œ ๋Œ€์‘์ด ์›ํ™œํ•˜์ง€ ์•Š์€ ๊ฒฝ์šฐ๋„ ์žˆ์„ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

FP ๋Š” ๋ฐ์ดํ„ฐ์™€ ํ•จ์ˆ˜๋ฅผ ๋ถ„๋ฆฌํ•ด์„œ ์„ ์–ธํ•˜๊ธฐ ๋•Œ๋ฌธ์—, ์ด๋Ÿฌํ•œ ์ œ์•ฝ์—์„œ ์ž์œ ๋กœ์šด ํŽธ์ธ๋ฐ, ์ด๋Š” ์†Œ๋น„ ์ฝ”๋“œ๊ฐ€ ๊ธฐ์กด ๋ชจ๋“ˆ์„ ๋ณ€๊ฒฝํ•˜์ง€ ์•Š๊ณ ๋„ ํ˜•์‹์— ํ–‰์œ„๋ฅผ ์ถ”๊ฐ€ํ•˜๋Š” ๊ฒƒ์ด ์–ผ๋งˆ๋“ ์ง€ ๊ฐ€๋Šฅํ•˜๋‹ค๋Š” ์˜๋ฏธ๊ฐ€ ๋ฉ๋‹ˆ๋‹ค.

C#์€ FP๊ฐ€ ์š”๊ตฌํ•˜๋Š” ๋ฐ์ดํ„ฐ ๋ถˆ๋ณ€์„ฑ(record), ๋ฐ์ดํ„ฐ ํ˜•์‹์˜ ํŒŒ์ƒ, ๊ฐ•๋ ฅํ•œ ํ˜•์‹์˜ ํ•จ์ˆ˜(delegate) ๋ฅผ ๋ชจ๋‘ ์ œ๊ณตํ•˜๊ธฐ ๋•Œ๋ฌธ์— FP ์˜ ์žฅ์ ๋„ ํ–ฅ์œ ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

๋งํฌ์˜ ์˜ˆ์ œ ์ค‘ ์ผ๋ถ€๋ฅผ FP ๋กœ ํ‘œํ˜„ํ•œ๋‹ค๋ฉด,

namespace Original;
public record CustomerType( // ... );
public record OrderType(//...);

์•„๋ž˜๋Š” ๋ฒ„์ „ 1์ž…๋‹ˆ๋‹ค.

using Original;
namespace Version1;
public static class Customer
{
   public static decimal ComputeLoyaltyDiscount(CustomerType customer)
   {
      DateTime TwoYearsAgo = DateTime.Now.AddYears(-2);
      if ((customer.DateJoined < TwoYearsAgo) && (customer.PreviousOrders.Count() > 10))
      {
          return 0.10m;
      }
      return 0;
   }
}

์•„๋ž˜๋Š” ๋ฒ„์ „1์˜ ์†Œ๋น„ ์ฝ”๋“œ์ž…๋‹ˆ๋‹ค.

using Version1;
// ...
// Check the discount:
Console.WriteLine($"Current discount: {Customer.ComputeLoyaltyDiscount(c)}");

์•„๋ž˜๋Š” ๋ฒ„์ „2์ž…๋‹ˆ๋‹ค.

using Original;
namespace Version2;
public static class Customer
{
   public static void SetLoyaltyThresholds(
      TimeSpan ago,
      int minimumOrders = 10,
      decimal percentageDiscount = 0.10m)
   {
      length = ago;
      orderCount = minimumOrders;
      discountPercent = percentageDiscount;
   }
   private static TimeSpan length = new TimeSpan(365 * 2, 0,0,0); // two years
   private static int orderCount = 10;
   private static decimal discountPercent = 0.10m;

   public static decimal ComputeLoyaltyDiscount(CustomerType customer)
   { 
       DateTime start = DateTime.Now - length;
       if ((customer.DateJoined < start) && (customer.PreviousOrders.Count() > orderCount))
       {
          return discountPercent;
       }
       return 0;
   }
}

์†Œ๋น„์ฝ”๋“œ

using Version2;
// ...
Customer.SetLoyaltyThresholds(new TimeSpan(30, 0, 0, 0), 1, 0.25m);
Console.WriteLine($"Current discount: {Customer.ComputeLoyaltyDiscount(c)}");

๋ณด์‹œ๋‹ค์‹œํ”ผ, ๊ตฌ๋ชจ๋“ˆ์˜ ์ฝ”๋“œ๋ฅผ ์ˆ˜์ •(๊ณผ ์žฌ๋นŒ๋“œ)ํ•  ํ•„์š”๋„ ์—†๊ณ , ๋‚˜์ค‘์— ์ž‘์„ฑ๋˜๋Š” ๋ฒ„์ „๋“ค์€ ๊ธฐ์กด ๋ฒ„์ „๋“ค ๋ฟ๋งŒ ์•„๋‹ˆ๋ผ ๊ณผ๊ฑฐ์˜ ์†Œ๋น„ ์ฝ”๋“œ์—๋„ ์–ด๋– ํ•œ ์˜ํ–ฅ์„ ๋ฏธ์น˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค.

๋ฟ๋งŒ ์•„๋‹ˆ๋ผ, ๋งํฌ์˜ OOP ์ฝ”๋“œ์— ๋น„ํ•ด์„œ, FP ์ฝ”๋“œ๋Š” ๊ฐ„๊ฒฐํ•˜๊ณ  ๊ฐ€๋…์„ฑ ์ธก๋ฉด์—์„œ๋„ ํฌ๊ฒŒ ๋‹ฌ๋ผ์ง€๋Š” ๋ถ€๋ถ„๋„ ์—†์Šต๋‹ˆ๋‹ค.

ํŠนํžˆ ์ฃผ๋ชฉํ•  ๋ถ€๋ถ„์€, ๋งํฌ๋œ ๊ธ€์˜ ๋‹ค์Œ ๊ธ€์˜

๊ธฐ๋ณธ ์ธํ„ฐํŽ˜์ด์Šค ๋ฉ”์„œ๋“œ๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ mixin ํ˜•์‹ ๋งŒ๋“ค๊ธฐ - C# | Microsoft Learn

๋งจ ๋งˆ์ง€๋ง‰์— ์žˆ๋Š” ์ฃผ์˜ ์‚ฌํ•ญ์— ๋Œ€ํ•œ ์šฐ๋ ค๋Š” FP์—๋Š” ์ ์šฉ๋˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค.

๋ฌผ๋ก , ๋ชจ๋“  C# ์ฝ”๋“œ๋ฅผ FP ์Šคํƒ€์ผ๋กœ ๋ฐ”๊ฟ€ ์ˆ˜๋Š” ์—†์Šต๋‹ˆ๋‹ค.
OOP ๋งŒ์˜ ์žฅ์ ์ด ํŠนํžˆ ๋ถ€๊ฐ๋˜๋Š” ๋ถ€๋ถ„, ์˜ˆ๋ฅผ ๋“ค๋ฉด ์„œ๋น„์Šค ๊ฐ์ฒด์˜ ๊ตฌํ˜„์—๋Š” ์ ์ ˆํ•˜์ง€ ์•Š์€ ๊ฒƒ ๊ฐ™์Šต๋‹ˆ๋‹ค.

์›๊ธ€์˜ ํ”„๋กœ์ ํŠธ๋Š” ์ด๋Ÿฌํ•œ FP ์˜ ์žฅ์ ์„ ์‹คํ—˜ํ•ด๋ณด๊ธฐ ์œ„ํ•œ ๊ฒƒ์ธ๋ฐ, ๋•๋ถ„์— ๊ทธ ์žฅ์ ๊ณผ ๋‹จ์ ์ด ๋”์šฑ ์„ ๋ช…ํ•ด์ง€๋Š” ๊ณ„๊ธฐ๊ฐ€ ๋œ ๊ฒƒ ๊ฐ™์•„ ํฐ ๋„์›€์ด ๋˜์—ˆ์Šต๋‹ˆ๋‹ค.

2๊ฐœ์˜ ์ข‹์•„์š”

๊ฐœ์ธ์ ์œผ๋กœ FP์— ๋Œ€ํ•ด์„œ๋Š” ํ•ญ์ƒ ์„ ํ•˜๋‹ค๊ณ  ์ƒ๊ฐํ•ฉ๋‹ˆ๋‹ค.

๋‹ค๋งŒ ๋Ÿฌ๋‹์ปค๋ธŒ๊ฐ€ ์žˆ๋‹ค๋ณด๋‹ˆ ์ฃผ๋ฅ˜ ํ”„๋กœ๊ทธ๋ž˜๋ฐ์ด ๋˜๊ธฐ ์–ด๋ ค์šด ๋ถ€๋ถ„์ด ์•„์‰ฝ์Šต๋‹ˆ๋‹ค.

FP์˜ ์žฅ์ ์€ ๋ถˆ๋ณ€์„ฑ๊ณผ ์ˆœ์ˆ˜ํ•จ์ˆ˜์— ์žˆ๋Š”๋ฐ์š”. 2๊ฐ€์ง€๋ฅผ ๋งŒ์กฑ์‹œํ‚ค๋ฉด ์ด์ƒ์ ์ธ ์ƒํƒœ์˜ ๋ถ€์ˆ˜ํšจ๊ณผ ์—†๋Š” ์ฝ”๋”ฉ์ด ๊ฐ€๋Šฅํ•ฉ๋‹ˆ๋‹ค. ๋ฆฌ์–ผ์›”๋“œ์—์„œ๋Š” ๋ถ€์ˆ˜ํšจ๊ณผ๋ฅผ IO ๋ชจ๋‚˜๋“œ๋ฅผ ํ†ตํ•ด ์ œ์–ด ๊ฐ€๋Šฅํ•˜์ง€์š”.

๋ถˆ๋ณ€์„ฑ๊ณผ ์ˆœ์ˆ˜ํ•จ์ˆ˜๋ฅผ ์ง€ํ‚ค๋ฉด ๋ชจ๋“  ํ•จ์ˆ˜๋ฅผ ํƒ€์ž…๊ณผ ํ™”์‚ดํ‘œ๋กœ ํ‘œํ˜„ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

๋ฒ„์ „2์˜ ๊ฒฝ์šฐ SetLoyaltyThresholds๋กœ ์ธํ•ด Customer.ComputeLoyaltyDiscount(c)๊ฐ€ ์ˆœ์ˆ˜ํ•จ์ˆ˜๋ฅผ ์œ„๋ฐ˜ํ•ฉ๋‹ˆ๋‹ค.
๋ฎคํ…Œ์ด์…˜์„ ๋ฐœ์ƒ์‹œํ‚ค์ง€์š”.

์ˆœ์ˆ˜ํ•จ์ˆ˜๋Š” ์ฐธ์กฐ ํˆฌ๋ช…์„ฑ์„ ์ค€์ˆ˜ํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค. ์ „์—ญ ๋ณ€์ˆ˜ ๋ณ€๊ฒฝ์œผ๋กœ ์ธํ•ด ์ถœ๋ ฅ๊ฐ’์ด ๋‹ฌ๋ผ์ ธ์„œ๋Š” ์•ˆ๋˜์š”.

C#์€ 9.0์ดํ›„ ์‹ค์šฉ์ ์ธก๋ฉด์—์„œ ๋งค์šฐ ์ง„๋ณด์ ์ธ ๋ฉ€ํ‹ฐ ํŽ˜๋Ÿฌ๋‹ค์ž„ ์–ธ์–ด๊ฐ€ ๋˜์—ˆ๋Š”๋ฐ์š”. C#์˜ FP ์ž…๋ฌธ์„œ๋กœ ๋‹ค์Œ์„ ์ถ”์ฒœํ•ฉ๋‹ˆ๋‹ค.

2๊ฐœ์˜ ์ข‹์•„์š”

์ถ”์ฒœํ•ด ์ฃผ์‹  ์ฑ…์€ ์‹œ๊ฐ„์ด ๋‚  ๋•Œ ๋ณด๊ณ ๋Š” ์‹ถ์€๋ฐ, 448 ํŽ˜์ด์ง€๋Š” ์ข€ ๋ถ€๋‹ด์Šค๋Ÿฝ๋„ค์š”. :sweat_smile:

๋ฒ„์ „2๋ฅผ ์ˆœ์ˆ˜์„ฑ์„ ์ค€์ˆ˜ํ•˜๋„๋ก ๋งŒ๋“ ๋‹ค๋ฉด, ์•„๋ž˜์™€ ๊ฐ™์„ ๊ฒƒ ๊ฐ™์€๋ฐ์š”.

using Original;
namespace Version2;
public static class Customer
{
   public static decimal ComputeLoyaltyDiscount(CustomerType customer, 
      TimeSpan age, int minimumOrders, decimal discountPercent)
   { 
       DateTime start = DateTime.Now - age;
       if ((customer.DateJoined < start) && (customer.PreviousOrders.Count() > minimumOrders))
       {
          return discountPercent;
       }
       return 0;
   }
}

์ข€ ๋” ๋ฐ”๋žŒ์งํ•œ ํ˜•ํƒœ๊ฐ€ ์žˆ์„๊นŒ์š”?

2๊ฐœ์˜ ์ข‹์•„์š”

์ˆœ์ˆ˜ํ•จ์ˆ˜ ๊ด€์ ์—์„œ ์ƒ๊ฐ์„ ํ•ด๋ณด๋ฉด ์ข‹์„ ๊ฑฐ ๊ฐ™์Šต๋‹ˆ๋‹ค.

Version1์˜ ๊ฒฝ์šฐ

A โ†’ B ๋กœ ํ‘œํ˜„๋˜๋Š”๋ฐ์š”.
Func<ICustomer, decimal>

Version2์˜ ๊ฒฝ์šฐ

C โ†’ A โ†’ B ๋กœ ์ •์˜ํ•˜๋ฉด ๋  ๊ฑฐ ๊ฐ™์•„์š”.
Func<ILoyaltyDiscount, Func<ICustomer, decimal>>

์ƒˆ๋กœ์šด ํ•จ์ˆ˜์—์„œ ๊ธฐ์กด ํ•จ์ˆ˜๋ฅผ ๋งŒ๋“œ๋ ค๋ฉด C๋ฅผ ์ปค๋งํ•˜๋ฉด ๋ฉ๋‹ˆ๋‹ค.
Func<ILoyaltyDiscount, Func<ICustomer, decimal>>์— ILoyaltyDiscount ๋ฅผ ๋„ฃ์œผ๋ฉด Func<ICustomer, decimal> ๊ฐ€ ๋‚˜์˜ค๊ฒŒ ๋˜์ฃ .

์‚ฌ์šฉํ•˜๋Š” ์ธก์—์„œ๋Š” Version1์œผ๋กœ ๊ตฌํ˜„๋˜์–ด๋„ ํ˜ธ์ถœ์— ์ด์Šˆ๊ฐ€ ์—†๊ณ  Version2๋กœ ํ˜ธ์ถœํ•˜๋ ค๋ฉด ILoyaltyDiscount๋ฅผ ๊ตฌํ˜„ํ•ด์„œ ์‚ฌ์šฉํ•˜๋ฉด ๋  ๊ฑฐ ๊ฐ™๊ตฐ์š”.

// Version 1:
decimal ComputeLoyaltyDiscount(ICustomer customer)
{
    DateTime TwoYearsAgo = DateTime.Now.AddYears(-2);
    if ((customer.DateJoined < TwoYearsAgo) && (customer.PreviousOrders.Count() > 10))
    {
        return 0.10m;
    }
    return 0;
}
// Version 2:
Func<ICustomer, decimal> ComputeLoyaltyDiscountV2(ILoyaltyDiscount loyaltyDiscount) => customer =>
{
    DateTime TwoYearsAgo = DateTime.Now.AddYears(-loyaltyDiscount.MinimumYears);
    if ((customer.DateJoined < TwoYearsAgo) && (customer.PreviousOrders.Count() > loyaltyDiscount.MinimumOrders))
    {
        return loyaltyDiscount.DiscountSize;
    }
    return 0;
};

decimal ComputeLoyaltyDiscount(ICustomer customer) =>
    ComputeLoyaltyDiscountV2(new LoyaltyDiscount
    {
        DiscountSize = 0.10m,
        MinimumOrders = 10,
        MinimumYears = 2
    })(customer);

2๊ฐœ์˜ ์ข‹์•„์š”

Version 2 ์ž‘์„ฑํ•˜๊ณ  ๋ณด๋‹ˆ ์ˆœ์ˆ˜ ํ•จ์ˆ˜๊ฐ€ ์•„๋‹ˆ๊ตฐ์š”.

DateTime.Now ๋Š” ํ˜ธ์ถœํ•  ๋•Œ ๋งˆ๋‹ค ๊ฐ’์ด ๋ฐ”๋€Œ์–ด์„œ ์ฐธ์กฐ ํˆฌ๋ช…์„ฑ์„ ์ œ๊ณตํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค. ๋”ฐ๋ผ์„œ impure ์„ pure function์œผ๋กœ ๋ถ€ํ„ฐ ๋ถ„๋ฆฌํ•˜๋Š” ์ž‘์—…์ด ํ•„์š”ํ•œ๋ฐ์š”. ์•„๋ž˜์ฒ˜๋Ÿผ ํ•จ์ˆ˜๋ฅผ ์ •์˜ํ•˜๋ฉด ๋ฉ๋‹ˆ๋‹ค.

DateTime โ†’ ILoyaltyDiscount โ†’ ICustomer โ†’ decimal

// Version 3:
[Pure]
Func<ILoyaltyDiscount, Func<ICustomer, decimal>> ComputeLoyaltyDiscountV3(DateTime now) => 
    loyaltyDiscount => customer =>
    {
        var yearsAgo = now.AddYears(-loyaltyDiscount.MinimumYears);
        if ((customer.DateJoined < yearsAgo) && (customer.PreviousOrders.Count() > loyaltyDiscount.MinimumOrders))
        {
            return loyaltyDiscount.DiscountSize;
        }
        return 0;
    };

Func<ICustomer, decimal> ComputeLoyaltyDiscountV2(ILoyaltyDiscount loyaltyDiscount) =>
    ComputeLoyaltyDiscountV3(DateTime.Now)(loyaltyDiscount);

decimal ComputeLoyaltyDiscount(ICustomer customer) =>
    ComputeLoyaltyDiscountV2(new LoyaltyDiscount
    {
        DiscountSize = 0.10m,
        MinimumOrders = 10,
        MinimumYears = 2
    })(customer);

ComputeLoyaltyDiscountV3 ๋Š” Pureํ•ด ์กŒ๊ตฐ์š”.

2๊ฐœ์˜ ์ข‹์•„์š”