Подтвердить что ты не робот

Monads в С# - почему реализация Bind требует переданной функции для возврата монады?

Большинство примеров монадов, которые я видел в С#, написаны несколько так:

public static Identity<B> Bind<A, B>(this Identity<A> a, Func<A, Identity<B>> func) {
    return func(a.Value);
}

Например, см. http://mikehadlow.blogspot.com/2011/01/monads-in-c-3-creating-our-first-monad.html.

Вопрос в том, в какой точке требуется func вернуть Identity<B>? Если я использую следующее определение:

public interface IValue<A> {
    public IValue<B> Bind<B>(Func<A, B> func)
}

то я действительно могу использовать те же func для Lazy<T>, Task<T>, Maybe<T> и т.д., фактически не завися от фактического внедрения типа IValue.

Есть ли что-то важное, что мне не хватает здесь?

4b9b3361

Ответ 1

Прежде всего, рассмотрим понятие композиции. Мы можем легко выразить композицию как операцию на делегатах:
public static Func<T, V> Compose<T, U, V>(this Func<U, V> f, Func<T, U> g)
{
    return x => f(g(x));
}

Итак, если у меня есть функция g, которая есть (int x) => x.ToString() и функция f, которая является (string s) => s.Length, тогда я могу сделать сложенную функцию h, которая является (int x) => x.ToString().Length, вызывая f.Compose(g).

Это должно быть ясно.

Теперь предположим, что у меня есть функция g от T до Monad<U> и функция f от U до Monad<V>. Я хочу написать метод, который объединяет эти две функции, которые возвращают монады в функцию, которая принимает T и возвращает a Monad<V>. Поэтому я пытаюсь написать, что:

public static Func<T, Monad<V>> Compose<T, U, V>(this Func<U, Monad<V>> f, Func<T, Monad<U>> g)
{
    return x => f(g(x));
}

Не работает. g возвращает a Monad<U>, но f принимает значение U. У меня есть способ "обернуть" U в Monad<U>, но у меня нет возможности "развернуть".

Однако, если у меня есть метод

public static Monad<V> Bind<U, V>(this Monad<U> m, Func<U, Monad<V>> k)
{ whatever }

то я могу написать метод, который объединяет два метода, которые возвращают монады:

public static Func<T, Monad<V>> Compose<T, U, V>(this Func<U, Monad<V>> f, Func<T, Monad<U>> g)
{
    return x => Bind(g(x), f);
}

Вот почему Bind принимает func от T до Monad<U> - потому что вся суть вещи состоит в том, чтобы иметь возможность взять функцию g от T до Monad<U> и функцию f из U to Monad<V> и скомпоновать их в функцию h от T до Monad<V>.

Если вы хотите взять функцию g от T до U и функцию f от U до Monad<V>, вам не понадобится Bind в первую очередь. Просто составьте функции, как правило, для получения метода от T до Monad<V>! Вся цель Bind - решить эту проблему; если вы отмахиваетесь от этой проблемы, тогда вам не нужно Bind в первую очередь.

UPDATE:

В большинстве случаев я хочу написать функцию g от T до Monad<U> и функцию f от U до V.

И я полагаю, что вы хотите записать это в функцию от T до V. Но вы не можете гарантировать, что такая операция определена! Например, возьмите "Maybe monad" в качестве монады, которая выражается в С# как T?. Предположим, что у вас есть g как (int x)=>(double?)null, и у вас есть функция f, которая (double y)=>(decimal)y. Как вы должны составлять f и g в методе, который принимает int и возвращает тип с нулевым типом decimal? Нет "разворачивания", которое разворачивает двойное значение с удлинением в двойное значение, которое может принимать f!

Вы можете использовать Bind для компиляции f и g в метод, который принимает int и возвращает десятичную величину с нулевым значением:

public static Func<T, Monad<V>> Compose<T, U, V>(this Func<U, V> f, Func<T, Monad<U>> g)
{
    return x => Bind(g(x), x=>Unit(f(x)));
}

где Unit - это функция, которая принимает V и возвращает a Monad<V>.

Но просто нет композиции из f и g, если g возвращает монаду, а f не возвращает монаду - нет никакой гарантии, что есть способ вернуться от экземпляра монады к "развернутому" тип. Возможно, в случае с некоторыми монадами всегда есть - как Lazy<T>. Или, может быть, иногда бывает, как с "возможно" монадой. Часто есть способ сделать это, но нет необходимости, чтобы вы могли это сделать.

Кстати, обратите внимание, как мы только что использовали "Bind" как швейцарский армейский нож, чтобы создать новый вид композиции. Привязка может сделать любую операцию! Например, предположим, что мы имеем операцию Bind в последовательности monad, которую мы называем "SelectMany" в типе IEnumerable<T> в С#:

static IEnumerable<V> SelectMany<U, V>(this IEnumerable<U> sequence, Func<U, IEnumerable<V>> f)
{
    foreach(U u in sequence)
        foreach(V v in f(u))
            yield return v;
}

У вас может также быть оператор по последовательностям:

static IEnumerable<A> Where<A>(this IEnumerable<A> sequence, Func<A, bool> predicate)
{
    foreach(A item in sequence)
        if (predicate(item)) 
            yield return item;
}

Вам действительно нужно написать этот код внутри Where? Нет! Вы можете построить его полностью из "Bind/SelectMany":

static IEnumerable<A> Where<A>(this IEnumerable<A> sequence, Func<A, bool> predicate)
{
    return sequence.SelectMany((A a)=>predicate(a) ? new A[] { a } : new A[] { } );  
}

Эффективное? Нет. Но ничего не может сделать Bind/SelectMany. Если вы действительно хотели, чтобы вы могли построить все операторы последовательности LINQ из ничего, кроме SelectMany.