Как монада Maybe действует как короткое замыкание? - PullRequest
7 голосов
/ 26 ноября 2011

Я пытаюсь получить более глубокое понимание монад.Поэтому я начал немного копаться в Может быть, Монаду.

Есть одна вещь, которую я, кажется, просто не понимаю.Прочитайте это:

"Таким образом, возможное связывание приводит к короткому замыканию. Если в какой-либо из цепочек операций какая-либо из них вернет Nothing, оценка прекратится и ничего не будет возвращено из всей цепочки."

From: http://mikehadlow.blogspot.com/2011/01/monads-in-c-5-maybe.html

И это:

" Для типа Maybe<T> привязка осуществляется согласно простому правилу:если цепочка возвращает пустое значение в какой-то момент, дальнейшие шаги в цепочке игнорируются, и вместо этого возвращается пустое значение "

From:" Функциональное программирование в C # "http://www.amazon.com/Functional-Programming-Techniques-Projects-Programmer/dp/0470744588/

Хорошо, давайте посмотрим на код.Вот моя Может быть Монада:

public class Maybe<T>
{
    public static readonly Maybe<T> Empty = new Maybe<T>(); 

    public Maybe(T value)
    {
        Value = value;
    }

    private Maybe()
    {
    }

    public bool HasValue()
    {
        return !EqualityComparer<T>.Default.Equals(Value, default(T));
    }

    public T Value { get; private set; }

    public Maybe<R> Bind<R>(Func<T, Maybe<R>> apply)
    {
        return HasValue() ? apply(Value) : Maybe<R>.Empty;
    }
}

public static class MaybeExtensions
{
    public static Maybe<T> ToMaybe<T>(this T value)
    {
        return new Maybe<T>(value);
    }
}

А вот мой пример кода с использованием монады:

class Program
{
    static void Main(string[] args)
    {
        var node = new Node("1", new Node("2", new Node("3", new Node("4", null))));

        var childNode = node.ChildNode
            .ToMaybe()
            .Bind(x => x.ChildNode.ToMaybe())
            .Bind(x => x.ChildNode.ToMaybe())
            .Bind(x => x.ChildNode.ToMaybe())
            .Bind(x => x.ChildNode.ToMaybe())
            .Bind(x => x.ChildNode.ToMaybe());

        Console.WriteLine(childNode.HasValue() ? childNode.Value.Value : "");

        Console.ReadLine();
    }
}

public class Node
{
    public Node(string value, Node childNode)
    {
        Value = value;
        ChildNode = childNode;
    }

    public string Value { get; set; }
    public Node ChildNode { get; private set; }
}

Понятно, что мы пытаемся копнуть глубже в дерево узлов, чемвозможный.Тем не менее, я не вижу, как он действует в соответствии с цитатами, которые я упомянул.Я имею в виду, конечно, я учел нулевые проверки, и пример работает.Тем не менее, это не разорвать цепь рано.Если вы установите точки останова, вы увидите, что каждая операция Bind() будет использоваться без значения для последних операций.Но это значит, что если я выкопаю 20 уровней в глубину, а на самом деле это всего лишь 3 уровня, я все равно проверим 20 уровней или я ошибаюсь?

Сравните это с немонадным подходом:

        if (node.ChildNode != null
            && node.ChildNode.ChildNode != null
            && node.ChildNode.ChildNode.ChildNode != null)
        {
            Console.WriteLine(node.ChildNode.ChildNode.ChildNode.Value);
        }

Разве это не то, что следует называть коротким замыканием?Потому что в этом случае if действительно ломается на уровне, где первое значение равно нулю.

Кто-нибудь может мне помочь, чтобы получить это ясно?

UPDATE

Как указал Патрик, да, это правда, что каждая привязка будет задействована, даже если у нас будет только 3 уровня и мы попытаемся пройти 20 уровней.Однако фактическое выражение, предоставленное для вызова Bind (), не будет оцениваться.Мы можем отредактировать пример, чтобы прояснить эффект:

        var childNode = node.ChildNode
            .ToMaybe()
            .Bind(x =>
                      {
                          Console.WriteLine("We will see this");
                          return x.ChildNode.ToMaybe();
                      })
            .Bind(x => x.ChildNode.ToMaybe())
            .Bind(x => x.ChildNode.ToMaybe())
            .Bind(x => x.ChildNode.ToMaybe())
            .Bind(x =>
                      {
                          Console.WriteLine("We won't see this");
                          return x.ChildNode.ToMaybe();
                      });

