diff --git a/CSharpFunctionalExtensions.Examples/LinqQuery/ResultQueryExamples.cs b/CSharpFunctionalExtensions.Examples/LinqQuery/ResultQueryExamples.cs new file mode 100644 index 00000000..7ec9998f --- /dev/null +++ b/CSharpFunctionalExtensions.Examples/LinqQuery/ResultQueryExamples.cs @@ -0,0 +1,185 @@ +using System; +using System.Text.RegularExpressions; +using System.Threading.Tasks; + +namespace CSharpFunctionalExtensions.Examples.LinqQuery +{ + /// + /// Demonstrates using LINQ query syntax (sugar) as an alternative to + /// fluent method chaining to compose operations in a monadic style, + /// analogous to F# computation expressions or Haskell's do-notation. + /// + public class ResultQueryExamples + { + public async Task Demo() + { + // Using the LINQ query syntax: + var validCustomer = + from name in CustomerName.Create("jsmith") + from email in Email.Create("jsmith@example.com") + select new Customer(name, email); + + // Equivalent to: + + var validCustomer_ = CustomerName + .Create("jsmith") + .Bind(name => + Email.Create("jsmith@example.com").Map(email => new Customer(name, email)) + ); + + // Success(Customer(Name: jsmith, Email: jsmith@example.com)) + Console.WriteLine(validCustomer); + + var invalidCustomer = + from name in CustomerName.Create("jsmith") + from email in Email.Create("no email") + select new Customer(name, email); + + // Failure(E-mail is invalid) + Console.WriteLine(invalidCustomer); + + //------------------------------------------------------------------------------ + // Also works with async methods. + + var billing = await ( + from customer in CustomerRepository.GetByIdAsync(1) // Task> + from billingInfo in PaymentGateway.ChargeCustomerAsync(customer, 1_000) // Task> + select billingInfo + ); + + // Equivalent to: + var billing_ = await CustomerRepository + .GetByIdAsync(1) + .Bind(customer => PaymentGateway.ChargeCustomerAsync(customer, 1_000)); + + // Success(BillingInfo(Customer: jsmith@example.com, ChargeAmount: 1000)) + Console.WriteLine(billing); + + var failedBilling = await ( + from customer in CustomerRepository.GetByIdAsync(1) + from billingInfo in PaymentGateway.ChargeCustomerAsync(customer, 5_000_000) + select billingInfo + ); + + // Failure(Insufficient balance.) + Console.WriteLine(failedBilling); + } + } + + public class Email + { + private readonly string _value; + + private Email(string value) + { + _value = value; + } + + public override string ToString() + { + return _value; + } + + public static Result Create(string email) + { + if (string.IsNullOrWhiteSpace(email)) + return Result.Failure("E-mail can't be empty"); + + if (email.Length > 100) + return Result.Failure("E-mail is too long"); + + if (!Regex.IsMatch(email, @"^([\w\.\-]+)@([\w\-]+)((\.(\w){2,3})+)$")) + return Result.Failure("E-mail is invalid"); + + return Result.Success(new Email(email)); + } + } + + public class CustomerName + { + private readonly string _value; + + private CustomerName(string value) + { + _value = value; + } + + public override string ToString() + { + return _value; + } + + public static Result Create(string name) + { + if (string.IsNullOrWhiteSpace(name)) + return Result.Failure("Name can't be empty"); + + if (name.Length > 50) + return Result.Failure("Name is too long"); + + return Result.Success(new CustomerName(name)); + } + } + + public class Customer + { + public CustomerName Name { get; private set; } + public Email Email { get; private set; } + + public Customer(CustomerName name, Email email) + { + if (name == null) + throw new ArgumentNullException("name"); + if (email == null) + throw new ArgumentNullException("email"); + + Name = name; + Email = email; + } + + public override string ToString() + { + return $"{nameof(Customer)}({nameof(Name)}: {Name}, {nameof(Email)}: {Email})"; + } + } + + public class CustomerRepository + { + public static async Task> GetByIdAsync(int id) + { + var customer = + from email in Email.Create("jsmith@example.com") + from name in CustomerName.Create("jsmith") + select new Customer(name, email); + + return customer; + } + } + + public class BillingInfo + { + public Customer Customer { get; set; } + public decimal ChargeAmount { get; set; } + + public override string ToString() + { + return $"{nameof(BillingInfo)}({nameof(Customer)}: {Customer.Email}, {nameof(ChargeAmount)}: {ChargeAmount})"; + } + } + + public class PaymentGateway + { + public static async Task> ChargeCustomerAsync( + Customer customer, + decimal chargeAmount + ) + { + if (chargeAmount > 1_000_000) + return Result.Failure("Insufficient balance"); + + return Result.Success( + new BillingInfo { Customer = customer, ChargeAmount = chargeAmount } + ); + } + } +} diff --git a/CSharpFunctionalExtensions/Maybe/Extensions/Select.Task.cs b/CSharpFunctionalExtensions/Maybe/Extensions/Select.Task.cs new file mode 100644 index 00000000..a3a8eba2 --- /dev/null +++ b/CSharpFunctionalExtensions/Maybe/Extensions/Select.Task.cs @@ -0,0 +1,16 @@ +using System; +using System.Threading.Tasks; + +namespace CSharpFunctionalExtensions +{ + public static partial class MaybeExtensions + { + /// + /// This method should be used in linq queries. We recommend using Map method. + /// + public static Task> Select(this Task> maybe, Func selector) + { + return maybe.Map(selector); + } + } +} diff --git a/CSharpFunctionalExtensions/Maybe/Extensions/Select.ValueTask.cs b/CSharpFunctionalExtensions/Maybe/Extensions/Select.ValueTask.cs new file mode 100644 index 00000000..47f111bc --- /dev/null +++ b/CSharpFunctionalExtensions/Maybe/Extensions/Select.ValueTask.cs @@ -0,0 +1,18 @@ +#if NET5_0_OR_GREATER +using System; +using System.Threading.Tasks; + +namespace CSharpFunctionalExtensions.ValueTasks +{ + public static partial class MaybeExtensions + { + /// + /// This method should be used in linq queries. We recommend using Map method. + /// + public static ValueTask> Select(in this ValueTask> maybe, Func selector) + { + return maybe.Map(selector); + } + } +} +#endif diff --git a/CSharpFunctionalExtensions/Maybe/Extensions/SelectMany.Task.Left.cs b/CSharpFunctionalExtensions/Maybe/Extensions/SelectMany.Task.Left.cs new file mode 100644 index 00000000..7910e4b0 --- /dev/null +++ b/CSharpFunctionalExtensions/Maybe/Extensions/SelectMany.Task.Left.cs @@ -0,0 +1,20 @@ +using System; +using System.Threading.Tasks; + +namespace CSharpFunctionalExtensions +{ + public static partial class MaybeExtensions + { + /// + /// This method should be used in linq queries. We recommend using Bind method. + /// + public static async Task> SelectMany( + this Task> maybeTask, + Func> selector, + Func project) + { + Maybe maybe = await maybeTask.DefaultAwait(); + return maybe.SelectMany(selector, project); + } + } +} diff --git a/CSharpFunctionalExtensions/Maybe/Extensions/SelectMany.Task.Right.cs b/CSharpFunctionalExtensions/Maybe/Extensions/SelectMany.Task.Right.cs new file mode 100644 index 00000000..fc6413e3 --- /dev/null +++ b/CSharpFunctionalExtensions/Maybe/Extensions/SelectMany.Task.Right.cs @@ -0,0 +1,21 @@ +using System; +using System.Threading.Tasks; + +namespace CSharpFunctionalExtensions +{ + public static partial class MaybeExtensions + { + /// + /// This method should be used in linq queries. We recommend using Bind method. + /// + public static Task> SelectMany( + this Maybe maybe, + Func>> selector, + Func project) + { + return maybe + .Bind(selector) + .Map(x => project(maybe.Value, x)); + } + } +} diff --git a/CSharpFunctionalExtensions/Maybe/Extensions/SelectMany.Task.cs b/CSharpFunctionalExtensions/Maybe/Extensions/SelectMany.Task.cs new file mode 100644 index 00000000..cf2413f8 --- /dev/null +++ b/CSharpFunctionalExtensions/Maybe/Extensions/SelectMany.Task.cs @@ -0,0 +1,40 @@ +using System; +using System.Threading.Tasks; + +namespace CSharpFunctionalExtensions +{ + public static partial class MaybeExtensions + { + /// + /// This method should be used in linq queries. We recommend using Bind method. + /// + public static Task> SelectMany( + this Maybe maybe, + Func>> selector) + { + return maybe.Bind(selector); + } + + /// + /// This method should be used in linq queries. We recommend using Bind method. + /// + public static Task> SelectMany( + this Task> maybeTask, + Func>> selector) + { + return maybeTask.Bind(selector); + } + + /// + /// This method should be used in linq queries. We recommend using Bind method. + /// + public static async Task> SelectMany( + this Task> maybeTask, + Func>> selector, + Func project) + { + var maybe = await maybeTask.DefaultAwait(); + return await maybe.SelectMany(selector, project).DefaultAwait(); + } + } +} diff --git a/CSharpFunctionalExtensions/Maybe/Extensions/SelectMany.ValueTask.Left.cs b/CSharpFunctionalExtensions/Maybe/Extensions/SelectMany.ValueTask.Left.cs new file mode 100644 index 00000000..bce4a8de --- /dev/null +++ b/CSharpFunctionalExtensions/Maybe/Extensions/SelectMany.ValueTask.Left.cs @@ -0,0 +1,22 @@ +#if NET5_0_OR_GREATER +using System; +using System.Threading.Tasks; + +namespace CSharpFunctionalExtensions.ValueTasks +{ + public static partial class MaybeExtensions + { + /// + /// This method should be used in linq queries. We recommend using Bind method. + /// + public static async ValueTask> SelectMany( + this ValueTask> maybeTask, + Func> selector, + Func project) + { + Maybe maybe = await maybeTask; + return maybe.SelectMany(selector, project); + } + } +} +#endif diff --git a/CSharpFunctionalExtensions/Maybe/Extensions/SelectMany.ValueTask.Right.cs b/CSharpFunctionalExtensions/Maybe/Extensions/SelectMany.ValueTask.Right.cs new file mode 100644 index 00000000..77d52704 --- /dev/null +++ b/CSharpFunctionalExtensions/Maybe/Extensions/SelectMany.ValueTask.Right.cs @@ -0,0 +1,23 @@ +#if NET5_0_OR_GREATER +using System; +using System.Threading.Tasks; + +namespace CSharpFunctionalExtensions.ValueTasks +{ + public static partial class MaybeExtensions + { + /// + /// This method should be used in linq queries. We recommend using Bind method. + /// + public static ValueTask> SelectMany( + this Maybe maybe, + Func>> selector, + Func project) + { + return maybe + .Bind(selector) + .Map(x => project(maybe.Value, x)); + } + } +} +#endif diff --git a/CSharpFunctionalExtensions/Maybe/Extensions/SelectMany.ValueTask.cs b/CSharpFunctionalExtensions/Maybe/Extensions/SelectMany.ValueTask.cs new file mode 100644 index 00000000..34cf3802 --- /dev/null +++ b/CSharpFunctionalExtensions/Maybe/Extensions/SelectMany.ValueTask.cs @@ -0,0 +1,42 @@ +#if NET5_0_OR_GREATER +using System; +using System.Threading.Tasks; + +namespace CSharpFunctionalExtensions.ValueTasks +{ + public static partial class MaybeExtensions + { + /// + /// This method should be used in linq queries. We recommend using Bind method. + /// + public static ValueTask> SelectMany( + this Maybe maybe, + Func>> selector) + { + return maybe.Bind(selector); + } + + /// + /// This method should be used in linq queries. We recommend using Bind method. + /// + public static ValueTask> SelectMany( + this ValueTask> maybeTask, + Func>> selector) + { + return maybeTask.Bind(selector); + } + + /// + /// This method should be used in linq queries. We recommend using Bind method. + /// + public static async ValueTask> SelectMany( + this ValueTask> maybeTask, + Func>> selector, + Func project) + { + var maybe = await maybeTask; + return await maybe.SelectMany(selector, project); + } + } +} +#endif diff --git a/CSharpFunctionalExtensions/Maybe/Extensions/SelectMany.cs b/CSharpFunctionalExtensions/Maybe/Extensions/SelectMany.cs index e914e8dd..18dff8a0 100644 --- a/CSharpFunctionalExtensions/Maybe/Extensions/SelectMany.cs +++ b/CSharpFunctionalExtensions/Maybe/Extensions/SelectMany.cs @@ -4,11 +4,17 @@ namespace CSharpFunctionalExtensions { public static partial class MaybeExtensions { + /// + /// This method should be used in linq queries. We recommend using Bind method. + /// public static Maybe SelectMany(in this Maybe maybe, Func> selector) { return maybe.Bind(selector); } + /// + /// This method should be used in linq queries. We recommend using Bind method. + /// public static Maybe SelectMany(in this Maybe maybe, Func> selector, Func project) @@ -18,4 +24,4 @@ public static Maybe SelectMany(in this Maybe maybe, Maybe.None); } } -} \ No newline at end of file +} diff --git a/CSharpFunctionalExtensions/Result/Methods/Extensions/Select.Task.cs b/CSharpFunctionalExtensions/Result/Methods/Extensions/Select.Task.cs new file mode 100644 index 00000000..697ebd0c --- /dev/null +++ b/CSharpFunctionalExtensions/Result/Methods/Extensions/Select.Task.cs @@ -0,0 +1,24 @@ +using System; +using System.Threading.Tasks; + +namespace CSharpFunctionalExtensions +{ + public static partial class ResultExtensions + { + /// + /// This method should be used in linq queries. We recommend using Map method. + /// + public static Task> Select(this Task> result, Func selector) + { + return result.Map(selector); + } + + /// + /// This method should be used in linq queries. We recommend using Map method. + /// + public static Task> Select(this Task> result, Func selector) + { + return result.Map(selector); + } + } +} diff --git a/CSharpFunctionalExtensions/Result/Methods/Extensions/Select.ValueTask.cs b/CSharpFunctionalExtensions/Result/Methods/Extensions/Select.ValueTask.cs new file mode 100644 index 00000000..af78bae4 --- /dev/null +++ b/CSharpFunctionalExtensions/Result/Methods/Extensions/Select.ValueTask.cs @@ -0,0 +1,26 @@ +#if NET5_0_OR_GREATER +using System; +using System.Threading.Tasks; + +namespace CSharpFunctionalExtensions.ValueTasks +{ + public static partial class ResultExtensions + { + /// + /// This method should be used in linq queries. We recommend using Map method. + /// + public static ValueTask> Select(in this ValueTask> result, Func selector) + { + return result.Map(selector); + } + + /// + /// This method should be used in linq queries. We recommend using Map method. + /// + public static ValueTask> Select(in this ValueTask> result, Func selector) + { + return result.Map(selector); + } + } +} +#endif diff --git a/CSharpFunctionalExtensions/Result/Methods/Extensions/Select.cs b/CSharpFunctionalExtensions/Result/Methods/Extensions/Select.cs index a5b98853..90a8e3bb 100644 --- a/CSharpFunctionalExtensions/Result/Methods/Extensions/Select.cs +++ b/CSharpFunctionalExtensions/Result/Methods/Extensions/Select.cs @@ -11,5 +11,13 @@ public static Result Select(in this Result result, Func select { return result.Map(selector); } + + /// + /// This method should be used in linq queries. We recommend using Map method. + /// + public static Result Select(in this Result result, Func selector) + { + return result.Map(selector); + } } } diff --git a/README.md b/README.md index 254dfd02..fd675e2f 100644 --- a/README.md +++ b/README.md @@ -583,6 +583,75 @@ Console.WriteLine(appleInventory.MapError(ErrorEnhancer).ToString()); // "Succes Console.WriteLine(bananaInventory.MapError(ErrorEnhancer).ToString()); // "Failed operation: Could not find any bananas" ``` +### Linq query syntax + +Lots of functional languages provide syntax sugars to make (Monadic) compositions of Maybe/Result types +more readable, examples include: + +- Haskell's do-notation: + ```haskell + foo = do + x <- Just 1 + y <- Just 2 + return x + y + ``` +- Scala's for-comprehension: + ```scala + def foo = for { + x <- Some(1) + y <- Some(2) + } yield x + y + ``` +- F# computation expressions (`option` isn't in the stdlib, but it's trivial to define ourselves): + ```fsharp + let foo = option { + let! x = Some 1 + let! y = Some 2 + return x + y + } + ``` + +C# wasn't designed as a functional language from the beginning, but surprisingly we can do the same using +the Linq query syntax. + +Let's say the following `Create` factory methods do some validation and return `Result`. + +With method chaining: +```csharp +var customer = CustomerName + .Create("jsmith") + .Bind(name => + Email.Create("jsmith@example.com").Map(email => new Customer(name, email)) + ); +``` + +Rewrite using the Linq query syntax: +```csharp +var customer = + from name in CustomerName.Create("jsmith") + from email in Email.Create("jsmith@example.com") + select new Customer(name, email); +``` + +They are technically the same, but the latter is more readable. + +And this also works with `async` methods: + +```csharp +var billing = await ( + from customer in _customerRepository.GetByIdAsync(id) // Task> + from billingInfo in _paymentGateway.ChargeCustomerAsync(customer, amount) // Task> + select billingInfo // Result +); +``` + +Which is equivalent to: +```csharp +var billing = await _customerRepository + .GetByIdAsync(id) + .Bind(customer => _paymentGateway.ChargeCustomerAsync(customer, amount)); +``` + ## Testing ### [CSharpFunctionalExtensions.FluentAssertions](https://github.com/NitroDevs/CSharpFunctionalExtensions.FluentAssertions)