Монада 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()); 

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

Все вместе можно увидеть здесь.

ссылка на оригинал статьи http://habrahabr.ru/post/269717/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *