Tuesday, October 28, 2014

Introduction to monads in C#


Статья, которая будет рассмотрена сегодня, посвящена использованию монад в языке C#. Монады − очень популярная и достаточно избитая на данный момент тема. Особенное распространение использование монад получило в функциональных языках, как, например, Haskell. В объектно-ориентированный язык C# магия монад стала также проникать довольно давно. Существует замечательная статья "The Marvels of Monads", которая датируется еще 2008 годом, но и на данный момент эта тема не потеряла своей актуальности (перевод данной статьи на русский язык вы можете найти здесь: "Чудо монад"). Термин "монады", используемый в функциональных, а также в объектно-ориентированных языках, берет начало с математики Monad (category theory).
In category theory, a branch of mathematics, a monad (also triple, triad, standard construction and fundamental construction) is an (endo-)functor, together with two natural transformationsMonads are used in the theory of pairs of adjoint functors, and they generalize closure operators on partially ordered sets to arbitrary categories. Saunders Mac Lane adopted the philosophical term "monad" (a single entity that generates all other entities) for this construct, noting the construct's ability to generate a corresponding category.
Предполагаю, что вам уже приходилось слышать в языке C# о монаде Maybe. Если же нет, то мы постараемся затронуть эту тему. Понятие о монадах разжевано во многих блогах, поэтому в качестве ввода достаточно кратких сведений о монадах; задачах, которые они решают, а также их использовании. Ну что ж, поехали.
Введение
К сожалению, я видел не так много фирм, которые используют свои монады для упрощения написания кода, композиции, синтаксического анализа и в других случаях. В данном контексте речь идет о фирмах, которые разрабатывают свои продукты на языке C#, так как в статье мы будем отталкиваться именно от этого языка. Даже если вы принадлежите к таким разработчикам, которые не использует явно монады, вы, вероятнее всего, использовали монады, которые нам предоставляет .NET Framework. Если у вас возникло удивление, где в .NET Framework могут быть монады, то я продемонстрирую код с .NET Framework, который доступен в открытом виде онлайн по ссылке.
public static IEnumerable<TResult> SelectMany<TSource, TCollection, TResult>(this IEnumerable<TSource> source, Func<TSource, IEnumerable<TCollection>> collectionSelector, Func<TSource, TCollection, TResult> resultSelector)
{
    if (source == null) throw Error.ArgumentNull("source");
    if (collectionSelector == null) throw Error.ArgumentNull("collectionSelector");
    if (resultSelector == null) throw Error.ArgumentNull("resultSelector");
    return SelectManyIterator<TSource, TCollection, TResult>(source, collectionSelector, resultSelector);
}

static IEnumerable<TResult> SelectManyIterator<TSource, TCollection, TResult>(IEnumerable<TSource> source, Func<TSource, IEnumerable<TCollection>> collectionSelector, Func<TSource, TCollection, TResult> resultSelector)
{
    foreach (TSource element in source)
    {
        foreach (TCollection subElement in collectionSelector(element))
        {
            yield return resultSelector(element, subElement);
        }
    }
}
Я добавил эти функции парой так же, как они описаны в самом фреймворке. Думаю, что вам приходилось использовать функцию SelectMany с LINQ, если писали на .NET Framework 3.5 и выше. Возможно, вы все еще разрабатываете на фреймворке более старой версии и могли об этом не слышать, но в такое сейчас очень сложно поверить. Также в конструкцию SelectMany транслируется компилятором вложенный from clause.
Пример:
class Program
{
    static void Main(string[] args)
    {
        int[] numbers = { 5, 4, 1, 3, 9, 8, 6, 7, 2, 0 };

        // Create the query.
        var lowNums = from num1 in numbers
                        from num2 in numbers
                        select num1 + num2;

        // Execute the query.
        foreach (int i in lowNums)
        {
            Console.Write(i + " ");
        }

        Console.ReadLine();
    }
}
Результат:
Почему-то многие разработчики уверены, что extension methods − это и есть монады. Это заблуждение. Интересен сам факт, что грешат этим не только начинающие разработчики, но и специалисты с большим опытом. Extension methods – это только способ записи метода через точку. Ничего более. Монада  − это намного более широкое понятие.
Определение монады 
Монадой называется тройка (MReturnBind), где:
  • М – аргумент generic типа M<T>
  • Return – функция которая возврящает результат M<T>
  • Bind – функция для связвания монадных вычислений
