Hacker News new | past | comments | ask | show | jobs | submit login
What's next for language design? (2017) (graydon2.dreamwidth.org)
151 points by isaacimagine on Nov 21, 2021 | hide | past | favorite | 123 comments



This list, together with already mainstream features, turns a programmer’s learning curve into an uncompromising cliff. It’s clear that right now big software has to be formal to live longer and stay healthy, but for me it feels like we’re losing something important on this way.

Computers ought to be our helpers which could understand our plans and implement them properly. Instead we are burying ourselves and those who will come later in what is essentially a deep math. And our software doesn’t even get faster or better, it just fits whatever performance:annoyance ratio is acceptable at the end of the day.

I think, and this is more of an intuitive rather than an informed guess, that the current practical languages are still too low level and that may be one of big reasons why software development is so complex. You probably feel it too, when you have to either A) go find, copy and fine-tune yet another boilerplate or a snippet, making it again too hairy to use as is next time, or B) use a standard solution which almost implemented another programming language on top of its configuration and isn’t really much easier to use than if it were just a from-scratch code. Shovels get better, but we are still digging careers with them.

And we had predecessors of a theoretical language which could be high-level enough to make programming great again. E.g. FoxPro with all its DOS restrictions was a platform where one could read, write, iterate, update data, never thinking how to map classes to tables or forms, or how to make it run. Or Delphi, in which most students could write client-server apps by dragging tables, queries, databases and forms right from a toolbar. The entry complexity skyrocketed since then or, more precisely, returned to “serious” levels of heavy C++ setups which only seasoned programmers by trade could manage without pulling hairs.

I don’t think that human[ity] will become any significantly smarter in the near future, and that, together with our all time high software demands, means that we will experience a disabling shortage of software developers until we fix our internalized tolerance to the increasing complexity.


> This list, together with already mainstream features, turns a programmer’s learning curve into an uncompromising cliff.

This is because many insist on having one giant programming language to do everything both low-level and high-level together. Rust just follows C++ along this same path. The resultant language will always be a complex compromise that is suboptimal for almost everything. For example, both Rust and C++ have too many high-level abstractions to easily reason about where allocations will happen and their manual memory management makes lambdas and async painful. I personally would favour the use of multiple languages. Such an approach is commonly emerging anyway due to practicalities, e.g. Python/C/Fortran use in machine learning.


> [because languages] do everything both low-level and high-level together

This is very far from true. Like, not even the tiniest bit true.

It is easy to program in C++ or Rust and pay no attention at all to where allocations happen. Almost all application-level coding in those languages is written exactly that way. Even in programs where such attention is needed, the overwhelming bulk of the program, such as all the initialization and setup code, gets away with completely ignoring allocation. Certain libraries whose value proposition includes having been heavily optimized need to pay attention to memory placement, but probably more than half of libraries would only ever be used in places where performance doesn't matter at all, and the author knew it.

Anyplace where you do need to pay attention to details of allocation, or of atomic event ordering, or of interrupt context, you are not obliged to use every last bit of abstraction your language enables. It is totally allowed to drop to a C level of specification. (In Rust you might even put some of it in "unsafe" blocks.)

Rust and C++ do impose inconveniences for various reasons -- C++ mainly because of backward compatibility, Rust mainly for extra static enforcement -- but the inconveniences are completely unrelated to the occasional need to pay attention to memory placement.

Where existing languages do still demand low-level attention is in obliging the programmer to be aware of control flow, and what thread is executing which bit of code. They are only starting to loosen that. It used to be that you didn't care because there was only one thread. Then, we made threads, but not too many, because of numerous costs. Now, we would mostly like our programs to use as many threads, scheduled any which way, as would be useful.


This statement:

>> do everything both low-level and high-level together

> This is very far from true. Like, not even the tiniest bit true

Does not really align with:

> It is easy to program in C++ or Rust and pay no attention at all to where allocations happen

(This is what high-level languages offer)

> It is totally allowed to drop to a C level of specification.

(This is what I mean by low-level)

I would also argue that in Rust the memory management unfortunately does affect the ergonomics of lambda and async, for example the need to explicitly pin memory.

I really don't mean to criticize all the recent effort that has gone into Rust, it is certainly a huge improvement over C++. But it's far from perfect and I'm arguing that is probably not the fault of any of the individual choices, but rather the language goals. I would like to see a future where we use more specialised languages; rather than the many languages that can both peek/poke bytes and implement the full lambda calculus. More specialised, simpler, languages are much more amenable to static analysis and formal methods.


Rust is, in fact still quite a long distance behind C++ in expressive power, and moving toward it. But C++ is not standing still.


I'd say that to appease the borrow checker is to care about allocation and especially deallocation.


Allocation and deallocation are divorced from borrow checking.


One could say that the point of a borrow checker is to guarantee that you don't deallocate an object until all references to it are gone (for some definitions of deallocate and object).


In my experience the annoying clashes with the borrow checker are more to do with mutability than de-allocation. It's not that the reference lasts longer than the allocation, it just overlaps other borrows you want to make.


