16

I'm learning about Elm from Seven More Languages in Seven Weeks. The following example confuses me:

import Keyboard
main = lift asText (foldp (\dir presses -> presses + dir.x) 0 Keyboard.arrows)

foldp is defined as:

Signal.foldp : (a -> b -> b) -> b -> Signal a -> Signal b

It appears to me that:

  • the initial value of the accumulator presses is only 0 on the first evaluation of main
  • after the first evaluation of main it seems that the initial value of presses is whatever the result of function (a -> b -> b), or (\dir presses -> presses + dir.x) in the example, was on the previous evaluation.

If this is indeed the case, then isn't this a violation of functional programming principles, since main now maintains internal state (or at least foldp does)?

How does this work when I use foldp in multiple places in my code? Does it keep multiple internal states, one for each time I use it?

The only other alternative I see is that foldp (in the example) starts counting from 0, so to say, each time it's evaluated, and somehow folds up the entire history provided by Keyboard.arrows. This seems to me to be extremely wasteful and sure to cause out-of-memory exceptions for long run times.

Am I missing something here?

Marcel
  • 367
  • 1
  • 12

3 Answers3

20

How it works

Yes, foldp keeps some internal state around. Saving the entire history would be wasteful and is not done.

If you use foldp multiple times in your code, doing distinct things or having distinct input signals, then each instance will keep it's own local state. Example:

import Keyboard

plus  = (foldp (\dir presses -> presses + dir.x) 0 Keyboard.arrows)
minus = (foldp (\dir presses -> presses - dir.x) 0 Keyboard.arrows)
showThem p m = flow down (map asText [p, m])
main  = lift2 showThem plus minus

But if you use the resulting signal from a foldp twice, only one foldp instance will be in your compiled program, the resulting changes will just be used in two place:

import Keyboard

plus  = (foldp (\dir presses -> presses + dir.x) 0 Keyboard.arrows)
showThem p m = flow down (map asText [p, m])
main  = lift2 showThem plus plus

The main question

If this is indeed the case, then isn't this a violation of functional programming principles, since main now maintains internal state (or at least foldp does)?

Functional programming doesn't have some great canonical definition that everybody uses. There are many examples of functional programming languages that allow for the use of mutable state. Some of these programming languages show you that a value is mutable in the type-system (you could see Haskell's State a type as such, it really depends on your viewpoint though).

But what is mutable state? What is a mutable value? It's a value inside the program, that is mutable. That is, it can change. It can be different things at different times. Ah, but we know how Elm calls values at change over time! That's a Signal.
So really a Signal in Elm is a value that can change over time, and can therefore be seen as a variable, a mutable value, or mutable state. It's just that we manage this value very strictly by allowing only a few well-chosen manipulations on Signals. Such a Signal can be based on other Signals in your program, or come from a library or come from the outside world (think of inputs like Mouse.position). And who knows how the outside world came up with that signal! So allowing your own Signals to be based on the past value of Signals is actually ok.

Conclusion / TL;DR

You could see Signal as a safety wrapper around mutable state. We assume that signals that come from the outside world (as input to your program) are not predictable, but because we have this safety wrapper that only allows lift/sample/filter/foldp, the program you write is otherwise completely predictable. Side-effects are contained and managed, therefore I think it's still "functional programming".