Ответы [ 3 ]

11 голосов
/ 26 ноября 2011

У меня есть реализация возможно монады в c #, которая немного отличается от вашей, во-первых, она не связана с нулевыми проверками, я считаю, что моя реализация более похожа на то, что происходит в стандартной реализации, например, в Haskel.

Моя реализация:

public abstract class Maybe<T>
{
    public static readonly Maybe<T> Nothing = new NothingMaybe();

    public static Maybe<T> Just(T value)
    {
        return new JustMaybe(value);
    }

    public abstract Maybe<T2> Bind<T2>(Func<T, Maybe<T2>> binder);

    private class JustMaybe
        : Maybe<T>
    {
        readonly T value;

        public JustMaybe(T value)
        {
            this.value = value;
        }

        public override Maybe<T2> Bind<T2>(Func<T, Maybe<T2>> binder)
        {
            return binder(this.value);
        }
    }

    private class NothingMaybe
        : Maybe<T>
    {
        public override Maybe<T2> Bind<T2>(Func<T, Maybe<T2>> binder)
        {
            return Maybe<T2>.Nothing;
        }
    }
}

Как вы видите здесь, функция связывания NothingMaybe просто возвращает новое ничто, поэтому передаваемое в выражение связывания никогда не оценивается. Это короткое замыкание в том смысле, что больше выражений связывания не будет оцениваться, как только вы попадете в состояние «ничего», однако сама функция Bind будет вызываться для каждой монады в цепочке.

Эта реализация может быть использована для любого типа "неопределенной операции", например, проверки на ноль или проверки на пустую строку, таким образом все эти различные типы операций могут быть объединены в цепочку:

public static class Maybe
{
    public static Maybe<T> NotNull<T>(T value) where T : class
    {
        return value != null ? Maybe<T>.Just(value) : Maybe<T>.Nothing;
    }

    public static Maybe<string> NotEmpty(string value)
    {
        return value.Length != 0 ? Maybe<string>.Just(value) : Maybe<string>.Nothing;
    }


}

string foo = "whatever";
Maybe.NotNull(foo).Bind(x => Maybe.NotEmpty(x)).Bind(x => { Console.WriteLine(x); return Maybe<string>.Just(x); });

Это выведет «что угодно» на консоль, однако, если значение будет нулевым или пустым, оно ничего не будет делать.

4 голосов
/ 26 ноября 2011

Насколько я понимаю, будут вызваны все методы Bind, но предоставленные выражения будут оцениваться только в том случае, если предыдущее возвращает значение. Это означает, что Bind методы, вызываемые после того, который возвращает null (или, точнее, default(T)), будут очень дешевыми.

0 голосов
/ 28 июня 2017

Мы можем сделать это более хитро.

Интерфейс записи, полученный из IEnumerable

public interface IOptional<T>: IEnumerable<T> {}

Это сохранит совместимость с методами LINQ

public class Maybe<T>: IOptional<T>
{
    private readonly IEnumerable<T> _element;
    public Maybe(T element)
        : this(new T[1] { element })
    {}
    public Maybe()
        : this(new T[0])
    {}
    private Maybe(T[] element)
    {
        _element = element;
    }
    public IEnumerator<T> GetEnumerator()
    {
        return _element.GetEnumerator();
    }
    IEnumerator IEnumerable.GetEnumerator()
    {
        return GetEnumerator();
    }
}

После этого мы можем использовать полную мощность LINQ и делать это

var node = new Node("1", new Node("2", new Node("3", new Node("4", null))));

var childNode =
    new Some<Node>(node.ChildNode)
        .SelectMany(n => new Maybe<Node>(n.ChildNode))
        .SelectMany(n => new Maybe<Node>(n.ChildNode))
        .SelectMany(n => new Maybe<Node>(n.ChildNode))
        .SelectMany(n => new Maybe<Node>(n.ChildNode))
        .SelectMany(n => new Maybe<Node>(n.ChildNode));

Console.WriteLine(childNode.Any() ? childNode.First().Value : "");
...