Железнодорожное программирование с асинхронными операциями

Ранее задавал аналогичный вопрос, но почему-то я не нахожу выхода, пытаясь снова с другим примером.

Код в качестве отправной точки (немного урезанный) доступен по адресу https://ideone.com/zkQcIU.

(есть проблемы с распознаванием типа Microsoft.FSharp.Core.Result, не знаю почему)

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

Требование состоит в том, чтобы дать вызывающему либо результат, либо ошибку. Все функции возвращают кортеж, заполненный либо Успехом type Article, либо Отказом с объектом type Error, имеющим описательные code и message, возвращенные с сервера.

Буду признателен за рабочий пример моего кода как для вызываемого, так и для вызывающего абонента в ответе.

Код вызываемого абонента

type Article = {
    name: string
}

type Error = {
    code: string
    message: string
}

let create (article: Article) : Result<Article, Error> =  
    let request = WebRequest.Create("http://example.com") :?> HttpWebRequest
    request.Method <- "GET"
    try
        use response = request.GetResponse() :?> HttpWebResponse
        use reader = new StreamReader(response.GetResponseStream())
        use memoryStream = new MemoryStream(Encoding.UTF8.GetBytes(reader.ReadToEnd())) 
        Ok ((new DataContractJsonSerializer(typeof<Article>)).ReadObject(memoryStream) :?> Article)
    with
        | :? WebException as e ->  
        use reader = new StreamReader(e.Response.GetResponseStream())
        use memoryStream = new MemoryStream(Encoding.UTF8.GetBytes(reader.ReadToEnd())) 
        Error ((new DataContractJsonSerializer(typeof<Error>)).ReadObject(memoryStream) :?> Error)

Остальные связанные методы – одинаковая сигнатура и похожие тела. Фактически вы можете повторно использовать тело create для update, upload и publish, чтобы иметь возможность тестировать и компилировать код.

let update (article: Article) : Result<Article, Error>
    // body (same as create, method <- PUT)

let upload (article: Article) : Result<Article, Error>
    // body (same as create, method <- PUT)

let publish (article: Article) : Result<Article, Error>
    // body (same as create, method < POST)

Код вызывающего абонента

let chain = create >> Result.bind update >> Result.bind upload >> Result.bind publish
match chain(schemaObject) with 
    | Ok article -> Debug.WriteLine(article.name)
    | Error error -> Debug.WriteLine(error.code + ":" + error.message)

Изменить

На основе ответа и сопоставления его с реализацией Скотта (https://i.stack.imgur.com/bIxpD.png), чтобы облегчить сравнение и лучшее понимание.

let bind2 (switchFunction : 'a -> Async<Result<'b, 'c>>) = 
    fun (asyncTwoTrackInput : Async<Result<'a, 'c>>) -> async {
        let! twoTrackInput = asyncTwoTrackInput
        match twoTrackInput with
        | Ok s -> return! switchFunction s
        | Error err -> return Error err
    }  

Редактировать 2 На основе реализации привязки F#

let bind3 (binder : 'a -> Async<Result<'b, 'c>>) (asyncResult : Async<Result<'a, 'c>>) = async {
    let! result = asyncResult
    match result with
    | Error e -> return Error e
    | Ok x -> return! binder x
}

person Developer11    schedule 21.03.2018    source источник


Ответы (2)


Взгляните на исходный код Suave и, в частности, функция WebPart.bind. В Suave WebPart — это функция, которая принимает контекст («контекст» — это текущий запрос и ответ на данный момент) и возвращает результат типа Async<context option>. Семантика их объединения в цепочку заключается в том, что если async возвращает None, следующий шаг пропускается; если он возвращает Some value, следующий шаг вызывается с value в качестве входных данных. Это в значительной степени та же семантика, что и тип Result, так что вы можете почти скопировать код Suave и настроить его для Result вместо Option. Например, что-то вроде этого:

module AsyncResult

let bind (f : 'a -> Async<Result<'b, 'c>>) (a : Async<Result<'a, 'c>>)  : Async<Result<'b, 'c>> = async {
    let! r = a
    match r with
    | Ok value ->
        let next : Async<Result<'b, 'c>> = f value
        return! next
    | Error err -> return (Error err)
}

let compose (f : 'a -> Async<Result<'b, 'e>>) (g : 'b -> Async<Result<'c, 'e>>) : 'a -> Async<Result<'c, 'e>> =
    fun x -> bind g (f x)

let (>>=) a f = bind f a
let (>=>) f g = compose f g

Теперь вы можете написать свою цепочку следующим образом:

let chain = create >=> update >=> upload >=> publish
let result = chain(schemaObject) |> Async.RunSynchronously
match result with 
| Ok article -> Debug.WriteLine(article.name)
| Error error -> Debug.WriteLine(error.code + ":" + error.message)

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

Обновление: в комментарии вы спросили, нужно ли вам определить операторы >>= и >=>, и упомянули, что не видели их использования в коде chain. Я определил оба, потому что они служат разным целям, точно так же, как операторы |> и >> служат разным целям. >>= похож на |>: он передает значение в функцию. В то время как >=> похож на >>: он берет две функции и объединяет их. Если бы вы написали следующее в контексте, отличном от AsyncResult:

let chain = step1 >> step2 >> step3

Тогда это переводится как:

let asyncResultChain = step1AR >=> step2AR >=> step3AR

Где я использую суффикс «AR» для обозначения версий тех функций, которые возвращают тип Async<Result<whatever>>. С другой стороны, если бы вы написали это в стиле передачи данных через конвейер:

let result = input |> step1 |> step2 |> step3

Тогда это будет означать:

let asyncResult = input >>= step1AR >>= step2AR >>= step3AR

Вот почему вам нужны функции bind и compose, а также соответствующие им операторы: чтобы у вас был эквивалент операторов |> или >> для ваших значений AsyncResult.

Кстати, имена операторов, которые я выбрал (>>= и >=>), я выбрал не случайно. Это стандартные операторы, которые повсеместно используются для операций «привязки» и «составления» над такими значениями, как Async, Result или AsyncResult. Поэтому, если вы определяете свои собственные, придерживайтесь «стандартных» имен операторов, и другие люди, читающие ваш код, не будут сбиты с толку.

Обновление 2. Вот как читать подписи этих типов:

'a -> Async<Result<'b, 'c>>

Это функция, которая принимает тип A и возвращает Async, обернутый вокруг Result. Result имеет тип B в качестве случая успеха и тип C в качестве случая отказа.

Async<Result<'a, 'c>>

Это значение, а не функция. Это Async, обернутый вокруг Result, где тип A — это случай успеха, а тип C — случай отказа.

Итак, функция bind принимает два параметра:

  • функция от A до асинхронного (либо B, либо C)).
  • значение, которое является асинхронным (либо A, либо C)).

