user1325696
user1325696

Reputation: 625

F# Railway Programming is this initialization can be improved?

I'm learning F# and I wonder if this method implementation which initializes first N elements of an array could be improved. At the moment it works perfectly fine. I mean if it fails when it attempts to initialize 2nd element by performing 2nd call to the factory, then if would call undo for the first successful result. The only minor problem it doesn't clean items in the array if error, but I do not worry about it. All I worry is that it should do undo for the first successful results if it fails on 2nd or 3rd or later. If it succeeds then success result should have in Undo list all the functors to do undo.

The thing is that I'd like to avoid recursion and use something like Linq to iterate and do stuff, but it is not clear how to do let with bang(let!) in that case

// val private initializeArrayInternal: 
//    arr    : 'a option [] ->
//    factory: unit -> RopWithUndo.Result<'a> ->
//    count  : int          ->
//    index  : int          
//          -> RopWithUndo.Result<'a option []>
let rec private initializeArrayInternal (arr: _ []) factory count index =
    if (arr.GetLength(0) < count) then 
        rwu.Failure "Count can not be greater than array length"
    else if (count = index ) then
        rwu.successNoUndo arr
    else 
        rwu.either {        
            let! element = factory()
            arr.[index] <- Some element 
            return (initializeArrayInternal arr factory count (index+1))
        }


// val initializeArray: 
//    arr    : 'a option [] ->
//    factory: unit -> RopWithUndo.Result<'a> ->
//    count  : int          
//          -> RopWithUndo.Result<'a option []>        
let rec initializeArray arr factory count =
    initializeArrayInternal arr factory count 0 

RopWinUndo

module RopWithUndo

type Undo = unit -> unit

type Result<'success> =
    | Success of 'success * Undo list
    | Failure of string

/// success with empty Undo list. It only applies to the curretn operation. The final list is concatenated of all list and no problem if some lists are empty.
let successNoUndo result =
    Success (result,[])

let doUndo undoList =
    undoList |> List.rev |> List.iter (fun undo -> undo())

let bind f x =
    match x with
    | Failure e -> Failure e 
    | Success (s1,undoList1) ->            
        try
            match f s1 with
            | Failure e ->
                // undo everything in reverse order 
                doUndo undoList1
                // return the error
                Failure e 
            | Success (s2,undoList2) ->
                // concatenate the undo lists
                Success (s2, undoList1 @ undoList2)
        with
        | _ -> 
            doUndo undoList1
            reraise()

type EitherBuilder () =
    member this.Bind(x, f) = bind f x

    member this.ReturnFrom x = x
    member this.Return x = x
    member this.Delay(f) = f()

let either = EitherBuilder ()

Upvotes: 3

Views: 164

Answers (1)

Tomas Petricek
Tomas Petricek

Reputation: 243126

If you add a few more operations to your computation expression builder, you will be able to use the for construct inside your computations, which makes this much nicer:

let initializeArray (arr:_[]) factory count =
    rwu.either {
      if (arr.GetLength(0) < count) then 
        return! rwu.Failure "Count can not be greater than array length"
      for index in 0 .. count - 1 do
        let! element = factory()
        arr.[index] <- Some element 
    }

To do this, I had to modify your Return to wrap results into Success (in your original version, you then need to change return to return! which is the right way of doing things anyway) and then I had to add Zero, Combine and For:

type EitherBuilder () =
  member this.Return x = Success(x, [])
  member this.Bind(x, f) = bind f x

  member this.ReturnFrom x = x
  member this.Delay(f) = f()

  member this.Zero() = this.Return ()
  member this.Combine(a, b) = this.Bind(a, fun () -> b)
  member this.For(s:seq<_>, b) = 
    let en = s.GetEnumerator()
    let rec loop () = 
      if en.MoveNext() then this.Bind(b en.Current, loop)
      else this.Zero()
    loop ()

Upvotes: 5

Related Questions