Hacker News new | ask | show | jobs
by blindwatchmaker 2958 days ago
Really enjoyed this book. Once you get currying, and using curried functions to pipe/compose, everything clicks into place. I found the examples of using Nothing/Maybe monads for error handling pretty neat as well - is that a common pattern, because I don't remember native support for those types when I briefly dabbled in elixir.

Also is his explanation of monads as 'functors that can flatten' a simplification for the purposes of teaching, or is that more or less what they are?

6 comments

There's really multiple definitions of "functional" right now, and the type of "functional" being discussed here, Erlang isn't, and I don't think Elixir particularly is either.

This is not a criticism of any kind; this is a point about definitions. There are definitions of functional where Erlang is functional, and IIRC Elixir can be said to support it.

(And there are definitions of "functional" where almost every language in current use is "functional". There's even some so weak that C is "functional" because it has "function pointers", though this one is now out-of-date and not currently being used by anyone. But, yes, there was once a time in which C would have been considered "unusually strong" in its "functional programming" support, because other contemporary languages didn't even have function pointers.)

"Also is his explanation of monads as 'functors that can flatten' a simplification for the purposes of teaching, or is that more or less what they are?"

A little of both. Technically it is correct, but the "flattening" in question applies to many things that most programmers wouldn't consider "flattening". For instance, consider monadic IO as Haskell uses. There is a way in which you can consider the execution of an IO value as "flattening" it, and it corresponds to the mathematical term, but it's not what most people have in mind. There's more to "flattening" than "simplifying data structures in some manner"; it doesn't even always involve what we'd traditionally think of as data structures at all, such as, again, IO.

Personally I think it is an actively unhelpful metaphor for these reasons, as it is very prone to leading to false understanding, but YMMV.

> There's really multiple definitions of "functional" right now

It would be very helpful to see an explanation of this spectrum you describe for someone who is not really familiar with the definitions. I would love to read an explanation of the various “functional” paradigms as they diverge from “conventional” (ie. C) programming languages.

The weakest definition of functional is that functions are a first-class object that can be passed around. This is the one that C conforms to in that old sense. This is a dead definition because almost everything in modern use conforms to this definition, and definitions are only useful to the extent they create distinct categories. Because almost every modern language has this, it can be difficult to imagine a language in which this is not true. But, yes, once upon a time, languages did not in general have a data type that could contain a function that you could call. (This is before my time, but I caught the tail end of these arguments.)

