Исключение или Либо монада в С#

Я пытаюсь grok получить предварительное понимание монадов.

У меня есть вызов уровня данных, результат которого я хотел бы возвращать монадически либо в качестве результата, например, ни для числа обновленных строк, ни для набора данных, ни для исключения. Я полагаю, что мне нужно использовать монадию Исключения, которую я мог бы увидеть как особый случай Либо монады

Я просмотрел различные образцы - тонны образцов Maybe, и я не совсем уверен, как или если обобщить это, чтобы стать Либо монадой, - но я не могу найти ни одного, что не было в haskell, и, к сожалению, я, конечно же, не забегаю haskell!

Мне было интересно, может ли кто-нибудь указать мне на любые образцы.

Ответ 1

Изучая немного о монадах в С#, для упражнений я реализовал монаду Exceptional для себя. С помощью этой монады вы можете связать операции, которые могут вызвать Exception, как в этих двух примерах:

var exc1 = from x in 0.ToExceptional()
           from y in Exceptional.Execute(() => 6 / x)
           from z in 7.ToExceptional()
           select x + y + z;
Console.WriteLine("Exceptional Result 1: " + exc1);

var exc2 = Exceptional.From(0)
           .ThenExecute(x => x + 6 / x)
           .ThenExecute(y => y + 7);
Console.WriteLine("Exceptional Result 2: " + exc2);

Оба выражения дают одинаковый результат, просто синтаксис отличается. Результатом будет Exceptional<T> с установленным как свойство DivideByZeroException. В первом примере показано "ядро" монады с использованием LINQ, второе содержит другой и, возможно, более читаемый синтаксис, который иллюстрирует цепочку методов более понятным образом.

Итак, как это реализовано? Здесь тип Exceptional<T>:

public class Exceptional<T>
{
    public bool HasException { get; private set; }
    public Exception Exception { get; private set; }
    public T Value { get; private set; }

    public Exceptional(T value)
    {
        HasException = false;
        Value = value;
    }

    public Exceptional(Exception exception)
    {
        HasException = true;
        Exception = exception;
    }

    public Exceptional(Func<T> getValue)
    {
        try
        {
            Value = getValue();
            HasException = false;
        }
        catch (Exception exc)
        {
            Exception = exc;
            HasException = true;
        }
    }

    public override string ToString()
    {
        return (this.HasException ? Exception.GetType().Name : ((Value != null) ? Value.ToString() : "null"));
    }
}

Монада завершается с помощью методов расширения ToExceptional<T>() и SelectMany<T, U>(), которые соответствуют функциям Monad Unit и Bind:

public static class ExceptionalMonadExtensions
{
    public static Exceptional<T> ToExceptional<T>(this T value)
    {
        return new Exceptional<T>(value);
    }

    public static Exceptional<T> ToExceptional<T>(this Func<T> getValue)
    {
        return new Exceptional<T>(getValue);
    }

    public static Exceptional<U> SelectMany<T, U>(this Exceptional<T> value, Func<T, Exceptional<U>> k)
    {
        return (value.HasException)
            ? new Exceptional<U>(value.Exception)
            : k(value.Value);
    }

    public static Exceptional<V> SelectMany<T, U, V>(this Exceptional<T> value, Func<T, Exceptional<U>> k, Func<T, U, V> m)
    {
        return value.SelectMany(t => k(t).SelectMany(u => m(t, u).ToExceptional()));
    }
}

И некоторые небольшие вспомогательные структуры, которые не являются частью ядра монады:

public static class Exceptional
{
    public static Exceptional<T> From<T>(T value)
    {
        return value.ToExceptional();
    }

    public static Exceptional<T> Execute<T>(Func<T> getValue)
    {
        return getValue.ToExceptional();
    }
}

public static class ExceptionalExtensions
{
    public static Exceptional<U> ThenExecute<T, U>(this Exceptional<T> value, Func<T, U> getValue)
    {
        return value.SelectMany(x => Exceptional.Execute(() => getValue(x)));
    }
}

Некоторое объяснение: цепочка методов, построенная с этой монадой, выполняется до тех пор, пока один метод цепи генерирует исключение. В этом случае больше не будет выполняться метод цепочки, и первое исключенное исключение будет возвращено как часть результата Exceptional<T>. В этом случае будут установлены свойства HasException и Exception. Если нет Exception, HasException будет false, и будет установлено свойство Value, содержащее результат выполняемой цепочки методов.

Обратите внимание, что конструктор Exceptional<T>(Func<T> getValue) отвечает за обработку исключений, а метод SelectMany<T,U>() отвечает за то, чтобы отличить, вызвал ли исключение метод, ранее выполненный.

Ответ 2

Мы реализовали структуру данных Either в нашем решении С#, и мы счастливы в этом. Вот самая простая версия такой реализации:

public class Either<TL, TR>
{
    private readonly TL left;
    private readonly TR right;
    private readonly bool isLeft;

    public Either(TL left)
    {
        this.left = left;
        this.isLeft = true;
    }

    public Either(TR right)
    {
        this.right = right;
        this.isLeft = false;
    }

    public T Match<T>(Func<TL, T> leftFunc, Func<TR, T> rightFunc)
        => this.isLeft ? leftFunc(this.left) : rightFunc(this.right);

    public static implicit operator Either<TL, TR>(TL left) => new Either<TL, TR>(left);

    public static implicit operator Either<TL, TR>(TR right) => new Either<TL, TR>(right);
}

(наш код имеет больше вспомогательных методов, но они являются необязательными)

