F# - railway-oriented-programming, my beef with seq<Result<'a,'e>> to Result<'a seq,'e>
I recently worked with Scott Wlaschins (fsharpforfunandprofit.com) Railway-oriented programming.
I have a mapping function, which maps a eventDTO to a event. For handling the validation, i use also Scotts approach to define special types for the properties of my command. (look here)
So leave the fact, that a validation of events from the eventstore maybe makes no sence, because the stored events are defined and stored by the domain. But I went this way and walk right into a "problem".
For example I have an event like this:
// event on domain layer
type ProductCreated = {
Id:ProductId
Name:String100
Description:String2000
Price:Price
Weight:Weight
}
// event dto for storing in eventStore
type ProductCreatedEvent = {
Id:Guid
Name:string
Description:string
Price:decimal
Weight:decimal
}
So I have defined fitting contraints for ProductId, String50, String2000, Price and Weight. (You will see more in another article in later times)
Now I have a function which maps the raw event from the eventstore to the event of the domain:
let mapEventToDomain (dto:obj):Result<Event,string> =
result {
match dto with
| :? ProductCreatedEvent as cmd ->
let! id = cmd.Id |> ProductId.create
let! name = cmd.Name |> String100.create "Name"
let! description = cmd.Description |> String2000.create "Description"
let! weight = cmd.Weight |> Weight.create
let! price = cmd.Price |> Price.create
return ProductCreated {
Id=id
Name=name
Description=description
Weight=weight
Price=price
}
| ... // and so on
}
As you see, the function returns a Result<Event,string>, where the "Event" is actually a discriminated union and the "string" is the error message.
So when I load the events from the eventstore and map the events into the events of the domain layer like this:
// results in seq<Result<Event,string>>
let events = loadFromEventFromEventStore
|> Seq.map mapEventsToDomain
I get a sequence of results. So how map these to a Result<Event seq,string> (or in generic form Result<'a seq,'e>).
So i decided to fold this things like that:
let resultFolder (state:Result<'a seq,'c>) (item:Result<'a,'c>) =
match state,item with
|Ok x,Ok y -> Ok (seq {yield! x;yield y })
|Error x, Ok _ -> Error x
|Ok _, Error y -> Error y
|Error x,Error _ -> Error x
let fold list =
(Ok Seq.empty,list) ||> Seq.fold resultFolder
The initial state of the fold is and "okay" empty sequence -> "Ok Seq.empty". This and the list of Results will be folded.
So the "Folder" function check via pattern match if the current state is okay and the next item is okay. If that's the case I concatenate the item to the state, which is in that case a sequence. In all other cases I will set to the state to an Error. If the actual state is already on "Error" than I leave this error in that case intact.
You can also modify the folder a little bit, to accumulate the validation errors. Maybe you return a seq of string as errors or you add the messages with a line feed or something:
// with seq of error
let resultFolder (state:Result<'a seq,'c seq>) (item:Result<'a,'c>) =
match state,item with
|Ok x,Ok y -> Ok (seq {yield! x;yield y })
|Error x, Ok y -> Error (seq { yield! x })
|Ok _, Error y -> Error (seq { yield y })
|Error x,Error y -> Error (seq {yield! x;yield y })
// with linefeed string
let resultFolder (state:Result<'a seq,string>) (item:Result<'a,string>) =
match state,item with
|Ok x,Ok y -> Ok (seq {yield! x;yield y })
|Error x, Ok y -> Error x
|Ok _, Error y -> Error y
|Error x,Error y -> Error (sprintf "%s\r\n%s" x y)
So want I need to know is, how do I get my nice "fold" function as a CustomOperation into my "result" comutation expression. I tried this with the CustomOperation-Attribute, but than I don't know, how to use this nicely inside my result { }. Maybe someone of you has an idea.
Have a nice day or night or something.