This was also one of the reasons that assembly programmers were always banging on about the power of assembly back in the day. Nowadays the only remnant of that argument is the claim that you can write more optimal assembly than the compiler. But back in the day, assembly programmers enjoyed the ability to have a pointer to a function and jump to it and/or call it (it's a bit fuzzier in assembler) and people using high-level languages were definitely getting a "weaker" experience. Today we expect our high level languages to also provide us significant power advantages over assembler. (Of course you can do anything in assembler, but not as concisely necessarily.)

When I got into computing, the definition of "functional" that excluded C included having "closures". This is a function pointer + an environment for the function to run in. C only has the function pointer; you don't get an environment. It is more convenient than nothing, but vastly less useful than a full closure. (You can do them manually, but they become problematic fast.)

Stepping up from there, you got languages that generally permitted an imperative style of programming, but "preferred" what we would today call a functional style, when you use map, filter, and such to perform operations. These languages loved them some linked lists; linked lists everywhere. With their own special names like "cons lists". They also tended to be garbage collected, which for a while was a "functional programming" thing, but is now also simply an accepted thing that a language may be, regardless of how "functional" it is.

This definition is still in some usage today, though some improvement in understanding the structure of the relevant of code ("iteration" as a concept you can abstract, rather than accidentally conflating "a linked list" with "the thing you can iterate on") and the fact that hardware is getting ever-more grumpy about non-locality has erased the linked list obsession. You can either have a "functional language" like Lisp, or you can program in a "functional style" in a multi-paradigm language like Javascript. In the latter case, you can do a lot of work with the functional paradigm, but technically you always end back up at structured programming with some flavor of OO, which is the dominant language paradigm. (Languages can be multi-paradigm, but there is always a dominant paradigm, one that when the paradigms conflict, is the winner. And my personal default definition of OO includes Javascript, considering the prototype system a detail relative to the fact you still have "collections of data with associated methods".) People who say that "Javascript is a functional language" mean this definition.

Finally, there's the Haskell definition. Here, immutability is the default, and possibly the only option. Type systems are very strong, able to express types like "a block of code that does not do any IO" that other languages can not express, or can only do very laboriously. You get "functor" and "monad" and such being not just demonstrated on a one-off basis, but being the foundational abstractions of libraries and entire applications. People argue over how much category theory you have to know to practically use these languages. F#, O'Caml, and Haskell live here. Haskell is as far as you can currently go in this direction and get a language usable for practical tasks, work that you can build a business on.

(As an interesting side bar, I think Erlang made an error here, although a perfectly understandable one. When it was written, one of the reasons immutability was favored at the academic level was that it helped write multi-core code. At the time, only big industry and academia had multi-core systems. But you only really need isolation between threads. Immutability is one way to achieve this, but you can also make it so that it is impossible to communicate "references" between processes/threads, so everything is a copy. Within an Erlang process there's no reason not to allow one to "assign" to existing variables. But at the time, "access control" and "immutable" were sort of conflated together. Rust is the first big language that seems to be levering those concepts apart in a really systematic way.)

However, the spectrum keeps going from here. Past Haskell there are functional languages that get really mathematical, and are focused on proving code, creating even more elaborate type systems such as dependent types ("this function takes a string whose length is a prime number", to give a silly example), and constraining the abstractions even further for things like total functional programming, which is one of the most interesting possible ways to limit programming so that it is not Turing Complete, but can still do real work. Here you can get such exotica as ways of using the type system to separate out what code is total, and what is not, in various principled ways. One of the common "I've been in this industry for 20 years and it sucks and here's what we all need to do to fix it" posts is to extol the virtues of one or more of these things. However, while there has been some interesting progress on many of these fronts in the past couple of decades, they remain impractical.

I'll add to the thanks for writing this!

While I'm happy to be corrected, I get the impression that the 'threshold' for calling something FP, by those who consider themselves FP 'practitioners', lies somewhere in the area you describe right before Haskell. And for those who consider FP-style programming alien, Haskell is what they imagine.

Basically, Javascript would be considered barely-enough to do FP style programming, but it's got the kitchen-sink nature against it, offering too easy an escape hatch.

Clojure/Lisp and perhaps Elixir more-so, would be squarely in the FP world, even though they're not strict about it. I'm not too familiar with the former, but the latter makes it rather inconvenient to not write most of your code in a functional style.

Personally my next goal is to go full-on FP and dive into Haskell (or something else, if someone would recommend it!), but Elixir has been the most useful language in my journey so far. I come from Javascript (and before that PHP), and while I've always tried doing things the FP way as much as possible, it's only after getting comfortable with Elixir that I've actually started to 'think functionally'. I feel it's greatly improved my programming even when I go back to JS stuff, because I'm less likely to fall back on the imperative/OO stuff. I'm not saying the latter is bad, but at the very least being consistent in my approach seems to bear fruit.

Excellent exposition of the current landscape and of the notion that there is no single definition of FP.

I'd like to add that type-systems are not a defining feature of functional programming because you can have type-systems in what are considered "non functional" languages as well.

Immutability it jives with functional but is really an orthogonal feature as well.

So what is left? I would say is the ability to create closures because that clearly MAKES IT EASY TO CREATE FUNCTIONS without having to separately type the separate source-code of every individual function. Closures make that easy. Closures make it easy to "calculate with functions" because it becomes so easy to create new functions.

Yeah, I kinda think the Haskell branch ought to have its own term, because of the number of things that are involved that don't relate to "functions" per se, but it's not my call. "Pure functional" sort of works, but I would mean something more like "Pure and functional", that is, two separate adjectives, not one where "pure" is modifying "functional". A pure, non-functional language is certainly possible. "Pure imperative" is a bit hard to conceive (Rust probably as close as you can get), but an SQL database set to a high transaction isolation level can be seen as a pure non-functional language. (And I say "can" because it can also be argued against. But it's at least debatable.)
> Yeah, I kinda think the Haskell branch ought to have its own term, because of the number of things that are involved that don't relate to "functions" per se, but it's not my call. "Pure functional" sort of works, but I would mean something more like "Pure and functional", that is, two separate adjectives, not one where "pure" is modifying "functional".

“Pure functional” programming is functional programming wherein the functions are pure functions, that is: (1) the result of the function is completely determined by the identity of the function and its arguments, and not any other external state, and (2) the function produces no side effects that would impact calls to the same or other functions. (On a language level, only the first is really necessary, because if everything is composed of functions and all functions results are independent of external state, any side effects would necessarily not be observable within the language; but functional purity can be discussed with regard to constructs within a language where purity is not required at the language level.)

> A pure, non-functional language is certainly possible.

You can have referential transparency in a language whose central structure is determined by a paradigm other than the functional (e.g., referential transparency is just as much a key feature in the logic programming paradigm as the functional paradigm, and pure logic programming is already used to describe logic programming with strict referential transparency in the same way pure functional programming refers to functional programming where with strict referential transparency.)

> "Pure imperative" is a bit hard to conceive (Rust probably as close as you can get),

Rust is basically an ML-family functional language that moved off in a different direction than the one toward purity; it's closer to impure functional than pure anything.

Thanks very much for writing this.
> There's really multiple definitions of "functional" right now

> ...

>> ...monads...

There are also a couple of definitions of "monad" going around -- in array languages (J, APL, Q) a "monad" is something with arity 1 (like unary negate), to be contrasted with "dyads" which take two parameters (infix operators etc.)

Yeah, and don't confuse O'Caml "functors" with Haskell functors or you're in for several very confusing hours. Hooray terminology!
Or Prolog functors...
Haskell-style types aren't a prerequisite for functional programming. Lisp and Elixir are dynamically-typed, which is why you don't see monads in those.

That said, they do occasionally form useful abstractions.

> Haskell-style types aren't a prerequisite for functional programming.

Sure.

> Lisp and Elixir are dynamically-typed, which is why you don't see monads in those.

More to the point, Lisp and Elixir are impure functional languages, which is why you don't have some pure construct, like monads, that isolates IO.

> you don't have some pure construct, like monads, that isolates IO

In Haskell IO is isolated, and then the Monad interface is used for some particularly important ways of interacting with values of that type. That's not quite the same thing as "monads isolate IO" - the Monad interface is useful for quite a few other types of values.

Can you explain why you don't need monads if your language is dynamically typed? That's a new one to me.
I don't agree with that "not needed" explanation. But they are quite useless if the compiler can not deduce that both the values returned from the functions you are calling are monads, and what kind of binding should apply to them.
I find them to be at least somewhat useful in a language like javascript. I imagine they are more useful in a typed language with proper inference, but I wouldn't say they're entirely useless in a dynamic language.
OK, for the slow of understanding:

What I think you said is that, in an untyped language, a function will just return "something" (as far as the compiler is concerned). But in a typed language, the compiler knows that a monad was returned. So the point of the monad is to make the compiler do magic, not to make the runtime do magic.

Is that accurate?

Well.. It's like 25% accurate, and you hit the part you were most worried about. But you have been seriously misled about monads in the past.

There is no magic in monads. At the level of runtime operations, they're just a couple of functions.

The magic taking place in the compiler is type inference guiding the selection of the right pair of functions during compilation. Without that, you have to explicitly use the correct functions. It's much easier to offload that bookkeeping to a computer than it is to do it yourself.

Functors have fmap. Pointeds have pure. Monads have fmap, pure and join/flatten. So what's missing from "functors that can flatten" is pure, and monads are more precisely "pointed functors that can flatten".

This is the canonical source: https://wiki.haskell.org/Typeclassopedia

It's a pretty usefully compact definition.

In my understanding however, it's valuable to note that the chain-ability of the bind operation also sets up a continuously nested set of closures, which is where the real power comes into play to give you a useful approximation to imperative programming. (This can easily be abused, of course, to circumvent thinking and structuring code functionally.)

Related to this, SJP stressed in a talk some years back about how monads conveniently encapsulate the unavoidable messiness of side-effects in the least painful way yet discovered.

The Option/Maybe monad is very common in FP languages. I don't know if it's native in Elixir, but apparently it's pretty easy to implement: http://www.zohaib.me/monads-in-elixir-2/#maybe

There are lots of ways to conceptualize monads. I think "functors that flatten" is accurate, but is just one way.

imo, that's basically what they are. altho the fact that they can be created via unit is actually really useful too (even tho thats technically part of applicative).