The Curse of the Excluded Middle (2014)
queue.acm.org
queue.acm.org
Except that people do it all the time, and they find it safer than not doing it.
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.'
- 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.
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.
(...)
Where does the fascination for not using proper tools come from? I'm reminded of this: https://www.youtube.com/watch?v=3I_Ds2ytz4o
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.
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.
> 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.
> 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."
Probably because programmers are already writing functional code in “mostly functional” languages, and they’re not writing separation logic.
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-...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!
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?
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.
[1]: https://sobolevn.me/2020/10/higher-kinded-types-in-python
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.
https://dev.to/rgeraldporter/building-expressive-monads-in-j...
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.
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.
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.
>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.
Isn't that less waste than trying to get them to learn a different language, well enough to understand an advanced concept in that language?
Do you want to spend time explaining the concept or do you want to spend time explaining a new language syntax?
Pick one. You can't have two goals for one explanation.
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.
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.
And like other fundamentalist thinking in other areas, the real world begs to differ.
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.
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
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?
I've been programming since the mid-90s. I've used both Lisp and Haskell.
I don't ever remember even crusty old C greybeards considering anonymous functions to be academic nonsense.
for the title, see https://en.wikipedia.org/wiki/Law_of_excluded_middle
(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?
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'.