Since first reading it, I’ve come to disagree with this post. For personal projects, my main language is Haskell, which Hague counts as a ‘puzzle language’. But that doesn’t match my experience: if I know the problem domain, I can write a solution very fluently in Haskell. (If I don’t know the problem domain, of course, it’s difficult in any language.) I find Haskell as easy to write programs in as Python, if not more so.
On the other hand, there are some other languages I find baffling. For instance, C++. Whenever I have to use C++, I find it horribly confusing. It takes me considerable effort to figure out how to structure my program in such a way that the compiler won’t, for instance, insert a destructor call before I’ve used an object. Or that I won’t run into any one of the considerable number of footguns C++ contains. This is a language I find truly puzzling.
For this reason, I suspect that what is a ‘puzzle language’ is mostly to do with what you’re familiar with and the way you think. For me, C++ is a puzzle language. For Hague, Haskell is a puzzle language. For both of us, J is a puzzle language — but I can easily imagine someone who can write programs easily in J.
Same. I struggle to write concise and complex code in Python. I often think of a problem in a functional sense, and would like to just map/filter/reduce and pipe my way out of it. With python I feel constrained by what the language gives me. Useless lambdas, list comprehensions that quickly become hard to grok, stdlib and syntax that forces you to wrap things like reduce(map(filter(iter))) instead of iter | filter | map | reduce.
I'm always in awe when I see the Advent of Code solutions from Norvig in Python, they often seem so elegant. But my head isn't wired to think of the problems that way.
It turns out that if your main paradigm is X then writing a solution in paradigm Y is a puzzle, as you have to convert things in your head from a natural representation to a different one. Writing performant C++ has genuinely become somewhat difficult after working in a semi-pure manner for a long time, despite C++ being my first and main language.
Perhaps, but there are languages that obviously differ from the "general" paradigm, such as stack programming. I guess if you only ever do stack programming, "stack programming" is just programming, but obviously stands out over all languages.
The article distinguishes between prog-lang specific puzzles and "programming in general is a puzzle", but without a universal paradigm it's not clear what that is - for example, stateless functional programming might be a puzzle, but the "norm" requires memory/state management as a puzzle; imperative/declarative is the same - which imperative is the norm, it's not clear to me it's less of a puzzle to have to explicitly think about sequence and state change.
I think it is not that simple. The OP seems to have worked with different programming language and paradigms. It also has to do with the type of problem domain you have to deal with. For some type of problems, functional languages are great. For other types, they are simply not.
In my experience things get 'complicated' when you have to deal with a distributed system where multiple actors are querying and changing a shared state. (And that shared state can be stored in one or more databases and/or in files in a file system.)
I disagree with this part "If I don’t know the problem domain, of course, it’s difficult in any language."
The main benefit of non-puzzle languages is incremental progress - even when I am not solving the entire problem, I am building functions and data structures which can be used to both understand the problem domain better and eventually to construct a complete solution. Sure, that might lead to an initial inefficient solution and require some rewriting, but often it's "good enough" (tm). That is why scientific Python is so successful, even though the final result is usually a patchwork of poor choices held together by duct tape.
Some languages map objectively better on that paradigm, independent of personal experience or ease of use.
I agree that haskell does not sound like a puzzle language to me even though i do not use it. Forth however sounds like a puzzle language to me. Probably because you have to completely shift your paradigm to solve the problem. It does feel a bit like those toy language you play with in certain games for example:
i would like to see an example of how the c++ compiler destructs an object before you use it. the only reason for this to happen that i can imagine is not understanding the concept of scope.
This could happen any time you return a pointer to a variable with automatic storage and then later attempt to dereference the pointer and do something with the value. Of course, you shouldn't do that, but the C++ compiler won't stop you. 'Scope' can have various meanings, but merely understanding how lexical scoping of variables works is not sufficient to understand why you shouldn't do that.
Ok, but then what you're saying is almost tautological. Indeed, you won't be surprised when an object is destroyed in C++ if you understand how object lifetimes work in C++.
As I recall, the issue in my case involved a default copy-constructor which destructed an object wrongly. I’ve since learnt to delete the implicit methods of a class, but I still find reasoning about copy and move semantics to be thoroughly puzzling.
i would still think this is a misunderstanding of scope. and you really don't want to be worrying about move semantics unless you are a performance maniac, a library writer, or (most likely) both.
I am such a person! I make J videos at http://youtube.com/@tangentstorm (many of them are recordings of live-coding on a single J application), work professionally in another array language called K, and also use forth (one of his other puzzle languages) for various things (currently working on a forth-like scripting language for an animation tool, for example).
I would definitely categorize J as having a strong puzzle culture. Many of the people who use it are just doing it for recreation and to stretch their brains, and insist on writing everything in "tacit" (J's version of point-free) style.
Personally, I'm more interested in building things. Most of my J and K code look like any other scripting language, but just very highly compressed.
I would agree that Haskell is not quite in the same category, and don't get the sense that Haskellers strongly favor a particular brand of bending over backwards to make things happen. But then again I'm not nearly as tuned in to the Haskell community, and he was writing this 14 years ago...
This article (and your counter) feels to me like a higher abstraction level of the all too common code "readability" argument. The language isn't necessarily a puzzle language any more than this or that code-segment isn't less readable. It's all just more or less familiar to the reader. Language readability is MOSTLY on syntax or small bits of idioms and how expressions are put together. This article is just another level up from that.
"readability" is an aspect of the reader, not the code.
I agree. Puzzle languages are only puzzling until you mind-meld with the language. TFA is right that non-puzzle languages are easier to get going with, at a [steep] price anyways. The problem with TFA is that it doesn't really address that price.
This is tangentially related to Puzzles-vs-Problems in Rich Hickey's Effective Programs
> Eventually I got back to scheduling and again wrote a new kind of scheduling system in Common Lisp, which again they did not want to run in production. And then I rewrote it in C++. Now at this point I was an expert C++ user and really loved C++, for some value of love. But as we'll see later I love the puzzle of C++. So I had to rewrite it in C++ and it took, you know, four times as long to rewrite it as it took to write it in the first place, it yielded five times as much code and it was no faster. And that's when I knew I was doing it wrong.
[...]
> So I mean for young programmers, if everybody's tired and old, this doesn't matter any more. But when I was young, when I was young, I really, you know, when you're young you've got lots of free space. I used to say "an empty head", but that's not right. You've got a lot of free space available and you can fill it with whatever you like. And these type systems they're quite fun, because from an endorphin standpoint solving puzzles and solving problems is the same, it gives you the same rush. Puzzle solving is really cool. But that's not what it should be about.
I definitely feel this effect, in that some languages are definitely about playing with the language. This can be helpful and make things more productive, but is definitely felt more in some languages than others. It's almost whimsy.
Haskell has this, Rust has this (where you're enticed to imagine everything as moving in and out of memory with a certain flow), C++ has this (trying to imagine how you can take advantage of as many features as possible to get beyond C's expressive ceiling).
Scala definitely has this, if only by making it impossible to write more than 2 lines of Scala without having to make 4 decisions around language feature usage.
You can see attraction to this kind of whimsy in the community.
I would definitely place Ruby in the whimsical language category though. There's way too much fun being had with mixins in that ecosystem for it to be considered a "straightforward" language.
I think the "puzzle" is just translating from your current mental model into one that fits the language. That's why the author sees roughly Algol-like languages as non-puzzle languages, and all other styles are puzzle languages to them. People with different experiences probably think differently.
I haven't heard this concept before, but it is an interesting one.
I immediately recognized that for me, Rust (which I am teaching myself) is a "puzzle language" centered around pleasing the borrow checker. Most of my time I find myself pleasantly enjoying the elegant design of the language and of the libraries written for it. But there is a solid chunk of my time spent knowing exactly what I want to happen and trying to figure out how to express that in a language that prohibits aliasing.
> A critical element of puzzle languages is providing an escape, a way to admit that the pretty solution is elusive, and it's time to get working code regardless of aesthetics.
and my mind immediately went to Rust and the "unsafe" keyword.
Rather than 'puzzles', I think the issue is that these are 'big idea languages'. (Eg., haskell = pure lazy). These ideas permeate all levels of program design, making programming often converting the naive interpretation of the problem into their Big Idea.
Phrased this way, it seems like they won't be puzzles in certain domain-specific areas; ie., when the naive problem suits their big idea.
So I'd say the 'puzzle issue' arises because big-idea langs are just not good for many problems.
It seems a puzzle language is one where you haven't fully grasped some conceptual foundation of the language.
For example:
> In Haskell and Erlang, the puzzle is how to manage with single assignment and without being able to reach up and out of the current environment.
You can say the same thing about Python: "Stupid language. I just want a block of RAM and a pointer. Pointer? Parlez vous memory access? Wo ist das Void-Pointeren? Where's that blasted phrasebook... " [1] Python has abstraction over direct memory access, Haskell has referential transparency and a type system that's both stronger and less confining than most type systems that get called 'strict' in the procedural world. Neither allow you to reach out of their environments, at least by default, and it makes both of them simpler and more regular.
[1] Non-English languages intentionally a bit misused. That's the point.
Obviously, the solution is to learn how the language wants to do things, and speak it like a native. This isn't about purity, really, just learning how to use tools as they're intended to be used.
> You can say the same thing about Python: "Stupid language. ..
I think you are misunderstanding the point of the article. The writer doesn't mean to say that his puzzle languages are bad, just that they take more thought up front.
I don't think that is bad, I think it can be a benefit. E.g. in Haskell you spend a bit more time upfront to get the types right and making sure everything is pure. The purpose of that is to have a program that won't run into any issues at runtime.
With a language like Python you have something ready to fun faster, but at runtime you can expect some errors, so you will have to spend your time at that point.
My point is that you don't spend as much time up front if you have fully internalized the way the language wants you to think. It took me a while to reach some kind of plateau in Haskell, and now I can simply compose working code at the REPL and build programs iteratively like how I can in Python and Lisp where, previously, I had to think hard about monads and types to get anything done.
It's hard to see the riddle some people would find in Python because we're all experienced programmers and imperative languages like Python were what we all started out on. For us, the riddle coming into them was learning how to program at all, so we didn't see the concepts we hadn't mastered as arbitrary stumbling blocks getting in the way of this new language being normal.
This is how I felt when trying to learn Elm: the program had to be correct, exactly correct, or it wouldn't work. You had to make every piece, every function, fit precisely, to define it's shape, it's exact inputs and outputs and effects ... in the end I found it very restrictive. I like the idea of loose-ness by default and adding contraints gradually (like javascript -> typescript).
> I can usually bang out a solution to just about anything in Python. I update locals and add globals and modify arrays and get working code
Globals in Python? They do exist but why ever use them? Not using them in Python invalidates most of the puzzliness attributed to Erlang and Haskell.
> Each [Erlang] process captures a bit of relevant data in a small, endlessly recursive loop. Imagine dozens or hundreds of these processes, each spinning away, holding onto important state data. Erlang string theory, if you will.
Calling an Erlang process to store state is equivalent to calling a method of an object. There are hundreds of important objects in any medium sized OO program, plus hundreds of thousands invisible ones, just because everything is an object. Nobody complains about creating the few important objects in Python and nobody complains about creating the few necessary processes in Erlang.
My complaint with Erlang and Elixir is that I have to define handle_cast and handle_call functions with the actual method name passed as argument instead of being able to define and use the real method names (or function names, it doesn't matter) as in any sensible language. It feels so low level, like OO programming in C around 1990. Erlang is excused because it's from that age. Elixir is not.
Go is not a puzzle language. I wonder what about Rust? Can we apply "puzzle language" to it? Maybe it depends on the level, how well you know the language?
> A critical element of puzzle languages is providing an escape, a way to admit that the pretty solution is elusive, and it's time to get working code regardless of aesthetics. It's interesting that these escapes tend to have a stigma; they induce a feeling of doing something wrong; they're guaranteed to result in pedantic lecturing if mentioned in a forum.
I immediately think of the borrow checker, and its escape called "unsafe" directly induces a feeling of doing something wrong. Yes, rust would be a puzzle language by this criteria.
I would say that if you want to do 'clean' Rust you could certainly end up in a maze of reference lifetime problems. However here your escape hatch is that you can just clone stuff or wrap it in Rc/Arc just to get it compiling, which is arguably the moral equivalent of sneaking a global or two into a python program: icky, but gets the job done
It was specifically designed for peons (from the creator's perspective) so the condescension is baked in:
> The key point here is our programmers are Googlers, they're not researchers. They're typically fairly young, fresh out of school, probably learned Java, maybe learned C or C++, probably learned Python. They're not capable of understanding a brilliant language, but we want to use them to build good software. So, the language that we give them has to be easy for them to understand and easy to adopt.
May i counter with:
"Debugging is twice as hard as writing the code in the first place. Therefore, if you write the code as cleverly as possible, you are, by definition, not smart enough to debug it."
The ‘puzzle’ of imperative languages is often ‘how do I keep less state?’. Because they're so verbose, figuring out what the code _does_ (in terms of actual transformations to the data) usually involves imperatively running the program in your head, and so it's important to keep the amount of state that could be touched in any particular part of the code to a minimum, since humans only have so much working memory.
When I write code in an imperative language, I often spend a lot of time trying to find the optimal balance between complexity in the data and complexity in the control flow. As a dumb example, if you have a loop over some data that does one thing up to a certain point then another thing for the rest of the data, you can a) keep a Boolean that indicates whether the switch-over condition has been reached, or b) break the loop up into two loops, and fall through to the second loop once you hit the condition in the first loop. Without additional constraints I often prefer the latter (keep as much state in the control-flow as possible), but it can be very puzzling to figure out how to map your problem into a linear state machine, or to try to group lines of code into blocks that share invariants without accidentally overwriting some important data.
C is particularly designed around this kind of puzzling, and has features carefully built in to enable and encourage it, such as the pre- and post-increment operators, or assignment returning its RHS. C code considered elegant has usually been thoroughly optimized in this way. A prime example is this argument-parsing code from K&R [1]. When reading, I find it interesting to bear in mind that this is pedagogical code intended to teach people how to write the language, written by the authors of the language itself. Python tried to take away the ability to optimize for this sort of elegance by removing the syntactic features that support it, but the core puzzle still remains — you can carefully organize your code to ascribe meaning to things like loop breaks (especially with `except`!) or function returns. At its core this is because procedural code gives you a particular active data structure to work with (a stack of function calls each consisting of a set of mutable variables and a list of statements that mutate those variables, possibly with loops in) that is insufficient for encoding the natural structure of many programming problems, and you so have to make choices about which bits of the problem to map to it and which to code up manually with explicit additional data structures. This is not specifically a criticism of procedural languages: all other paradigms also limit you to one data structure or another, FORTH being the one that is most up-front about it.
In general, I think it is a fallacy of perspective to think that the difficulty of programming is broken down into ‘understanding what the code should do’ and ‘explaining that thing in your programming language of choice’. Rather, the problem of programming is precisely ‘understanding what the code should do in the computational model used by your programming language of choice’. If there is a universal model of computation in which we (as human programmers working on commercial timescales) can understand the meaning of programs in a way that is completely independent of the target language (such that mapping it to any target language is a purely mechanical process) I don't think we've found it yet. Someone steeped in a particular programming model will tend to consider their model effectively universal, and translating their mental model (expressed in terms of that programming model) into a different model of computation to be a puzzle; but in fact even encoding the ‘understood’ behaviour into a real-world programming language based on the same model usually involves making non-trivial decisions, which implies that our mental models of computational models are usually low-fidelity and/or may disagree with reality.
On the other hand, there are some other languages I find baffling. For instance, C++. Whenever I have to use C++, I find it horribly confusing. It takes me considerable effort to figure out how to structure my program in such a way that the compiler won’t, for instance, insert a destructor call before I’ve used an object. Or that I won’t run into any one of the considerable number of footguns C++ contains. This is a language I find truly puzzling.
For this reason, I suspect that what is a ‘puzzle language’ is mostly to do with what you’re familiar with and the way you think. For me, C++ is a puzzle language. For Hague, Haskell is a puzzle language. For both of us, J is a puzzle language — but I can easily imagine someone who can write programs easily in J.