The perfect programming language
cygni.se
cygni.se
Then when he says that XSLT is the best language I was thinking that we are very differently minded. So I suppose it makes sense that the Tailspin language is completely incomprehensible to me.
To me, it is pretty clear that the author has a very specific world view, probably defined by the set of problems he's solving. I can't quite tell what that perspective is. But this makes his claims a lot less universal than he admits.
Eve is a really interesting language where you define global pattern matches, using datalog-style unification rather than XPath, and these "templates" can result in state changes. So for example you can trivially define global invariants over the entire state, kind of like defining a database integrity constraint but for your program's state.
As an aside, I'm recently feeling disillusioned with Haskell—after loving it since 2004—as any kind of ultimate or next-level general purpose language, precisely because of the lack of this dimension. Dealing with relational data in Haskell is quite tedious. Even writing multipass compilers in Haskell is not very pleasant.
Now I'm puzzled because I know that you know about lenses, so you must mean something different by "pattern match on data nested deeply" and I'm wondering what. Could you expand?
”XSLT is my favourite language of all time and it made all sorts of lights go on in my head. This is what sparked my analysis of programming languages, I wanted to program generally in an XSLT-like style, whatever that really means, and more. Of course, all data should really be in XML at all times, but that got killed with the whole XMLSchema nonsense...”
But seriously, XSLT's recursive templates are a nice idea for transformation, and the homoiconicism enables transformation of XSLT stylesheets theselves.
“Impure” means the language design is by definition compromised. The moment you allow imperative behaviors to leak into a declarative system it loses its ability to reason reliably about operations over time; and you’re back to flying seat-of-your-pants a-la C &co. Haskell at least has the good grace to firewall any imperative crap so that the remainder of the program can still be reasoned about.
That doesn’t mean OCaml and F# can’t still be useful, in the same way that C is still useful, but they cannot be more than the sum of their own limitations; they are already evolutionary dead-ends. Thus article is correct not to expend any space considering them.
By your (imo, extremist) line of reasoning, wouldn't the existence of `unsafePerformIO` mean Haskell itself "looses its ability to reason reliably about operations over time"? I think a more reasonable position allows for gradations of reliability, and appreciates the benefits of encouraging pure computations while allowing room for judicious use of benevolent side effects.
> they are already evolutionary dead-ends
That is patently false, imo. OCaml has a number of interesting features which most statically typed FP languages are yet to explore (e.g., polymorphic variants, (typed) named and optional arguments, first-class modules, binding operators). More to the point, ongoing development on a novel approach to multi-core, native support for algebraic effects, and modular implicits, testify to it being a live evolutionary branch. OCaml aside, innovative languages and approaches like like F, 1Ml, Frank, BER MetaOcaml, and Rust seem to give clear evidence that we still have much to learn from impure* dialects of ML.
Well no, because for some reason, be it social or technical, people simply don't use unsafePerformIO in way that is actually unsafe.
> To the extent that the libraries and the ecosystem doen't make inappropriate use of unsafe behavior, you can have your impurity and reliable referential transparency too.
The key qualitative difference between Haskell and e.g. O'Caml/Scala/whatever here is that it's quite trivial to verify that your program doesn't use side effects inappropriately (aka. is not referentially transparent): Just make sure it contains no unsafeFoo calls. There's no such mechanism in O'Caml/Scala, etc. (I'm not sure, but I imagine it might even be undecidable unless you restrict to a teeny tiny subset of the language, e.g. just the pure arithmetic expression language.)
The qualitative difference in how confident you can be that moving a bit of code around won't change the semantics of your program is profound (or: bonkers) -- and before I'd experienced it myself I could have (rightly!) been extremely skeptical of such claims, yet here I am making that claim.
Yes, unless you really want to fully vet all of your transitive dependencies you will have quite a large TCB, but I find that it's actually really rare for libraries to use unsafeFoo willy-nilly. Some of the low-level stuff like Text, Vector, etc. do, but AFAICT it's mostly for performance reasons... and DGMW quite serious bugs have been found in these. However, because these are isolated data structures these bugs get fixed once and they are immediately fixed for all users.
Another wart wrt. a large TCB in Haskell is IO. Unfortunately, it has a very large surface area, but there's no really much that can be done about that at this point. Even still, as a programmer you can choose to use e.g. polysemy/fused-effects/etc. to define the semantics of effectful code in a much more fine-grained way such that the only point of contact between your code and IO is in a tiny "interpretation of semantics" layer.
and rust wasn’t mentioned!
Not at CERN, but it used to be the case. It's C++ now.
> It turns out that FORTRAN is a very good match for the way physicists think about their work
I don't even know what this means and I have a PhD in Physics.
> You simply specify what fields you have and what pattern they are written in and the computer takes care of all the reading and writing for you
This is possible in any language with decent reflection.
> and layers upon layers of unnecessary complexity
None of the complexity in C++ is unnecessary. There were, as there are always, trade-offs.
> Go was created explicitly to replace C++, it turned out that the programmers who love C++
IMHO, this is regurgitated nonsense from Rob Pike. What I got from what he wrote on the subject is that he doesn't grok C++ at all. If he did, he wouldn't be surprised that C++ developers didn't flock to Go.
> efficient development of efficient programs
I'm not aware of Go programs that are particularly efficient. I'd also argue against "efficient development".
> there is an elegant practical solution in Go
Elegance is in the eye of the beholder. I don't think pretty much anything in Go is elegant nor possible to be because of the lack of generics. It forces people to write for loops, which is just... mind boggling to me. That alone makes me wonder what kind of Python devs are taking it up.
Go occupies the same niche as Python, Ruby and JavaScript (server-side business logic). Compared to programs in those languages, Go programs are insanely efficient.
Is this in a world in which Java and C# never happened??
> Compared to programs in those languages, Go programs are insanely efficient.
In other news, compared to walking, bicycles are insanely fast. Also, let's pretend cars don't exist.
Java has a bad reputation because its community appears to favor over-engineered opaque solutions (that whole ProxyAdapterFactorySingletons business).
Go pushes a quite different style, favoring explicit bindings over dependency injection.
Disclaimer: I write a lot of Go, but I don't have productive experience with C# or Java, except for a semester-long project at university where Spring Boot was mandated. I recall most students having trouble with its implicitness, i.e. they could not build a mental model for why certain changes had certain effects.
I think the rationale for that is twofold: that as a static typed language that prefers stack allocation, you can get some very good performance without putting much effort into it, and that it's usually clear what the idiomatic way to accomplish something is. On the latter point, I appreciate golang's simplicity when I need to review other people's code or dig into third party libraries.
For the record, I agree with most of what you're saying. I also don't find it elegant, and most developers I know are eager to work in a different language after spending any amount of time in golang. Its benefits come at the expense of expressiveness, and I never thought I'd be doing things like code gen to work around a lack of generics in 2019.
Compared to... ?
> I appreciate golang's simplicity when I need to review other people's code or dig into third party libraries.
See, I don't. Every for loop has to be inspected to see if it's one of the 99.9% cases where it's a map, reduce, or filter. For loops can't be chained. Error handling takes up over half of a function. An algorithm library can't be written because of the lack of generics.
Switching to Python, I'd much rather read:
[x for x in xs if x % 2 == 0]
Than: res = []
for x in xs:
if x %2 == 0:
res.append(x)
3 lines of boilerplate to do 1 line of work. Pass.Then there's the fact that everything is mutable in Go, so I have to track every and all variables in case they change.
All in all, for me, reviewing code written in it is harder, not easier.
(~2!)#x
(Not (~) modulo 2 (2!) filter (#) of x.)Or perhaps more idiomatically,
x@&~2!x
(x indexed (@) where (&) not (~) modulo 2 (2!) of x.)Reading a python comprehension requires awkward skipping around, and composing them together is a mess. APL-style uniform precedence is something I wish more languages imitated.
Still, anything's better than an endless sea of for loops.
{x∣x∈N,x<10}mean = sum(Weight*Point) / sum(Weight)
where sum() is an intrinsic Fortran function. I am curious to know how this computation would be done in C++?
When a programming blog has a headline which teases knowledge of the perfect technology, the answer is always “I don’t know”.
Both baits work on me, apparently, but I sure wish they didn’t.
(Article’s author probably realizes this and merely chose that title to increase clickbateyness; but given the continued creation and persistence of kitchen-sink mountains like C++ and Apple Swift it’s clear that plenty still don’t.)
"first Ada was so complex that even the compilers had performance problems, so there was a bit of a delay getting it out"
This was largely true. The complexity of compilation really bit. But,
", and then, second, C and Unix happened"
This is silly. Unix and C had been around for years before Ada's design was accepted. I'd been working on C in a Unix environment for five years when I started working on Ada. In fact that's why I was hired since one of our first target ALS implementations was for Unix which included several C system level drivers.
Until the mid-90's Portuguese computing world, UNIX was only seen at some universities and eventually government level businesses.
Everyone else was using some kind of mainframe, or the typical 8 and 16 bit micros, mostly networking over Novel NetWare.
I only bothered to learn C when we had introduction to OSes, where our teacher would carry a single 486 PC from an university lab into the class room, running Xenix, to be shared across the whole set of students.
Each preparing their samples in Atari, Amiga and MS-DOS systems, followed by copying the stuff into a floppy and being allowed a short timeslot to try out our stuff.
So maybe that was where he was coming from, or then again, maybe not.
So we have a post fix (RPN, similar to HP calculators) expression style, which is really a mirror image of Lisp. You have genuine first class functions, introspection, and something I don't see that often is first class environments. That is you can, within any part of the code, activate an environment (they call it a dictionary, which is very similar to a C structure). And they stack, so that something not in the active environment can be automatically looked up in a parent environment.
$ gs
GPL Ghostscript 9.19 (2016-03-23)
Copyright (C) 2016 Artifex Software, Inc. All rights reserved.
This software comes with NO WARRANTY: see the file PUBLIC for details.
GS>2 3 add stack pop
5
GS>
Many of you probably already have it installed on your machines!Here's another illustration. Since values in Haskell are immutable, implementing something like foo.bar.baz+=1 becomes a bit clunky, so they wrote a library to make it easier. Check it out, especially the diagram that says "simplified": http://hackage.haskell.org/package/lens
So I agree with the article that Haskell can be dauntingly abstract. (Though you can write code that's perfectly straightforward if you want.)
The OP article, too, focusing on reducing errors, went into the factorial weeds here. How about presenting the air traffic control example in Haskell to contrast with the Ada? Real concurrency, real I/O, and real error cases.
It's very nice for some algorithms. For example, see Appendix A of this paper: https://pdfs.semanticscholar.org/b47b/41a9b2dcf6b3ffe3ce7a04... An implementation of red-black trees in only 60 lines, including deletion, which alone can take hundreds of lines in a curly brace language.
It's also very nice for some APIs. For example, software transactional memory fits the Haskell type system very well, because it can ensure that data can't be modified outside a transaction and can be rolled back when needed. A team at Microsoft, including some well-known folks, tried to port it from Haskell to C# and failed due to the type system: https://www.infoq.com/news/2010/05/STM-Dropped/
Haskell has a bunch of non-mainstream features that work together unusually well. It enables programmers to think a certain kind of thoughts that wouldn't naturally arise in many other languages. It's like painting with oil when the rest of the world uses watercolor: in theory they can draw the same things, but in practice they'll guide your hand toward different things, so having both makes the world richer.
The first two bullet points may have some truth to them, the rest seem like a result of using google search to find the "cons".
Not sure if that jab was intended or not, but Python, one of the most popular programming languages in the world, doesn't have a switch statement, probably for this exact reason. Python programmers use chains of if/elif*/else? statements.
Despite the way he tries to crap on Perl (5) by referring to a Perl 6 (now Raku) example, I find a Raku FizzBuzz far easier to read than his mess:
multi sub fb( Int $n where * %% none(3,5) ) { return $n }
multi sub fb( Int $n where * %% all(3,5) ) { return 'fizzbuzz' }
multi sub fb( Int $n where * %% 3 ) { return 'fizz' }
multi sub fb( Int $n where * %% 5 ) { return 'buzz' }
.say for (1..15).map({ fb($_) } );
Or if you want to name your specialized types: subset Fizzy of Int where * %% 3;
subset Buzzy of Int where * %% 5;
subset FizzBuzzy of Int where * %% (3&5);
subset Otherwyzy of Int where not * %% (3|5);
multi sub sfb( Otherwyzy $n ) { return $n }
multi sub sfb( FizzBuzzy $n ) { return 'fizzbuzz' }
multi sub sfb( Fizzy $n ) { return 'fizz' }
multi sub sfb( Buzzy $n ) { return 'buzz' }
.say for (1..15).map({ sfb($_) } );Languages like Rust seem to be a reaction against the problems caused by dynamic typing, but I'm not sure that forcing all that burden onto the programmer is the right answer yet. The languages that ease newcomers into the language tend to do well. Rust is like a brick to the face.
Does that mean Rust won't take over the world? Who knows. I do know that what they're doing with it does seem to be the future, but the language is difficult, even for experienced programmers to learn.
Compared to, say Javascript, Ruby, or Go, Rust has a secondary set of rules that programmers have to learn before they can even do the most simple things. As an experienced programmer, I look at most Rust code with bewilderment. This is after a week of studying the language.
I can look at Python and instantly understand what's happening, without any knowledge of the language. I can look at Go's standard library code and instantly know what it's doing, without any previous knowledge of the language. That seems to me to be the goal of a language. I don't know much about those languages and if they scale or whatever, but they look like they'd be productive.
I look at most C++ and it's on the edge of being incomprehensible if anyone does anything remotely abstract. And that's with 10+ years of experience in the language.
Rust, to me seems like it chooses to prioritize memory correctness and speed over everything else, including productivity. Maybe I'll hack away at learning it for another few weeks and something will click, but for now it's an uphill battle. That to me is a barrier to entry that will not help it gain traction.
The rules are not that hard if you're somewhat familiar with the constraints of functional programming. Rust requires you not to rely on shared, mutable state, unless specifically enabled - thus, by default, you're essentially writing FP code using a 'procedural' paradigm and syntax.
It is also possible to do "quick, exploratory" programming in Rust; it just requires somewhat liberal use of language facilities such as .clone() and RefCell<>, to specify things that would mostly be implicit in other memory-safe languages. (And because these somewhat problematic features are so clearly marked in the resulting code, it becomes fairly easy to directly refactor it into better-performing, idiomatic Rust.)
I've not heard it described this way. It's an idea I can get behind. A lot of the native code I write tends to try and be as functional as possible, or at least avoiding relying on shared mutable state.
And don't get me wrong. I think that Rust is a significant upgrade to the status quo. Perhaps it's just time to keep hacking away at it until it clicks.
You are forced to use Rc<RefCell<>> and clone everywhere, or copy objects around, reactive style, to kind of work around borrow checker ergonomics.
The "ability to know everything about the language" seems to (possibly) point to simplicity as "not complex", but the discussion around "what you already know" seems to point to the interpretation of "not hard."
I'm not sure I understood the paragraph as it was intended.
SQL isn't so much a programming language as a query language. It never claims to be an efficient way to write a factorial function.
And why do people freak out when they see a JOIN? It's one of the simplest concepts in databases: intersecting two sets. Everything in column A that matches everything in column B. You don't even have to concern yourself with the most optimal way to do it, just say it!
Actually no, that's not what a join is. A join is a cartesian product (not an intersection), followed by a WHERE clause (which doesn't have to be an exact match of a value between columns). This is further complicated by outer joins where empty rows are added to one or both of the sets involved in the cartesian product.
Well, no. Translated - "a language should serve the lowest possible denominator".
Apart from the obvious fact that it is impossible, it's also definitely not the way to go if you want quality software.
I have a thoery that C++ compiler writers obsession with pointless micro optimizations is because C++'s compilation model scales horribly. Thus they desperately want their compiler to be faster. But they are fucked because the more optimizations just slows down the compile times even more.
- Use binary libraries (don't build the world from scratch)
- Use incremental compilation and linking (yes I know lld support is kind of working on nightly)
- Start adopting C++ modules
https://devblogs.microsoft.com/cppblog/improved-linker-funda...
https://devblogs.microsoft.com/cppblog/msvc-backend-updates-...
https://devblogs.microsoft.com/cppblog/introducing-c-build-i...
The existence of optional optimisations isn't a problem.
Because that is what every language has to do. There is always something about a language that is crappy/boring/verbose/difficult/complected. And even with highly extensible languages (Lisp) you are at least forced to be disciplined, which also 'sucks' in a way.
Having the right trade-offs/constraints around a purpose/domain opens up many opportunities for optimization of performance, readability, accessibility and so on.
I would not call Java complicated at all. The way they wrote some over-engineered libs for it however is a different story. FactoryFactoryFactory my a$$. I personally do not like Java for performance related reasons and inability to do some lower level tasks but for generic enterprise development those features are not really needed.
As for C++ - yes it IS SUPER COMPLICATED . I do not know all of it and do not give a flying hoot about this fact. What good about C++ for me personally is that basic stuff is easy enough to comprehend and I have no problem mostly sticking to C with classes approach. However whenever I feel that I need some exotic "feature" usually I can do a bit of reading and find it.
Build time is not great but at least in my cases incremental compile time is fine.
My only real problem with C++ is when I need to use some third party piece of code and it is written by some cool person who has decided to write it using every feature of language available and suddenly I can not comprehend what does every second f..ng line mean.
- Write modular applications and make use of binary libraries instead of compiling the world from scratch
- Enable incremental compilation, incremental linking and pre-compiled headers
- Don't go crazy with compile time algorithms / meta-programming
But still, moving code from headers to implementation files helps reduce build times. (Very innovative!)
Where I work, the stuff I do in C compiles in less than a tenth of the time. And that's compared to MFC-era C++ (C with classes, not C++11).
that requires to include i.e. vector everywhere, which drives up compile times and adds dependencies. Maybe slightly better: some slice type. But it's still a lot of boilerplate, and doesn't play nice with parallel arrays.
> [no use of] implicit enum conversations > [no use of] nullable pointers
How do you deal with "missing or N/A values"? Because, as every database guy will assert you, these things exist, and they are very common.
How do you deal with the situations where only a subset of the enum values is applicable anyway? Because these situations are extremely common as well, and once again, types can't help here without A LOT of additional boilerplate.
> lack of namespacing
It's a feature, not a bug, to know that every thing is referenced by exactly one name. Makes reading code so much easier.
I deal with nullable types when needed, not for 100% of pointers, like C requires to.
A feature that requires linker support to work around third party libraries clashes, C engineering at its best.
The internal implementation can (and usually will be) heinously complicated but as long as it adheres to that external contract any nasty surprises are ones users create for themselves, not the language’s doing.
At the very least these concepts are less efficient, which lowers quality in the efficiency dimension. For example, in assembly it's very easy to detect integer overflow; in C, which is still fairly low-level, there's no direct way to do this and people invented various workarounds. They are less efficient, but portable. Does this raise the quality as you understand it?
(BTW, there were a few attempts to write an OS in assembly (Menuet, Kolibri), now they seem to be mostly abandoned, but the intermediate results were fairly impressive: getting from boot to GUI in less than 10 seconds is something modern "big" OSes are utterly incapable of. This definitely looks like quality at least in some dimension.)
edit: typos.
Dr. Dobbs Journal had an article about it, back in the 90's when they run a piece of possible C++ replacements.
If I dig out my archive I can post the information as reply later on.
https://www.drdobbs.com/cpp/the-c-programming-language/18440...
However, doing CLIs or binaries for whatever reason, then it wouldn't be so neat. Ruby wouldn't either, but GraalVM might be able to provide something good for it in the future as well.
The other blocker was historically the lack of threading, but now that seems to be implemented.