We need less powerful languages (2015)
lukeplant.me.uk
lukeplant.me.uk
Excel (without scripting) uses a terminating (and therefore incomplete) model of computation and it still manages to do a lot of heavy lifting in the real world.
I wholeheartedly agree with the author that declarative languages and Domain specific languages are great, and even if they do not have a (hidden) embedded lisp, they can still be useful.
Once you are in a computationally complete language (and you don't just use a subset of language features that is "easy"), most problems are generally undecidable. Turns out, that's a bad property for static analysis.
Interesting. What is the general area of your thesis (your field of research that is)? I study approaches that learn datalog (or Prolog) programs from examples.
Good luck with your thesis!
I didn't know about statelog and dedalus. How do you extract the partial programs? And how do you do the compilation to C?
If you don't mind me asking! I find the description of your work very interesting :)
Edit: Have you heard of HEX-MIL [1] or ILASP [2]? HEX-MIL is an ASP system that learns datalog programs and ILASP learns ASP programs:
Usually on microcontrollers you don't have enough space to have a control system and a logic program. So I extend LP with IO and compile efficient code.
Partial programs are basically extracted using the Datalog research about parallelism and distribution over components. Queries evaluated in parallel/distribute can be compiled to separate programs. And if the program is bounded, through an abstract execution we can basically generate all possible extentions (parameterized) of the minimal model. Where the proof can not be obtained, we compile a small embedded deductive database.
I have to look at the stuff you've linked. Interesting and adjacent. Is the github in your profile an appropriate way to contact you? I'll shoot you an email in a few days.
I should think so!
>> Partial programs are basically extracted using the Datalog research about parallelism and distribution over components.
I think I understand- it's a program transformation technique?
Yes, you can contact me via my github. You're very welcome to do so! :)
- not having indexes
- a bad execution plan
- an overcomplex query (tend to kill the optimiser)
- bad distribution stats (again -> a bad plan)
- a large dataset
- and maybe others.
Practially speaking... well IDK, how do you reconcile that with your work? It's a real puzzler for me (language theory is an interest of mine, but sql brings home the bacon when I have work) so your input will be very welcome.
If you can't prove termination, you can't prove time bounds. And then how do you improve on that, besides guesstimating and benchmarking?
I mean, sure, sometimes it's a good trade-off to limit expressiveness, but most often than not we could make a lot of progress by making certain features simpler, more accessible, or following a model that's easier to understand for humans (matching better human intuition, or more accessible, so it's easy to get easy things done, and you are not hit by complexity unless you really need it). I argue that we can still make a lot of progress and create better models for many things related to programming languages, including (but not limited to): unicode, interprocess communication (and in general compatibility of data across programs and languages), access to graphics and audio APIs, pass by value vs pass by reference, mutability vs immutability, transparency of memory models, cross-compatibility, database accessibility, etc.
The more power you have the more opportunities you also have to shoot yourself in the foot.
Removing mutability as a feature from a language also removes all mutable logic errors from a language. Removing null values from the language removes all null related runtime errors from a language.
In short, powerful languages exist because they need to exist.
There are lots of examples where DSLs achieve this goal and the result is really nice and, unfortunately, a lot where they don't.
There is a problem of a language gap, a mismatch between SQL structures and internal types, and all in all maybe a gap in typed code and unit tests.
That does not mean you should implement your own database.
What we have are in fact imperative state mutating machines. A language can pretend otherwise by trying to encapsulate the associated complexity of translating between these models. Or it can just let the programmer do it.
Specialized languages have syntax that is more streamlined towards specific domains, but they lock the programs into their narrow world views, requiring possibly more effort than what was saved to shovel your way out.
Looking at some really good programmers it is remarkable how elegant and efficient (measured in LOC) they can do some things in simple procedural languages that one would normally think are poster child use cases for some of the more limiting approaches and techniques (GC, DSL, OOP/FP/whatever).
For some balance, I do agree that ergonomics around tooling matters a lot. I believe this requires similar tradeoffs - a narrow world view can make it easier to write tools that understand the structure of the code, and can assist in the development. For example, C is a pain to write tooling for. On the other hand, it is still one of the faster languages to compile and build because there are fewer tasks that it tries to free the programmer from.
It must be your IDE then... Have you tried VS Code?
Joking aside, I agree with you and have experienced the same insight. It has since helped me focus on what matters as I attempt to solve problems a bit outside my abilities.
> switching languages is no fast path towards solving hard problems
If you're talking about switching from one mainstream, general purpose language to another, then I mostly agree. Some tradeoffs can be definite wins for particular domains, e.g. for Web development Go is almost always a better choice than C (in terms of memory management, string handling, buffer overflows, etc.). Other choices can just shift problems around, e.g. in Python it's easy to get something running, but it requires a lot of testing to avoid errors.
However, when it comes to domain-specific, non-general-purpose languages I strongly disagree! There are certain problems that are incredibly difficult to solve (often undecidable) in the context of some generic language; which become trivial when the language is designed with that problem in mind.
For example:
- Incremental computation https://inc-lc.github.io
- Resource handling https://en.wikipedia.org/wiki/Substructural_type_system
- Client/server/DB consistency https://en.wikipedia.org/wiki/Ur_(programming_language)
- Ruling out unwanted effects https://deno.land/manual@v1.0.0/getting_started/permissions https://en.wikipedia.org/wiki/Purely_functional_programming
- Hard realtime timing https://en.wikipedia.org/wiki/Esterel
There are a load more examples on sites like http://lambda-the-ultimate.org
Such languages certainly introduce a bunch of problems, like lack of libraries, but they might be the difference between something being difficult, or it being impossible.
Another thing to keep in mind with domain-specific languages is that it's often useful to "embed" them inside some other, general-purpose language. This lets a DSL avoid the need for its own parsing, tooling, etc. For example https://wiki.haskell.org/Embedded_domain_specific_language#E... https://beautifulracket.com/appendix/domain-specific-languag...
> There are certain problems that are incredibly difficult to solve (often undecidable) in the context of some generic language
That statement is objectively wrong. Was the use of undecidable here intentional?
It was intentional, and here's a proof (sketch) that it's objectively right. Consider the question of whether a procedure's return value (if it halts) will be a string?
We can prove that this question is undecidable in Python, by considering procedures of the form:
def foo():
if bar():
return "hello"
else:
return 123
The answer to our question will be true if 'bar' returns a truthy value (or doesn't halt), or false if 'bar' returns a falsy value. Since the truthiness of bar's return value (if it halts) is a non-trivial property of the (partial) function it implements, it is undecidable for arbitrary 'bar' (via Rice's theorem). QED.Yet in Java this question is trivially decidable, since all return values must have the same static type; i.e. procedures like the above aren't valid Java programs, and this can be determined statically, so they present no difficulties.
In this particular case, Java's static type system makes this a trivial question about the procedure's syntax; rather than a non-trivial question about the (partial) function it implements.
This same argument applies to all sorts of DSLs too. Many use a similar type-based approach, e.g. linear types forbid expressions like 'if (bar()) { free(myMemory); }'.
Others work by providing an extra semantics, alongside the normal result-calculating one; e.g. incremental lambda calculus has a semantics for calculating changes; differentiable languages (e.g. provided by machine learning libraries) have a semantics for calculating derivatives; probabilistic programming languages have a semantics for sampling and induction; etc. We generally can't apply these semantics to other languages, due to the existence of language constructs which don't exist in that semantics (e.g. exceptions, or GOTO, or whatever).
But here it's really comparing apples vs oranges. Of course you can find questions that don't have a definitive answer for programs (or program parts) written in any programming language. Much more so in a less rigid language. That doesn't say anything about problem solving abilities, though.
> This same argument applies to all sorts of DSLs too. Many use a similar type-based approach, e.g. linear types forbid expressions like 'if (bar()) { free(myMemory); }'.
That's nice, but the concern I expressed in my original post is - at what cost?
It's completely true that all Turing-complete languages can solve the same set of computer science problems (e.g. recognising certain grammars, implementing certain partial functions, etc.). Of course, actually coming up with such solutions can vary between e.g. Brainfuck versus Java.
I was focusing more on "real world problems" or "business problems", e.g. allowing users to query a server, or adding a plugin mechanism to a game, or distributing an application across multiple locations, etc. These are not "computer science problems" (akin to, say, recognising a grammar) since they're underspecified; we're free to make various choices about what counts as correct, including the input format.
These examples are particularly well-suited to solutions involving a language (e.g. a query language), as opposed to something less expressive (e.g. a pre-determined list of options). For such "real world problems" our choice of language can be the difference between a trivial solution or an undecidable quagmire. For example, in the case of querying we could give users a pure language (with a timeout); that's trivially safe from effects, as well as being immune to many side-channels (no shared state, etc.). If we instead allowed users to write queries using Python, we'd face the impossible task of detecting which programs are safe to run on our server (unless we only provide a safe sub-set of the language; which is just a round-about way of saying we should create a custom language!).
> the concern I expressed in my original post is - at what cost?
Yes, there's always a cost for these things. In my experience, this usually involves explicitly encoding our reasoning/assumptions into a form the language will accept (e.g. type annotations, if they can't be inferred; refactoring some generic loop to fit a certain pattern; etc.). However, that's not all bad, since our programs capture more of our intent, and will warn us if we're wrong (either immediately, or after some refactoring invalidates our assumptions).
These tradeoffs certainly exist on a spectrum, and the location of the "bang for buck" sweet spots varies depending on the domain. It's usually not worth encoding a correctness proof into Coq's type system; yet it usually is worth encoding our control flow into structured programming rather than GOTOs. There's a whole heap of techniques in between, with varying tradeoffs, which make more or less sense depending on the domain, external constraints, etc.
You may not want more powerful languages, but as soon as a single person wants the power, they'll craft it themselves no matter what (and then it may end up being one of your dependencies :-) and the effect will be pretty much the same - you'll have to work with generics anyways, but with homebrew instead of "official" ones).
So I am very very not convinced by the thesis exposed in that article - people will use whatever tool they find to automate what they think is automatable in the process of writing programs, even if that means creating ad-hoc AutoHotkey scripts that type repetitive macros according to a certain pattern (true story :-)). Having more powerful languages means that you have to spend much less time trying to inspect whatever eldritch horror combination of tool the people before you used there as you just need to check the language docs or StackOverflow.
I’ve managed to generic myself into a corner before with type constraints in C# as an example.
* Operator overloading
* Function overloading
* Constructors and destructors
* Implicit type conversion
* Implicit types (classes)
Go also doesn't support type templates and function templates yet (aka generics or contracts). This also plays significant role in easy-to-read Go code.
P.S. I wrote many packages and apps in Go during the last 10 years [1] and I absolutely love Go! Check out my last project in Go - VictoriaMetrics - fast time series database and monitoring solution [2].
While Go supports exception-like panics, they are mostly used for really exceptional cases, when the program cannot proceed further, such as out-of-range slice accces or nil pointer dereference. Almost all these cases are triggered by programming errors, which must be fixed after the panic occurs. Traditional error handling is performed explicitly in Go instead of relying on panics - the error is checked explicitly after function call. This simplifies code reading and makes easy to spot cases with missing or incorrect error handling. These tasks are almost impossible with try/catch error handling.
huh ? the destructors are just in reverse order than the declaration order
A recent HN discussion on microservices had people saying (paraphrasing) “it’s the only way we can collaboratively build software”.
Programming languages currently seem to be built for programming as a solitary activity. Seems to me that programming languages could be designed with the aim of solving the large scale collaboration problem.
No, it isn’t.
C, C++, Unix, and even Windows, have long sinced solved what you describe as being ignored. See also CPAN[0].
I can see collaborative debugging and sharing of environments, especially with web services, as possible avenues. Observability as well.
I'm having a harder time coming up with specific language features though.
Contract, contracts and contracts!
Collaborative development depends on people not freely overstepping outside of their bounds and clear communication of those.
OOP creates some actually strong kinds of contracts by abstract interfaces and information hiding. Flexible typed languages do the same with generics and specific types. Any language feature that creates more contracts will help collaboration.
What do microservices bring to collaborative programming that OOP do not?
There is actual benefits in removing fundamental core features of programming. You can remove iteration to make something not turing complete but before that you can maintain turing completeness while removing certain core programming features.
In fact There is an entire style of programming that removes these core features and is "less" powerful and "less" expressive as well. It is called functional programming.
Functional Programming is simply the same thing as imperative programming only without the power of mutability. No variable reassignment. No looping.
The strange thing is I always hear people talk about things how functional languages like say... Haskell are more expressive and more powerful than imperative languages. They are technically wrong. These languages are LESS powerful.
So yeah the author is really talking more about cognitive overload of say something like C++ but at a more fundamental level making our core programming model less powerful actually has some interesting benefits outside of just "cognitive overload"
Modules in functional programs (called combinators) are the most modular primitives available in all of programming and FP pushes the programmer into encapsulating his logic into these primitives leading to actually more modular and better constructed programs. The structure of programs changes for the better when things are made less powerful.
You're just arguing from different definitions, here. In fact, it's worth checking whether your definitions match up whenever you find yourself saying "technically" since it suggests you're applying a particular definition.
The set of all things you can do to your computer with Haskell is less than the things you can do to it with C++. If you look at it from every possible angle you will see that either you do less with Haskell or Haskell makes it Much harder to do certain things. There's really no angle where you can say Haskell is more powerful than C++ unless you really stretch the definition which I'm sure your subsequent reply (if you reply at all) will be doing.
Golang doesn't have parametric polymorphism as a restrictive option . Instead you can use interface {} which removes all restrictions makes go as unrestrictive as python. Type checking and any feature related to it in general exists to make languages less "expressive"
This is still a restriction. A function that operates only on a "traversable" type is still a function that is restricted to that type class.
In other words, if you recognize that someone is using a term incorrectly, first take the time to understand what they're trying to communicate. Maybe start talking about that concept using words you think are a better fit, but stay on the territory. Responding that "powerful" isn't the right word doesn't address their claim that Haskell is more <whatever they mean by powerful>.
This charitable approach shifts the conversation away from scoring points (as would leaving out asides like "(if you reply at all)") and toward collaborating on developing each other's understanding, which is more valuable for everyone involved.
My argument in response to your reply is to say there is no reasonable alternate definition and people people are using the term because of an incorrect notion that haskell has more expressive power then say c++ when in fact it has much less expressive power.
I put "if you reply at all" to acknowledge the fact that I could've went into a long expose into something and you could just ignore it as an optional retort. My reply is anticipating all possible logical counters. I also wanted to emphasize the fact that I do not believe an alternate definition is reasonable but I am aware that it is the most likely reasoning you will be using in your response. I am letting you know that I am already aware of it. It saves the discussion from going down an avenue of things we already are aware of.
I did not anticipate you to go meta and talk about the discussion itself but maybe I should've as I sort of boxed you out of all other alternative paths. I personally think it's unnecessary to go here still. If you disagree stay on topic and propose why you think an alternate definition is reasonable. If you agree you can still stay on topic by conceding I'm right... but human nature prevents 99 percent of all people from ever conceding as conceding is subconsciously perceived to be a form of weakness. By probability, I am hypothesizing that you are within this 99 percent and that you will likely never take this path as an option in your next reply, if you reply at all.
I chose the sentence I responded to because I thought you were putting focus on the term, specifically where you say people are "technically wrong" because functional languages are "less powerful." My mistake. I was trying to encourage you to consider that the people saying that may be correct in what they mean, even if what they mean doesn't correctly align with the use of "powerful."
Just to point out, as well: you have also had the opportunity to acknowledge that I'm right. If these people are arguing from an incorrect definition of "powerful," as you say, then they are indeed arguing from a different definition of "powerful" than you are. That fulfills what my original comment set out, which is that you and these people are arguing from different definitions.
Perhaps you should have anticipated my focusing on the conversation itself, not because you had boxed me out, but because that was the substance of my comment in the first place.
Not only do I often fail to anticipate this. I find the whole thing pointless. I'm uninterested in personal details or meta aspects of the conversation. I am only interested in the topic.
Your initial comment was on the definition of the word "powerful" your subsequent comment was on meta aspects of the discussion which I find very tangential.
Saying Haskell is more powerful when it comes to compile time guarantees is like saying Haskell is more powerful because it is more restrictive. The guarantee exists because Haskell is not expressive enough allow you to break that guarantee.
I mean yeah. An obese person is more powerful than an olympic athlete because the obese person is better at weighing more.
I honestly don't think most people are using the word in the way you define it. They think Haskell is less restrictive than python/javascript because really they haven't thought about why Haskell is better. Haskell is more restrictive and less expressive and less "powerful" and that is why it is "better."
Case in point your example of highly polymorphic functions as a unique feature.... Completely mistaken given that polymorphism is available on most typed programming languages and that all data structures in untyped languages are polymorphic to each other.
After having worked with Clojure I realized that you can have a small and focused language that's incredibly expressive without being overwhelming. A well designed language will allow you to apply a small number of common patterns to a wide range of problems.
1. Holy hell, I always have to look up examples for the ns macro and imports. 2. Protocols, records, and reifying... there's a flowchart out there for this but it frustrated me that I didn't Just Know. Scala has a similar thing for this where sometimes Eta expansion happens and sometimes it doesn't, and you just need to know where you can use sugar.
Of course, such a thing is impossible, because we're always learning, and we can always do better, but it leads to some interesting avenues of thought. We can claim that certain concepts are mandatory, which can be used to reject languages that can never meet our ultimate requirements.
For example, a common complaint is the "impedance mismatch" between, say, query languages such as SQL or MDX, and object oriented languages such as Java and C#. Similarly, JSON has become popular precisely because it has zero "mismatch" with JavaScript -- it is a nearly perfect subset.
This leads one to the obvious conclusion: An ideal language must have subsets, and probably quite a few. If it doesn't, some other special-purpose language would be forced on the developers, and then our ideal language isn't perfect any more!
The way I envision this is that the perfect language would have a pure "data" subset similar to JSON or SQL tables, with absolutely no special features of any type.
The next step up is data with references.
Then data with references and "expression" code that is guaranteed to terminate.
Then data with expressions and pure functions that may loop forever, but have no side-effects of any kind.
At some point, you'd want fully procedural code, but not unsafe code, and no user-controlled threads.
At the full procedural programming language level, I'd like to see a Rust-like ownership model that's optional, so that business glue code can have easy-to-use reference types as seen in C# and Java, but if a library function is called that uses ownership, the compiler takes care of things. You get the performance benefit where it matters, but you can be lazy when it doesn't matter.
Interestingly, C# is half-way there now with their Span and Memory types, except that this was tacked on to an existing language and virtual machine, so the abstractions are more than a bit leaky. Unlike Rust, where the compiler provides safety guarantees, there are a lot of warnings-to-be-heeded in the dotnet core doco.
TL;DR: We don't need separate simplified languages, we need powerful languages with many simpler subsets for specialist purposes.
I think this is a very important point, but it should IMO be considered along with context. Languages are always used with a particular tech stack, targeting particular hardware, all with its own performance characteristics. For example languages with garbage collection will have to have a different design from languages that use reference counting. And that's OK!
It's useful IMO to think of languages in terms of the use cases they're effective in. Some are good for resource constrained environments, others are highly flexible, others are great at expressing mathematical concepts. All those are unique use-cases and it shouldn't be surprising that the best languages for each are very different.
Where I think languages can run into trouble is where they try to be all things to all people. A universal tool sounds cool in theory, in reality they risk becoming mediocre at everything or developing what are affectionately named footguns. These steepen the learning curve and can also cause problems in real-world deployments of software in that language.
A common observation by professionals is that when they have a zoom lens attached to their camera, the pictures tend to be either one extreme zoom or the other. That is, they wanted "as much as possible" and just set the zoom range to the maximum in that direction.
Which means that most of the zoom range is (almost) never utilised.
The big advantage of prime lenses is that by sacrificing the ability to zoom, the quality can be better. A 35-50mm zoom is never going to be as good as two separate 35mm and 50mm prime lenses.
So if you want to maximise quality, get primes.
But then your camera bag will be heavier, and your wallet lighter.
Also, you now cannot have any intermediate zoom range in those rare times that you do need it.
So there's always some trade-off being made.
Languages like C++ are like zoom lenses. They allow almost any programming paradigm, and various mixes too. You can have a procedural program with memory safety, with functional bits thrown in, and call out to unsafe C code if you want.
Languages like Haskell or JavaScript are like a prime lens, with a lot of decisions fixed at one extreme or another.
I suspect that what's needed is the zoom-like flexibility of languages like C++, but with defined subsets that work more like a "prime" language. With physical lenses, this is impossible. You can't take the zoom bits out, leaving the prime behind. With software... I think it can be done.
This isn't that unusual an idea. Mainstream languages are already slowly converging on this concept. For example, C# has a project-level "unsafe" flag, which turns a set of language features on or off.
For example, Dhall (https://dhall-lang.org/) could be seen as a subset of Haskell, with less features but guaranteed termination!
I'm reading through the presentation slides for Noether, and it almost exactly follows my line of thinking, but uses much more precise definitions and restrictions than my own hand waving.
However, it only goes "down" to a very pure functional language. I would argue to that there is a need to take a step further to a data-only language also.
Ban recursion and unbounded loops. Proclaim the language is "Turing incomplete" and that all programs terminate.
Declare that Turing incomplete programs are simpler. Have non-technical people conflate terminate quickly with terminate eventually.
Realise lacking recursion makes things incredibly clunky to express, turning simple problems into brain teasers.
Add recursion.
Realise that the everything is better.
https://neilmitchell.blogspot.com/2020/11/turing-incomplete-...I've generally assumed that the goal of the GoF (design patterns) was the same: with a canonical and prescribed set of ways of doing certain things you're less likely to get into trouble and and your code more likely to be understood by another reader.
While I believe the issue of cognitive overhead due to increased optionality (i.e. power in this case) the converse is also true: the additional expressive power of a language takes the place of an IDE and permits more compact code which can be easier to understand than the more diffuse and boilerplate-laden code of a less expressive language, where the scaffolding can obscure the place where the work is actually done.
http://neilmitchell.blogspot.com/2020/11/turing-incomplete-l...