Monads as a code reuse pattern


In a previous article, we discussed why functional programming is not at all what is PR, and that it does not contradict OOP at all, so that even Fowler writes about a good FP design that generates a good OOP program design (and vice versa).


Now I want to tell you what monads really are , how they are useful for an ordinary practicing developer, and I will give examples of why insufficient support in common languages ​​leads to copy-paste and unreliable solutions.


But there are literally hundreds of articles on the Internet about FP and monads on the Internet, why write another?


, ( ) : , T , T- β€” . , , β€” , , , , , … - .


, , , : , .


, , , : , , , , , . .



, ? : , , , . , , , , , .


, , . C#. Scala , Rust , , Haskell .


, C# 10 ( ), Shapes HKT. (aka , aka ). , ,


public shape JsonDeserialize<T>
{
    static T Deserialize(JObject input);
}

JsonSerializationException: Could not create an instance . , .


: -, ( ), , -, . , ICollection<T> IReadOnlyCollection<T>, . , . . - - where T : Number, , , : Number .


- . , :


public static T<int> CreateSomethingOfInts<T>() where T : <>, new() 
{
    return new T<int>();
}

, , , :


//         

var list = CreateSomethingOfInts<List>();
var hashSet = CreateSomethingOfInts<HashSet>();
// …
// Array<T>, Nullable<T>, LinkedList<T>, ... -     
//    Array<int>, Nullable<int>, LinkedList<int>, ...

C# 8. , , . , .


, , .


Functor


β€” . " , !" β€” . , β€” , , .


, ? , , β€” , Functor :


public shape Functor<T> where T : <>
{
    static T<B> Map<A, B>(T<A> source, Func<A, B> mapFunc);
}

, . , " , ". , Mappable, , , Map, -, A, , B.


, ToString, . , . , , -. β€” , - . A => B, . Map , , A, B.


Map : mapFunc Identity- x => x, . , "" , :


Map(something, x => x) === something

, , , , , mapFunc. , , , Map .


?
, , :


public extension EnumerableFunctor of IEnumerable : Functor<IEnumerable>
{
    public static IEnumerable<B> Map<A, B>(IEnumerable<A> source, Func<A, B> map) =>
        source.Select(map);
}

//   
var range = Enumerable.Range(1, 10);
Console.WriteLine(Map(range, x => x).SequenceEquals(x)) //  True

, , ! , IEnumerable β€” ,


? , 100 .


Nullable. :


public extension NullableFunctor of Nullable : Functor<Nullable>
{
    public static B? Map<A, B>(A? source, Func<A, B> map) =>
        source is A notNullSource ? map(notNullSource) : default(B?);
}

//   
int? nullableTen = 10;
int? nullableNull = null;
Console.WriteLine(Map(nullableTen, x => x) == nullableTen); //  True
Console.WriteLine(Map(nullableNull, x => x) == nullableNull); //  True


, Nullable β€” .


β€” Task, Map .




, : , . , " ". , , , .



Map , ,


β€” ! ...


Applicative


! , :


public shape Applicative<T> where T : <>
{
    static T<A> Pure<A>(A a);
    static T<C> LiftA2<A, B, C>(T<A> ta, T<B> tb, Func<A,B,C> map2);
}

? , , :


  1. . where T : new(), , new() , .
  2. . , T<A> T<B> , A, B C, T<C>. LiftA2 , "" A B T<A> T<B> .

? . - – - . , , , - ?


"Talk is cheap, show me the code", , , . β€” , , - β€” . :


public extension EnumerableApplicative of IEnumerable : Applicative<IEnumerable>
{
    static IEnumerable<A> Pure<A>(A a) => new[] { a };
    static IEnumerable<C> LiftA2<A, B, C>(IEnumerable<A> ta, 
                                          IEnumerable<B> tb, 
                                          Func<A, B, C> map2) =>
            ta.SelectMany(a => tb.Select(b => map2(a, b)));
}

, . β€” , public extension. - , , ( ).


Nullable? :


public extension NullableApplicative of IEnumerable : Applicative<Nullable>
{
    static A? Pure<A>(A a) => a;
    static C? LiftA2<A, B, C>(A? ta, B? tb, Func<A, B, C> map2) =>
        (ta, tb) switch {
            (A a, B b) => map2(a, b), //    null -  
            _ => default(C?)          // -  -  null
        };
}

" ", , " , , ...". , , , . ZipList:


public class ZipList<T> : IEnumerable<T>
{
    private IEnumerable<T> _inner;

    // ..    IEnumerable<T>   
}

