Hacker News new | ask | show | jobs
by daxfohl 3661 days ago
Agreed, I just recently took some recursive F# code from a photobooth (display countdown, async sleep, take photo, download from camera, recur with new byte[] list)->reverse list, and rewrote it it an imperative loop appending to a normal List<byte[]> and the code is much more intuitive.

Some things are more intuitive as a list of things to do and changes to make.

2 comments

Aren't IO Actions just a list of things to do and changes to make? I'm asking genuinely to try to understand the difference.
Yes they are, but I think "for loops" and "mutable lists" a'la Java's ArrayList and Vector and C#'s List<T> don't exist even in IO (I'm not a Haskell expert). That kind of thing has to be done with recursion. They exist in F# but are not idiomatic. However sometimes they're more intuitive (to me at least).
forM is a for each loop over anything iterable (Traversable in Haskell) that will execute a given monadic action on each element in the collection. A more traditional for loop is this over a range of integers or whatever else you feel like.

    forM (range 5) println
Mutable collections exist in various forms depending on what kind of mutability you want. For example, there's transactional mutability that gives you mutable data structures inside an STM but forbids IO. And there's IO mutability that removes all the bounds but restricts you to the IO Monad. TVars, MVars, and the various kinds of Refs are what you're looking for here.
If someone is asking for mutability for reasons of performance and clarity, giving them STM is like a kick in the teeth.
How is STM like a kick in the teeth? In performance? How? In clarity? How?

Can you give examples of STM being a kick in the teeth versus mutability.

I suspect he didn't keep the size of his stm atomically programs small. The docs and associated papers and book materials all make it very very clear that you want to keep stm transactions small and try to defer computation into thinks that are only evaluated when the transaction succeeds.
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.