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.