Кроме того, монаду должны связывать три монадных закона: левое тождество, правое тождество и ассоциативность.
   1. Left Identity
Bind(Unit(e), k) = k(e)
   2. Right Identity
Bind(m, Unit) = m
   3. Associative
Bind(m, x =>Bind(k(x), y => h(y))= Bind(Bind(m, x => k(x)), y => h(y))
Давайте начнем рассмотрение, начиная из самых простых.
Монада Identity
Сутью монады Identity является понятие "тождественный". По сути, это класс, который представляет собой обертку над значением.
public class Identity<T>
{
    public T Value { get; private set; }

    public Identity(T value)
    {
        Value = value;
    }
}
Эту монаду удобно расширять с помощью extension методов.
public static class IdentityExtensions
{
    public static Identity<B> Bind<A, B>(this Identity<A> a, Func<A, Identity<B>> func)
    {
        return func(a.Value);
    }

    static Identity<T> ToIdentity<T>(this T value)
    {
        return new Identity<T>(value);
    }
}
Здесь метод ToIdentity принимает значение и возвращает новый экземпляр, который содержит данное значение. Метод Bind принимает экземпляр класса Identity, извлекает значения и вызывает делегат func с извлеченным значением.
Пример использования:
class Program
{
    static void Main(string[] args)
    {
        var r = 5.ToIdentity().Bind(x =>
                6.ToIdentity().Bind(y =>
                    (x + y).ToIdentity()));

        Console.WriteLine(r.Value);
        Console.ReadLine();
    }
}
Результат:
Данный код можно еще более опростить. Так как мы помним, что для того чтобы использовать, например, sql подобный LINQ синтаксис, нужно реализовать метод SelectMany. Это будет работать благодаря утиной типизации. По сути, мы заюзаем так званный грязный хак. (В реальных программах рекомендую все-таки удержаться от такого подхода, так как он не способствует нормальному пониманию кода). Об утиной типизации и о том, как она работает, вы можете прочитать в блоге Сергея Теплякова в статье "Duck typing или “так ли прост старина foreach?”. А мы же просто выбросим метод Bind и заменим его методом SelectMany. Теперь наш код стал еще проще.
class Program
{
    static void Main(string[] args)
    {
        var r = from x in 5.ToIdentity()
                from y in 6.ToIdentity()
                select x + y;
        Console.WriteLine(r.Value);
        Console.ReadLine();
    }
}

public static class IdentityExtensions
{

    public static Identity<T> ToIdentity<T>(this T value)
    {
        return new Identity<T>(value);
    }

    public static Identity<V> SelectMany<T, U, V>(this Identity<T> id, Func<T, Identity<U>> k, Func<T, U, V> s)
    {
        return s(id.Value, k(id.Value).Value).ToIdentity();
    }
}
Согласитесь, что по читабельности эти две выборки очень отличаются.
Монада Maybe
Следующей стандартной монадой, которая пришла из языка Haskell в язык C#, является монада Maybe. Данная монада связывает каскад вычислений, каждое из которых может закончиться ничем, и последующие вычисления не потребуются.
public class Maybe<T>
{
    public readonly static Maybe<T> Nothing = new Maybe<T>();
    public T Value { get; private set; }
    public bool HasValue { get; private set; }
    Maybe()
    {
        HasValue = false;
    }
    public Maybe(T value)
    {
        Value = value;
        HasValue = true;
    }
}
Но такой тип записи без расширения с помощью extension методов стоит немногого, так как такой тип записи неудобно использовать совместно с LINQ. Поэтому нам необходима небольшая допилка этого всего.
public static class MaybeExtensions
{
    public static Maybe<T> ToMaybe<T>(this T value)
    {
        return new Maybe<T>(value);
    }

    public static Maybe<U> SelectMany<T, U>(this Maybe<T> m, Func<T, Maybe<U>> k)
    {
        if (!m.HasValue)
            return Maybe<U>.Nothing;
        return k(m.Value);
    }

    public static Maybe<C> SelectMany<A, B, C>(this Maybe<A> a, Func<A, Maybe<B>> func, Func<A, B, C> select)
    {
        return a.SelectMany(aval =>
                func(aval).SelectMany(bval =>
                select(aval, bval).ToMaybe()));
    }
}
Пример использования:
var result = from a in "Hello World!".ToMaybe()
                from b in Maybe<string>.Nothing
                from c in (new DateTime(2010, 1, 14)).ToMaybe()
                select a + " " + c.ToShortDateString();

Console.WriteLine(result.HasValue);
Результат:
Если мы уберем Maybe<string>.Nothing, то получим нормальный результат, если перепишем наш код следующим образом:
var result = from a in "Hello World!".ToMaybe()
                from c in (new DateTime(2010, 1, 14)).ToMaybe()
                select a + " " + c.ToShortDateString();

Console.WriteLine(result.Value);
Результат:
Монада List
В этой монаде значения представляют собой списки, которые можно использовать как несколько вариантов одного вычисления. Реализуется эта монада в языке C# очень просто.
public static class ListExtension
{
    public static IEnumerable<T> ToList<T>(this T value)
    {
        yield return value;
    }
}
Пример использования:
var r = from x in 5.ToList()
        from y in 6.ToList()
        select x + y;

foreach (var i in r)
    Console.WriteLine(i);
Результат:

Монада State
Это монада вычислений с изменяемым состоянием. В ней каждое вычисление возвращает какое-то значение, а также изменяет значение переменной состояния. Это, наверное, самая сложная и мозгодробительная монада, которая есть в мире C#. Для того чтобы понять, как работает эта монада, рекомендую посмотреть статью "State Monad in C# and F#".
Монада Continuation
Это монада продолжения, которая позволяет писать CPS-код (continuation-passing style) более удобочитаемым. Что такое CPS,вы можете почитать в блоге Эрика Липперта ("Возвращаясь к стилю передачи продолжения. Часть 1") или в статье "Continuation-Passing Style".
Пример:
class Program
{
    static void Main(string[] args)
    {
        Identity("foo", s => Console.WriteLine(s));

        Console.ReadLine();
    }

    static void Identity<T>(T value, Action<T> k)
    {
        k(value);
    }
}
Результат:
Мы все время говорили о примерах монад с использованием классов и т.д. А сейчас немного поговорим о готовых монадах, которые вы можете использовать в своих программах. Если мы введем в Manage NuGet Packages слово для поиска "Monads", то сможем найти несколько популярных библиотек с использованием монад, реализованных в виде extension методов.
Самой популярной на данный момент является библиотека "Monads.NET", за ней идет библиотека, которая на рисунке выше находится на первом месте, – это библиотека "Monads for .NET" автора Сергея Звездина – обладателя статуса MVP. Персональный блог Сергея можно найти здесь. К сожалению, к библиотеке Сергея Звездина у меня есть претензии, так как название библиотеки не соответствует тому, что реализовано внутри. Сергей реализовывает обычные extension методы, которые он называет монадами. Повторюсь еще раз и выделю это более ярко.
Монада должна удовлетворять, как минимум, три условия. Я специально выделил функции с данной библиотеки, где обычные extension методы выдаются за монады.
К первой библиотеке "Monads.NET" у меня таких претензий нет. Хотя некоторые методы в данной библиотеке также не являются монадами. Ниже приведен список функций с библиотеки "Monads.NET".
Пример использования монад с данной библиотеки, которая написана с помощью extension методов, приведен ниже.
class Program
{
    static void Main(string[] args)
    {
        string str =
            @"<?xml version=""1.0""?>
            <!-- comment at the root level -->
            <Root>
                <Child>
                    <Child1>Content</Child1>
                </Child>
            </Root>";
        XDocument doc = XDocument.Parse(str);

        var result = doc.Root
            .With(x => x.Element("Child"))
            .With(x => x.Element("Child1"))
            .With(x => x.Value);

        Console.WriteLine(result);
        Console.ReadLine();
    }
}
Результат:

Заключение
Надеюсь, что данная статья дала вам небольшой толчек к изучению монад, и что вы поняли, что монады не так страшны, как кажется на первый взгляд. Мы также увидели, что некоторые монады уже реализованы в языке C#, например, методы SelectMany, по сути, – реализация монадного метода Bind. Постараюсь в следующей статье полнее раскрыть понятие о монадах на примере библиотеки "Monads.NET", которую мы затронули лишь немного. 

No comments:

Post a Comment