Live data from Hacker News

Functional languages should be so much better at mutation than they are

cohost.org

21–30 of 188 posts

Re: Functional languages should be so much better at mutation than they are

#21
post #16

The article utterly falls apart in its first paragraph where it itself acknowledges that the whole ML family including Ocaml has perfect support for mutation, rightfully assume most Ocaml programmers would choose to not use it most of the time but then assume incorrectly that it’s because the language makes it somehow uneasy. It’s not. It’s just that mutation is very rarely optimal. Even the exemple given fails: > Fo…

> Well, no, this is straight confusion between what’s expressed by the program and what’s compiled. The idiomatic code in Ocaml will end up generating machine code which is as performant than using mutable array. This cannot be true in general. There are machine code patterns for which arrays are faster than linked lists. The OCaml compiler, great as it is, won't turn linked list source code into array machine code.…

Well, it doesn't make the substance wrong. This paragraph rightly summarizes it:

"The fact that most programming languages don’t give enough semantic information for their compiler to do a good job doesn’t mean it necessary has to be so. Functional programmers just trust that their compiler will properly optimize their code."

Re: Functional languages should be so much better at mutation than they are

#22

The article utterly falls apart in its first paragraph where it itself acknowledges that the whole ML family including Ocaml has perfect support for mutation, rightfully assume most Ocaml programmers would choose to not use it most of the time but then assume incorrectly that it’s because the language makes it somehow uneasy. It’s not. It’s just that mutation is very rarely optimal. Even the exemple given fails: > Fo…

[deleted]

Re: Functional languages should be so much better at mutation than they are

#23

The article utterly falls apart in its first paragraph where it itself acknowledges that the whole ML family including Ocaml has perfect support for mutation, rightfully assume most Ocaml programmers would choose to not use it most of the time but then assume incorrectly that it’s because the language makes it somehow uneasy. It’s not. It’s just that mutation is very rarely optimal. Even the exemple given fails: > Fo…

I use f# daily at my company and am actually glad that many dotnet api integrations use array buffers (e.g. a byte array for streaming) this forces me to optimize the f# code by thinking in terms of low-memory, mutable data structures when interfacing with external libraries.

Yeah, F# in general is pretty mutation friendly given how functional it is.

Re: Functional languages should be so much better at mutation than they are

#24

Earlier quoted context omitted.

