Minimalism in programming language design
pointersgonewild.com
pointersgonewild.com
In a wood shop, you can do everything with a hammer and chisel. You can make a table leg with them. It'll take a long time and won't be super round. However, having a lathe makes it much faster and gives you a better end result.
Also, it's been said that design patterns are just ways around missing language features. What one person considers language complexity, another seems as an essential abstraction.
Go also made the fantastic decision to design in concurrency as a primitive... And then kinda for on the hill of generics. There's a lot to be said for choosing the right set of features, keeping the code readable as a first priority, and letting libraries handle the rest... Need a lathe? Import lathe!
There's nothing to add for what its creators stated: Go is meant for under-average programmers, not for professional software developers.
It depends on what benefits those language features have. More often than not, they do not provide benefit to your business.
Plenty of professional systems are built in Go. This opinion that Go developers are subpar is silly rhetoric. It is not meant for everything, but for distributed systems it is great.
I don't think so. But it's a fact that Go was meant for sub-par programmers.
Seeing the kind of usage go is known to have inside Google, and how is generally accepted that Google hires only the best engineers, this fails to be consistent. I doubt it very much that anyone involved in Go development has said anything that can be interpreted anywhere close to what you said.
Also, to continue being pedantic I must say that by definition every programmer that gets paid for their work is a "professional software developer" despite the hyperboles grandparent seems to be fond of.
The C language is a relatively small language, but has been used to write complex databases and operating systems. So is C simple or complex? Or both? (Everyone has a different opinion).
Also, there's also a distinction from the small language core (the syntax and lanuage features) and the much bigger standard library that provides additional functionality.
I think this is equivalent to the "spherical cow" problem in physics. A lot of calculations in physics are much simpler, if you assume everything is a spherical cow. However, the real world is messy, and not everything is a spherical cow.
In programming, a lot of complexity comes from interfacing to the real world. Whether it is CPU performance/memory hierarchy, or messy business logic.
The drive for simplicity, often ends up sweeping these under the rug.
it is an optimization problem, and the optimum isn't at the extremum of the constraints, it is somewhere in the middle.
just because C++ is a bit of a mess after 37 years of evolution doesn't mean that the simplest language is best.
rust is also the first widely used language with a borrow checker and may suffer from having to discover the correct primitives while hunting around in the dark to begin with.
Rust is the first widely used language that addresses the question, how do you have memory safety without garbage collection and loss of efficiency. It comes up with (IMO) a very good answer, but likely not the final answer to this question.
But I think trying to answer that question, more than justifies the complexity.
I pretty much agree with everything you said, and think it's a good analogy, but eventually come back to the point of the article we're responding to, which is that a lot of the complicated syntax, and features that languages introduce, just ends up non-essential complexity around the inherent complexity of the real world, that you're highlighting.
So you can't abstract over data types in C? I often do. Some random thing I often do:
void linear_allocate(Linear_Allocator *alloc, size_t size, size_t align, size_t count);
#define linear_allocate_type(alloc, type, count) ((type *) (linear_allocate((alloc), sizeof (type), alignof (type), (count))))
(Anybody objecting they don't like macros and this is not a real abstraction: You are all fools. To a certain point, that is - before you possibly start being right. But don't underestimate how far this can go, and how long this can be a superior solution over alternatives.)You know something that is very hard to abstract over? Programming language FEATURES! Anybody, please show me the ways of doing computation over all the data that was encoded as fancy programming language syntax. This requires even more language features to get this data back from the compiler in some weird form... if possible at all.
Happily! https://lisp-lang.org/
At risk of stereotyping myself, if you like this kind of thing, check out SICP. It is a really good dive into exactly how far you can go with this kind of tool, and also has applicability across the engineering discipline (not just for lisp folks).
Representing code as data is not very interesting IMO. Code is code, data is data. Granted both are stored in RAM, but they are very different beasts considering how they flow through the machine. Almost any language allows to parameterize over functions (e.g. function pointers), but there is no need to make closures a first class citizen. After all, closures are a brutal abstraction, isolating any context from the caller. When overdone this ends up in a terrible mess where you don't know what's happening anymore (otherwise known as callback hell). Conclusion, use this only as a last resort.
Most of the complexity should be in the libraries. Only a small set of select people can modify the core language. Anyone can make a library.
You can have a conceptually simple language, but include a massive standard library with everything most users would need.
I think there are two different definitions of simple in programming languages: simple to implement and simple to use. Simple to use programming languages can be quite complex under the hood. Python’s bigints are an example of a relatively complex to implement feature that simplifies life for developers.
Whether language features and restrictions simplify life for developers depends on what programs they want to write, though. If you want to emulate wrap-around 32-bit unsigned integers, for example, that’s harder in python than in C.
So, whether a language is simple to use will depend on what programs developers want to write in it. Consequently, language designers should know their audience when designing a language.
In a similar way, even though the lambda calculus, SKI combinator calculus, or and MOV-only assembly are minimal in very real ways, the cognitive burden of expressing yourself in these languages is very, very high!
To pick a friendlier example, I'm a big fan of Lua. Lua is deeply minimalist, and from what I know, its developers try hard to keep its feature set orthogonal (and it has my favorite language feature, coroutines!). It's a great fit for certain domains. Nonetheless, it's not a great fit for most domains; for instance, I never got the sense that it could scale much beyond one-person projects.
General-purpose computing languages are in a bit of a catch-22. People need to do such wildly varying things with them, that in order to be minimalist, they must necessarily put some distance between the features they provide and the use-cases they are applied to. Conversely, to be expressive, they must minimize the amount of encoding required to take ideas in the problem domain and represent them in the language.
A domain-specific language (with high expressivity for the problem domain) invariably tends to take on general-purpose features, as users' needs expand and they push the language to its limits. A general-purpose language (with low expressivity for most problem domains) tends to take on more and more high-level features, to help bridge the gap between the language and the domains it is most often put to use in.
I think a polyglot environment makes a lot of sense. Use high-expressivity DSLs to serve your business logic, and high-generality languages to serve those DSLs. In some ways, that's the ideal of Lisp: your DSLs are indistinguishable from the host language itself, so the experience is seamless. But folks don't seem keen on polyglot environments, Lisp or not. So we're at an impasse.
I do not think this shows that Java is a simpler language, if anything it shows that it has more features (ie. being more complex as a language) that make it easier to match what you want to do.
Java might be simpler (easier) to use, but that doesn't make it simpler as a language itself.
For a more extreme example take C vs C++ - C is certainly simpler as a language than C++ and nobody would ever claim that C++ has any relation to simplicity as a language whatsoever, but a lot of things are much simpler to do in C++ than they are in C because of all the additional features C++ provides.
Sure, you may not be touching those during your web development, but that’s because you don’t know how to have fun and outsourced it all.
> it would be a tad slower
a tad? I'll use C++ as an example, but the last time I profiled a hash map in C++ vs C#, the C++ lookup took 1 nanosecond[0] and the C# version took 1ms (1000x slower) (I don't have these metrics anymore but I believe it was one ms or slower, and Google seems to confirm that).
Now, if the difference between C++, a machine compiled language, and C#, a runtime compiled language, is 1000x for a very common operation, how much slower will an interpreted language that is running at an even higher level be??
The reason 90% of our apps are such slow trash these days is because programmers think that there is no difference in a couple milliseconds when it is a huge difference if that couple milliseconds is being run thousands of times through hundreds of thousands of lines of code! Take a look at how fast the Jai compiler is compared to a modern C++ compiler. Or even a C compiler vs a C++ compiler. It's night and day because of how much cruft slowly gets added on.
So I find your claim that most of the Unix utilities are only written in C because that was mainstream in the day highly dubious. If anything, they would be written in something like Rust today, but definitely not JS or Python.
Edit: I would also like to point out how much of a pain it would be to write low level network code, like what's necessary in curl, in a language like Python. When you're writing low level net code you want as little interference between the bytes and the programming language. Because if your bytes have some hidden padding, or the endianess is swapped, or a data structure is aligned a byte off, it means the difference between between correct transfer over the net, and sending garbage over the net. Python is not conducive at all to understanding how an object will be transformed into q series of bytes, whereas C is almost a direct translation from a struct to a series of bytes.
[0]: https://gist.github.com/ambrosiogabe/66a6e2fdc77e6a600e570f4...
The ratio between 1 ns and 1 ms is a million.
Regardless, the benefit of not using C++ is and always has been: Not having to use C++.
C# benchmarks on my machine. FastFindRecipe just does a hash set lookup, it looks like this:
private static readonly HashSet<CraftingRecipe> FastList = new HashSet<CraftingRecipe>(new CraftingComparer());
public static CraftingRecipe? FastFind(CraftingRecipe r) => FastList.TryGetValue(r, out var res) ? res : null;
| Method | Mean | Error | StdDev | Min | Max |
|--------------- |---------:|--------:|--------:|---------:|---------:|
| FindRecipe | 315.2 ns | 1.17 ns | 1.09 ns | 313.6 ns | 317.4 ns |
| FastFindRecipe | 124.5 ns | 0.23 ns | 0.21 ns | 124.1 ns | 124.9 ns |Two orders of magnitude.
If you have to have multiple structures that need to be precisely laid out in binary, python is the wrong tool for the job. Delivering content using Tcp or Udp packets requires very explicit binary definitions of the data your sending. Sure you can use a wrapper on top of a library that deals with the low level stuff, and that's what Python is all about.
Use the proper tools for the proper jobs. If you're using C to write one off scripts or simple data manipulation like analyzing csv contents or something, then you should probably use a higher level language instead. If though, like curl, you have to be very explicit about the binary description of objects to accurately send and receive those objects between a network, then use a low level language.
> runtime isn't the constraint that we're hitting anymore
If you're sending data over the network you're absolutely hitting this constraint. Reading some data from RAM is on the order of like nanoseconds, sending data over the network is on the order of hundreds of milliseconds depending on the size and location you're sending that data. When it comes to sending data over the network, literally every byte does matter.
It seems like you were advocating using a higher level language for low level, systems programs. If that's the case I disagree. My original argument was about Unix utilities. Unix utilities contain everything from "list the files in a directory" to "compile this C program". The original poster was talking about how Curl would probably be written in Python if it was written today. This is just incorrect. From the author of curl[0]:
>> If I would start the project today, would I’ve picked another language? Maybe. Maybe not. If memory safety and related issues was the primary concern I had, then sure. But as I’ve mentioned above there are several others concerns too so it would really depend on my priorities.
> If you're talking low level microcontroller stuff, or bit banging, sure, do it in C if you like.
This is the argument I'm making. Unix utilities are usually dealing with low level bit banging stuff. Unix utilities are also meant to be extremely portable. C is the glue that holds all programming languages together, whether we like that or not. So programming a library in C automatically ensures that the library can be used in practically any higher level language.
https://floooh.github.io/tiny8bit/
https://floooh.github.io/sokol-html5/
https://floooh.github.io/visual6502remix/
The common pattern is though that both the C and C++ stdlib are mostly useless for this type of project, you need cross-platform libraries where 'cross-platform' also includes peculiarities of the web browser runtime, specific language features are (mostly) irrelevant in this case - except maybe for the bit twiddling thing in the emulators where C is clearly superior to JS (plus you get lean native versions "for free" without requiring a heavy Electron runtime).
Thankfully there are two different words with very separate meanings that people nonetheless tend to conflate [1]. "Simple" means "not many features", and "easy" means "good to get things done with".
[1] Maybe because we have lots of people speaking English as a second language? In my mother tongue, German, there is no distinction in vocabulary between "simple" and "easy". Both translate as "einfach".
I'm not sure about this. What utility is a language with only simple constructs that is hard to use.
Not quite a counterpoint, but a thought in that direction is:
There can be features that make it easy to implement something which some find hard to understand.
I suppose I question the value of simplicity for it's own sake in a language and would rather defer to how simple and understandable the resulting code is.
I frequently consider the added complexity cost of a more complex compiler inherent complexity because I find the simplicity of use dwarfs compiler simplicity or simplicity "just cuz".
Though if we want to make the clear distinction, like in the Clojure talk below, for the programming domain that’s fine.
For example, I consider most multi paradigm languages complex because in them functions, variables, objects, control flow, etc. are fundamentally different concepts.
But something like LISP, smalltalk or Haskell are simple in my eyes, due to almost everything being the same concept on a fundamental level and the other stuff being built on top of that concept.
Conway's game of life has an incredibly simple definition, and is Turing Complete. The Brainfuck language has a minimal number of language concepts and barely any syntax. And yet, programming in these languages is very hard.
Humans need a certain level of language complexity to be able to express themselves. When you simplify things it's not always clear if you're removing superfluous complexity, or removing tools needed to deal with inherent complexity of the problems you need to tackle.
I don't think there's even any single objectively appropriate level of complexity, because it depends on the complexity of the problem the language tackles. By analogy to human languages: scientific language has its own jargon (sometimes overused!), but it needs an extended vocabulary to precisely describe the things and problems it works with. Without the added complexity it'd be unproductive due to being verbose and sometimes ambiguous, like the amusing example of Up Goer Five: https://xkcd.com/1133
Specifically what I wanted to express, is that, for me, the turtles all the way down is what I understand as simplicity.
And yeah, simplicity alone can't be used as a value judgement.
Adobe Lightroom?
I don't think it's really missing features compared to Python or Javascript that would help with this. Tooling could improve a lot though :)
But much less high with the lambda calculus than with the other two; Haskell is not far from being a layer of syntactic sugar on top of the lambda calculus.
For an example of a non-trivial lambda calculus program, see this self-interpreter [1] for binary lambda calculus [2].
Point being, nobody wants to model their data structures as pure lambdas; even primitive integers are an addition on top of the lambda calculus.
> For an example of a non-trivial program
I don't think I'm taking the point you wanted me to from this program. It's under 30 lines, yet I'd need to pore over this for twice as many minutes to comprehend what it's doing. I can see the lambda abstractions and applications clear as day, but what they're doing I couldn't say. (The comments clearly say it's a universal binary LC interpreter, but we don't usually judge languages by their comments.)
EDIT: Right, okay, it's a submission to the the International Obfuscated C Code Contest. If it's obfuscated, then indeed, I'm not meant to comprehend it easily. Not sure that sells the lambda calculus.
Also, in lambda calcalus there is no recursion built-in. You have to build it yourself with the Y combinator.
let Y = \f. (\x. x x) (\x. f (x x)) in program-using-Y
which is the slightly more legible version of
(\Y. program-using-Y) (\f. (\x. x x) (\x. f (x x))
Not that much less, since you can manually translate back your Haskell code to lambda calculus. E.g. your
List a = Nil | Cons a (List a)
rewrites to let
Nil = \n\c. n
Cons = \x\y. \n\c. c x y
in ...
Of course the type system is what helps you write correct Haskell code in the first place, so the untyped lambda calculus will be painful for debugging wrongly typed code.In practice though it doesn't give you any of the tools you need to do anything except make tools to do those things. Which appeals to yak-shavers like us for sure. But if you need to sit down with it for months at a time and just get work out the door it fucking sucks.
Also 5.1's GC is goddamn tragic. In practice you're usually at the mercy of whatever was implemented by the embedding runtime, but if you're ever left alone with just the raw 5.1 GC you'll wish you weren't. Later versions improved that dramatically.
The main practical issues I've run into with 5.1:
- per-object overhead in time and space both increased significantly, making many small allocations much more expensive than fewer big ones. further pushing you into the "mutate a few huge global tables" pattern that is already a problem in lua conventions.
- some of lua's more lauded and genuinely powerful features, esp metatables, don't work smoothly or predictably with the GC. it leads to people trying to manually tune it with __gc metamethods, which is hard to do correctly and causes memory leaks if done incorrectly.
- weak tables idk,. esp with strings for some reason. similar to the __gc, there is a solution there but in practice it's very hard to be sure you've got it right and it's worse to get it wrong than to not try.
I'm sure a PL expert can explain and justify all this but I am just a normal person who codes for money. Lua is admired for being simple and accessible, but especially in the older versions you get into the low level weeds real fast over this stuff. Kind of a good example of minimal implementation exposing you directly to irrelevant complexity, really.> you're usually at the mercy of whatever was implemented by the embedding runtime
Are we talking about the same Lua? The engines I know (PUC Rio and LuaJIT) are not at the mercy of embedding runtimes, but haver their own, sufficiently good memory management.
I'm not an expert user of anything though, or building tools. My professional experience of it has been using ancient versions that are embedded into C or C++ systems to build simple guis and automation within those systems. I don't think it's well suited to this, but this is a common (I believe even it's original?) use case!
I've never used luajit, or any version where I had full control over the runtime at all. I understand people love this language and I'm not saying they're wrong to. But for the things I've needed to use it for it has not been enjoyable at all, particularly 5.1 which is the one I have the most experience with by far.
> I'm not an expert user of anything though
But apparently confident enough to make negative comments about the technology.
I might be wrong about the technical reasons why, and I'm genuinely sorry if I've unfairly maligned your baby here. But it has always been unpleasant for me and I think I'm entitled to my confidence about my own experiences in it.
I have, yes, it's great. No complaints, would choose LuaJIT again.
I do regret that the quad-color collector will likely never be implemented, I'd content myself with a back-port of the new generational collector, but the allocation sinking in LuaJIT has kept me out of trouble for the most part since it can put 'tables' on the stack if it can prove they don't escape.
I had a misfortune to deal with more than 200K lines of Lua code back in time [1]. It seems that my experience doesn't necessarily match those of other big Lua users though; it does seem to work when everyone is willing to invest in self-contained environments. In the other words successful Lua users doesn't exactly use Lua, they use their own programming systems based on Lua. (I should note that such systems are not guaranteed to be successful, as evident from my case.)
It can be done, but it requires a lot of discipline.
You might enjoy Teal, a sort of "Typescript for Lua" https://github.com/teal-language/tl
Rust can't do anything without people fearing it's will inevitably end up like C++, or even just look at shallow syntactic similarities and conclude it's already just as bad. But really it's not. Rust has two+ kinds of strings on purpose because it has two kinds of ownership, not because it has a historical baggage.
Really, C++'s featuritis and exponential complexity is unique to it. Pretty much every other language has survived decades of evolution without fucking up, and gracefully grew into something better. Java and C# are doing fine. JS and PHP have cleaned up a lot of mess. Even Ada and Fortran have been evolving for decades, and are fine.
People learning Rust keep asking "but whyyyyy I have to write this syntax!?", and often they're surprised that the answer isn't "because BCPL on PDP-11…", but reasonable cases of bugs and ambiguities it avoids. Robust code at scale must avoid implicit behaviors, and good compiler suggestions depend on having redundancy in the syntax, so there's a limit to how much syntax sugar Rust can have to avoid creating gotchas.
Frankly, that's an incredibly naive take. IME engineers that are confident to try out and work in multiple languages don't get scared off by syntax, rather the semantics. The issue with Rust in this regard to many experienced C/C++ developers is that the compiler forbids programs that are valid in C or C++ (for good reason, don't get me wrong), generic programming is highly encouraged and difficult to avoid, "unsafe" code is required yet significantly more difficult to write correctly than equivalent C and C++, and frankly, lifetimes are obnoxious.
I say this as a massive Rust fan and shill. I don't think the syntax is that bad, it's certainly a lot, but I prefer it to something like Idris or Haskell that has a similarly expressive type system with noise-less syntax (that is significantly harder to read/understand compared to Rust).
> C++'s featuritis and exponential complexity is unique to it
Java is similar here, things like Project Loom for example are important but shift the paradigm for concurrency signficantly.
To be totally honest, I dismiss almost all arguments towards languages based on syntax (I think there are some to be made, don't get me wrong). It usually comes from a very naive or inexperienced place. Syntax is important, but so is correctness and non-ambiguity.
I have never been able to get any one of my many colleagues to have more than a superficial look at a Lisp or a ml.
Swift, like Rust, is a very complex language. Even six months after I started, I keep learning new language features. In some cases, I had to watch WWDC talks and read the language spec to understand why some features were included in Swift. And even then it left me wondering why they could not tolerate a slightly more ugly syntax, in favor of readability.
I should mention that I have been programming for forty years and and I have written several compilers and language run time systems in my career.
Well said.
I've also been coding for a couple of decades (mostly in C++) and I also found advanced, real-world Swift unbearably... "irritating", for the lack of a better term. Surprisingly, I do not have that problem with Rust, even though Rust can be quite complex in order to be able to express ideas that Swift cannot handle at all (lifetime, ownership, immutability of reference types, mutable reference exclusivity, dynamic trait types, etc.) Somehow my brain is happy to consume complex Rust, while it's often "irritated" about consuming complex Swift.
I think the reason why Rust is so overwhelmingly better than Swift (for me, at least) is the fact that a lot of (ingenious) effort must have been put by the Rust Gods into Rust language >>consistency<< and >>predictability<<. My proverbial "sh*t, I like Rust..." moment came after I noticed a few things: 1.) Rust did not allow me to force-implement the Copy trait for a type that was not actually simply bitwise-clonable, 2.) Rust was expecting the "await" keyword after an expression, like a function call, yet uniquely without parenthesis, and 3.) Rust closures turned out to be based on the very easily noticeable double-pipe syntax.
My plea to the Rust Gods That Be: do not ever allow Rust to lose that unique "explicitness" and "predictability". It is one of the language's greatest assets.
Mostly Oberon - a series of blog posts documenting exploration of the Oberon Language - https://rsdoiel.github.io/blog/2020/04/11/Mostly-Oberon.html
Oberon to C Compiler - https://github.com/GunterMueller/OBNC
Oberon+ - Oberon parser, code model & browser, compiler and IDE with debugger - https://github.com/rochus-keller/Oberon
One of the advantages of small languages is that you can actually learn or master the whole language. However, small doesn't always mean simple. An example is the frequent discussion over Go's supposed simplicity. Other new languages fall in the medium-to-large size: Julia, Nim, Rust, Swift, Kotlin, Crystal.
Back to Niklaus Wirth and his thoughts on Oberon:
> There was obviously no way [for Oberon] to play an influence next to Java and C# and C++...But yes, I thought, as I did in the Pascal times, the foundations have been laid when a person learns his first programming language. That’s the key. In order to do a good experience, you have to have a clean language with a good structure concentrating on the essential concepts and not being snowed in.
> That is my primary objection to the commercial languages – they’re simply too huge, and they’re so huge that nobody can understand them in their entirety. And it’s not necessary that they’re so huge. But people don’t want to see that. Particularly industry doesn’t. I think the industry thinks it’s a big asset to have a complex thing that increases their reputation of being sophisticated.
(From a 2018 interview with Niklaus Wirth: https://www.youtube.com/watch?v=SUgrS_KbSI8)
The point is that the functionality of current systems hasn't progressed much beyond what you could manage three or four decades ago despite Moore's law because complexity has eaten the gains.
To me it sounds like you and the other person were saying that Pascal and Oberon weren't "large enough". I'm just pointing out that they were large enough that people can and did do serious work with them, and that, in many ways, our modern systems aren't that much better than what they achieved with those relatively simple and primitive tools. They weren't perfect, both languages grew and became more elaborate, that's true, but the original systems were solid.
You're welcome. I don't look at usernames on HN so it didn't occur to me until now that it's your project. It really does seem like a well-considered extension of Oberon, I'm going to check it out in more depth later.
> The argument was, that Wirth's original language designs were less successful than the extended versions designed by others, e.g. Boarland.
In a sense, the extensions of his work are the success, eh?
> The original designs are undoubtedly sufficiently expressive to build things of decent size, but apparently too little attractive for wide adoption.
In the quote from the interview with Wirth he says "I think the industry thinks it’s a big asset to have a complex thing that increases their reputation of being sophisticated." So rather than a curing a lack of expressivity, complexity instead provides a marketing edge, in his opinion.
> Not surprisingly, variants of all Wirth languages have quickly emerged
Again, to me that seems like a kind of success. The alternatives are that Wirth birthed perfect languages, like Athena springing from the head of Zeus, or that they were all barren and died on the vine.
His focus was educating students at ETH; the languages he created with this focus were fit for purpose; it is true that there are developers in industry who prefer complex languages for sophistication reasons; but there are also pragmatic people; even they apparently prefer Delphi over Pascal; I don't consider Delphi complex e.g. in comparison with C++11.
> to me that seems like a kind of success
Agree; the original languages are great and a robust basis; and e.g. Go is a very successful offspring of Oberon-2.
I don't like that Oberon+ removed uppercase requirement for the keywords. While I too found the uppercase requirement to be weird initially, I have now come to appreciate the sentiment behind it - it does make it easy to read and review the Oberon code. Just like Python uses indentation to increase readability, the uppercase keywords were a good defining feature of the original Oberon, and shouldn't have been removed. After all, now that modern editors can automatically make the keywords uppercase, programmers don't really have any reason to complain about it. It's a simple usability trick that doesn't rely on colours or font size or weight to increase readability. (One can also argue that if you want to remove that as a language feature, why stop there - why not also remove BEGIN and END and replace that with brackets or something else?).
Eww. Pascal is the worst first language ever. A first language should show you and familiarize you the concepts that are the best we know, and that you are likely to encounter.
It should be as multiparadigm and as easy to access the tools as possible.
Javascript beginners soon will know the concepts of oop, functional paradigm, async, etc, will recognize every pattern when they see them, also will use the best approaches to problems.
While pascal beginners after years still won't understand shit (mapping over elements of an array? what sorcery is that?), and will write you procedural code to every problem that clearly just don't work.
Please don't force newbies to grind to write procedural code, it's way worse than nothing.
That said I think there is value in the idea of a minimal language, so I'll try being the devil's advocate. As far as I can tell Wirth does not think that students should be picking up Pascal these days. He was referring to his thinking back in the 60s when he was discussing the same ideas.
I agree that a first language should show you the concepts that are the best we know, and likely to encounter.
Oberon has numbers, identifiers, expressions, statements, etc. It has subroutines, references to locations in memory, structured control flow, and both simple and complex data types. Its type system is what provides the support for object-oriented programming. This seems like a reasonable set of things that a starting student will encounter.
Can maps be done in this language? Yes. You could in a course introduce the concept of maps by having the students implement them on their own. This reduces the amount of what feels like magic involved in the language, which is something Wirth seems concerned with the most.
Are other elements of functional paradigms likely to be encountered? Really depends. Some languages do not have support for closures even while calling themself functional. They may not guarantee that tail-recursion does not expand the stack. At the same time, Lisp had none of these things in its first implementation, and was still functional-enough to be a educational and research tool. I am not sure it is something that you are likely to encounter in any one form, but the concepts should be taught. You could teach what a closure is by implementing a restricted one inside Oberon, and can do recursive functions as long as the stack allows.
Async has a lot of variation, even if you are likely to encounter it. IMO parallelism is better taught as a concept and then applied using games. It is difficult to build an intuition of it at a programming language level.
As a newcomer to Rust, a thing I noticed is the syntactic explicitness around generics: In particular, I had to learn to write Foo::<T>::bar(), i.e. the double `::` seems verbose. But it is also unambiguous that the `<` is not a less-than, because it comes to the right of a `::`. There are other `<T>`s that don't need `::` because they're already somewhere that could only be a type. A human probably wouldn't mistake those. So you might have syntactic complexity in a syntax that is easy for a human to wrap their minds around, and perhaps even makes expressing yourself easier.
It wasn't until recently that someone on ##rust pointed out that with unambiguous syntax, you can more easily transform old code, too. So in some future where a piece of syntax changes, you can more easily write a tool that transforms old code into new code.
That gets to the point.
But there is a lower bound of minimalism, below which the complexity increases again; I remember the time where a few academic ergonomists spread the idea that you should have only one knob in the user interface instead of many elements; the result was that you needed many more steps for each function and the operation was anything but intuitive.
It's the same with a programming language: if it's designed too spartan, I have to write too much code that is always similar, which causes effort and obscures the real intention of the code. And if the syntax is too minimalistic, complexity is just moved to semantics or the standard library. So the art is to find the right balance, not just to omit.
I think Go is the perfect example of this. On paper, it's a simple language. But:
- Not having sum types means it's awkward and unsafe to express concepts such as "X or Y". This is a big deal, because "Result or Error" is one of the most common ideas we have to deal with in programming. I think this tweet visually captures the awkwardness: https://twitter.com/GabriellaG439/status/1521860707444133888. Another place this shows up is pointers: in Go, since there is no way to represent "present or missing" at the type level, Go has to bake that possibility into the semantics of pointers. The billion dollar mistake.
- Every type in Go has a "zero" value. On the surface, this seems simple: when you declare a variable, you get a predictable value without having to explicitly initialize it. But this prevents you from implementing abstract data types which are guaranteed to be constructed by a smart constructor that establishes all the relevant invariants. Now you always have to worry about this zero value, since it might not satisfy the invariants of your data structure (consider the simplest data structure with an invariant: a pointer which is non-null!). Also, you can easily forget to set a field in a struct, which means the zero value will show up in unexpected places (resulting in subtle bugs that might go undetected even at runtime).
- Until recently, Go didn't have generics. Simpler, right? But that means if you want to build a reusable data type, you needed to either (a) make N copies of it, being careful to keep them in sync, with no help from the compiler, or (b) sacrifice type safety and represent data as interface{} (essentially a void* pointer), adding dangerous casts all over the place.
Languages like Haskell and Rust are more complicated than Go. But once you've paid the upfront cost of learning them, common programming patterns actually become simpler.
Of course, there's a flip side to this. Most languages have a lot of accidental complexity too, which isn't what my argument is about. So I almost hesitate to make this comment, fearing that it will be quoted out of context to justify adding badly designed features to programming languages.
In such a small system, the benefits of any feature are much smaller. Let's say you have some feature that could simplify your program by 1%. Over 10,000 lines of code that means save 100 lines of "complexity". If implementing that feature takes more than 100 lines, your small 10K lines system will end up more complex, not less.
Go however is fairly widely used. Simplifying by 1% on average is huge, and likely worth it even if it requires 10,000 lines of code all by itself.
But, people can like what they like, it's all good.
I think many languages have some implicit set or class of functions in mind, and some of the frustrations people encounter is in a mismatch between what the language was designed for and what they want to apply it to.
I still think minimalism comes into play as a language design goal for an application domain, in the sense some languages are unnecessarily obtuse. But I also think there is something else — I'm not sure what the term is, but "scalability" or "abstractability" comes close — related to a language's capacity for minimalism at multiple levels of abstraction. That is, can you simplify code as need be if youre not interested in the details, but delve into the details if need be?
Many (most?) genuinely widely used languages are more-of-less static. C hasn't changed substantially in 20+ years.
And many of us have worked in projects where a subset of the nominated language is mandated, or certain "features" banned - specifically to prevent the use of overly complex, poorly known, or misunderstood language features.
I'm a fan of "if it works, don't make it worse."
Not my impression at all. The author presents the design process itself as coming up with a list of features. What is much more sought after is to latch onto some of the following principles:
1. Turtles all the way down
2. Everything is an X
3. Uniformly treating [traditionaly different] things as "the same", like collapsing the value and type levels or letting you run code both at runtime and compile time
You then try to implement the features that you need with one of the above starting points. For example, Zig apparently implements generics using number 3 (compile time code).
In fact, the pinnacle of PL design to some people is to have very little in the way of built-in features and then let the users themselves implement whatever it is they need. Which I guess the Lisp family is the best at.
The quoted paragraph seems like a caricature of the design process of a langauge. She says that this line of thinking "is simplistic". Indeed it is—non-believably so.
> The quoted paragraph seems like a caricature of the design process of a langauge. (sic)
It sounds like what's happening e.g. Python or JavaScript, the "design process" seems to be to add features, and then add features to your features so you can program while you program.
The article is predicated on the assumption that this maximalist design is popular. But I have argued that it isn’t.
A polemic needs an adversary. If not then it’s not a polemic and instead just a feel-good article about what most people agree on to begin with. Programming languages should be simple? And certainly not go out of its way to be complicated? Hmm, yes. yawn.
It's becoming more and more important to treat complexity as a kind of technical debt that has the potential at any moment to metastasize into eldritch horror.
I've been playing with what you could call a minimalist programming language: Joy created by Manfred von Thun (who was a philosophy professor! See "A Conversation with Manfred von Thun" http://archive.vector.org.uk/art10000350 ) It combines features of Lisp and Forth, and I suspect it may be the simplest useful language. The syntax is enormously simple, and the semantics are likewise simple. It's easy to reason about functions and derive them in a mathematical way using simple algebra, so it's hard to introduce bugs. I recently found a very good 2017 talk by Jon Purdy describing concatinative programming and it's quirks and (potential) advantages: Stanford Seminar - "Concatenative Programming: From Ivory to Metal" https://www.youtube.com/watch?v=_IgqJr8jG8M
Anyway, it's made me "allergic" to complexity. I tried Rust. Wrote the beginning of a Joy interpreter. I paused for a month and came back to it and it had become Greek to me, and I can't read Greek. :( Now part of that is just I 'm getting old, but a lot of it is the complexity of Rust.
I don't want to pick on Rust though, complexity itself is the enemy (Rust is, if anything, an improvement on most of what has come before it, eh? I think only Ada can really be said to be Rust's peer?)
It may be different for professional programmers, whose programs often have a different purpose, are created by multiple developers, and have much more effort put into reusable, clean code. Here, the code base is more the product than the finished program.
I feel though that the article hasn't aged well. The talk was given in the age where Java didn't yet have generic types (he mentions this alongside with operator overloading). As to how Java successfully grew since then, I'm not very sure. Not all faults of Java are his, but like many others, he only seems to have a (very good) idea about the general direction, not the path itself.
He mentioned generics* is needed to "grow the language". Then Java added it, making the language more expressive. This talk aged like fine wine if you ask me.
* and operator overloading, and value types. The former I'm happy using in Kotlin. The latter we will have to wait. But for code that is not performance-sensitive, data classes/records work just fine.
I had the same experience with Rust, and you know what? It was right. My intuition was in fact wrong, and the code the borrow checker complained about would have lead to memory access violations in other languages. Rust catches it at compile time so my program doesn't fall over at runtime. If you think you know better than the compiler, good on you, more power to you. But in my experience I've found that trusting the borrow checker has lead to rock solid stable code that is performant and never crashes during runtime with segfaults and other memory related symptoms of bad code.
The oft-cited Rich Hickey talk [0] I think gets _somewhat_ closer to vindicating the concept of simplicity being something we can talk about more objectively, but I'm still not convinced the concept is worth redeeming. Instead I would propose that languages should have a clear purpose and context in mind. Designing a language with clarity about the intended scale and pace of change in software that will be built in the language, the domain/category of software it is well adapted to build (i.e. embedded, web, OS, compilers, mathematics, mobile, servers, games, and so forth), and the nature of the teams and the public community it intends to support supersedes and encompasses whatever we want to call "simplicity" or "ease". What is easy, simple, helpful, etc. is incidental to how purposefully built a language is to a set of use cases.
There are only a few languages designed with minimalism explicitly in mind (Go is a good example), but even there (a language within which application developers readily accept doing the job normally assigned to the language and compiler) the demand for some minimal set of useful abstractions is forcing the language to grow over time.
Let's be honest: Most complaints about bloat are actually complaints about stupid design decisions.
But at the same time, it's admittedly difficult to learn something when it has a lot of one-off corner cases.
Otherwise, it's not a great idea. Basic is a very minimalist language but good luck trying to do anything complex.
Rust might be unnecessary complex and that is the other side of the coin.
Based on the title, I thought they were going to talk about how all operators should be expressed with 3 characters (i.e. `:=:`, `|=>`, etc.) or how all reserved words should be 4 character words.
Code in other mainstream languages remains generally readable even if the coding style changes.
And when you look at c23... same tracks.
RISC-V quick!