Составить пересечение мира асинхронных функций с привязкой

У меня есть образец железнодорожного трубопровода, который хорошо работает:

open FSharpPlus

let funA n =
    if n < 10 then Ok n
    else Error "not less than 10"

let funB n =
    if n < 5 then Ok (n, n * 2)
    else Error "not less than 5"

let funC n = // int -> Result<(int * int), string>
    n
    |> funA
    >>= funB // it works

Но когда я хочу повернуть funB в асинхронную функцию я получил ошибку компиляции. По логике это не должно быть иначе. Тот же вывод / ввод... Что не так?

Что нужно сделать, чтобы это работало?!

open FSharpPlus

let funA n =
    if n < 10 then Ok n
    else Error "not less than 10"

let funB n = async {
    if n < 5 then return Ok (n, n * 2)
    else return Error "not less than 5" }

let funC n = // int -> Async<Result<(int * int), string>>
    n
    |> funA
    >>= funB // compile error

1 ответ

Решение

Тот же вывод / ввод... Что не так?

Нет, они не имеют одинакового вывода / ввода.

Если вы посмотрите на тип (>>=) это что-то вроде 'Monad<'T> -> ('T -> 'Monad<'U>) -> 'Monad<'U> которая является фиктивной сигнатурой универсальной операции связывания, перегруженной для монад в целом. В вашем первом примере Монада Result<_,'TError>поэтому ваш первый пример может быть переписан как:

let funC n = // int -> Result<(int * int), string>
    n
    |> funA
    |> Result.bind funB

Подпись Result.bind является ('T -> Result<'U,'TError>) -> Result<'T,'TError> -> Result<'U,'TError>, Это имеет смысл, если вы думаете об этом. Это как применить замену Monad<_> с участием Result<_,'TError> и у вас есть аргументы перевернуты, поэтому мы используем |>,

Тогда ваши функции оба int -> Result<_,'TError> так что типы совпадают, и это имеет смысл (и это работает).

Теперь, переходя к вашему второму фрагменту кода, функция funB имеет другую подпись, он имеет Async<Result<_,'TError>> так что теперь типы не совпадают. И это также имеет смысл, вы не можете использовать реализацию связывания Result для Async,

Итак, каково решение?

Самое простое решение - не использовать связывание, по крайней мере, для 2 монад. Вы можете "поднять" свою первую функцию Async и использовать async.Bindс родовым >>= или стандартный асинхронный рабочий процесс, но внутри него вам придется использовать руководство match привязать результаты ко второй функции.

Другой подход более интересен, но также более сложен для понимания, он состоит в использовании абстракции под названием Monad Transformers:

open FSharpPlus.Data

let funC n = // int -> Result<(int * int), string>
    n
    |> (funA >> async.Return >> ResultT)
    >>= (funB >> ResultT)
    |> ResultT.run

Итак, что мы делаем здесь, мы "поднимаем" funA функция к Asyncзатем оборачиваем ResultT который является монадным трансформатором для Result, так что у нас есть операция связывания, которая заботится о связывании и на внешней монаде, в нашем случае Async,

Тогда мы просто завернем funB в ResultT и в самом конце функции мы разворачиваем ResultT с участием Result.run,

Дополнительные примеры многослойных монад в F# см. В этих вопросах.

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

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

Другие вопросы по тегам