Составить пересечение мира асинхронных функций с привязкой
У меня есть образец железнодорожного трубопровода, который хорошо работает:
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# см. В этих вопросах.
Существуют и другие подходы: некоторые библиотеки предоставляют некоторые "магические рабочие процессы", в которых используется специальная перегрузка для объединения монад с составными монадами (так называемые многослойные монады), поэтому вы пишете меньше кода, но рассуждать о типах не так просто, поскольку Перегрузки не следуют никакому правилу подстановки, вам придется посмотреть на исходный код, чтобы понять, что происходит.
Примечание. Подобное кодирование является хорошим упражнением, но в реальной жизни подумайте и об исключениях, чтобы не усложнять код.