public extension ZipListApplicative of ZipList : Applicative<IEnumerable>
{
    static IEnumerable<A> Pure<A>(A a) =>
        //        'a'
        Enumerable.Repeat(a, int.MaxValue); 
    static IEnumerable<C> LiftA2<A, B, C>(IEnumerable<A> ta, 
                                          IEnumerable<B> tb, 
                                          Func<A, B, C> map2) =>
        ta.Zip(tb, map2);
}

, ZipList , . Applicative , Zip.


, , - ? ? , β€” - , - , , - . , , , : (F<A>, F<B>) -> F<(A, B)>. :


public static T<(A, B)> Combine(T<A> ta, T<B> tb) =>
    LiftA2(ta, tb, (a, b) => (a, b));

var eta = Enumerable.Range(3, 2);
var etb = Enumerable.Range(15, 4);

int? nta = 10;
int? ntb = null;

Combine(eta, etb) // [(3, 15), (3, 16), (3, 17), (3, 18), (4, 15), (4, 16), (4, 17), (4, 18)]
Combine(nta, nta) // (10, 10)
Combine(nta, ntb) // Null
Combine(new ZipList<int>(eta), new ZipList<int>(etb)) //  [(3, 15), (4, 16)]


, , . β€” , Combine, , . , , , null. ZipList , . , ( (a, b) => (a, b)) ( T<>). , , , ( /// ), , , , .


, , , , . , , , " ". , .



pure ,



liftA2 ,
,


, , , " ". ? "" . , , . LiftA2 . , , , null β€” . - .


? LiftA2 Pure Map:


static T<B> MapAnyFunctor<T, A, B>(T<A> source, Func<A, B> map) where T : Applicative =>
    LiftA2(source, Pure(0), (a, _) => map(a));

? β€” Pure T<>. T<A> T<>, LiftA2. , , map T<A>. , . :


public shape Applicative<T> : Functor<T> where T : <>
{
    static T<A> Pure<A>(A a);
    static T<C> LiftA2<A, B, C>(T<A> ta, T<B> tb, Func<A,B,C> map2);
}

. , , .


Task<T>:


public extension TaskApplicative of Task: Applicative<Task>
{
    public static Task<A> Pure<A>(A a) => Task.FromResult(a);

    public static async Task<C> LiftA2<A, B, C>(Task<A> ta, Task<B> tb, Func<A, B, C> map2)
    {
        await Task.WhenAll(ta, tb);
        return map2(ta.Result, tb.Result);
    }
}


Monad


. , .
, :


public shape Monad<T> where T : <>
{
    static T<A> Pure<A>(A a);
    static T<B> Bind<A, B>(T<A> ta, Func<A, T<B>> mapInner);
}

-, :


β€” Pure Bind, T<A> , A T<B>, T<B>

, . , : . A , B. Bind "" , (T<A>, A => T<B>) T<B>.


β€” , , :


public extension EnumerableMonad of IEnumerable : Monad<IEnumerable>
{
    static IEnumerable<B> Bind<A, B>(IEnumerable<A> ta, Func<A, IEnumerable<B>> mapInner) =>
        ta.SelectMany(mapInner);
    // Pure      
}

, Nullable . ZipList? , , : , , , ( ).
, , . ZipList , .




, Pure : . LiftA2, Pure Bind, .


, , , . , , do-. :


let foo = do
  a <- someA
  b <- someB
  pure (doSomethingWith a b)

Bind :


var foo = Bind(someA, a => Bind(SomeB, b => Pure(DoSomethingWIth(a,b)))

. <- Bind,
, β€” , .
.


, , .


? . , ?


var values = from x in new []{ 1, 2, 3 }
             from y in new []{ 4, 5, 6 }
             select x + y;

, do- ( ):


let values = do 
    x <- [1,2,3]
    y <- [4,5,6]
    pure (x + y)

:


var maybeA = GetMaybeA();
var maybeB = maybeA?.GetB();
var maybeC = maybeB?.GetC();
var result = maybeC;

do- Maybe ( Option, β€” Nullable):


let result = do
    maybeA <- getMaybeA
    maybeB <- getB maybeA
    maybeC <- getC maybeB
    pure maybeC

?


var valueA = await GetSomeA();
var valueB = await GetSomeB(valueA);
var result = valueB;

do- IO ( , , , Task ):


let result = do
    valueA <- getSomeA
    valueB <- getSomeB valueA
    pure valueB

,
: T<>
A T<B>, T<B>, A[] A -> B[], A? A -> B?,
Task<A> A -> Task<B>,… .


...




, , . , , " -, - ". -, LINQ, null propagation . . nullable-? , - , , - . ? , . , , , - LINQ- (, First ).


, : MapAnyFunctor . async enumerable, - ( , CancallationToken?), ? - , ?


, : List IO.


, , : . Rust , . : try-, Try-, -, -. , async enumerable, , , .


do-, , , , , .


: , -. , , , . , ( , Par)? , , C# 15, , , . .



, " -, -". , Bind. ContinueWith, SelectMany, . , Rust . Option/Result/Future , , : Monad, MonadFail/Bifunctor/..., .


? - . 1.29 flatten , , . , .


transpose Option/Result , , transpose 0.8 2013 . transpose ( IO ) , 7 , .


, : Monad , transpose/flatten/... . , - , , . , 2015 . , , , .


. System.Linq.Async. , LINQ corefx, async-await. , , . , . , - . filterM/mapM/whateverM, //… , ( M ), .


, ,



, Monad ( ) , . , , , . , "" β€” , , ( ), , , , .


, , , Go . Functor/Applicative/Monad/… . HKT β€” , , , . , , , . , , .


, , . , , IEnumerable. β€” . ORM IQueryable, , ORM . - , , ? ?


, . Haskell, My.Big.Corporation.Utils, , " ". , , , , -.


, β€” , . β€” , . , " ", , , . β€” , .


, , , : - . . Akka, . , β€” , ? , , - , , , . , , , , - , , - . , , . , , , , .



, , , , β€” . , , - , β€” , - , , , . β€” , , β€” , β€” , .



. , , : SOLID. ? , , , :


public M<Comment> GetArticleComment<M>(int articleId) 
    where M : MonadWriter<LogMessage[]>, MonadReader<Config>, MonadHttp<AllowedSite>

MonadWriter/MonadReader/MonadHttp β€” SOLID , . , ( LogMessage!), ( Config!) Http ( AllowedSite!), .


, , . , , Task. return 10, Task<int>.


, Task M, β€” , .


, SOLID, β€” . Autofac/Windsor/Ninject/… " ", where , , . , , , .



, , , .


, , , . : , , - - .


class MyService 
{
    async Task<Comment> SomeBusinessLogicAsync(int commentId) {
        var comment = await this.remoteClient.GetAsync($"some/url/{commentId}");
        // .. do stuff ..
        return await DoOtherStuffAsync(comment);
    }
}

. C# ?
, , MyService remoteClient , .


- , httpClient, ( , ), act/assert .


? , ( ) Id, :


public class Id<T>
{
    public T Value { get; }

    public Id(T value)
    {
        Value = value
    }
}

public extension IdMonad of Id : Monad<Id>
{
    static IdMonad<A> Pure<A>(A a) => new Id<A>(a); //   
    static IdMonad<B> Bind<A, B>(IdMonad<A> ta, Func<A, IdMonad<B>> mapInner)  =>
        mapInner(ta.Value); //      
    //  map  liftA2  -
}

:


Task<Comment> SomeBusinessLogicAsync(int commentId);


M<Comment> SomeBusinessLogicAsync<M>(int commentId) where M : Monad =>
    this.remoteClient.Get($"some/url/{commentId}").Bind(comment => 
        // .. do stuff ..
        return DoOtherStuff(comment);
    );

do- 11, . :


var comment = await myService.SomeBusinessLogicAsync<Task>(547);

:


var comment = myService.SomeBusinessLogicAsync<Id>(547).Value;

! , SOLID ( ). , , , . , . , - . , . . , .


β€” . , Task.FromResult - . , , . : : , . " ", . , , .


, M , Task Id, , , , ( M ), ( M β€” Option), ( M β€” Result), . β€” . , , , β€” , . : β€” , β€” .




, , . Traversable, Foldable, , , , .


, :


  • (. Mappable)


    : , Map , .


    : , T<A> T<B>.


    : ;


  • (, . PairMappable)


    : , Pure LiftA2,
    ( , ).
    "".


    : T<A> T<B> T<C>.


    : (, List, Option, ZipList, ..);
    -


  • (. NestedJoinMappable):


    : , Pure Bind ( ).
    "".


    : , T<B> A,
    T<A>


    : , ; -



, , ( ) - , ,


  • ( )
  • ,

, , , , .


Monads are just a tool to be able to use. Studying it is a successful investment that will save more than one month of life, destroying the cause of many questions β€œwell, WHAT the hell does it not work, I checked it all ” in the bud. And although no programming technique will save you from all problems , it is a sin not to use a tool that solves a significant part of them.


All Articles