Как стать автором
Поиск
Написать публикацию
Обновить

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

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

Я не буду расписывать, что такое монада, я просто покажу одну забавную реализацию монады 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());

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

Все вместе можно увидеть здесь.
Теги:
Хабы:
Всего голосов 18: ↑18 и ↓0+18
Комментарии11

Публикации

Ближайшие события