I mostly agree with your sentiment but this: > Well, no, this is straight confusion between what’s expressed by the program and what’s compiled. The idiomatic code in Ocaml will end up generating machine code which is as performant than using mutable array. I disagree with. There are different ways to get close to the performance of `Array.map` with lists (best case scenario you don't care about order and can use `Li…

That’s not what the article is talking about. The proposed exemple is a traversal of a different data structure to collect results in an array. That’s a fold and will properly be tco-ed to something equivalent to adding to an array if you use list cons in the aggregation, might actually be better depending on how much resizing of the array you have to do while traversing.

I think `Array.map` is a perfectly reasonable reading of "you're iterating over some structure and collecting your results in a sequence".

But sure, in the `fold` scenario where you don't know the number of results in advance (you are more likely to know if you use imperative data structures, e.g. `Hashtbl.length` is constant-time whereas `Map.cardinal` is not), lists might be faster than growing arrays with copies. They are still going to use more memory, and they are unlikely to to be faster than a rope-like structure that grows with no copies.

Re: Functional languages should be so much better at mutation than they are

#25
How come the CoW method requires runtime reference counting? A lot of the same benefit (but not all) should be available based on static analysis right?

Especially if the approach isn't really Copy on Write, but Copy only when someone might want to use the old value. Default to trying to mutate in place, if you can prove that is safe.

For most locals, that should be rather doable, and it would be a pretty big gain. For function parameters it probably gets hairy though.

Re: Functional languages should be so much better at mutation than they are

#26
post #2

A variant of option 4 is to keep track of references you know cannot possibly be shared, and update those by mutation. Compared to reference counting, it misses some opportunities for mutation, but avoids the false sharing. I think Roc is doing this.

To what extent is this already being done by other functional blanguages that have CoW mutability? This seems like a legal compiler optimization to make in most cases no?

Re: Functional languages should be so much better at mutation than they are

#27
I'm not convinced about the dismissal of option 2. I agree ST is clunky but not for the reasons given. It's clunky because it's impossible to mix with other effects. What if I want ST and exceptions, for example, and I want the presence of both to be tracked in the type signature? ST can't do that. But my effect system, Bluefin, can. In fact it can mix not only state references and exceptions, but arbitrary other effects such as streams and IO.

* https://hackage.haskell.org/package/bluefin-0.0.2.0/docs/Blu...

* https://hackage.haskell.org/package/bluefin-0.0.6.0/docs/Blu...

Re: Functional languages should be so much better at mutation than they are

#28
post #16

The article utterly falls apart in its first paragraph where it itself acknowledges that the whole ML family including Ocaml has perfect support for mutation, rightfully assume most Ocaml programmers would choose to not use it most of the time but then assume incorrectly that it’s because the language makes it somehow uneasy. It’s not. It’s just that mutation is very rarely optimal. Even the exemple given fails: > Fo…

> Well, no, this is straight confusion between what’s expressed by the program and what’s compiled. The idiomatic code in Ocaml will end up generating machine code which is as performant than using mutable array. This cannot be true in general. There are machine code patterns for which arrays are faster than linked lists. The OCaml compiler, great as it is, won't turn linked list source code into array machine code.…

> The OCaml compiler, great as it is, won't turn linked list source code into array machine code.

Why not? If the compiler can see that you have a short-lived local linked list and are using it in a way for which an array would be faster, why would it not do the same thing that an array would do?

Re: Functional languages should be so much better at mutation than they are

#29
I recently ran into this issue when trying to memoize a simple numerical sequence in Hoon (yes, that Hoon. I know, I know...).

Let's use the fibonacci sequence as an example. Let's write it the classic, elegant way: f(n) = f(n-1) + f(n-2). Gorgeous. It's the sum of the two previous. With the caveat that f(n=0|1) = n. In Python:

  # fib for basic b's
  def fib(n):
    ## Base case
    if n == 0 or n == 1:
      return n
    
    return fib(n-1) + fib(n-2)
Right off the bat, performance is O(n)=n*2. Every call to f(n-1) will also need to compute f(n-2) anyways! It's a mess. But since Python passes arrays and dictionaries as pointers (cough, sorry! I meant to say references) it's super easy to memoize:

  # optimize-pilled memoize-chad version
  def fib(n, saved={}):
    if n in saved:
      return saved[n]
    
    if n == 0 or n == 1:
      saved[n] = n
    else:
      saved[n] = fib(n-1) + fib(n-2)
    
    return saved[n]
Okay, now our version is nearly as fast as the iterative approach.

This is the normal pattern in most languages, memoizing otherwise "pure" functions is easy because you can reference a shared object using references, right? Even with multithreading, we're fine, since we have shared memory.

Okay, but in Hoon, there are no pointers! Well, there kinda are. The operating system lets you update the "subject" of your Urbit (the context in which your programs run), and you can do this via the filesystem (Clay) or daemons (Gall agents, which have their own state kind of).

But to do this within a simple function, not relying on fancy OS features? It's totally possible, but a huge pain the Aslan.

First, here's our bog-standard fib in Hoon:

  |=  n=@ud
  ?:  (lte n 1)
    n
  %+  add
    $(n (dec n))
  $(n (sub n 2))
Now, I memoize on the way down, by calculating just f(n-1) and memoizing those values, to acquire f(n-2):

  :-  %say
  |=  [* [n=@ud ~] [cache=(map @ud @ud) ~]]
  :-  %noun
  ^-  [sum=@ud cache=(map @ud @ud)]
  =/  has-n  (~(get by cache) n)
  ?~  has-n
    ?:  (lte n 1)
      [n (~(put by cache) n n)]
    =/  minus-1  $(n (dec n))
    =/  minus-2 
      =/  search  (~(get by cache.minus-1) (sub n 2))
      ?~  search  0
      (need search)
    :-  (add sum.minus-1 minus-2)
    (~(put by cache.minus-1) n (add sum.minus-1 minus-2))
  [(need has-n) cache]
and that works in the Dojo:

  > =fib-8 +fib 8
  > sum.fib-8
  21
but it sure is easier in Python! And I'm not picking on Hoon here, it's just pure functional programming that makes you think this way - which as a hacker is fun, but in practice is kinda inconvenient.

I even wonder how much faster I actually made things. Let's see:

  > =old now
  > =res +fib 18
  > sum.res
  2.584
  > (sub now old)
  1.688.849.860.263.936
  :: now with the non-memoized code...
  > =before now
  > +fib 18
  2.584
  > (sub now before)
  1.125.899.906.842.624
Ha! My super improved memoized code is actually slower! That's because computing the copies of the map costs more than just recurring a bunch. This math should change if I try to compute a bigger fib number...

Wait. Nevermind. My memoized version is faster. I tested it with the Unix time command. It's just that Urbit Dojo has a wierd way of handling time that doesn't match my intuition. Oh well, I guess I can learn how that works. But my point is, thinking is hard, and in Python or JS or C I only have to think in terms of values and pointers. And yes, that comes with subtle bugs where you think you have a value but you really have a pointer! But most of the time it's pretty easy.

Btw sorry for rambling on with this trivial nonsense - I'm a devops guy so this is probably super boring and basic for all you master hn swe's. But it's just a tiny example of the constant frustrations I've had trying to do things that would be super simple if I could just grab a reference and modify something in memory, which for better or worse, is how every imperative language implicitly does things.

Re: Functional languages should be so much better at mutation than they are

#30
Disclosure: I work on Koka's FBIP optimization (Option 4).

> The most efficient data structure to use here would be a mutable dynamic array and in an imperative language that's what pretty much everyone would use. But if you asked an OCaml programmer, they would almost certainly use a linked list instead.

I agree with this sentiment. However, OCaml does have mutable arrays that are both efficient and convenient to use. Why would a programmer prefer a list over them? In my opinion, the main benefit of lists in this context is that they allow pattern matching and inductive reasoning. To make functional programming languages more suited for array programming, we would thus need something like View Patterns for arrays.

A related issue is that mutation can actually be slower than fresh allocations in OCaml. The reason for this is that the garbage collector is optimized for immutable datastructures and has both a very fast minor heap that makes allocations cheap and expensive tracking for references that do not go from younger to older elements. See: https://dev.realworldocaml.org/garbage-collector.html#scroll...

> Unfortunately, this makes it impossible to use any standard functions like map on linear values and either makes linearity nearly useless or inevitably creates a parallel, incomplete universe of functions that also work on linear values.

You can implement polymorphism over linearity: this is done in Frank Pfenning's SNAX language and planned for the uniqueness types in a branch of OCaml.

> This might sound a little dangerous since accidentally holding on to a reference could turn a linear time algorithm quadratic

No, the in-place reuse optimization does not affect the asymptotic time complexity. But it can indeed change the performance drastically if a value is no longer shared since copies are needed then.

> A tracing garbage collector just doesn't give you this sort of information.

It is possible to add One-bit Reference Counts to a garbage collector, see https://gitlab.haskell.org/ghc/ghc/-/issues/23943

> for now even these struggle to keep up with tracing garbage collectors even when factoring in automatic reuse analysis.

I investigated the linked benchmarks for a while. The gap between Koka and Haskell is smaller than described in that initial comment, but a tuned GHC is indeed a bit faster than Koka on that benchmark.

Post reply on HN