Монады в C # - почему реализации Bind требуют, чтобы переданная функция возвращала монаду? - PullRequest
17 голосов
/ 20 октября 2011

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

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.

Есть ли что-то важное, что я здесь упускаю?

1 Ответ

33 голосов
/ 20 октября 2011

Прежде всего, рассмотрим понятие состав .Мы можем легко выразить композицию как операцию над делегатами:

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 и возвращает 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 возвращает 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 принимает функцию от T до Monad<U> - потому чтовесь смысл в том, чтобы иметь возможность взять функцию g от T до Monad<U> и функцию f от U до 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.Но вы не можете гарантировать, что такая операция определена!Например, возьмите «Монаду Может быть» в качестве монады, которая выражается в C # как T?.Предположим, у вас есть g как (int x)=>(double?)null, и у вас есть функция f, которая равна (double y)=>(decimal)y.Как вы должны составить f и g в метод, который принимает int и возвращает ненулевой тип decimal?Не существует «развёртывания», которое разворачивает обнуляемый double в двойное значение, которое может принимать 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 и возвращает Monad<V>.

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

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

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.

...