Though maybe I stand corrected. I revisited that. Here's what I had initially:
(include the following stubs to compile):
let upload _ = async.Return()
let takePhoto i = async.Return [|0uy|]
// i stands for "photo i of N", a required parameter for our workflow
---
let runSequence n =
async {
let rec runSequenceRec (framesSoFar: byte[] list) =
async {
if framesSoFar.Length = n then
return List.rev framesSoFar
else
let! bytes = takePhoto framesSoFar.Length
do! upload bytes
return! runSequenceRec (bytes::framesSoFar)
}
return! runSequenceRec []
}
Nasty dual-level async calls, list reversal, recursion, if/else, an initializing []. A lot of visual junk that takes away from what the code is actually doing. I rewrote it imperatively to this:
let runSequence' n =
async {
let frames = System.Collections.Generic.List<byte[]>()
for i = 0 to n do
let! bytes = takePhoto i
do! upload bytes
frames.Add bytes
return frames
}
Much cleaner, you can see exactly what's going on. I was fine with that, and that's where I left it until now.
However reading some of the Haskell comments, I thought there might be something better. So I tried replicating Haskell's forM:
let forNAsync f n =
let rec runOnce (soFar: _ list) =
async {
if soFar.Length = n then
return List.rev soFar
else
let! result = f soFar.Length
return! runOnce (result::soFar)
}
async.ReturnFrom(runOnce [])
Okay, not pretty but it's a library function so we should never have to look at it again. With that in hand though, we can define:
let runOneShot i =
async {
let! bytes = takePhoto i
do! upload bytes
return bytes
}
let runSequence'' = forNAsync runOneShot
This is terrific. Shorter and cleaner than the imperative method, preserves immutability throughout, and composes easily.
If you were to try to compose `runOneShot` from the imperative method, you'd end up with this, because of your frames variable
let runSequence''' n =
async {
let frames = System.Collections.Generic.List<byte[]>()
for i = 0 to n do
let! bytes = runOneShot i
frames.Add bytes
return frames
}
Or if you wanted to try to rework the `runOneShot` to include the frames variable, it wouldn't be much easier:
let runOneShot'''' i (frames: System.Collections.Generic.List<byte[]>) =
async {
let! bytes = takePhoto i
do! upload bytes
frames.Add bytes
}
let runSequence'''' n =
async {
let frames = System.Collections.Generic.List<byte[]>()
for i = 0 to n do
do! runOneShot'''' i frames
return frames
}
So, maybe things that seem imperative do work better functionally. You've just got to create some combinators that let you hook them up.
(include the following stubs to compile):
--- Nasty dual-level async calls, list reversal, recursion, if/else, an initializing []. A lot of visual junk that takes away from what the code is actually doing. I rewrote it imperatively to this: Much cleaner, you can see exactly what's going on. I was fine with that, and that's where I left it until now.However reading some of the Haskell comments, I thought there might be something better. So I tried replicating Haskell's forM:
Okay, not pretty but it's a library function so we should never have to look at it again. With that in hand though, we can define: This is terrific. Shorter and cleaner than the imperative method, preserves immutability throughout, and composes easily.If you were to try to compose `runOneShot` from the imperative method, you'd end up with this, because of your frames variable
Or if you wanted to try to rework the `runOneShot` to include the frames variable, it wouldn't be much easier: So, maybe things that seem imperative do work better functionally. You've just got to create some combinators that let you hook them up.