И он возвращает:

  • значение, которое является асинхронным (либо B, либо C).

Глядя на эти сигнатуры типов, вы уже начинаете понимать, что будет делать функция bind. Он возьмет это значение, равное A или C, и "развернет" его. Если это C, то будет получено значение "либо B, либо C", равное C (и функцию вызывать не нужно). Если это A, то для преобразования его в значение «B или C» он вызовет функцию f (которая принимает A).

Все это происходит в асинхронном контексте, что добавляет дополнительный уровень сложности типам. Возможно, вам будет легче понять все это, если вы посмотрите на базовая версия Result.bind без использования асинхронности:

let bind (f : 'a -> Result<'b, 'c>) (a : Result<'a, 'c>) =
    match a with
    | Ok val -> f val
    | Error err -> Error err

В этом фрагменте тип val'a, а тип err'c.

Последнее обновление: во время сеанса чата был один комментарий, который, по моему мнению, стоит сохранить в ответе (поскольку люди почти никогда не переходят по ссылкам в чате). Разработчик11 спросил,

... если бы я спросил вас, что Result.bind в моем примере кода соответствует вашему подходу, можем ли мы переписать его как create >> AsyncResult.bind update? Однако это сработало. Просто интересно, мне понравилась краткая форма и, как вы сказали, они имеют стандартное значение? (в сообществе haskell?)

Мой ответ был:

да. Если оператор >=> написан правильно, то f >=> g всегда будет эквивалентен f >> bind g. На самом деле это именно то определение функции compose, хотя это может быть не сразу очевидно для вас, потому что compose пишется как fun x -> bind g (f x), а не как f >> bind g. Но эти два способа написания функции compose были бы полностью эквивалентны. Вероятно, для вас было бы очень поучительно сесть с листом бумаги и нарисовать функциональные «фигуры» (входы и выходы) обоих способов написания сочинения.

person rmunn    schedule 21.03.2018
comment
Комментарии не для расширенного обсуждения; этот разговор был перемещен в чат< /а>. - person Bhargav Rao; 25.03.2018
comment
P.S. Для всех, кто найдет это позже, stackoverflow.com/questions/47697022/ имеет еще одну хорошую реализацию AsyncResult (в ответе Густаво) - person rmunn; 26.03.2018
comment
@rmunn есть ли проблема, если моя асинхронная забава меняет свою подпись на встроенный тип вместо пользовательского типа, теперь это Async<Result<string, Error>>, раньше это было Async<Result<Article, Error>> - person Developer11; 31.03.2018
comment
Пожалуйста, взгляните на это. stackoverflow.com/questions/49586488 / - person Developer11; 31.03.2018

Почему вы хотите использовать железнодорожно-ориентированное программирование здесь? Если вы просто хотите запустить последовательность операций и вернуть информацию о первом возникшем исключении, то F# уже предоставляет языковую поддержку для этого с использованием исключений. Для этого вам не нужно железнодорожно-ориентированное программирование. Просто определите свой Error как исключение:

exception Error of code:string * message:string

Измените код, чтобы генерировать исключение (также обратите внимание, что ваша функция create принимает article, но не использует его, поэтому я удалил это):

let create () = async {  
    let ds = new DataContractJsonSerializer(typeof<Error>)
    let request = WebRequest.Create("http://example.com") :?> HttpWebRequest
    request.Method <- "GET"
    try
        use response = request.GetResponse() :?> HttpWebResponse
        use reader = new StreamReader(response.GetResponseStream())
        use memoryStream = new MemoryStream(Encoding.UTF8.GetBytes(reader.ReadToEnd())) 
        return ds.ReadObject(memoryStream) :?> Article
    with
        | :? WebException as e ->  
        use reader = new StreamReader(e.Response.GetResponseStream())
        use memoryStream = new MemoryStream(Encoding.UTF8.GetBytes(reader.ReadToEnd())) 
        return raise (Error (ds.ReadObject(memoryStream) :?> Error)) }

И тогда вы можете составлять функции, просто упорядочивая их в блоке async с помощью let! и добавляя обработку исключений:

let main () = async {
  try
    let! created = create ()
    let! updated = update created
    let! uploaded = upload updated
    Debug.WriteLine(uploaded.name)
  with Error(code, message) ->
    Debug.WriteLine(code + ":" + message) }

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

person Tomas Petricek    schedule 21.03.2018
comment
Спасибо, Томас, я ценю ваш ответ, чтобы ответить на ваш вопрос Why do you want to use Railway Oriented Programming here, потому что я хочу иметь свободный функциональный интерфейс, программирование становится обязательным, когда я беру результат из create и передаю его в update вручную и продолжаю то же самое с остальной частью цепочки. - person Developer11; 21.03.2018
comment
теперь позвольте мне спросить вас кое о чем, нужны ли мне эти функции, чтобы они были асинхронными, или я продолжаю использовать их синхронно так, как они есть сейчас? - person Developer11; 21.03.2018
comment
@ Developer11 Нет ничего обязательного в передаче значений с помощью let. Законная причина использования ROP заключается в том, что вам нужен другой способ составления ошибок (скажем, собрать все, а не только первую), но если это не так, вы просто излишне усложняете свой код. - person Tomas Petricek; 22.03.2018
comment
Вам нужно, чтобы функции были асинхронными, если вы хотите избежать блокировки системных потоков. Это зависит от того, что вы на самом деле делаете :) - person Tomas Petricek; 22.03.2018
comment
say, collect all rather than just the first one, не совсем понял, что ты имеешь в виду? но цепочка не может собрать все ошибки, так как как только любой метод в цепочке выдает ошибку, цепочка разорвется, и вызывающая сторона получит ошибку. - person Developer11; 23.03.2018
comment
@ Developer11 Именно поэтому вы должны использовать исключения :-). ROP удобен для таких задач, как проверка ввода, когда вы можете продолжить, даже если есть ошибка — тогда вы можете собрать несколько ошибок и сообщить о них всех, а не по одной. Но в данном случае исключения кодируют нужную вам логику. - person Tomas Petricek; 23.03.2018
comment
в ROP я мог бы сделать и то, и другое, либо разорвать цепочку, либо продолжить собирать все ошибки, и я также получаю представление о вашей идее, я склоняюсь к ROP из-за объединения методов/конвейеров >>=, и если я не ошибаюсь это невозможно из-за async и let! в этом ответе, мы вынуждены сначала получить результат, а затем передать его следующей функции в другом вызове - person Developer11; 23.03.2018
comment
if you want to avoid blocking system threads скажем, я развернул это как веб-службу, и если я не использую асинхронность, по сути это означает, что если запрос пользователя A выполняется, пользователь B должен ждать завершения пользователя A? - person Developer11; 23.03.2018
comment
@ Developer11 Developer11 Это выходит за рамки того, на что мы можем разумно ответить в комментариях SO. Я действительно рекомендую прочитать более подробное введение в асинхронные рабочие процессы в одной из книг по F# — она должна ответить на эти вопросы гораздо лучше, чем комментарий. - person Tomas Petricek; 25.03.2018
comment
@Developer11 Глава 13 manning.com/books/real-world-functional-programming — это один из источников, но большинство других книг по F# посвящены асинхронным вычислениям. - person Tomas Petricek; 25.03.2018