Монада Maybe на стероидах

    Про монады на Хабре было уже столько много публикаций, что, мне кажется, не хватает еще одной.

    Я не буду расписывать, что такое монада, я просто покажу одну забавную реализацию монады Maybe (мы же в хабе «Ненормальное программирование»?).

    Давайте объявим вот такой простой делегат:

    public delegate T IMaybe<out T>();
    

    Сейчас я покажу, что такого простого определения будет достаточно, чтобы создать полноценный опциональный тип (Optional type).

    Монада должна иметь два метода — Return и Bind. Первый «заворачивает» немонадическое значение в монаду, второй — позволяет связывать два монадических вычисления.

    Для удобства создадим статический класс и все нужные функции сделаем функциями-расширениями (extension methods) от нашего типа и все методы будем складывать в него:

    public static class Maybe 
    {
    }
    

    Первая функция — Return — достаточно простая. Из значения мы должны сделать делегат, который его возвращает:

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

    У Maybe также должно быть объявлено нечто, отвечающие за отсутствие значения. В нашем случае это будет делегат, который бросает исключение:

    public static IMaybe<T> Nothing<T>()
    {
    	return () => { throw new InvalidOperationException("Нельзя получить значение"); };
    }
    

    Второй метод у монады — Bind — должен связывать два вычисления. Его сигнатура:

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

    Давайте с ним разберемся поближе.

    Первый аргумент — собственно, первое монадическое значение. Второй аргумент — функция, которая из значения внутри монады создает новое монадическое значение. Реализация метода Bind должна уметь получать значение из монады. В нашем случае, чтобы получить значение, достаточно просто вызвать наш делегат.

    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();        
       };
    }
    

    Здесь есть некоторая хитрость. Метод Bind вполне мог иметь и такую реализацию:

    public static IMaybe<TResult> Bind<TArg, TResult>(this IMaybe<TArg> maybe, Func<TArg, IMaybe<TResult>> func)
    {
    	// неправильная реализация!
    	return func(maybe());
    }
    

    Однако тут есть подвох. Если первым аргументом мы передаем Nothing, то метод Bind выбросит исключение сразу после вызова. Но мы-то хотим, чтобы Bind связывал два вычисления, а не производил их. Поэтому Bind должен отложить получение результата из первой монады и собственно вычисление над значением из монады до тех пор, пока значение не понадобится потребителю нашей Maybe.

    Добавим еще несколько методов для нашего Maybe: Select, Where, SelectMany

    Метод Select производит некоторую трансформацию над объектом внутри Maybe. Он может быть реализован с помощью Bind и Return:

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


    Where фильтрует значение внутри Maybe и возвращает Nothing, если значение не удовлетворяет предикату:

    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 — это аналог Bind, который позволит писать нам выражения, используя Linq синтаксис. От простого Bind отличается наличием финальной проекции от значений обоих монад:

    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)));
    }
    

    Примечательно, что методы Select, Where и SelectMany ничего не знают о внутреннем устройстве нашего Maybe — они используют только Bind, Return и пустое значение (Nothing для Maybe). Мы могли бы подставить другую реализацию Maybe — и эти методы остались бы неизменны. Более того, мы могли бы подставить другую монаду, например 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;
    		}
    	}
    }
    

    … и снова эти методы остались бы такими же. Если бы у нас были тайп-классы (type class), мы бы объявили эти методы над тайп-классом Monad (как это делается* в Хаскелле) (*на самом деле нет).

    Последнее, что осталось — это, собственно, использование нашего 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());
    

    У нас нет другого способа получить значение из монады, кроме как вызвать делегат, что и происходит в последних трех строчках. Последняя строчка ожидаемо падает с исключением «Нельзя получить значение».

    Все вместе можно увидеть здесь.
    Поделиться публикацией

    Комментарии 11

      +1
        +8
        Ничем. Это же «Ненормальное программирование», просто забавное наблюдение, что ленивое вычисление (или, скорее, call-by-ref) — это тоже монада, эквивалетная maybe.
          0
          Как меня поправили — call-by-ref и Lazy не эквивалентны maybe, они просто являются монадами.
        +1
        Добрый день,

        Спасибо за статью. Если переходит от забавы к серьезным вещам, то скажите, вы используете монады в ваших реальных/продакшен проектах? Если так то как вы смогли аргументировать факт их использования? Я по опыту могу сказать, что мне, например не удалось этого сделать так как команда сказала что монады это для хаскела а они не видят никакой прибыли от их использования в C# и что им пофиг на чистоту функций, неизменяемость и теорию категорий.
          +3
          Да, мы используем nuget.org/packages/Tp.Core.Functional повсеместно в наших продакшн проектах. Как перешли? Да, в принципе, никто и не был против. У нас в компании привествуется software craftsmanship движение, а maybe просто добавляют коду явность — я явно указываю, что ожидаю праметр с возможно отсутствующим значением или наоборот — мой метод может вернут пустое значение. И компилятор мне подскажет, где я пренебрегаю этим, выдав ошибку типов. Ошибка в компайл-тайм дешевле ошибки, пойманной в тестах в десятки раз и ошибки из продакшена в сотни — простая арифметика.
            0
            Ну а Linq в вашей команде используют — если что это монада List. Async/Await — тоже монада(название у нее разное в разных языках). Даже null value (использование null вместо значения по ссылке) — тоже монада.
            Ну и самый главный прикол что просто последовательное исполнение кода в C# это тоже монада — в хаскеле ее называют IO.
            Все пользуются монадами — не все об этом догадываются :)
              0
              Согласен с вами полностью, мне никто не смог возразить что SelectMany это тот же самый Bind, так как крыть было нечем. Но увы невежество людское и нежелание прогрессировать на службе у толпы посильнее всех здравых убеждений. Ничего, работу всегда можно сменить, благо ее навалом.
            +2
            На самом деле тут ничего удивительного нет. Вы используете не столько ленивые вычисления, сколько продолжения (continuations). Существует очень глубокая связь между монадами и продолжениями. Об этом довольно хорошо написал Dan Piponi в статье The Mother of all Monads.
              0
              А вы реально эксепшены на русском пишете (по поводу фиддла)? :D
                0
                Нет, конечно. :)
                0
                У Марка Симана есть хорошая заметка насчет монады Maybe в .NET. Соль в том, что можно ничего не подключать и не реализовывать, ведь она у вас давно уже есть, вы просто раньше ее не замечали.

                Пример кода
                using System;
                using System.Collections.Generic;
                using System.Linq;
                using System.Text;
                using System.Collections;
                
                namespace Ploeh.Samples.Booking.DomainModel
                {
                    public class Maybe<T> : IEnumerable<T>
                    {
                        private readonly IEnumerable<T> values;
                
                        public Maybe()
                        {
                            this.values = new T[0];
                        }
                
                        public Maybe(T value)
                        {
                            this.values = new[] { value };
                        }
                
                        public IEnumerator<T> GetEnumerator()
                        {
                            return this.values.GetEnumerator();
                        }
                
                        IEnumerator IEnumerable.GetEnumerator()
                        {
                            return this.GetEnumerator();
                        }
                    }
                
                    public static class Maybe
                    {
                        public static Maybe<T> ToMaybe<T>(this T value)
                        {
                            return new Maybe<T>(value);
                        }
                
                        public static Maybe<T> Empty<T>()
                        {
                            return new Maybe<T>();
                        }
                    }
                }
                

                Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

                Самое читаемое