> The idea of "mostly functional programming" is unfeasible. It is impossible to make imperative programming languages safer by only partially removing implicit side effects.
Except that people do it all the time, and they find it safer than not doing it.
If I understand the article correctly, even if the programmer is reasoning and coding functionally in an imperative language, the compiler can not assert that the code is safe for concurrency and parallelism. (You may find you get other benefits from programming functionally.)
Rust seems to have solved some of this with its borrow checking semantics that automatically prove which parts of code are automatically paralellizable without running into issues like simple data races - or am I wrong?
The Rust compiler doesn't automatically parallelize, but all single-threaded Rust code upholds the invariants necessary to be thread-safe, so it's relatively easy (and sometimes entirely trivial) to turn a single-threaded program into a parallel program.
> Imperative programs describe computations by repeatedly performing implicit effects on a shared global state.
Is this necessarily true? In Rust programs, operations do not operate on a shared global state – at least not in the way I think most people would understand 'effects on a shared global state.'
The enemy is shared mutable state. Functional languages solve the problem by focusing on immutability and statelessness, whereas Rust solves the problem by letting your state be shared xor mutable.
Tools that prevent the developer from expressing fundamentally invalid ideas? Great. Not so great are:
- Tools that force the user to adopt a mindset that the tool designer prefers; essentially this bounds the expressive power of the tool artificially for aesthetic or other opinions.
- Tools that prevent the user from even understanding the danger that the tool is guarding against. IMO developers need to know what concurrency is, what the range of various numeric types are, how signed values actually work. I know others disagree on this, but idiot-proof tooling doesn’t help people grow past a certain point.
Idiot-proof tooling helps non-idiots, whereas idiots may not by definition be helped in any way.
I know the internal representation of integers and floats and I know how important that knowledge is. I reverse-engineered a gnarly bug in a codebase just last month using it. I'd still rather my programming language have 10*100 resolve to the actually correct answer rather than undefined behavior.
I have written production code with pthreads, but I'd still rather use async if realistic for the task at hand.
I know how to manage my own memory but I'd still rather use a GC'd language if realistic for the task at hand.
I know how to write queries directly to the postgres socket, but I'd still rather use a proper client.
I know how to write code in vi but I'd still rather write code in jetbrains.
I'd really love to use an IDE, but I haven't found any modern ones that I don't hate. Which is curious, since I used to love the Borland IDEs (Turbo Pascal, Turbo C). Maybe I'm just spoiled, but all the IDEs that I've tried since have managed to turn me off quite thoroughly, in one way or another.
> I'd really love to use an IDE, but I haven't found any modern ones that I don't hate.
I agree with this so very much. The modern IDEs I've used are pretty awful. They get in my way and doing anything uncommon in them often requires a small research project.
Some more so than others, of course. I think my least favorite may be VS, although the Jetbrains offerings are a very close second.
I've taken to just not using an IDE at all for my personal projects. At work, I use what I'm required to use.
> I know how to write code in vi but I'd still rather write code in jetbrains
I hard pass on that, Jetbrains is probably the slowest IDE i have ever used. Probably great if your PC is a war machine, mine usually aren't (company provided ones, and my laptops).
I'd rather code with an old vi with minimal plugin and color support than with Jetbrains. I was highly critical of Netbeans thanks to similar slowness issues when i used it for the first time 11 years ago (it was with a VERY large codebase), but Jetbrains is the worst. And this slowness triggers me too much to do anything.
Like the sibling comment said, Rust is a special case. The ownership model is there to fight shared mutable state.
With respect to other languages, this part is interesting:
> at least not in the way I think most people would understand 'effects on a shared global state.'
You're right - most programmers do not think of it as shared global state, once it's put behind something like 'private', and exposed via getters and setters, but it is.
You can take any race condition you like, wrap the offending operations in getters and setters, and you'll still have the same race condition.
> Like dieters falling for magic 10-minute-miracle exercise gadgets, developers seem ready to fall for easy solutions to the latest crises in their field.
> Just like "mostly secure," "mostly pure" is wishful thinking. The slightest implicit imperative effect erases all the benefits of purity, just as a single bacterium can infect a sterile wound.
Taking the analogy to its logical conclusion, the only healthy diet is a 100% strict diet allowing no impure substances. Very few people can actually live this way and it's questionable whether anyone should.
The author's audience is those who wish to make safe programs. He advocates modeling all effectful computations using the type system using the framework of monads. He acknowledges that there are even more expressive frameworks like separation logic systems, but that these require too much theory to be useful to the everyday developer. So the author clearly acknowledges the tradeoff between pragmatism and safety, but it's unclear why it's better to draw the line at monads rather than separation logic.
Many software developers draw the line at, "seems like it should work" and most are satisfied with the type of "mostly safe, mostly functional" assurances offered by Rust. Very few go as far as to prove their programs safe in separation logic, and indeed the cost benefit is questionable for most software systems.
In my earlier career, I might have read an article like this and become some sort of a monad zealot and insist that the entire codebase has to be rewritten using monads in pure functional style. In most cases, this would be a highly questionable decision. If you are writing a game, or making a web server for a dating app, an imperative style is probably the better choice.
>> Just like "mostly secure," "mostly pure" is wishful thinking. The slightest implicit imperative effect erases all the benefits of purity, just as a single bacterium can infect a sterile wound.
> Taking the analogy to its logical conclusion, the only healthy diet is a 100% strict diet allowing no impure substances. Very few people can actually live this way and it's questionable whether anyone should.
I have a simpler one: "Mostly const".
Mostly-const gives you the best of both worlds: you get the benefits of knowing it stays the same, and the practicaliy of being able to change it (despite those const zealots who demand that you shouldn't change const variables)
Erik Meijer is pushing back on language designers that push self-contradictory feature: "It's OO AND functional! Best of both worlds."
> … [T]he author clearly acknowledges the tradeoff between pragmatism and safety, but it's unclear why it's better to draw the line at monads rather than separation logic.
Probably because programmers are already writing functional code in “mostly functional” languages, and they’re not writing separation logic.
If we take familiarity out of the equation, why is a web server inherently better implemented in an imperative style rather than a functional one using monads?
Familiarity is part of the equation. Writing an efficient web server requires being familiar with the imperative model since the underlying specs, protocol reference implementations, and libraries are probably given in imperative style.
Familiarity aside, using a monadic style introduces constraints in your code. I concede that sometimes those constraints will protect you. But for most use cases, these constraints will just produce nuisance compiler errors, forcing you to write dummy wrapper functions to coerce certain types into other types.
Consider the example of a loop whose condition and body are dependent on user input and a global state variable.
global max_iterations = 100
def get_messages():
iterations = 0
messages = []
while input() != 'quit' and iterations <
max_iterations:
messages.append(input())
iterations += 1
Writing this in monadic style requires much more finesse. The global variable must be eliminated. Then function body has to be represented as an expression of type IO[list[string]]. The loop body evidently has type (bool, int, list[string]) -> IO[(int, list[string])], and the conditional has type int -> IO[bool]. Sequencing the conditional and the body requires a recursive function and then maybe a lift operators to convince the compiler that the types work out. See https://conscientiousprogrammer.com/blog/2015/12/11/24-days-...
Sorry for dumb question, but can someone please explain to a non-programmer, why the author made the title "The Curse of the Excluded Middle"? What is so special about "middle"? What is middle in programming?
I am a fairly dumb person, but I am lately super interested in 1=0 math paradox/proof and it just occurred to me, what is the middle of rational numbers? What is the middle of irrational numbers? To me, it feels like the middle of all numbers is 0, but if irrational numbers are not considered "real", it can't be 0.
These 2 things are probably wildly unrelated, but I would appreciate any explanation of the linked blog post in the simplest terms. Thank you!
Why do people slip into Haskell syntax the second they task themselves with explaining monads to us, peons, once again?
Use C, JS, Python, Rust, PHP. Show us you really understand the essence of them not that you can just reproduce notation like they were some magic incantation that needs to be invoked verbatim otherwise magic won't work. Why do any signs of being polyglot vanish immediately when they mention monads?
A (correct) description of monads requires talking about higher-kinded types, which rules out those languages immediately, unless MyPy has come a long way since I last looked.
Apart from that, frankly, I think that Haskell syntax (or just mathematical type syntax, which looks very similar) is so much cleaner when talking about higher-order functions that it's worth spending a little while explaining what `m a → (a → m b) → m b` means in the prerequisites of the article rather than have the reader have to wade through `Callable[[Kind[M, A]], Callable[[Callable[[A], Kind[M, B]]], Kind[M, B]]]` [1] for the rest of it. When you're trying to explain a novel topic, the last thing you want to force your reader to do is waste their brainpower counting brackets.
> A (correct) description of monads requires talking about higher-kinded types, which rules out those languages immediately
No it doesn't. The fact that those languages don't have syntax for some specific form of typing doesn't mean there is no way to express concept of a monad in those languages.
It's way easier to explain something if you can express it in the language that the recipient already knows like js or English instead of introducing completely new bizzare syntax and vocabulary.
If I want to know what's a variance I don't need to know all associated theorems with proofs and special notations and vocabulary. I need a formula and examples of application.
I used the ‘(correct)’ there to notate a little bit of pedantry :) You can of course write interesting things about monads in languages without HKTs, and use them as a bridge to explain the operations of real monads — but those things will not, strictly speaking, _be_ monads, which are a sort of type constructor (map on objects) with some extra operations.
You can of course also always implement a language with HKTs in a language without HKTs, deciding which parts of the language should be read as types and which as terms, but it's unclear to me once you start doing that exactly where we should say you're no longer expressing the concept in the meta-language.
There is a lot of theory but it is way more informative to say that a monad is just type (in colloquial sense of the word) of containers that have few particular behaviors associated that do wrapping and unwrapping and applying functions to their contents. The problem with that is that all of that is trivial and barely a curiosity. To make monads interesting you have to show specific instances of monads like Maybe. So the monad part is not interesting. The interesting part is what they do besides being a monad. And in some cases you need to depart from monadic purity pretty quickly to get useful behavior (Promises). So I suspect that people talk about monads mostly in the context of HKTs because that's the only place they can seem impressive.
To give analogy, I'm sure that integrals in formalization of Lebesgue are super impressive but when you try to teach someone new about integrals you don't start with that and the minutia of all the math notation required for correct formalization, because "area under the curve" or "inverse of differentiation" are way better as they have immediate utility and operate in the context of what the learner already most likely knows.
There's a problem in the teaching of monads that containers are the easiest ones to explain, but also the least interesting in practice :)
>To make monads interesting you have to show specific instances of monads like Maybe.
While individual monads are useful by themselves, the _concept_ of ‘monad’ is only interesting once you can generalize over it. If you can't write (interesting) code that's generic over an interface, there's not much point in doing the work to generalize from the specific instances of that interface. But part of why monads are interesting is that it turns out a lot of natural ‘sequential’ code that one might write in a procedural language actually generalizes naturally to an arbitrary monad, in ways that make the resulting code drastically (even unintuitively, for someone used to thinking in terms procedural code just meaning single-valued strictly-ordered sequencing) more powerful.
I think to me the idea that talking about monads as monads is only useful once you can generalize over monads, and HKTs are necessary to effectively generalize over monads (‘effectively’ meaning without undue cognitive or syntactic load), is a much more likely explanation than the slightly uncharitable one that people are pulling in HKTs as a smokescreen for bloviation.
> In some cases you need to depart from monadic purity pretty quickly to get useful behavior
One of the main motivations for introducing the monad interface to semantics, and thence to programming, is that notions like stateful, multivalued, or asynchronous execution that are ‘impure’ (i.e. outside the model) when considering a program as a function (Scott semantics) can in fact be reasoned about in a ‘pure’ way by adding a monad to the codomain of the function. So things like `Promise` are not ‘departing from monadic purity’, they're good and true examples of upstanding monads :) Even ‘naughty’ monads like Haskell's `IO` can notionally be thought of as pure — the fact that the interpretation of the monad is done by a magical external runtime and not by pure functional code that passes the world around is more an artefact of the external code of the OS/computer you're running on, and you can think of it as a sort of implementation detail/performance optimization.
> "area under the curve" or "inverse of differentiation" are way better as they have immediate utility and operate in the context of what the learner already most likely knows.
Fully agree with you there, but you don't then try to explain algebraic topology in terms of the area under the curve. You can (and generally should!) start from a point the reader is already familiar with, but it's a perfectly defensible decision to go from there on a path through intermediate concepts that make explaining the target notion easier, rather than throwing the reader straight into the deep end. Of course the choice of how long or short the path should be (and therefore how big the jumps one expects the reader to make) is a decision that is the writer's responsibility.
I'd agree if we were talking about learning the full language, but a little lightweight notation often pays for its cognitive cost quite quickly. It's a trade-off, of course, but I can see why an author would choose this point on the spectrum, especially for an audience like advanced (probably polyglot) programmers who are likely to be skilled in quickly picking up new notations.
> but a little lightweight notation often pays for its cognitive cost quite quickly.
While I broadly agree in principle, it appears in this particular case, all the explanations that start with Haskell syntax never gets the point across, while a single explanation in mainstream syntax did on the first attempt.
I think Haskell syntax is just too far off from mainstream to serve as a vehicle for teaching.
It would be really interesting to see some empirical data on this!
I think there's a continuum on which one can position a piece of writing, from ‘starting from a prerequisite of the piece, jump directly to the target concept’ (Feynman style) to ‘provide a long winding path of semi-related ideas each building on the last, such that by the time the reader has grasped them all the target concept is obvious’ (Grothendieck style). My intuition says that the ideal point on that continuum varies from person to person, but there's probably an optimal average. Maybe it's been studied in didactic theory?
I notice there are different cultural norms, as well. In the mathematical world it's very common to start by introducing a bunch of new notations that make the concepts in the rest of the paper easier to discuss, and the designing of those notations is an art that people spend a lot of time on. The equivalent in the programming world, I guess, is the ‘language-oriented programming’ style propounded by Lispers (and, to a lesser extent, Rubyists) in which you write a program by first defining an EDSL for your business domain, and then directly writing down your business logic in that language. I notice this syntactic approach hasn't survived very well, even though the philosophy behind it (e.g. ‘programming as theory-building’) is still largely in vogue.
Ironically: via the lens of ideas embodied by classical / full linear logic, there’s very elegant programming ideas the exactly correspond with the law of excluded middle.
Unfortunately: as much of programming (eg any non-degenerate evaluation) fails to be injective, the law of excluded middle fails just where one might wish most to apply it.
This of course explains quite clearly why Haskell has taken over all large-scale software development in the decade since it was written.
Personally, I prefer to just not be careless with side-effects. And maybe add some runtime invariant checks if I do have to write something that's too easy to misuse.
honestly, I've yet to see a practical application of pure functional programming that is that much better than mostly functional.
it's hard for most people to follow a completely pure functional programming model, but certain concepts like anonymous functions are still very nice and practical
Every ten years it's discovered that another idea PL researchers have been harping on about about for forty years is very nice and practical once people get used to it :)
I remember when anonymous functions (indeed, first-class functions at all) were considered academic nonsense that just obfuscated problems. After all, who can keep track of all that complexity of functions calling functions?
There are many practical examples already in Haskell. Pure functional programming is not necessarily about avoiding effects, but about controlling, tracking and managing them. This enables better reasoning and more safety guarantees. For example, software transactional memory, the STM monad, allows optimistic updates to shared mutable state by safely supporting rollbacks.
imperative programming is like sitting in one chair; pure functional programming like sitting in the neighbouring chair; and Meijer's thesis in TFA is that "mostly functional" programming will turn out to be like attempting to sit in the space between them.
(given his involvement with C#, I am willing to consider the notion that his views may well reflect having had unpleasant experiences. If Rust moves beyond the "innovator" segment, it would be a counterexample.)
Edit: NB (2014) ; he may well have revisited this topic more recently?
thanks, I know erik uses kotlin often, does he think we just have to live in that middle area? is the paper just an observation ? how do you think rust solves the problem?
In imperative programming, you write statements which change the state of the system.
When you go to debug a method, you need to be able to recreate the state of the system in order see how the method misbehaves.
But a function always has the same output given the same input. It doesn't matter how big the function gets or how many other functions it calls. So if f(1, "foo", 47) throws an exception on a prod server, you can run exactly that function locally and watch it throw the same exception.
Now someone might come along and change f's source code so that it mutates something, or reads from something which can be mutated. And they might call f 'mostly functional'. But it no longer returns the same output for the same input. f(1, "foo", 47) may or may not equal f(1, "foo", 47), so you're back to debugging methods, not functions.
A method being 'mostly-functional' is like 'int i' being 'mostly-const'.
thanks! so does erik suggest one or the other? or that we must all live in that middle? last I checked he was knee deep in kotlin which I'm assuming is in that middle area?
Except that people do it all the time, and they find it safer than not doing it.