It's fun to think about useful languages that are not computationally complete. I am writing my PhD thesis about Datalog used as a general purpose programming language, specifically for robots and microcontrollers. And you know what? You don't need to be able to simulate a Turing machine for a lot of tasks. On the other hand, you have guaranteed termination.
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.
If you don't use recursive CTEs in SQL you have a guaranteed-terminating subset. It actually doesn't help any because time to terminate can be effectively unlimited depending on (in any combintaion)
- 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.
>> It's fun to think about useful languages that are not computationally complete. I am writing my PhD thesis about Datalog used as a general purpose programming language, specifically for robots and microcontrollers.
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.
I have a variant of Datalog with an explicit model of time (like statelog or dedalus) and action atoms/external atoms from HEX (asp). Then I extract partial programs that provably use finite memory and construct finite state machines. Combined with a bit of runtime framework, I can compile to C which I actually run on arduino or lego ev3.
Usually something what I call a "decision core" is embedded in every interactive program. Deciding what to do with input. Sometimes this is just an if-else-tree, sometimes a statistical model, sometimes a logic program. Execution is done in an imperative fashion.
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 agree with the analysis of the problem, but disagree with the solution: while it's true that increased expressiveness in a system tends to increase the cognitive burden of understanding it, what you want is to maximize expressiveness at the smallest cognitive cost possible, not to limit expressiveness.
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.
There are problems outside of cognitive burden. More expressiveness actually leads to a higher error surface area.
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.
That's a nice idea in theory but making something Turing complete with loops and conditionals etc. simply can't have the same low cognitive cost as decent languages which do not have such features.
While semantically weak languages are appealing at first, all they end up doing is encoding the intrinsic problem domain complexity implicitly. This results in “brittle code” and the inability to adapt.
In short, powerful languages exist because they need to exist.
That's not true though. SQL is not computationally complete. It does abstract over the complexity of access to relational data. And it is still used and useful. SQL is also not automatically brittle.
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.
Plain SQL is not turning complete, however SQL/PSM is and something along those lines is supported by (most?) database implementations like PostgreSQL, Oracle SQL, MySQL, etc. Seems to me like SQL is a perfect example of most people agreeing that the extra power/complexity is indeed a requirement.
Not sure the extensions were really needed though. Certainly the gaps could be filled externally and in a lot of shops it's what's mandated. I think an argument could be made that the extentions you're bringing up is more about a turf war between DBA's and Developers than about neccessity.
It's not inevitable. Carefully ringfencing the capabilities of a weaker language and making sure that it is as powerful as is necessary enables you to have something conceptually simpler, easier to maintain and easier to read while maintaining the necessary level of expressive power.
There are lots of examples where DSLs achieve this goal and the result is really nice and, unfortunately, a lot where they don't.
I tend to think that languages aren't that important. A basic imperative languages has you covered for the vast majority of tasks. Having spent some time chasing languages, I've come to think that there is no silver bullet, and while looking at ongoing efforts is inspirational and insightful, switching languages is no fast path towards solving hard problems.
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.
> Having spent some time chasing languages, I've come to think that there is no silver bulle
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.
> I tend to think that languages aren't that important.
> 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.
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.
> 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).
I thought you were talking about the actual problems - the ones that you use a programming language to solve.
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?
Ah, I suppose there was a conflation between (roughly) "business problem" versus "computer science problem".
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.
Nicely put! It's important to be aware of the spectrum ranging from very rigid and introspectable, to very flexible. We should not be religious about approaches, and it is certainly a good idea to consider new approaches. But there are other economic forces unrelated to this spectrum - for example, it can be a good idea to just stay with old and proven tech because of better tooling, compatibility and familiarity.
This is why I like Go and hope they don’t ruin it with a poor generics implementation. It fits in my head. My other usual language is C# which is starting to feel like a London bus at rush hour with feature burden.
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.
It's certainly possible for individuals to swim against the current. I am not a Go programmer but all the Go open source code I have read has been brutally simple and easy to consume. I don't think that's a coincidence.
I'm a Go programmer and I can confirm that Go code (both written by me and by anybody else) is easy to read, debug and refactor. This is thanks to simple yet powerful Go syntax, which doesn't allow constructing implicitly executed code. I.e. it doesn't support the following "features" from other programming languages, which instantly increase code complexity because it becomes impossible to understand the code by just reading it top down:
* 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].
As a C++ developer, highlighting the lack of destructors made me curious how resource closure is typically handled in Go. A brief review suggests that it is often handled using a "defer statement", which accumulates a stack of statements to be executed when the function returns, to execute a 'close()' method. While I can see some benefit to the explicit appearance of "defer" in the source code, I think it is outweighed by the risk someone forgets to write it (or tries to close something without defer but misses a path). The execution of a destructor is inevitable and automatic.
The main issue with destructors is that it is impossible to determine by reading the code which destructors in which order at which lines may be called. Actually, destructors can be called on every line of C++ code because of exceptions. This complicates understanding code flow and makes almost impossible to write exception-safe C++ code, which properly releases resources at any exception.
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.
> The main issue with destructors is that it is impossible to determine by reading the code which destructors in which order at which lines may be called.
huh ? the destructors are just in reverse order than the declaration order
C# is a really bad example to say Go is good. I always felt people were too lazy to explore proper alternatives. People don't like something in C# or Java and jump to go because you can learn it in a day instead of investing 2 days to write in Rust or Erlang or Haskell or Ocaml or whatever. Go is just a better C with coroutines
Slightly off topic but collaborative programming seems to be a big unsolved and ignored problem in programming.
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.
I'm really not found of object oriented programming, but don't you think the basic idea is the same? A class is a kind of "microservice" and can be seen as exactly what you describe.
What do microservices bring to collaborative programming that OOP do not?
Yeah, microservices should be about addressing horizonal scaling and runtime resource constraint issues, not about collaborative programming issues. We have things like modules, packages and coding standards for the latter.
> What's a possible way to design a language for collaboration besides encouraging ever more fine-grained modularization and code reuse?
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.
Maybe I misunderstand you (and I definitely misunderstand people who advocate micro-services like everything and the kitchen sink), but aren't we collaborating on enormous projects for many years already? Linux, Windows, all large open source projects (without them being micro-services) etc. But again, maybe I misunderstand what you mean.
Not sure why you're being down voted here. I see why the CPAN reference is relevant but maybe it in particular is viewed as an issue because of Perl's TMTOWTDI. I take your comment to be that code modularization and sharing was a problem solved long ago and that most collaborative programming issues are more social than technical.
I sometimes wonder why syntax (or in the case of this article, the "expressiveness") and runtime features of languages often come in package deals. Shouldn't it be possible to connect a preferred syntax system to a preferred compiler/runtime system? In other words, more modular programming languages?
It's well known that less power leads to less opportunities to shoot yourself in the foot. The article talks about "less" power from a qualitative perspective like how "Go" is less "powerful" than "C++" because the language primitives are less dense. It also talks about turing completeness as well but still from a qualitative perspective.
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.
> 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.
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.
So tell me what do people mean when they say Haskell is more "powerful"? Tell me the definition. I don't think there is a different definition. I think it's just that most people aren't sure why Haskell is "better" than say C++.... They know it's "better" but they can't put their finger on "why" and they mistakenly attribute it to a more powerful language without thinking that Haskell is actually less powerful in every possible way.
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.
Let's assume you're right and there isn't a different definition. Still: by focusing on the use of the term, you're driving the conversation away from the substance of what the other person is saying and toward a linguistic discussion.
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.
You're the one that put focus on the term. I didn't do that. I'm defining powerful in the same sense the article defines it.
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 concede as often as I can, actually. Partly it's because I view being wrong as a temporary state of affairs, that doesn't reflect poorly on me as a human, and that I can grow from. Partly it's tactical: by conceding, I can focus on more substantive territory, make the conversation less adversarial, and build credibility with others both in the conversation and observing it. I've found it works better than arrogance or insistence.
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.
>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.
Haskell has fully-featured generics and Go does not. In this sense Haskell is more powerful than Go. However, Go has mutability and (mostly) Haskell does not. In this other sense, Go is more powerful than Haskell.
Kind of but if you think about it generics and parametric polymorphism in general is a restriction on the types that a function can take as parameters. It is a feature that allows you to restrict how a function is used. In a sense you are using generics to deliberately make a function less powerful.
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"
Types in Haskell are not just restriction, they're also used to generate code automatically for you using typeclasses. Something like [traversable](https://wiki.haskell.org/Foldable_and_Traversable) provides behavior just based on types, and is far too powerful to be expressible in Go (or in most typed languages anyway).
This isn't code generation. This exists in other typed languages including Go. It's called interfaces or polymorphism. It's done via interfaces or inheritance in other languages.
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.
Haskell is substantially more powerful when it comes to features like enforcing compile time guarantees about properties of your program, as well as writing highly polymorphic functions.
I would say python/javascript is more powerful with polymorphism as it defines polymorphism over everything.
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.
One of the things I appreciate about Clojure is its simplicity. Before I started working with it, I was of the mindset that having more features in a language was important because that's what made the language more expressive.
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.
Clojure is really great this way. My fumbling about with argument order aside, I only had a couple complexity problems when I was writing it:
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.
This comment section is not complete without mentioning Oberon, designed by maestro prof. Niklaus Wirth. First appearing in 1988 it may not be a new language but it has gone through several simplifications and the latest revision is from 2016. The programming language Go borrowed quite a few ideas from it although Oberon is simpler and has an orthogonal feature set. How many programming languages are defined in 16 A4 pages including examples?
For over a decade now, I've been pondering a "perfect" language.
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 you would be interested in Noether, a full language design based around this principle: https://tahoe-lafs.org/~davidsarah/noether-friam4.pdf. I've always been sad that an implementation was never created. It's one of the most unique designs for a language in the past decade.
Thank you for the reference, it's very interesting to see that someone has has had the same line of thought!
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.
I agree. For an extreme example, by taking an extremely restricted subset of Python, Numba is able to get crazy speed-ups, automatic compilation to GPU kernels, etc. I've been waiting for someone to implement the subset of Python that allows for no GIL and huge speedups by cutting out some of the dynamic stuff. I'd bet 99% of the code I write would fit in that subset.
> because we're always learning, and we can always do better
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.
An analogy I like to use is prime vs zoom lenses on DSLR cameras.
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.
Interestingly, just recently, Neil Mitchell has published an article claiming that this is a fallacy and in practice, people are forced to back-pedal on it.
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.
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.
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.