Right. At any point that an object could possibly be deallocated, everybody who wanted to look at it has, perforce, already lost interest. The system knows at what point this is, and can quietly dismantle the object and free its storage without bothering the programmer.


> a complex compromise that is suboptimal for almost everything

Whenever you have a programming language with multiple goals (or, let's say, more than two distinct goals) - it's almost certainly going to be suboptimal for any individual one of them.

The questions are:

* Whether choice of goals is worthy/reasonable; and

* Whether a good balance and compromise has been struck by that language's specifiers and implementers.

Also remember that languages evolve; and if one of the language goals is backwards compatibility (which is the case for C++, less so for Rust) - then part of the compromise is that they have to support multiple older and less-recommended idioms and facilities while offering newer, hopefully better ones.


I hope it was clear from my earlier post that I am arguing "No" for your questions above, especially regarding C++ and Rust. This doesn't necessarily mean I don't think great software cannot be built with these languages, but rather I do not see them as the future.


Which parts?

The points on modules and effect systems are totally in line with creating a shallower learning curve.

My reading of his point on formal specifications was that it was important to design software that works as it behaves (like compilers that compile the languages they say they do, paraphrasing). That's not a burden on users of the language directly, more on the creators.


An effect system is redundant to code and is not free (compilation time cost) and neither helps when you dont know your future program spec or behavior. https://en.wikipedia.org/wiki/Effect_system

It only prevents some misuse classes of APIs.

I think what you want more dearly is to express atomiticity of functions and (automatic) rollback on error to a "known good state".


What do you mean "an effect system is redundant to code"? An effect system may or not come with additional static typing. I would argue that static effect types are certainly not redundant, having programmed Haskell professionally for many years. But that's just the usual static types debate.

Unrelated to typing, algebraic effect handlers probably have the most ergonomic implementation of async I have ever seen (looking at the OCaml multicore previews).


> algebraic effect handlers probably have the most ergonomic implementation of async I have ever seen (looking at the OCaml multicore previews)

Just dropping in to say strong agree.

The biggest stumbling block for me has been that the effect handlers are captured as part of the continuation; I was expecting them to need to be manually re-established every time the continuation is resumed. The way it works makes sense when you always `continue` within the body of an effect handler, but it feel a little unfortunate that there's no way to change the handlers for future resumptions.

Other than that, the possibilities for userland coroutines, dynamically-scoped variables, and centralization of state (like `ref` but provided by a scoped allocator) are incredibly exciting. I've wanted a system like this for a long time, and for some use-cases (scheduling of concurrent simulation tasks) I had to abuse threads and thread-locals to get the separation I wanted.


This is the difference between deep and shallow handlers for effects. Since shallow handlers can easily implement deep handlers (by re-installing the handler itself) but deep handlers are simpler to use when they fit your use case, the plan for OCaml 5.0 is to give access to both version to users (without syntactic sugar nor a type system however).


Oh, that's great to hear -- and thanks for the terminology! Do you have any pointers for where shallow vs. deep handlers can be compared in the Multicore OCaml literature? I've seen the non-syntactic support via `try_with`; I'd guess there's a similar function for the other flavor of handlers, but as a newcomer to the OCaml community it's not obvious where I should be looking.


Until recently Multicore OCaml was focused on deep handlers. The people working on the formalization of effects (either for program proofs or typed effects) were quite keen to have shallow handler integrated however. Thus, the effect module of the OCaml 5 preview contains both (see https://github.com/ocaml-multicore/ocaml-multicore/blob/5.00...) since September. I fear that non-academic literature has not followed this change (on the academic side, see https://dl.acm.org/doi/10.1145/3434314 for a program proofs point of view).


That paper's introduction really lays things out very nicely. Thanks for the refs!


The point I was trying to make is that you can get comparable guarantees, if you structure the code with atomticity + rollback and trace generalizations. Most people do not code like this due to missing language support for good error handling. (explicitly encoding errors as integers sucks and more so that you need to do it globally)

Yes. That is compiler provided functionality on top, which looks excellent.


I for one can’t wait to have to climb that cliff. Unfortunately, our industry has a relatively shallow onramp, followed by a ton of difficulty using our building blocks to make stuff that does what we want in reliable, maintainable, performant ways. If we can have more difficult to learn, but easier to use tools to get us where we’re trying to go, I’m all for it.

I guess, instead of seeing it as an increase in complexity, I see it as a tradeoff of increased learning curve for decreased complexity.

You’re spot on about the labor market side though. More learning curve for the best tools has tons of labor market implications.

I’d bet the end game is more computing being in the general education tracks to deal with the overhead, as well as a wider (hopefully continuous) spectrum of computing tools for people ranging from those using computing as a small part of their work to those using computing for all of it. Like gradual typing. You don’t need to add types to python, but you can add mypy. Gradual depth, maybe.


Well said, and I think every new language should take its learning curve seriously. A language should get one of one's way, so they're thinking about their project's inherent complexity, not about the language's complexities.

I might be biased, but I'm excited for Zig and Cone, which I think will make it easier for newcomers to the low-level space.


We should optimize for those who have mastered the language, because that is where each of us will spend most of his career. Helping newcomers is nice if it doesn’t come at the expense of the majority.

The top of the learning curve is the point where the language no longer has any help to offer, and that should be as high as feasible.


> big software has to be formal to live longer and stay healthy

Can’t disagree, but where are the languages for little software? For example, I write lots of small (100-1000 LoC) command-line tools. I’m not a full-time programmer, so I don’t have time to learn a complex language - what I ideally want is executable pseudocode.

Are there languages that fit that description? Python used to, but even it has now become very complex - because its designers are all employees of big companies working on big software.


I suppose that'd be the original intended use of "scripting" languages. And we came to realize that things get start small but can grow significantly larger and we can no longer use those languages. Many languages that got initially advertised as scripting languages became much more complex partly in order to handle larger softwares. As such I'm not confident that your tools have zero potential to grow larger.


Lua is phenomenal. AutoIt if you're on windows - it's underappreciated for sheer utility.


Try Ruby. It still fits the bill. And of course there's Perl 6 aka. Raku.


Isn't Raku considered an extremely complex language?


I wouldn't say that Raku is a complex language. Raku is a typical example of a programming language in which the *developers* of the language are tortured to make the *use* of the language much easier.

A carefully balanced quantity of DWIM (Do What I Mean) without too much WAT, while having awesome error messages to help you get going and continue to be productive.


For CLI Go or Ruby. Go has nicer distribution.


I think it’s fine. Software usually Darwins out a language winner. There’s a reason a lot of things got built with Perl, python, js, Java, c++, ruby, php, and there will be a reason why things won’t be written in those languages anymore (or aren’t anymore). There’s no amount of will that can force the hand of god and make Rust or Typescript ubiquitous or not-ubiquitous in the long run.

We learn our lessons slowly. The winning language always surfaces itself in the form of useful software. If it’s true that these new language constructs are useful, pain-free, then we will surely see the ubiquity of those languages and the corresponding software with it.

If it’s not useful, then nothing useful will come of it. It will be just another exercise in technical virtuosity, a master violinist playing a technically profound tune that no one likes to listen to. You’ll just have to enjoy it for the feat that it is, but it’s not something people will want to sit through regularly.

Cheers, design away, I’ll see you on the other side of the cacophony.


On the flip side, supply and demand will keep wages high.

But, it will also bring more people into the field that will likely increase the amount of poorly written code that needs to be maintained.

What happened to all the talk around making the compiler do all of the work?


> I don’t think that human[ity] will become any significantly smarter in the near future

Anecdotal side note: I recently visited a archaeological museum on greek civilization. Some of the artifacts were 6000 years old and I was really surprised how ... delicate and "modern" for the lack of a better word ... the earthenware looked.

That really made me think that humans back then were just as intelligent as we are today. I think that may be a fact that may be supported by biological evidence? Brain size comparisons of skulls maybe?

Either way, if humans have not been less intelligent back then, there is only technical progress left.


The list includes, from my reading, modules, errors, asynchronicity, side-effect types, fancy linting, and formalization. Elements 1, 2, and 5 exist in some sense in almost every area of engineering, while 3 and 6 are easily ignored if you don't need them. That leaves side-effects, the old bugbear of all functional programming.

The difficulty with Rust is that it's "batteries not included", which is a term we use in software for the equivalent of a car sold with no wheels, body panels or seats. Sure there's a package manager, there's also an auto parts catalog.


<enso-fanboi mode="on" disclaimer="&employee-soon;">

Have you seen https://enso.org? Personally, I'm strongly hoping for it to become a modern contender in such space (of making programming easier and faster, for non- and programmers alike)


I would add sructural types, for example anonymous (extensible) records and variants. Nominal types are overused in nearly all programming languages, even futuristic ones like Haskell. The default type definition should be a structural type, because it is easy to then simply name it if required.

Every C or Java programmer writes a function definition in terms of a tuple of parameters, despite not having first class tuples. Functional programming languages can also return tuples, but where are the anonymous records? These would allow defining functions with named arguments essentially for free.


TypeScript's types are structural, and I don't consider this to be a good thing. It's useful for JavaScript interop, yes, but just because two values have the same representation and method names does not mean they're interchangeable. Case in point: time units.

So while there may be some use of structural subtyping, I don't think it's codebase-wide.


After working in nominally typed languages for years I’m finding Typescript’s approach to types very liberating. I think structural typing with the option of defining nominal types is a sweet spot in type ergonomics.


I agree. I think part of the reason for this is that Typescript's type system was created with a goal of expressing the APIs of many existing js APIs, and many of those APIs are designed without any real constraint imposed by the type system.


Here's a counter example. I've seen numerous Java projects define a nominal predicate type, for example com.google.common.base.Predicate. None of these are interoperable without writing adapter code. ML and Haskell would essentially use a structural type:

    t -> bool
Much simpler and easier to consume.

How would you assign a nominal type to the result of a SQL query?

So there are arguments on both sides. My point was that structural types can always easily be turned into a nominal type, but not the other way around. Therefore they should be the default.


There are interoperable since Java 8, using the operator ::

  com.google.common.base.Predicate<String> guavaPredicate = ...
  java.util.function.Predicate<String> predicate = guavaPredicate::apply;


The :: syntax minimizes the pain, but the fact you have to do it at all is still irritating, and IIUC it incurs a small runtime cost due to the intermediate object that's generated to convert between the two interfaces. I recently cleared out some of these intermediaries in a hot area of code and got a small but significant performance boost.


function types are structural in most nominally-typed languages I have used, perhaps a poor example?


Are you sure? What language are you referring to?

For example, although Java method definitions are essentially defined in terms of tuples, function classes and objects in Java are all entirely nominal.


Right, there should be the ability to use either structural or nominal as appropriate.


One of the issue with structural types is compiler error messages. By example, C++ templates are structural types and error messages are really hard to decipher.

Recent versions of Java (Java 8+) avoid structural types but provide structural conversions + inference.

We will see with the recent introduction of named records/tuples goes in the same direction or not.


> By example, C++ templates are structural types and error messages are really hard to decipher.

To be fair, C++ templates are not structural types, they are (together with C++ function overload resolution) an (almost) purely functional untyped programming language, producing C++ programs as output.


C++ templates used to be structural types. Since C++20, C++ supports meta-type constraints on templates.

Of course, backward compatibility means those are optional, but the designer of the template decides. Existing libraries are all structural, but new ones will rely more on nominal meta-typing, and error messages for those are clean.

There will be pressure to retrofit older libraries, so that error messages from misuses of those get nicer too. Gripes from users who had successfully abused those libraries will be heard.


Before C++20 templates were untyped. Since C++20 it's possible to define structural types for templates via concepts. Concepts are an explicitly structural type system for templates.

Nominal typing of generic code would be what most other languages that supports generic programming use. For example Rust has traits where one defines a named trait and must explicitly specify that a type satisfies a trait by name.

In C++ there is no such means, a concept does strictly structural checks (via constraints) to determine if a template satisfies a concept, no names are involved. In other words, a type T satisfies a concept C defined by a constraint R if and only if a T can be substituted for C's placeholder type during the evaluation of R.


Everything you said is very much true, but there is a kind of nominal typing even in template land via (partial) specialization of special trait classes.

For an example see std::tuple_size/tuple_element and their relation with (ironically) structured binding.


Right: C++ offers a continuum between pure structural typing and nominal typing. Author chooses according to need. Failing to support structural typing is not an advantage.


Yes, as usual, name a language feature and it is probably somewhere in C++ :) (mind, I'm not complaining).


Partially, because they only work for the caller, on the implementation itself, one can still make use of structural typing on top of concept constrained parameters and the compiler will be silent on that until the template gets instatiated and the traditional compiler vomit ensues.


You are just talking about when they are applied, not what can be done. Nothing was removed from C++: you can still write templates with structural constraints, or trait constraints, or any mix.

In Rust, a generic is only right or wrong, in isolation. In C++, how it behaves, including whether it compiles, depends on how you choose to use it. That makes C++ templates strictly more powerful than Rust facilities. Rust could relax some restrictions, selectively, to gain that power, and might someday.

Preventing the "compiler vomit" is a main selling point of Concepts, and it works. It is still possible to subvert it, if you work at that (then you are no worse off than before). So, don't.


I am talking about what we got is concepts light, and not the full deal.

When one works with teams our options don't work in isolation.

As for concepts working in preventing compiler vomit, there is still plenty of work to do in 2021.


The "full deal" turned out to be undesirable, and undesired.

There is always more work to do. That is not a failing, it's just life.


The full deal failed due to the usual ISO politics, in the end it was concepts light or nothing.

That is what defines modern C++, a standard full of political compromises, slowly becoming a niche language.


It failed because it turned out not to be usable, in practice. Getting something equally ambitious and also usable was a research project. Blaming ISO politics is a way to avoid need to understand unpleasant technical details.

You wish C++ were becoming a niche language, but in fact its usage is still growing by leaps and bounds, as it has been continually since C++11 came out. Every week more people pick up C++ than the whole population now employed coding that other language. That will be true next year, too.


You wish C++ were as widely used across the whole OS stack as back in its glory 1990's days.

The fact is that it has become a niche language for GPGPU programing, OS drivers and embedded standards like AUTOSAR, everywhere else another language takes the crown jewels, iOS, Android, macOS, ChromeOS, Windows Apps, Web, cloud computing infrastructure,... where is your growing C++?

So much that Apple and Google are now rather focusing on their own languages, with C++17 being good enough for their own purposes, hence why clang is trailing its ISO C++20 compliance.

When everyone brings their own agenda to the table regarding implementation details it is politics, not only concepts, contracts, ABI breaks (god forbid!), reflection, networking, graphics,...., definitly politics.

But hey, it is growing on GPGPU, HPC, Machine Learning libraries for Python, LLVM/GCC implementation language, so there is that.


The problem with structural types are the errors. That one choice leads to a problem doesn't mean that the alternative won't have a worse one.


A simple solution might be to just look for name aliases that match a type and report that to the user. I don't see why you can't name the shape of a structural type without having to use nominal types.


But you could have a dozen name aliases for a specific type. So you can name the type, but the name is no longer unique, as two names for the same structure are two names assigned to the same type.


I want both, so I get to decide the most appropriate for my problem.


F# has anonymous records, which are great, but I definitely wouldn't want them to be the default.


The F# ones are tacked-on and have awkward syntax, but otherwise, why not? As I said, anonymous records could permit named arguments for free, e.g.

    let foo {x, y} = x + y
    in foo {x=1, y=2}
The function "foo" above has type:

    { x:int, y:int } -> int
Again, a nominal type can always easily be created from a structural type.


Elm Records and OCaml objects can be used structurally, so it's not even an unusual concept in the FP space.


And PureScript.

OCaml also has polymorphic variants. Which could also be called anonymous sum types.


Mixing procedural and reactive code in the same program. Imagine if A := B worked in the normal pascal assignment sense, but A :<<: B caused A to be recalculated every time B changed, like a magical assignment operator.

If you assigned something to the system clock, it would act like an interval timer, for example.


https://svelte.dev/ is an example of this idea.


Snapshot state in Jetpack Compose is a less magical version that works well with multithreading.


Surplus too, which came before it (but is less popular because it wasn't marketed at all).


An issue would be spooky action at a distance. A ripple effect of a single assignment could cause a cascade of computations. What order do they happen in? How do you find them all?


Use an algorithm similar to excel spreadsheets, mark a result dirty, and ripple through all the dependencies.


This is always comprehensible in excel because state changes only ever come about as a result of a user changing a value in a cell. In the general case where there are a bunch of different asynchronous sources of change, this could become pretty confusing.


This is why you would opt for a declarative language like Excel for this kind of feature. You define a relationship, and the programming language runtime maintains it as an invariant. Then you don’t care where or when or how the value changes, because that doesn’t factor in to the definition of the thing.

The language can help with debugging by providing traces of execution that you can record and replay to inspect. Then you can query the trace to answer any questions you have about the execution of the program.


The next issue is that you will want atomic updates of multiple variables, because in "x = y + z", you might not want an intermediate "x" value to be used somewhere if both "y" and "z" change.

It gets messy fast, and I absolutely would not want to debug a big program that was full of implicit dependencies like this. This was tried in Java FX. AFAIK it was a total disaster.


I wouldn't say that's an issue, that's how programming is done in such systems. It's like noting that maintaining and managing state is an issue for imperative languages, and you wouldn't want to debug a program that's full of state changes and side effectful functions. Yes, it can be messy, but that doesn't preclude language constructs, compiler support, and best practices to help you write correct programs. Today's newest imperative programming languages try to smooth out the rough edges of old languages in this way.

The issue with reactive, declarative languages is they have not been given the same opportunity to optimize. We've devoted a lot of resources to making imperative programming more accommodating despite all of its warts, but with declarative programming so far we have decline to go through the same process.

Declarative reactive programming has proven to have a lot of potential. After all, if we call Excel a programming language (which it is), then reactive programmers far outnumber those of any other style. We should take that direction as far as it can go. Mine it for every good idea the same way imperative programming has been mined. Sure maybe JavaFX was a total disaster. But why can't its successor be better? Why exactly was it a disaster? What could potentially be done to improve that?


K has this: http://johnearnest.github.io/ok/index.html?run=%20a%3A%3Ab*c...

a::b*c; b:2; c:3; a results in a being equal to 6, and if you update b or c a will update too.


How is this different than a method pointer, Action<T>, Runnable etc? Maybe I don't understand what you're suggesting. Is there something more to it than assigning a method to a variable and calling it as if it was a plain variable?


What GP is suggesting is more about push systems than pull. In a push system you'd have something like this:

  a = 10
  b = 20
  c = a * b
  s = "{c}"
  widget.display(s) -- will show '200' at the start
  a = 2             -- c = 40 and widget shows '40'
It's like a spreadsheet. In a pull system, each dependent would have to periodically update itself, which means they'd be doing unnecessary work when there haven't been changes, and be stale (for longer than necessary) when there have been changes.


So maybe something closer to an observable but sugar built into the language? I suppose it could be interesting if everything was an observable and you could implicitly chain observables through assignment.


Yes, and I actually meant to include them in my comment but for some reason didn't.


Have A be a pointer to B?


I'm curious about the bit on module systems; I've heard that the MLs have a more powerful system than most languages, but not having used them, I don't know what advantages it provides/what problems it solves. All the explanation of ML-style modules seems rather abstract, focused more on how they work rather than what they're useful for. Can anyone point to some good resources/explanations about what ML modules allow you to do that can't be easily done otherwise?


This is probably not technically correct, but I tend to think of ML modules as being almost unrelated to modules in other languages. ML modules are closer to being compile-time records than the plain namespaces of almost every other language, with their own sub language for creating and transforming them. I'm not sure they're fully restricted to compile-time, either.


It may not be exactly what you're looking for, but "The History of Standard ML" published last year has a nice section on ML modules with design considerations.


Some more things I see coming soon:

* Decoupled allocation, to use any allocator with any existing code. Cone has some really cool things on the way here, where we can package up allocators into modules and others can import them and use them [0]. Odin is also trailblazing here, with its implicit allocator "context" parameter [1]. Zig does this in an explicit way in its standard library [2].

* First-class isolated regions, where certain areas of memory cannot point to each other, but within a particular region we can freely mutably alias. Vale [3] and Verona [4] are doing some interesting experiments here.

* Better static analysis, to close the gap between memory-managed languages and C. Lobster's algorithm is very impressive here [5], and Vale is attempting an "automatic Rust" approach [6].

* True "structured parallelism", where we can "freeze" all existing memory and freely access it in parallel from a loop's body. Pure functional languages might be able to do this, but it would be interesting in an imperative language. Rust can almost do this, but can only share things with Sync.

* Blending the borrow checker with other paradigms. Cone is going all-in on this concept [7], and looks really promising. The pieces are also there in D and Vale, but we'll see!

I also have some things on my wish list (full deterministic replayability!) but they aren't really on the horizon yet.

(Disclaimer: I work on Vale, which aims to bring many of these features into the mainstream)

[0]: https://cone.jondgoodwin.com/coneref/refregionglo.html

[1]: https://odin-lang.org/docs/overview/#allocators

[2]: https://ziglearn.org/chapter-2/

[3]: https://vale.dev/blog/zero-cost-refs-regions

[4]: https://www.microsoft.com/en-us/research/project/project-ver...

[5]: https://aardappel.github.io/lobster/memory_management.html

[6]: https://vale.dev/blog/hybrid-generational-memory

[7]: https://cone.jondgoodwin.com/coneref/refperm.html


I might also want to add that rust allows decoupled allocation rn. There are some popular allocators, like jemalloc, but also some project specific ones, especially for some custom OS no-std projects.

Edit: See https://docs.rust-embedded.org/book/collections/index.html#u... for a through explanation


Something about Odin I like is that you can have multiple allocators, and use a bump allocator for just a specific call (and its subcalls), and afterward free it all at once, and go back to normal heap allocation. It would be cool if Rust offered a built-in way to do that too.


Strictly speaking, Rust doesn't need this as a built-in language feature, because its design allows it to be implemented as a third-party library: https://docs.rs/bumpalo

The biggest problem is that there's some awkwardness around RAII; I'm not sure whether that could have been avoided with a different approach.

Of course, ideally you'd want it to be compatible with the standard-library APIs that allocate. This is implemented, but is not yet at the point where they're sure they won't want to make backwards-incompatible changes to it, so you can only use it on nightly. https://doc.rust-lang.org/stable/std/alloc/trait.Allocator.h...

Or are you suggesting that the choice of allocator should be dynamically scoped, so that allocations that occur while the bump allocator is alive automatically use it even if they're in code that doesn't know about it? I think it's not possible for that to be memory-safe; all allocations using the bump allocator need to know about its lifetime, so that they can be sure not to outlive it, which would cause use-after-free bugs. I'm assuming that Odin just makes the programmer responsible for this, and if they get it wrong then memory corruption might occur; for a memory-safe language like Rust, that's not acceptable.


It actually is possible to have decoupled allocation in a memory safe way. We have an open proposal for this in [0], for Vale. TL;DR: Have some bits in the malloc header which instruct the language on which deallocator function to use.

I've never used Odin, so I don't know whether/how they'd keep it safe.

[0]: https://docs.google.com/document/d/1243br9VVluZN0ZD9MVKSQMAw...


Hmm. It seems the way this prevents use-after-free is by having the allocator's destructor check at runtime whether everything in it has already been dropped, and if not, abort the process. With bumpalo's default API (which seems to be designed to have the lowest possible per-allocation runtime overhead), that wouldn't work, because it deliberately doesn't keep track of whether the things in it have been dropped (and also because there's no header to store the bits in).

On the other hand, if you required an API along the lines of bumpalo::boxed, and also were willing to add a bit more runtime overhead on top of that (for the tracking bits and the allocation count), then this could be done in Rust as a third-party library. Each executable that transitively depended on it would have to opt in with #[global_allocator], though. Also, I personally would rather have an API where the compiler makes sure I don't screw this up, than one where the runtime crashes my program if I do; this is generally a common sentiment in Rust, and points towards a bumpalo-style API.


That Sounds cool. Also sounds like an interesting way to potentially optimize applications for specofic workloads, with e.g. optomizations that build on top of applications usage logs. Maybe in a far, far future an addition to LLVM or other optimizing compilers.

Edit: Spelling. PS: Though might also be an awesome way to shoot yourself in the foot.


Off topic question, how did you find out all these non-mainstream language that are doing interesting things and research? First time I heard of Cone and Vale.

And I think your post deserve to be at the top. I wish I could super upvote.


Thanks =) I suspect my post was penalized down from the top once I edited in those link citations. HN works in mysterious ways!

I've mostly been in the https://reddit.com/r/ProgrammingLanguages subreddit and its discord server, and chat with these folks a lot, about interesting innovations.

It's a fascinating time to be into programming languages!


Non-mainstream languages tend to find their way to HN. Take a look at my favorite list, I favorite every PL posted here that I come across (over 250 so far)


Wow, that's an impressive collection! Are there any that stood out to you?

(I'm especially interested in new memory management techniques)


I like the 1ML idea (unifying records and modules), even if I don't really understand it. I just like the idea of fewer - but more powerful - constructs in a language.

The devil on my shoulder is kind of telling me that it's just another example of FP people slowly re-inventing a form of Object Orientation, though. But I think that's a good thing. A rose by any other name...


Zig does this too I believe


I'd bet on a future language that makes it easy to program professionally on mobile devices.

Yes, I know that for us "real" programmers this is dreadful, but it would make programming more accessible and this could have interesting network effects on our society.

I'd imagine that this language would have some traces of visual programming (like Enso[0]) and the denseness of APL[1].

[0] https://enso.org [1] https://aplwiki.com/wiki/Simple_examples


Programming on mobile devices is hard because that is the way the platform owners like it.

So, first, mobile devices need to be busted loose from the gatekeepers.


My impression is that programming language design for single-threaded, CPU-based algorithmic programs is basically a solved problem. There's a bunch of innovative ideas sitting around in research projects, but most bare little resemblance to actual pain points of programmers in industry. A more complex, expressive type system is not going to make an average SWE's day job significantly easier.

Rather, I think that PL design headroom is largely in new programming domains, including:

Distributed systems. Right now, these are cobbled together between single processes written in conventional programming languages; RPC & serialization frameworks; container or cloud orchestration mini-languages; and a lot of proprietary tooling. There's no unified way of expressing "this computation occurs on this box", while abstracting away the details of communication & provisioning and still providing robust handling for debugging and the complex failure modes that distributed systems introduce.

Machine-learning. This is handled through frameworks (TensorFlow, PyTorch, JAX) that basically provide EDSLs in a host language, usually Python. But a lot of them would really benefit from language-level support; for example, differentiability is an important concept for SGD, and yet is bolted on with a bunch of kludges in these frameworks. There's also the whole issue of how to store & version training data; how to collect it; how to easily support feature extraction and labeling as part of a developer's workflow; how to run in development vs. production mode; and so on.

GPGPU. Your options here are basically CUDA, Futhark, and a few other niche languages. Yet the underlying hardware and computing model is fundamentally different from single-process CPU code, or even multithreaded concurrent CPU code. It seems like there's a lot of headroom by thinking in terms of the operations a GPU provides and then exploring the space of problems that could be solved with that much computing capacity.

Smart contracts. Solidity paves the way here, but Solidity is still pretty kludgey, and makes it easy to shoot your foot off in a far more expensive way than C++. A single vulnerability can cost literally billions of dollars, and often in a very public (and permanent) manner. They're also (by default) immutable, so you can't fix bugs after the fact, and introducing mutability to them also often introduces gaping security holes. You have to worry about gas costs, and each computation operation is a lot more expensive than in traditional computer programs.

Basically, headroom for improvement in programming languages comes from handling things that are not handled in languages right now, where solutions get cobbled together from a bunch of libraries or custom code. That's how it's always been with product design: the products that most need to exist are those that cover problems where no existing product exists, not ones that are minor improvements on existing products.


+1 to GPGPU specifically. It feels like there is enormous opportunity here, as existing languages have serious limitations (CUDA is effectively Nvidia-only, choices for compute shaders are extremely low level), and at the same time I think there's a lot of very cool stuff you can do, largely held back right now by poor tooling.

I suspect that progress on this front will overlap a bit with the previous category, as more often than not you run machine learning workloads on GPU anyway.


They talk about parametric mutability -- I've always wondered what is the hurdle to const in c++ being like noexcept, parameterizable with a bool?


Note that const is already parametric in C++. Template arguments can carry constness in addition to the type itself. Also constness can be queried and added/removed programmatically at compile time.

I think the note about C++ was specifically about member functions qualification, where currently it isn't possible to parametrize over const this vs non-const this. The "Deducing This" [1] proposal will hopefully fix this [2] and other issues.

[1] http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2021/p084... [2] pun fully intended.


There are some lower hanging fruit in my opinion. You see it when dealing a lot with beginners: - variable names that could automatically rejected as bad "car = Apple()" - general confusion about singular vs plural: "cars = car"

Variable names are too important for the computer to be totally oblivious to their meaning, yet this is how all programming languages work.


F# has had a few of these features for a long time


Gonna shoutout F#'s unit-of-measure types

> It's "small" but rather important not to mix units!

With unit of measure, it's possible to easily/simply create types like CustomerId, which is actually a Guid/Int under the covers. Means you can't pass a CustomerId to something that expects an OrderId. These types are erased at compile time, so there's zero effect on runtime performance. It's lovely.


See also: Haskell newtypes and Kotlin value classes (née inline classes).


Does Kotlin have the equivalent of GeneralizedNewTypeDeriving or whatever is required to make math operators work without boilerplate?


One unexpected aspect of unit-of-measurement types is that they don't generalize well at all to anything more complex than basic arithmetic. For example, it's very hard to create a type-safe library that allows you to compute the dot product of (1m 1Hz) and (1Hz 1m) and get 2m/s, but doesn't allow you to accidentally compute the dot product of (1m 1Hz) with itself.


Great to see this catalogue of language features, thought provoking. The thing that makes a language good (instead of just usable) is how well its various features fit together. All that I can think of, fail with this - their usability damaged by special cases, edges, and forcing the use of various contortions.


> Parametric mutability, permission / state polymorphism, ???

> ... does the system of function types and effects scale to expressing polymorphism over the mutable-ness of a type ...

That's a very thought-provoking handful of sentences. Anyone know of work on this question/area?


Perhaps an example that immediately comes to mind is c++ overloaded functions with mutable and immutable versions of the same base type (const ref, non const ref, rvalue reference).


My guess is that less imperative languages is the future. Today all programming languages are basically about exact instructions what to do: languages differ in exactness and creativity of expressing said exactness, but they all share this idea. The next gen languages will let us tell what we want to get at each step, and such languages will differ in granurality of these steps. A basic example: "a=2; b=5; a>b" means "make sure a is greater than b", which is same as swap(a,b) in imperative languages. I know, some would say "ah, that's prolog!" but I don't believe an esoteric language like prolog can win. It must be something javascrptish, understandable by average programmers, and easily mixable with traditional imperative code.


I don't think that's gonna happen at any time. And if it does, we have an AI that is as smart as a human, so we will all be out of our jobs.

Because, a>b could also mean a should become 6. So you need to understand the intent to decide what you should do. In that sense, it's not even prolog, it's "worse" (read: harder to achieve).


A better example would be sorting. Most of the time we don't care about how numbers are sorted, we only care about the result. In this hypothetical new language, sorting would look like "input: A1..An. output: for all k, Ak < Ak+1." It's implied somewhere in the constraints of the upper function that it'a a permutation, but if you want to be pedantic, you can add "for all k exists m such that Bk=Am."


You are making things more complicated than they currently are. Currently I call "list.sorted" and I get a sorted list.

What you propose is essentially what some languages like Idris already offer: general purpose programming with constraints that allow the compiler to generate an implementation, given that the constraints are precise enough.


> Engineer syndrome

Hah, I've seen that and had no name for it. It's definitely a thing, and an annoying one, when designing a language in committee.


My own suspicions is that we've taken programming as far as it can go. All the big ideas were in invented in 1980 or earlier: subroutines, libraries, functional programming, automatic garbage collection, etc.. We may have refined some ideas since then, but it seems that we are mostly arguing over what colour the wheel should be.

Alan Kay compared out current practises as akin to pyramid building; very little is understood about structural engineering properties of the materials, we just place block on top of block. He argues that there is no programming equivalent of the arch.

But suppose that big blocks of stone is all there is. I recall Linus Torvalds talking about one of his proudest achievements. I don't remember specifics, but it was some memory allocator IIRC. All of this was achieved at the microscopic level using C. It's one human sitting down and figuring out exactly what was required to solve the problem, which bits to twiddle, and which bits to leave alone. No shortcuts.

I would argue that programming is not like other engineering disciplines. It boils down to this: we can create specific solutions to specific problems, but we can't create specific solutions to general problems. That is to say, we can create programs like spreadsheets, do accounting, model airflow through turbines, whatever you want, because we understand the problem domain, and it is constrained. As to "how do I write my program", well, what program are you trying to write?

Compilers cannot reason about the structural integrity of a program. They are general purpose. They do not have any domain knowledge about the type of program you are trying to construct. They can therefore only tell you if a program is syntactically valid, not whether the program is flawed at the domain level.

Can we give compilers (or some other near equivalent) domain knowledge? In an exceptionally limited sense, perhaps. I have in mind configuration tools that some microcontroller vendors release. These programs have domain-specific knowledge about how their microcontrollers can, and cannot, be configured and for what purpose.

They are extraordinarily constrained in scope. The hard work of actually getting the hardware to do what you want it to do is left up to you.

So we shouldn't expect too much out of any domain-specific solution. Their scope is highly limited. We shouldn't expect many to be available.

So I'm not expecting much in the way of paradigm shifts in the near future.


Unfortunately the author is not very explicit on the interesting formal stuff: 1.implicit and 2.explicit proof-carrying code and 3. verifying compilers with its cost (there are as of today exactly 2 general usable and fully verified compiler implementations: compcert and cakeml), 4. lowering languages to Isabelle or other interesting proof systems as compiler backends (or using codegen for source code and proof like what cogent does).

The author appears also not to know formally proved and domain specific languages, which are "Effect systems" (they check if conditions are uphold) to generate C code. Generally life is a tradeoff and the same holds for showing semantically expressive properties (or one needs a proof system).

Most languages rely on LLVM and its linker with linker speak, which are both not formally proven to be consistent and both have no model. The spec also does only describe basic hardware abstraction and without a formal model from CPU vendors this wont improve much.

"Cost-model type systems": only sel4. check the blog of the head with papers why https://microkerneldude.org/


The author does, in fact, know about effect systems.

Generating C code is pointless unless a C compiler is the only way you have available to emit machine instructions.




Guidelines | FAQ | Lists | API | Security | Legal | Apply to YC | Contact

Search: