Hacker News new | ask | show | jobs
by chadaustin 3661 days ago
I find both mental models useful. For example, "accumulate all of the monoids in this collection" (https://hackage.haskell.org/package/base-4.9.0.0/docs/Data-F...) makes total sense as a functional operation. You have a collection and some pure operations you want to apply to combine it all into one thing.

But then, sometimes you just want to iterate across a list of directories, read some files, and put some results in lists. Tail-recursion here feels dumb. Like, dammit, I'm the human and you're the computer, I know you can turn a sequence of instructions into a tail recursive loop for me, so don't make me think.

Haskell, due to its effectful / monadic vs. pure syntax distinction isn't terribly great here. Ideally I'd write all of my code in an "imperative style" all the time, but a row-typed effect inference system would determine which functions were pure, or in IO, or async, or in ST, or threw exceptions, etc. Koka [1] is an interesting language that explores some of these ideas.

[1] http://research.microsoft.com/en-us/projects/koka/

5 comments

Some other projects that are doing cool things in this space are Idris[1] and F-Star[2] (the latter is also a Microsoft Research project). I am particularly impressed by Idris's algebraic effect approach, since it gives you a general effect system that combines the ability to easily extend that monad transformer approaches provide with the ease of use and inference of F*'s effect system.

I think the theory behind algebraic effects is a key step in making it easy to develop code with more stringent and customized safety properties than is available in languages like Rust.

[1]: http://www.idris-lang.org/ [2]: https://www.fstar-lang.org/

> Ideally I'd write all of my code in an "imperative style" all the time, but a row-typed effect inference system would determine which functions were pure, or in IO, or async, or in ST, or threw exceptions

Yes, this would be a really nice approach (and it's not one that Haskell lovers would be upset by, BTW).

I would be upset if it meant no visible distinction between effectful and non-effectful calls. When refactoring it's really important to know when the order of calls is significant vs when it's incidental.
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.

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.

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.
I find writing recursive code is often very short and easier to read and show intent than the same code written non recursively.
Additionally, in languages with good, convenient support for functional programming like Haskell and Scala, I find that I tend to write lots of short functions, often making use of higher-order functions to decompose code into really simple, and potentially reusable, pieces. And like you say, this really helps to show intent. The same sort of thing, in fact, that object-oriented programmers try to do with design patterns such as the Strategy pattern - but with fewer lines of code.
That is definitely right. For things involving the machine, doing things declaratively is wrong.

Also, thanks for telling about Koka. It sounds very interesting!

I'm not sure I agree. Functional Reactive Programming is a declarative way of doing user interaction, but it's still a computer science research topic (Elm used to claim to be FRP, but has now decided to move away from its old allegedly FRP-like style). I need to play around with the latest FRP research libraries some time!

In addition there is also machines/pipes/conduits, which all mix imperative I/O with declarative style code in an interesting and new way.

FRP is awesome and is an example of how something previously thought of as an inherently imperative problem can be much easily done declaratively; we completely agree on this!

What I was talking about is: almost all lower-level abstractions are designed with the view of computer as a state machine. If we are solving a problem at an abstraction low enough to make us deal with this analogy, it is easier not to have a declarative layer in between.

machines/pipes/conduits are all very interesting and I was going to mention them too, but I still don't think they solve this problem in the way OP was referring to.