Hacker News new | ask | show | jobs
by epolanski 1546 days ago
I like to explain functional programs like this.

Functional programs don't _do_ anything, they are a list of commands some external interpreter (what you call the runtime, but doesn't have to be a runtime) will execute or compile.

Functional programs only contain descriptions of the various commands, but it is an external interpreter executing the commands.

Functional programs return "recipes".

"recipes" are picked up by some executor that will turn them in dishes (the side effect).

If all of this seems generic allow me some typescript.

```

// we can declare a a function that takes no argument and return a value IO.

type IO<A> = () => A

declare const program: () => void; // thus can be rewritten as

declare const program: IO<void> // this is the PURE function I can reason about

declare const execute: IO<void> => void // this is the interpreter that will execute the commands and have side effects

```

Now, let's declare a side effectful functions:

```

const log: (s: string) => IO<void>

// desugarized: (s: string) => () => void `` `

Notice how the signature is similar to the standard console.log: (s: string) => void

except that it's lazy. Laziness is one of the most convenient ways to express "commands" rather than side effects in a language like JavaScript, but there are also alternatives.

Now we can have a pure program that logs to the console some string.

`log("foo")` does not "execute" any console.log, it still needs to be executed:

```

const program: IO<void> = log("foo");

// program() will actually print "foo" in stdout

```

Note: we could've encoded IO in different ways, e.g. with a struct/interface rather than a function and had the interpreter actually reason about the side effect on its own.

Now, the only missing part is: how do I compose such IO functions together? Well, there are various ways but the most common ones are applicative functors and monads. I am not going to delve deeper in this comment on those topics because it would take long but I hope I have transmitted my point:

in functional programs you return programs (I like to think about them as recipes, recipes don't DO anything), those programs may compose effectful commands, the actual execution of the commands is shoved inside an external interpreter.

This is quite obvious in some languages like Haskell, it's less obvious in others.

1 comments

This I can understand.

But why would someone want to do this? It feels like mental overhead. So what advantages are there to a system like this?

“Have you tried turning off and on again?” is advice to perform a total state reset, which works because the system’s state is inconsistent but its sources of truth are not.

You never have to reset state if you do not have it in the first place. You get other bugs still, but you don't have the most common class of bugs.

But, you need some state. You just want to discourage it. Some of it can come from laziness, but with difficulty. (Chris Okasaki’s Purely Functional Data Structures uses it to de-amortize the bound on a FIFO queue.) Other things, like ring buffers, are harder to argue.

So you want to be able to express an array of IORefs, say, for your ring buffer. But the people who use it become aware that it is a stateful construct and must be used that way.

Read “what color is your function?” for a counterpoint, of course.

You do it if you're using a language where there's a rule that there are only functions. For example there's no concept of doing one thing followed by another. The only way you can arrange that is to have two functions and compose them: g(f(x)). So now you know that the code in g is executed after the code in f. Monadic composition is basically about doing that while having code that looks like it would in a regular language that has statements and sequential execution. There are a few other reasons, such as you can create your own control flow as-code, lifting, etc. But the primary driver is: because you can't write useful programs in a pure functional language without this trick.
Referential transparency, as the original post says. For example, in a functional language you're always allowed to transform

  a = f()
  b = f()
  c = g(a, b)
into

  a = f()
  c = g(a, a)
because f() is guaranteed not to have side effects. (If it does return side effects through the IO monad, instead of actually performing the side effects, g receives two data structures that encode what IO operations to perform.) In an imperative/procedural language, f() may have side effects, and if it does the two programs are not equivalent.

This may (not) make programs easier to reason about.

You can reason and compose pure effectful programs leveraging referential transparency.