⬆️ ⬇️

Maybe monad on steroids

About monads on Habré there were already so many publications that , it seems to me , one more is missing .



I will not describe what a monad is, I will just show one funny implementation of the Maybe monad (are we in the “Abnormal Programming” hub?).



Let's declare this simple delegate:



public delegate T IMaybe<out T>(); 


Now I will show that such a simple definition will be enough to create a full optional type (Optional type).

')

A monad should have two methods - Return and Bind. The first one “wraps” the nonmonadic value into a monad, the second one allows to connect two monadic calculations.



For convenience, we will create a static class and make all the necessary functions with extension functions of our type and put all the methods into it:



 public static class Maybe { } 


The first function, Return, is fairly simple. From the value we have to make the delegate that returns it:



 public static IMaybe<T> Return<T>(T x) { return () => x; } 


Maybe should also be declared something responsible for the lack of value. In our case, it will be the delegate who throws an exception:



 public static IMaybe<T> Nothing<T>() { return () => { throw new InvalidOperationException("  "); }; } 


The second method of the monad - Bind - must connect two calculations. His signature:



 public static IMaybe<TResult> Bind<TArg, TResult>(this IMaybe<TArg> maybe, Func<TArg, IMaybe<TResult>> func) 


Let's take a closer look at it.



The first argument is actually the first monadic value. The second argument is a function that, from the value inside the monad, creates a new monadic value. The implementation of the Bind method should be able to get the value from the monad. In our case, to get the value, just call our delegate.



 public static IMaybe<TResult> Bind<TArg, TResult>(this IMaybe<TArg> maybe, Func<TArg, IMaybe<TResult>> func) { return () => { var value = maybe(); var newMaybe = func(value); return newMaybe(); }; } 


There is some trick here. The Bind method could well have such an implementation:



 public static IMaybe<TResult> Bind<TArg, TResult>(this IMaybe<TArg> maybe, Func<TArg, IMaybe<TResult>> func) { //  ! return func(maybe()); } 


However, there is a catch. If the first argument is Nothing, then the Bind method will throw an exception immediately after the call. But we want Bind to link two calculations, not to produce them. Therefore, Bind should postpone getting the result from the first monad and actually calculating over the value from the monad until the value is needed by the consumer, our Maybe.



Add a few more methods for our Maybe: Select, Where, SelectMany



The Select method performs some transformation on the object inside Maybe. It can be implemented using Bind and Return:



 public static IMaybe<TResult> Select<TArg, TResult>(this IMaybe<TArg> maybe, Func<TArg, TResult> func) { return maybe.Bind(value => Return(func(value))); } 




Where filters the value inside Maybe and returns Nothing if the value does not satisfy the predicate:



 public static IMaybe<T> Where<T>(this IMaybe<T> maybe, Func<T, bool> predicate) { return maybe.Bind(x => predicate(x) ? Return(x) : Nothing<T>()); } 


SelectMany is an analogue of Bind that allows us to write expressions using Linq syntax. It differs from the simple Bind by the presence of the final projection from the values ​​of both monads:



 public static IMaybe<TC> SelectMany<TA, TB, TC>(this IMaybe<TA> ma, Func<TA, IMaybe<TB>> maybeSelector, Func<TA, TB, TC> resultSelector) { return ma.Bind(a => maybeSelector(a).Select(b => resultSelector(a, b))); } 


It is noteworthy that the Select, Where and SelectMany methods know nothing about the internal structure of our Maybe - they use only Bind, Return and a null value (Nothing for Maybe). We could substitute another implementation of Maybe - and these methods would remain unchanged. Moreover, we could substitute another monad, for example List:



 public static IEnumerable<T> Return<T>(T x) { return new[] { x }; } public static IEnumerable<T> Nothing<T>() { yield break; } public static IEnumerable<TResult> Bind<TArg, TResult>(this IEnumerable<TArg> m, Func<TArg, IEnumerable<TResult>> func) { foreach (var arg in m) { foreach (var result in func(arg)) { yield return result; } } } 


... and again these methods would remain the same. If we had type classes, we would declare these methods on the Monad type (as is done * in Haskell) (* actually not).



The last thing that remains is actually the use of our Maybe:



 var one = Maybe.Return(1); var nothing = Maybe.Nothing<int>(); var nothing2 = from ax in one from ay in nothing select ax + ay; var two = one.Where(z => z > 0).Select(z => z + 1); Console.WriteLine(one()); Console.WriteLine(two()); Console.WriteLine(nothing2()); 


We have no other way to get the value from the monad, except to call the delegate, which happens in the last three lines. The last line is expected to fall with the exception "You can not get the value."



All together you can see here .

Source: https://habr.com/ru/post/269717/



All Articles