问题
Coming from nodejs where I could chain asynchronous events using Promises and then operator I'm trying to explore how things are done in idiomatic F#.
The calls I'm trying to chain are HTTP rest calls on some entity from creation to update to uploading images to publishing.
Function composition says the output of one function should match the input of the second one to be composed and that common input and output in my case will be string
, i.e. JSON serialized string as input and output of all of these functions.
I've learned that you can compose functions using >>
operator. Ideally functions should not throw errors but things happen with IO, for instance in this case what if the id of the entity I'm trying to create exists etc.
The unknown and the question is what happens if an error occurs during the chained sequence, how the caller will know what went wrong along with description message? The operation could fail in the middle or towards the end or right in the beginning of the chain sequence.
What I'm expecting from these functions upon error to stop executing the chain and return the error message to the caller. Error message is also a JSON string
so there's no incompatibility between inputs and outputs of a function so you know.
I looked at Choice too but not sure if that's the direction I should be going for.
The code is not necessarily complete and all I'm looking for a is a direction to research further to get answers and possibly improve this question. Here's some code to start with.
let create schema =
// POST request
"""{"id": 1, "title": "title 1"}""" // result output
let update schema =
// PUT request, update title
"""{"id": 1, "title": "title 2"}""" // output
let upload schema =
// PUT request, upload image and add thumbnail to json
"""{"id": 1, "title": "title 2", "thumbnail": "image.jpg"}"""
let publish schema =
// PUT request, publish the entity, add url for the entity
if response.StatusCode <> HttpStatusCode.OK then
"""{"code": "100", "message": "file size above limit"}"""
else
"""{"id": 1, "title": "title 2", "thumbnail": "image.jpg", "url": "http://example.com/1"}"""
let chain = create >> update >> upload >> publish
Edit - Attempt
Trying to parameterize the image thumbnail in the upload part
let create (schema: string) : Result<string,string> =
Ok """{"id": 1, "title": "title 1"}""" // result output
let update (schema: string) : Result<string,string> =
Ok """{"id": 1, "title": "title 2"}""" // output
let upload2 (img: string) (schema: string) : Result<string,string> =
printf "upload image %s\n" img
let statusCode = HttpStatusCode.OK
match statusCode with
| HttpStatusCode.OK -> Ok """{"id": 1, "title": "title 2", "thumbnail": "image.jpg"}"""
| x -> Error (sprintf "%A happened" x)
let publish (schema: string) =
let statusCode = HttpStatusCode.InternalServerError
match statusCode with
| HttpStatusCode.OK -> Ok """{"id": 1, "title": "title 2", "thumbnail": "image.jpg", "url": "http://example.com/1"}"""
| _ -> Error """{"code": "100", "message": "couldn't publish, file size above limit"}"""
let chain = create >> Result.bind update >> Result.bind (upload2 "image.jpg") >> Result.bind publish
回答1:
A good general approach to this problem is wrapping your functions' return values in a Choice/Either-like type and using a higher-order function to bind them together such that a failure propagates/short-circuits with some meaningful data. F# has a Result
type with a bind
function that can be used like this:
type MyResult = Result<string,string>
let f1 x : MyResult = printfn "%s" x; Ok "yep"
let f2 x : MyResult = printfn "%s" x; Ok "yep!"
let f3 x : MyResult = printfn "%s" x; Error "nope :("
let fAll = f1 >> Result.bind f2 >> Result.bind f3
> fAll "howdy";;
howdy
yep
yep!
[<Struct>]
val it : Result<string,string> = Error "nope :("
The first two functions succeed, but the third fails and so you get an Error
value back.
Also check out this article on Railway-oriented programming.
Update to be more specific to your example:
let create (schema: string) : Result<string,string> =
Ok """{"id": 1, "title": "title 1"}""" // result output
let update (schema: string) : Result<string,string> =
Ok """{"id": 1, "title": "title 2"}""" // output
let upload (schema: string) =
let statusCode = HttpStatusCode.OK
match statusCode with
| HttpStatusCode.OK -> Ok """{"id": 1, "title": "title 2", "thumbnail": "image.jpg"}"""
| x -> Error (sprintf "%A happened" x)
let publish (schema: string) =
let statusCode = HttpStatusCode.InternalServerError
match statusCode with
| HttpStatusCode.OK -> Ok """{"id": 1, "title": "title 2", "thumbnail": "image.jpg", "url": "http://example.com/1"}"""
| _ -> Error """{"code": "100", "message": "file size above limit"}"""
let chain =
create >> Result.bind update >> Result.bind upload >> Result.bind publish
来源:https://stackoverflow.com/questions/49226552/chaining-rest-calls-in-a-pipeline-while-managing-errors