Основные моменты

  • Вы можете установить Left или Right
  • Существуют неявные операторы, облегчающие создание экземпляров
  • Существует метод Match для сопоставления шаблонов

Я также описал, как мы используем этот тип для проверки данных.

Ответ 3

Так что - не знаю, интересуется ли кто-либо - я придумал очень предварительную реализацию, следуя Майк Хэдлоу. Некоторые из них не совсем правы, но это начало. (Сказав это, я не буду использовать его - вы можете потерять миллион долларов или даже убить кого-то - просто мое предостережение!)

Очень простой пример кода, который может быть записан, - это

var exp = from a in 12.Div(2)
          from b in a.Div(2)
          select a + b;

Assert.AreEqual(9, exp.Value());

var exp = from a in 12.Div(0)
          from b in a.Div(2)
          select b;

Assert.IsTrue(exp.IsException());

с использованием метода Div, реализованного следующим образом:

public static IExceptional<int> Div(this int numerator, int denominator)
{
    return denominator == 0
           ? new DivideByZeroException().ToExceptional<int, DivideByZeroException>()
           : (numerator / denominator).ToExceptional();
}

или

public static IExceptional<int> Div_Throw(this int numerator, int denominator)
{
    try
    {
        return (numerator / denominator).ToExceptional();
    }
    catch (DivideByZeroException e)
    {
        return e.ToExceptional<int, DivideByZeroException>();
    }            
 }

(Я сразу вижу потенциальное улучшение api, но я не уверен, как его достичь. Я думаю, что это

new DivideByZeroException().ToExceptional<int, DivideByZeroException>()

было бы лучше, если бы оно было

new DivideByZeroException().ToExceptional<int>()

Вы увидите мою реализацию позже и, надеюсь, кто-то сможет перепроектировать ее для вышеуказанного.)

Монадический бит выполняется здесь (главным образом)

public static class Exceptional
{
    public static IExceptional<TValue> ToExceptional<TValue>(this TValue result)
    {
        return new Value<TValue>(result);
    }

    public static IExceptional<TValue> ToExceptional<TValue,TException>(this TException exception) where TException : System.Exception
    {
        return new Exception<TValue, TException>(exception);
    }


    public static IExceptional<TResultOut> Bind<TResultIn, TResultOut>(this IExceptional<TResultIn> first, Func<TResultIn, IExceptional<TResultOut>> func)
    {                
        return first.IsException() ? 
                ((IInternalException)first).Copy<TResultOut>() : 
                func(first.Value());
    }


    public static IExceptional<TResultOut> SelectMany<TResultIn, TResultBind, TResultOut>(this IExceptional<TResultIn> first, Func<TResultIn, IExceptional<TResultBind>> func, Func<TResultIn, TResultBind, TResultOut> select)
    {
        return first.Bind(aval => func(aval)
                    .Bind(bval => select(aval, bval)
                    .ToExceptional()));
    }   
}

Основной интерфейс указан как

public interface IExceptional<TValue>
{
    bool IsException();    
    TValue Value();
}

и у меня есть внутренний интерфейс, который я использую для получения исключения, которое было выбрано (более поздним)

internal interface IInternalException 
{
    IExceptional<TValue> Copy<TValue>();     
}

Конкретные реализации заключаются в следующем:

public class Value<TValue> : IExceptional<TValue>
{
    TValue _value = default(TValue);

    public Value(TValue value)
    {
        _value = value;
    }

    bool IExceptional<TValue>.IsException()
    {
        return false;
    }

    TValue IExceptional<TValue>.Value()
    {
        return _value;
    }
}

public class Exception<TValue, TException> : IInternalException, IExceptional<TValue> where TException : System.Exception
{
    TException _exception = default(TException);

    public Exception(TException exception)
    {
        _exception = exception;
    }

    bool IExceptional<TValue>.IsException()
    {
        return true;
    }

    IExceptional<TOutValue> IInternalException.Copy<TOutValue>()
    {
        return _exception.ToExceptional<TOutValue,TException>();
    }

    TException GetException()
    {
        return _exception;
    }

    TValue IExceptional<TValue>.Value()
    {
        return default(TValue);
    }
}

Просто слово объяснения... для меня самым сложным моментом была операция Bind, когда возникло исключение. Если вы имеете дело с конвейером операций и исключение получает на раннем этапе процесс, вам нужно увековечить это исключение по конвейеру, чтобы при завершении выражения возвращаемый IExceptional содержит исключение, которое произошло ранее. Это является причиной исключения IInternalException. Это позволяет мне создать новый IExceptional того же или (потенциально другого) типа (например, IExceptional → IExceptional), но копирует все исключения из нового IExceptional без необходимости знать тип внутреннего исключения.

Несомненно, есть множество улучшений. например, я мог видеть, что вы, возможно, захотите отслеживать стек ошибок в IExceptional. Вероятно, существует избыточный код или более эффективные способы достижения целей... но... он должен был немного учиться для меня.

Любые мысли/предложения будут с благодарностью получены.

Ответ 4

Стоит отметить, что теперь доступны библиотеки С#, которые содержат реализации Either:

<.pdf language-ext доступна для .Net 4.5.1 и .Net Standard 1.3

Библиотека LaYumba доступна для .Net Standard 1.6 и .Net Core 1.1.

Обе библиотеки хорошо документированы, а LaYumba используется в качестве основы книги Manning Функциональное программирование на С#.

Ответ 5

С# не поддерживает много модов (а поддержка, которая существует в виде LINQ, на самом деле не предназначена для общих монадов), нет встроенных исключений или любых монад. Вы должны throw исключение, а затем catch it.