Apanatshka
  • 5,958
  • 27
  • 38
  • Your point about _canonical definitions_ is well taken. To me it seems that there is some magic going on between evaluations of the same statement. You say that in Elm `Signal`s are wrappers around mutable state, much like Haskell's `State` monad, and sure enough `foldp` returns `Signal b`. But - from the definition `Signal.foldp : (a -> b -> b) -> b -> Signal a -> Signal b` - the accumulator is just `b`, not `Signal b`. Between evaluations `Signal b` is magically unwrapped and inserted as `b`, ignoring the `0` on subsequent evaluations. I guess the notation just bothers me :-) – Marcel Jul 27 '14 at 04:45
  • 2
    Wouldn't you say that that same magical unwrapping happens for `a`, and also happens in `Signal.lift : (a -> b) -> Signal a -> Signal b`? Anyway I understand how it might feels a little weird. Have you seen a signal graph yet? They are explained in section 4 of https://www.seas.harvard.edu/sites/default/files/files/archived/Czaplicki.pdf. There you see a foldp node has internal state. But you can also see it differently, as a tight loop where foldp uses it's output signal as an input signal: https://groups.google.com/group/elm-discuss/attach/4b33ca584290b011/elm_fold.svg?part=0.1&authuser=0 – Apanatshka Jul 27 '14 at 07:59
  • I'll have a look and simply plough on for the moment, I'm sure I'll overcome my initial sense that the statement 'smells funny' as I get more used to Elm's semantics. Thanks for the extra links, I'll use them to read up on `foldp`. – Marcel Jul 28 '14 at 12:38
  • See also my comment to CheatEx to give you an idea of why it smells funny to me ;-) – Marcel Jul 28 '14 at 12:47
  • What is it called foldp and not just fold? – CMCDragonkai Oct 02 '14 at 03:48
  • @CMCDragonkai folding is a general concept. You can fold a list from the left or the right, called `foldl` and `foldr` respectively. So folding over signal is also given a direction, you fold from the past – Apanatshka Oct 02 '14 at 09:57
  • Yea I realised while I was reading Evan's paper. Hmm wouldn't it be cleaner to have them all called fold and use ad-hoc polymorphism. – CMCDragonkai Oct 02 '14 at 11:56
  • 1
    @CMCDragonkai Elm currently doesn't have type-class-like functionality, so you would have to pass around dictionaries with the fold function in it. For now it's easier to just have the added letter. – Apanatshka Oct 06 '14 at 06:44
10

You're confusing an implementation detail with a conceptual detail. Every functional programming language eventually gets translated down to assembly code, which is decidedly imperative. That doesn't mean you can't have purity at the language level.

Don't think of main as being repeatedly evaluated, returning different results every time. A Signal is conceptually an infinite list of values. main takes an infinite list of keyboard arrows as input and translates that into an infinite list of elements. Given the same list of arrows, it will always return the exact same list of elements, without side effects. At this level of abstraction, it is therefore a pure function.

Now, it so happens that we are only interested in the last element of the sequence. This allows for some optimizations in the implementation, one of which is storing the accumulated value. What's important is that the implementation is referentially transparent. From the language's point of view, you're getting the exact same answer as if you stored the entire sequence and recomputed it from scratch every time a value is added to the end. You get the same output given the same input. The only difference is storage space and execution time.

In other words, the whole idea of functional programming is not to eliminate state tracking, but to abstract it away from the purview of the programmer. Programmers get to play in the ideal world, while the compiler and runtime slave away in the sewers of mutable state to make the ideal world possible for the rest of us.

Karl Bielefeldt
  • 47,314
  • 10
  • 60
  • 94
3

You should note that "doesnt maintain internal state" isn't really strong definition of FP. Its much more like an implementation constraint. What definition I like more is "built from pure functions". Without diving deep, in plain English it means that all functions return same output when given same input. This definition unlike previous gives you huge reasoning power and a simple way to check whether some program follows it while keeping some optimization space on current hardware.

Given reformulated restriction functional languages are free to use mutables as long as it modelled with pure functions. Answering your question, elm programs built out of pure functions so its probably a functional language. Elm uses special data structure, Signal, to model outside world interactions and internal state as well as any other functional language does.

CheatEx
  • 2,082
  • 2
  • 19
  • 28
  • Now that I think about it more, what bothers me about the semantics of the statement is that the definition you provide — **all functions return same output when given same input** — doesn't _apprear to_ hold. Upon the second evaluation of `foldp` the provided accumulator — `b` or `presses` or `0` — is unceremoniously dumped in favour of some internal state. Surely you'll agree that this doesn't really give the appearance of returning the same output when given the same input :-) – Marcel Jul 28 '14 at 12:46
  • 1
    @MarcelOomens Sorry, I don't really understand what confuses you. Particularly, what do you mean by the "second evaluation" of `foldp`? It's evaluated only once in your example. – CheatEx Jul 29 '14 at 13:04
  • 2
    @MarcelOomens `foldp` DOES return the same output when given the same input - it returns an infinite Signal. The shape of that Signal is determined by the accumulator function and the initial value. – fbonetti Sep 02 '15 at 19:20