Reputation: 625
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
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