Комбинированные монады в F #

Я пытаюсь склонить голову к монадам в F #, и я ищу пример их компоновки.

В haskell похоже, что вы использовали Monad Transformers, но в F # кажется, что вы создадите собственный построитель выражений вычислений.

Я могу справиться с этим, но есть ли примеры некоторых комбинаций стандартных монадов и как их использовать?

Мне особенно интересно объединять Reader, Writer и Либо, чтобы создавать функции, которые принимают среду, настраивают ее, а затем используя Writer возвращают изменения в среду, которая имела место. Либо будет использоваться для дифференциации успехов и сбоев.

В настоящее время было бы здорово получить пример выражения вычисления EitherWriter, которое выдает значение + log или ошибку.

Ответ 1

Я собираюсь показать, как вы можете создать EitherWriter, есть два способа выстроить один из них в зависимости от того, как вы заказываете Either и Writer, но я собираюсь показать пример, который, похоже, больше всего напоминает ваш желаемый рабочий процесс.

Я также собираюсь упростить запись таким образом, чтобы она записывалась только в string list. Более полная реализация сценария использовала бы mempty и mappend для абстрактного над соответствующими типами.

Определение типа:

type EitherWriter<'a,'b>  = EWriter of string list * Choice<'a,'b>

Основные функции:

let runEitherWriter = function
    |EWriter (st, v) -> st, v

let return' x = EWriter ([], Choice1Of2 x)

let bind x f =
    let (st, v) = runEitherWriter x
    match v with
    |Choice1Of2 a -> 
        match runEitherWriter (f a) with
        |st', Choice1Of2 a -> EWriter(st @ st', Choice1Of2 a)
        |st', Choice2Of2 b -> EWriter(st @ st', Choice2Of2 b)
    |Choice2Of2 b -> EWriter(st, Choice2Of2 b)

Мне нравится определять их в автономном модуле, а затем я могу использовать их напрямую или ссылаться на них для создания выражения вычисления. Опять же, я собираюсь сохранить его простым и просто выполнить самую основную полезную реализацию:

type EitherWriterBuilder() =
    member this.Return x = return' x
    member this.ReturnFrom x = x
    member this.Bind(x,f) = bind x f
    member this.Zero() = return' ()

let eitherWriter = EitherWriterBuilder()

Является ли это практическим?

F # для удовольствия и прибыли имеет отличную информацию о железнодорожном ориентированном программировании и о преимуществах, которые он приносит по сравнению с конкурирующими методами.

Эти примеры основаны на пользовательском Result<'TSuccess,'TFailure>, но, конечно же, они могут быть одинаково применены с использованием встроенного типа Choice<'a,'b> F #.

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

Вот пример функции success/fail:

let divide5By = function
    |0.0 -> Choice2Of2 "Divide by zero"
    |x -> Choice1Of2 (5.0/x)

Эта функция просто делит 5 на поставляемый номер. Если это число отличное от нуля, оно возвращает успех, содержащий результат, если заданный номер равен нулю, он возвращает ошибку, сообщающую нам, что мы пытались делить на ноль.

Теперь нам нужна вспомогательная функция для преобразования таких функций в нечто, что можно использовать в нашем EitherWriter. Функция, которая может это сделать:

let eitherConv logSuccessF logFailF f = 
    fun v ->
        match f v with
        |Choice1Of2 a -> EWriter(["Success: " + logSuccessF a], Choice1Of2 a)
        |Choice2Of2 b -> EWriter(["ERROR: " + logFailF b], Choice2Of2 b)

Требуется функция, описывающая, как регистрировать успехи, функцию, описывающую, как регистрировать ошибки и функцию привязки для монады Either, и возвращает функцию привязки для монады EitherWriter.

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

let ew = eitherWriter {
    let! x = eitherConv (sprintf "%f") (sprintf "%s") divide5By 6.0
    let! y = eitherConv (sprintf "%f") (sprintf "%s") divide5By 3.0
    let! z = eitherConv (sprintf "%f") (sprintf "%s") divide5By 0.0
    return (x, y, z)
}

let (log, _) = runEitherWriter ew

printfn "%A" log

Затем он возвращает:

[ "Успех: 0.833333"; "Успех: 1.666667"; "ОШИБКА: Деление на ноль" ]

Ответ 2

Написание "комбинированного" строителя - это то, как вы это сделаете в F #, если вы это сделаете. Однако это не типичный подход и, конечно, не практический.

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

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

Ответ 3

Я знаю, что это вообще не считается идиоматическим в F #, но для любопытного читателя здесь @TheInnerLight ответ, используя F # +:

#r @"FSharpPlus.1.0.0-CI00089\lib\net40\FSharpPlus.dll"

open FSharpPlus

let divide5By = function
    |0.0 -> Choice2Of2 "Divide by zero"
    |x   -> Choice1Of2 (5.0/x)

let eitherConv logSuccessF logFailF f v = 
    ErrorT (
        match f v with
        | Choice1Of2 a -> Writer(Choice1Of2 a, ["Success: " + logSuccessF a])
        | Choice2Of2 b -> Writer(Choice2Of2 b, ["ERROR: "   + logFailF b]  ))

let ew = monad {
    let! x = eitherConv (sprintf "%f") (sprintf "%s") divide5By 6.0
    let! y = eitherConv (sprintf "%f") (sprintf "%s") divide5By 3.0
    let! z = eitherConv (sprintf "%f") (sprintf "%s") divide5By 0.0
    return (x, y, z)
}

let (_, log) = ew |> ErrorT.run |> Writer.run

И, конечно, это работает с любым моноидом.

Этот подход в основном относится к подходу Haskell, трансформаторы работают с любой монадой, и в приведенном выше коде вы можете легко переключиться на OptionT, заменить Choice1Of2 на Some и Choice2Of2 на None, и это будет просто работать.

Лично я предпочитаю использовать этот подход во-первых, его гораздо проще писать и, конечно, путь короче. Как только у меня появится желаемая функциональность, я могу настроить свой трансформатор или оставить его, если он достаточно хорош для того, что я пытаюсь решить.