245 comments

[ 4.7 ms ] story [ 238 ms ] thread
Is it just me or whatever "Capabilities" is, is not explained at all?
It is not. I didn't want to give a half explanation, but it is another case of the increasing difficulty in coming up with good Google searches anymore.

https://erights.org/elib/capability/ode/ode-capabilities.htm... is a good start.

But you use capabilities all the time... operating system users work that way. As a user, you can't "just" execute some binary somewhere and thereby get access to parts of the system your user doesn't have rights to. (Forget setuid for a second, which is intended precisely to get around this, and let's just look at the underlying primitive.)

Capabilities in programming languages take the granularity further down. You might call some image manipulation code in a way that it doesn't have the capability to manipulate the file system in general, for example, or call a function to change a user's login name with capabilities that only allow changing that user, even if another user ID somehow gets in there.

It would be a fairly comprehensive answer to the software dependency issues that continue to bubble up; it would matter less if a bad actor took over "leftpad" if leftpad was actively constrained by the language to only be able to manipulate strings, so the worst an actor could do is make it manipulate strings the wrong way, rather than start running arbitrary code. Or put another way, if the result of the bad actor taking the package wasn't that people got hacked but users started getting

    compile error in file.X:28: library "leftpad" tried to open a file without file system capabilities
    compile error in file.X:30: library "leftpad" tried to open a socket without network capabilities
which would immediately raise eyebrows.

It's not a new idea, in that E already tried it, and bits and pieces of it are everywhere ("microkernels" is another place where you'll see this idea, but at the OS level and implemented in languages that have no native concept of the capabilities), but for the most part our programming languages do not reflect this.

> But you use capabilities all the time... operating system users work that way.

Most operating systems don't have proper capabilities - they use things like ACLS, RBAC, MAC, etc for permissions.

The golden rule of capabilities is that you should not separate designation from authority. The capability itself represents the authority to access something, and designates what is being accessed.

(comment deleted)
For the equivalent in operating systems land, look at the respective manual pages for Linux capabilities[1] or OpenBSD pledge[2] and unveil[3]. The general idea is that there are some operations that might be dangerous, and maybe we don't want our program to have unrestricted access to them. Instead, we opt-in to the subset that we know we need, and don't have access to the rest.

There's some interest in the same thing, but at the programming language level. I'm only aware of it being implemented academically.

[1]: https://man7.org/linux/man-pages/man7/capabilities.7.html [2]: https://man.openbsd.org/pledge.2 [3]: https://man.openbsd.org/unveil.2

It's implemented in Java! .NET tried it too, UNIX file descriptors are capabilities, Mach ports are capabilities. Capabilities are widely used far outside of academia and have been for a long time.

What people often mean when they say this is a so-called pure capability system, where there are no ambient permissions at all. Such systems have terrible usability and indeed have never been made to work anywhere, not even in academia as far as I know.

I don't think that Linux capabilities have much to do with the capabilities that the OP intends.

In a capabilities system, a program has permission to act on any object if it has a reference (aka a capability) to the object, there is no other access control. A program acquires a capability either by receiving it from is parent (or caller in the case of a function) or some other way like message passing. There is no other source of capabilities and they are unforgeable.

Unix file descriptors act in many ways as capabilities: they are inherited by processes from their parents and can be passed around via Unix sockets, and grant to the FD holder the same permissions to the referenced object as the creator of the file descriptor.

Of course as Unix has other ways from creating file descriptors other than inheritance and message passing is not truly a capabilities system.

> This is not a new idea, so I won’t go deeply into what it is

So, no, the author claims it too.

Capabilities are a way to do access control where the client holds the key to access something, instead of the server holds a list of what is allowed based on the clients identities.

But when people use that word, they are usually talking about fine-grained access control. On a language level, that would mean not granting access for example for a library to do network connections, even though your program as a whole has that kind of access.

Kind of. At a more fundamental level, it's applying the idea that "invalid states should be unrepresentable" (e.g. https://hugotunius.se/2020/05/16/making-invalid-state-unrepr... ) but to our code itself.

For example, consider a simple function to copy files. We could implement it like this:

    def copy(fs: Filesystem, in: Path, out: Path) {
      inH: HandleRead = fs.openRead(in);
      outH: HandleWrite = fs.openWrite("/tmp/TEST_OUTPUT");
      finished: Boolean = false;
      while (!finished) {
        match (inH.read()) {
          case None: finished = true;
          case Some(data) = outH.write(data);
        }
      }
      inH.close();
      outH.close();
    }
However, there are many ways that things could go awry when writing code like this; e.g. it will write to the wrong file, since I forgot to put the real `out` value back after testing (oops!). Such problems are only possible because we've given this function the capability to call `fs.open` (in many languages the situation's even worse, since that capability is "ambient": available everywhere, without having to be passed in like `fs` above). There are also other capabilities/permissions/authorities implicit in this code, since any call to `fs.open` has to have the right permissions to read/write those files.

In contrast, consider this alternative implementation:

    def copy(inH: HandleRead, outH: HandleWrite) {
      finished: Boolean = false;
      while (!finished) {
        match (inH.read()) {
          case None: finished = true;
          case Some(data) = outH.write(data);
        }
      }
      inH.close();
      outH.close();
    }
This version can't use the wrong files, since it doesn't have any access to the filesystem: there's literally nothing we could write here that would mean "open a file"; it's unrepresentable. This code also can't mix up the input/output, since only `inH` has a `.read()` method and only `outH` has a `.write()` method. The `fs.open` calls will still need to be made somewhere, but there's no reason to give our `copy` function that capability.

In fact, we can see the same thing on the CLI:

- The first version is like `cp oldPath newPath`. Here, the `cp` command needs access to the filesystem, it needs permission to open files, and we have to trust that it won't open the wrong files.

- The second version is like `cat < oldPath > newPath`. The `cat` command doesn't need any filesystem access or permissions, it just dumps data from stdin to stdout; and there's no way it can get them mixed up.

The fundamental idea is that trying to choose whether an action should be allowed or not (e.g. based on permissions) is too late. It's better if those who shouldn't be allowed to do an action, aren't even able to express it at all.

You're right that this can often involve "keys", but that's quite artificial: it's like adding extra arguments to each function, and limiting which code is scoped to see the values that need to be passed as those arguments (e.g. `fs.openRead(inPath, keyThatAllowsAccess)`), when we could have instead scoped our code to limit access to the functions themselves (though for HTTP APIs, everything is a URL; so "unguessable function endpoint URL" is essentially the same as "URL with secret key in it")

It's a fancy word for "access things through a handle".
The key property being that everything can only be accessed via handles, including, recursively, other handles (i.e. to get an handle to an object you need first to already have an handle to the handle-giver for that object).
A capability is basically a reference which both designates some resource to be accessed and provides the authority to access it. The authority is not held somewhere else like an Access Control List - the reference is the authority. Capabilities must be unforgeable - they're obtained by delegation.

---

To give an example of where this has been used in a programming language, Kernel[1] uses a capability model for mutating environments. Every function (or operative) has an environment which holds all of its local variables, the environment is encapsulated and internally holds a reference to the parent environment (the surrounding scope). The rule is that we can only mutate the local variables of an environment to which we have a direct reference, but we cannot mutate variables in the parents. In order to mutate the variables in the parent, we must have a direct reference to the parent, but there is no mechanism in the language to extract the parent reference from the environment it is encapsulated in.

For example, consider the following trivial bit of code: We define some variable `x` with initial value "foo", we then mutate it to have the value "bar", then look up `x`.

    ($define! x "foo")
    ($set! (get-current-environment) x "bar") 
    x
As expected, this returns "bar". We have a direct reference to the local environment via `(get-current-environment)`.

Technically we could've just written `($define! x "bar")`, where the current environment is assumed, but I used `$set!` because we need it for the next example.

When we introduce a new scope, the story is different.

    ($define! x "foo")
    ($define! foo
      ($lambda ()
        ($set! (get-current-environment) x "bar")))
    (foo)
    x
Here we create a function foo, which has its own local environment, with the top-level environment as its parent. We can read `x` from inside this environment, but we can't mutate it. In fact, this code inserts a new variable `x` into the child environment which shadows the existing one within the scope of the function, but after `foo` has returned, this environment is lost, so the result of the computation is "foo". There is no way for the body of this lambda to mutate the top-level environment here because it doesn't have a direct reference to it.

So far basically the same static scoping rules you are used to, but environments in Kernel are first-class, so we can get a reference to the top-level environment which grants the child environment the authority to mutate the top level environment.

    ($define! x "foo")
    ($define! env (get-current-environment))
    ($define! foo
      ($lambda ()
        ($set! env x "bar")))
    (foo)
    x
And the result of this computation is "bar".

However, by binding `env` in the top-level environment, all child scopes can now have the ability to mutate the top-level.

To avoid polluting the environment in such way, the better way to write this is with an operative (as opposed to $lambda), which implicitly receives the caller's environment as an argument, which it binds to a variable in its local environment.

    ($define! x "foo")
    ($define! foo
      (wrap ($vau () caller-env
        ($set! caller-env x "bar"))))
    (foo)
    x
Now `foo` specifically can mutate it's caller's local environment, but it can't mutate the variables of the caller of the caller, and we have not exposed this authority to all children of the top-level.

---

This is only a trivial example, but we can do much more clever things with environments in Kernel. We can construct new environments at runtime, and they can have multiple parents, ultimately forming a DAG, where environment lookup is a Depth-First-Search, but the references to the parent environments ...

My 4 cents:

- I like the idea of a multiparadigm programming language (many exists) but where you can write part of the code in a different language, not trying to embed everything in the same syntax. I think in this way you can write code and express your ideas differently.

- A [social] programming language where some variables and workflows are shared between users [1][2].

- A superreflective programming language inspired by Python, Ruby, and others where you can override practically everything to behave different. For example, in Python you can override a function call for an object but not for the base system, globals() dict cannot be overriden. See [3]. In this way you save a lot of time writing a parser and the language basic logic.

- A declarative language to stop reinventing the wheel: "I need a website with a secure login/logout/forgot_your_password_etc, choose a random() template". It doesn't need to be in natural language though.

[1] https://blog.databigbang.com/ideas-egont-a-web-orchestration...

[2] https://blog.databigbang.com/egont-part-ii/

[3] https://www.moserware.com/2008/06/ometa-who-what-when-where-...

Egont sounds a bit like SQL, no? A social way to share data and work with it ... a shared RDBMS where everyone has a user account and can create tables/share them with other users, built in security, etc. Splat a GUI on top and you have something similar.

Modern web frameworks are getting pretty declarative. If you want a basic web app with a log in/out page that's not hard to do. I'm more familiar with Micronaut than Spring but you'd just add:

    micronaut.security.authentication=cookie
and the relevant dependencies. Now you write a class that checks the username/password, or use LDAP, or configure OAuth and the /login URL takes a POST of username/password. Write a bit of HTML that looks good for your website and you're done.

https://micronaut-projects.github.io/micronaut-security/late...

> Egont sounds a bit like SQL, no? A social way to share data and work with it ... a shared RDBMS where everyone has a user account and can create tables/share them with other users, built in security, etc. Splat a GUI on top and you have something similar.

Yes, SQL or a global spreadsheet. I would say that it is like SQL plus a DAG or, we can imagine an aggregation of SQLs. The interesting thing is that parts of the global system are only recalculated if there is a change, like in a spreadsheet.

> a shared RDBMS where everyone has a user account and can create tables/share them with other users, built in security, etc. Splat a GUI on top and you have something similar.

We need a little bit more but not much more: security by namespaces and/or rows so the same database is shared but you can restrict who change what: your "rows" are yours. I think something like OrbitDB but with namespaces will be cool.

> Modern web frameworks are getting pretty declarative.

Yes but my proposal was at a higher level. I don't want to know what a cookie is when I just want to create a website. I am not saying that you can create complex components with this idea but you can create common use cases.

For "value database", it seems to me that the trick is, you can't just ship the executable. You have to ship the executable plus the stored values, together, as your installation image. Then what you run in production is what you tested in staging, which is what you debugged on your development system.

I mean, there still may be other things that make this a Bad Idea(TM). But I think this could get around the specific issue mentioned in the article.

Basically, it's the Windows registry.

If it's about well-contained applications in a well designed (and user-centric) OS with a proper concept of "application" and "installation", with a usable enough mechanism, I don't see anything that would make it bad.

On Windows it's a disaster. To the point that dumping random text files around in Linux works better.

It's not very convincing to me when the article talks about truly relational language but fails to mention Prolog and anything that we learned from it.
Logic languages are definitely not what I'd expect a relational-first language to look like.

What we learned from Prolog is mostly that starting from an exponentially-complex primitive and then trying to beat it into submission doesn't work at scale. Relational DBs don't have that problem. They do go n-squared and n-cubed and so forth easily, but there are lots of solutions to that as well.

I'm not sure what you mean with "an exponentially-complex primitive". In my opinion, Prolog lets you start with simple relations (n-squared, using your terms) and then enables you to build more complex relations using them.
for "Semi-Dynamic Language" it might be worth looking into rpython: interpreters written in rpython have two phases, in the first phase one has full python semantics, but in the second phase everything is assumed to be less dynamic, more restricted (the r of rpython?) so the residual interpreter is then transpiled to C sources, which, although compiled, can also make use of the built-in GC and JIT.
I immediately thought of Julia as a semi-dynamic language. Julia is a dynamic language, but (as I understand it) the first time a function is called with a specific type signature, that specific method is JIT compiled as static LLVM.
Which is then used for future dispatches on that same signature and gives it very good performance. Julia is dynamic, and definitely beats the 10x slower than C barrier jerf mentioned.

For what I was using it for at the time (~3 years ago when I used it seriously) it offered performance close to the compiled orbital analysis code we had (in more conventional languages, Fortran and C) but with the flexibility for developing models of Python and other dynamic/interactive languages. An excellent tradeoff: very small performance cost for better interactivity and flexibility.

Of all the non-esolangs (=exolangs?) APL+kith seem to be almost designorismic/estuarine (formerly, Riverian) beasts..

In your informed opinion, how would it make sense to be thrilled thinking about (not just a semi-dynamic APL (=S-DAPL?) as above but) designing

  a APL-kith for widespread adoption by BOTH (demoscenic)gamecoders & Analysts (the academic variety)???
Specifically, which of the sneering checklist items[0] would be killer to cross off?

[0] https://www.mcmillen.dev/language_checklist.html

(Note in particular that the very APL inspired Wolfram has monopoly with physicists BUT does nothing for engineers

https://www.stephenwolfram.com/media/physics-whiz-goes-into-...

1988

>“But we tricked him, so to speak,” says Nobelist Murray Gell-Mann, who helped to bring Wolfram west. “We gave him a Ph.D.” )

Well, I'm thrilled by something in the neighbourhood, but your widespread adoption there is more a "well known in small circles" kind of thing: instead of selling shovels to the gold miners, the target of demosceners+analysts sounds like selling paint brushes to the garret-dwelling artists?

For me, I guess the killer checklist item is "[X] Programming in this language is an adequate punishment for inventing it".

For this putative APL-kith, I'd guess the combination of "[X] You require the compiler to be present at runtime [X] You require the language runtime to be present at compile-time" would be killer; after all wirth-style compilers, these days, can run out of L1$. However, does sufficient staging run into the "fewer than 100 programmers un-algoled* enough" problem?

(could relative popularity of the square bracket language be because phykers display formulae in order to convey insights, but for imkers the calculations are their own point?)

* on the "[X] Rejection of orthodox systems programming without justification" front, what might be to algol-inspired programming as Thistlethwaite's algorithm is to plain old cube solving? https://news.ycombinator.com/item?id=42426716 instead of reducing the group operators as one proceeds, one would reduce the dynamism as the loops nested... (one of the things I find impressive about the rpython JIT is that it roughly manages to implicitly do that reduction!)

>implicitly

Now, do "tacitly"?

Some phykers will admit that making useful (as opposed to publication-quality) graphics in bracket-language remains a PitA, these days of overpowered GPUs..

Do you have a better combination of target audience?

the uiua drawing their logo in uiua made me hopeful that something like that could go viral on yt/tt (& get undergrads running the gauntlet en masse hunting navierstokes singularities on their (still wirthy?) HBMs)

? The analogous cut for EU leadership https://news.ycombinator.com/item?id=42656040

Edit: does this look (relevant) to you, if not, other recs?

https://hackage.haskell.org/package/Operads

Interesting points!

We're working on a language with some of these ideas:

https://www.firefly-lang.org/

Object capabilities, async calls as easy as sync calls, modular monoliths, and (eventually) unified logging.

None of the relational language features though.

Feedback appreciated!

[dead]
Hi, this language looks very cool!

I am interested in the "No Hidden I/O" section. Can you store, say, the system value (?) to a list? Because I'm afraid this is not enough to properly track side-effects.

Unfortunately I think capabilities/an effect system is required for that (so that any point that actually uses a side effect, taints its whole call tree - the same way async/checked exceptions do, both of which are specific Effects.

Thanks!

Yes, you can store the system value (or indeed any other object capability) in a list.

If a function takes in a List[NodeSystem] parameter, then the function can perform side effects through the methods on NodeSystem.

However, if a function takes in a List[T] where T is a type parameter, then that is not enough to perform side effects, even if that function is later called with a List[NodeSystem] argument. There is no way for the function to access the methods of the concrete type of T.

Hence, you can tell whether or not a function may have side effects from the function signature alone.

Realistically, in a bigger project, you might have a NodeSystem type n layers deep buried.

E.g. you just grabbed a hypothetical canvas dependency, which has a Canvas. Can you tell if it does any form of IO or not?

Only capability type can have fields containing other (concrete) capability types.

If Canvas contains NodeSystem, then Canvas must be a capability type itself.

Interesting. You have a bug in the name of your language. :)
- Looser functions (badly chosen name)

Timeouts on calls are, as the OP mentions, a thing in Erlang. Inter-process and inter-computer calls in QNX can optionally time out, and this includes all system calls that can block. Real-time programs use such features. Probably don't want it on more than that. It's like having exceptions raised in things you thought worked.

- Capabilities

They've been tried at the hardware level, and IBM used them in the System/38, but they never caught on. They're not really compatible with C's flat memory model, which is partly they fell out of fashion. Capabilities mean having multiple types of memory. Might come back if partially-shared multiprocessors make a comeback.

- Production-Level Releases

That's kind of vague. Semantic versioning is a related concept. It's more of a tooling thing than a language thing.

- Semi-Dynamic Language

I once proposed this for Python. The idea was that, at some point, the program made a call that told the system "Done initializing". After that point, you couldn't load more code, and some other things that inhibit optimization would be prohibited. At that point, the JIT compiler runs, once. No need for the horrors inside PyPy which deal with cleanup when someone patches one module from another.

Guido didn't like it.

- Value Database

The OP has a good criticism of why this is a bad idea. It's an old idea, mostly from LISP land, where early systems saved the whole LISP environment state. Source control? What's that?

- A Truly Relational Language

Well, in Python, almost everything is a key/value store. The NoSQL people were going in that direction. Then people remembered that you want atomic transactions to keep the database from turning to junk, and mostly backed off from NoSQL where the data matters long-term.

- A Language To Encourage Modular Monoliths

Hm. Needs further development. Yes, we still have trouble putting parts together. There's been real progress. Nobody has to keep rewriting Vol. I of Knuth algorithms in each new project any more. But what's being proposed here?

- Modular Linting

That's mostly a hack for when the original language design was botched. View this from the point of the maintenance programmer - what guarantees apply to this code? What's been prevented from happening? Rust has one linter, and you can add directives in the code which allow exceptions. This allows future maintenance programmers to see what is being allowed.

> In general, while I can’t control how people react to this list, should this end up on, say, Hacker News, I’m looking more for replies of the form “that’s interesting and it makes me think of this other interesting idea” and less “that’s stupid and could never work because X, Y, and Z so everyone stop talking about new ideas” or “why hasn’t jerf heard of this other obscure language that tried that 30 years ago”. (Because, again, of course I don’t know everything that has been tried.)
OK, a few things that languages ought to have:

- Everything except C now has standard strings, not just arrays of characters. Almost all languages now have some standard way to do key/value sets. What else ought to be standard?

-- Arrays of more than one dimension would be helpful for numerical work. Most languages descended from C lack this. They only have arrays of arrays. Even Rust lacks it. Proposals run into bikeshedding - some people want rectangular slices out of arrays, which means carrying stride info around.

-- Standard types for 2, 3 and 4-element vectors would help in graphics work. There are too many different implementations of those in most language and too much conversion.

Things to think about:

- Rust's ownership restrictions are harsh. Can we keep the safety and do more?

-- The back-reference problem needs to be solved somehow. Back references can be done with Rc and Weak, but it's clunky.

-- Can what Rust does with Rc, RefCell, and .borrow() be checked at compile time? That allows eliminating the run-time check, and provides assurance that the run-time check won't fail. Something has to look at the entire call tree at compile time, and sometimes it won't be possible to verify this at compile time. But most of the time, it should be.

-- There's a scheme for ownership where there's one owning reference and N using references. The idea is to verify at compile time that the using references cannot outlive the owning one. Then there's no need for reference counds.

-- Can this be extended to the multi-thread case? There have been academic demos of static deadlock detection, but that doesn't seem to have made it into production languages.

-- A common idiom involves things being owned by handles, but also indexed for lookup by various keys. Dropping the handle drops the object and removes it from the indices. Is that a useful general purpose operation? It's one that gets botched rather often.

-- Should compilers have SAT-solver level proof systems built in?

-- Do programs really have to be in monospaced fonts? (Mesa on the Alto used the Bravo word processor as its text editor. Nobody does that any more.)

-- There's async, there are threads, and there are "green threads", such as Go's "goroutines". Where's that going?

-- Can we have programs which run partly in a CPU and partly in a GPU, compiled together with the appropriate consistency checks, so the data structures and calls must match to compile?

-- How about "big objects?" These are separately built program components which have internal state and some protection from their callers. Microsoft OLE did that, some .dll files do that, and Intel used to have rings of protection and call gates to help with that, hardware features nobody used. But languages never directly supported such objects.

So there are a few simple ideas to think about.

> Do programs really have to be in monospaced fonts?

Of course not. I've been using a proportional font for at least 10 years and I'm still in business working on code bases shared with developers using monospaced fonts. Both work, none disturb the other, proportional is easier to read as any book can demonstrate. Alignment doesn't matter much.

How do you deal with writing code with multicursors when you have to type the same thing multiple times? With monospace I just ctrl+alt+down a couple times on aligned text and then type. With proportional fonts I don't suppose it's easy to align text exactly, so do you just not use multicursors or is there a solution you came up with that works?
I’ve been a professional programmer for 17 years and have never used multicursors. I don’t even fathom under what conditions you’d want to. I use Find and Replace.
Everyone programs a little differently. I often use it when e.g. using intrinsics and I want to change types. Find and replace isn't especially helpful when they're different names with substructure you need to modify locally.
Yes and ctrl-V in vim would be super awkward.
I don't use multicursors much, not every year. I'm using emacs. I register a sequence of keys and apply it multiple times with control e, control e, control e etc.
Not sure it counts as "multicursors" but overwriting a rectangle using C-x r t (string-rectangle) is pretty handy in emacs.
C has arrays with more than one dimension. (but no direct support for strides). Of course, it is just the same thing as arrays of arrays.
Are C multidimensional arrays guaranteed to be contiguous in memory? In practice they are, but can one iterate through them just by incrementing a pointer which points to the first element without UB?
Yes. All arrays in C are contiguous. Multidimensional arrays are just arrays of arrays, and are therefore also contiguous.

Source: https://stackoverflow.com/questions/36647286/are-c-multidime...

Yes, but is one allowed to move a pointer inside it as they see fit? On a one-dimensional array, one can iterate through it starting with a pointer pointing to the first element and ending with a pointer pointing one position past the last element (which the user is not allowed to dereference). For multidimensional arrays, the element type is an array too (with a smaller rank than the original one), so one could perform that type of iteration with a pointer to an array. My question is whether a pointer to the underlying scalar type can freely move inside the multidimensional array without UB, since it may have to actually leave the array it was originally part of. If that's not allowed, how could one build slices and other view types?
Yes, you can do that, it's fine as long as you stay within the bounds of the indexes. Under the hood, it's a single contiguous block of memory.

Although at least with 2d arrays I prefer to just use a 1d array and index it with [x * width + y], because one problem with multidimensional arrays in C is they need multiple allocations/frees.

Why would you need multiple allocations?

edit: Isn't it just:

  float (*arr)[m][n] = malloc(sizeof(*arr));
You have to allocate the size for m and n, because C arrays decay bare pointers.

But it seems to work, which I didn't expect[0]. It also seems like you have to de-reference it to use it which is weird.

[0]https://onlinegdb.com/HwX-WTL5t

Double indirection arrays with multiple allocations are 25 years obsolete (ok, there are some use cases) but since C99 we prefer to do it like the parent.

In your code link you over allocate memory, sizeof *arr is enough and you need to dereference like with (*arr)[i][j]. You need to dereference it because it is a pointer to an array, if you dereference you get an array. You can also let the first dimensions decay then it looks like:

  double (*arr)[m] = malloc(n * sizeof *arr);
  arr[i][j] = ...
but this is not as safe because the bound of the outermost dimension is lost. (Edited)
> Everything except C now has standard strings, not just arrays of characters. Almost all languages now have some standard way to do key/value sets. What else ought to be standard?

I think that character strings should not be restricted to (or generally expected to be) Unicode (although using Unicode and other character sets will still be possible).

I also think that key/value lists should allow any or most types as keys, including references to objects. (PostScript allows any type to be used as keys except strings (they are converted to names if you use them as keys) and nulls.)

I think that big integers (which can have a program-specified limited length in programming languages with typed variables) and arrays of fixed-length records (which C already has; JavaScript has typed arrays which is a more limited implementation of this) are also things that would be helpful to include as standard.

> Arrays of more than one dimension would be helpful for numerical work.

I agree with this too; it is a good idea.

> Standard types for 2, 3 and 4-element vectors would help in graphics work.

This is probably helpful, too, although they can be used for stuff other than graphics work as well.

> Do programs really have to be in monospaced fonts?

No, but that is due to how it is displayed and is not normally a feature of the program itself. Many people including myself do use monospace fonts, but this should not usually be required.

> There's async, there are threads, and there are "green threads", such as Go's "goroutines". Where's that going?

I had read about "green threads" and I think that it is a good idea.

> How about "big objects?" These are separately built program components which have internal state and some protection from their callers. Microsoft OLE did that, some .dll files do that, and Intel used to have rings of protection and call gates to help with that, hardware features nobody used. But languages never directly supported such objects.

I also think it is sensible to have components that can be separated from the callers, and that operating system support (and perhaps hardware support) for such thing might be helpful. I would design a programming language for such an operating system that would directly support such objects.

> - Everything except C now has standard strings, not just arrays of characters.

Zig's strings are null-terminated arrays of bytes. It's an unfortunate mistake from an otherwise interesting language.

Other than comptime, most of Zig ideas were already present in Modula-2, @ all over the place gives a "I miss Objective-C" flavour to it, and only source code ecosystem on a language supposed to be a systems programming language?
(comment deleted)
> Source control? What's that?

I think squeak had Monticello for source control with their image based approach almost 20+ years ago and there was something else for smalltalk in the '80s too.

But yeah people like text and hate images, and I believe Pharo switched back to some git integration.

> something else for smalltalk

"ENVY/Manager augments this model by providing configuration management and version control facilities. All code is stored in a central database rather than in files associated with a particular image. Developers are continuously connected to this database; therefore changes are immediately visible to all developers."

https://www.google.com/books/edition/Mastering_ENVY_Develope...

~

1992 Product Review: Object Technology’s ENVY Developer

http://archive.esug.org/HistoricalDocuments/TheSmalltalkRepo...

Commercial image based systems have had source control like management for decades.

If anything many "modern" low code SaaS products are much worse in this regard, than what Lisp and Smalltalk have been offering for years.

> Capabilities ... They're not really compatible with C's flat memory model ... Capabilities mean having multiple types of memory

C is not really dependent on a flat memory model - instead, it models memory allocations as separate "objects" (quite reniniscent of "object orientation in hardware" which is yet another name for capabilities), and a pointer to "object" A cannot be offset to point into some distinct "object" B.

> A Truly Relational Language

This is broadly speaking how PROLOG and other logic-programming languages work. The foundational operation in such languages is a knowledge-base query, and "relations" are the unifying concept as opposed to functions with predefined inputs and outputs.

Possibly the nearest to applying capabilities to C is pointer authentication: https://lwn.net/Articles/718888/

(This is one of those times where the C memory model as described in the spec is very different from the mental PDP-11 that C programmers actually use to reason about)

C's file descriptors are capabilities.
> But what's being proposed here?

He proposes that there is a need for a way to connect modules, i.e. dependency injection, without the modules having explicit knowledge of each other, with compile-time verification that the modules being connected are compatible, without the interface song and dance.

> - A Language To Encourage Modular Monolith

> But what's being proposed here?

I read it, and to me it seems like they're worried about the wrong things. As I understand it, they're worried about the difficulty and hassle of calling unrelated code in the monolith, and proposing things that would make it easier. But that's wrongheaded. Monoliths don't suffer because it's too hard to reuse functionality. They suffer because it's too easy. Programmers create connections and dependencies that shouldn't exist, and the monolith starts to strangle itself (if you have good tests) or shake itself to pieces (if you don't) because of unnecessary coupling. You need mechanisms that enforce modularity, that force programmers to reuse code at designated, designed module interfaces, not mechanisms that make it easier to call arbitrary code elsewhere in the monolith.

In my opinion, a great deal of the success of microservices is due to the physical impossibility of bypassing a network API and calling the code of another service directly. Because of this, programmers respect the importance of designing and evolving APIs in microservices. Essentially, microservices enforce modularity, forcing programmers to carefully design and evolve the API to their code, and this is such a powerful force for good design that it makes microservices appealing even when their architectural aspects aren't helpful.

A language that made it possible to enforce modularity in a monolith as effectively as it is enforced in microservices would make monoliths a no-brainer when you don't need the architectural aspects of microservices.

This is literally the core tenet of OOP, and arguably Java and friends did a decent enough job of that via encapsulation and visibility modifiers.
Not really. Look at any open source Java library, and you'll see that unless it's small enough to implement in a single package, it probably contains public classes that aren't meant to be part of the public API but are declared public so they can be used in other packages in the library codebase.

That's why they introduced a module system for Java in Java 9. It sounded pretty cool to me when they announced it, but it was a bit too late to make much difference in the Java ecosystem (Java 9 came out 21 years after Java 1) and I haven't heard much about it since then.

The module system is used extensively by the core java ecosystem (e.g. the jlink tool will create lean "JREs" based on which modules are actually used), and is getting more and more common in projects that are using a relatively modern Java version.

Besides, it makes sense to encapsulate at different scopes - a class, a package, a module, a library/application. These are "abstractions" on different levels, semantics, constraints.

> - A Truly Relational Language

> Well, in Python, almost everything is a key/value store.

Why would that be anywhere near an adequate substitute? KV stores are not relational, they don't support relational algebra. KV stores in PLs are common as dirt, so if they were relevant to the question of ending relations in a language I think the author would have noticed.

> The OP has a good criticism of why this is a bad idea.

They simply assert "twiddling a run-time variable for debugging in your staging environment can propagate straight into a bug on production".

As-if straight into production without re-testing.

> Source control? What's that?

"ENVY/Manager augments this model by providing configuration management and version control facilities. All code is stored in a central database rather than in files associated with a particular image. Developers are continuously connected to this database; therefore changes are immediately visible to all developers."

https://www.google.com/books/edition/Mastering_ENVY_Develope...

~

1992 Product Review: Object Technology’s ENVY Developer

http://archive.esug.org/HistoricalDocuments/TheSmalltalkRepo...

Many of these things (not only what you describe here but also the linked article) are stuff that I had intended to be available in the built-in command shell (called "Command, Automation, and Query Language", which is meant to describe some of the intentions) of an operating system design, so that they would have support from the operating system.

About capabilities, I think that capabilities should be a feature of the operating system, although hardware support would be helpful. However, I think that it could be done with tagged memory, without necessarily needing multiple types of memory, and programming languages such as C could still be capable of using them (although some things might not work as it would be expected on other computers, e.g. if you try to copy a reference to a capability into a memory area that is expected to be a number and then try to perform arithmetic on that number, the program is likely to crash even if the result is never dereferenced).

However, my idea also involves "proxy capabilities" too, so that you can effectively make up your own capabilities and other programs receive them without necessarily knowing where they came from (this allows supporting many things, including (but not limited to) many of the idea of "divergent desktop" of Arcan).

> Capabilities > > Capabilities mean having multiple types of memory. Might come back if partially-shared multiprocessors make a comeback.

I found this description amusing because all modern memory safe languages have capabilities, and they all have multiple types of memory: that's what an object is! Memory safety partitions memory into different types, and object references are capabilities!

What languages do next is where they break the security properties of capabilities: they add "ambient authority" and "rights amplification". Quick primer:

Ambient authority is basically the same problem as globally mutable state. Globally mutable state impedes modular reasoning about code, but if that state also carries authority to do something dangerous in the real world, like launch missiles, then it also impedes modular reasoning about security for the exact same reasons.

Rights amplification is the ability to turn a reference to an object with little to no authority, into a reference to an object with more authority. File.Open is the quintessential example, where you can turn an immutable string that conveys no authority, into a file handle to your root file system!

File.Open is also typically accessible to all code, meaning it's also ambient authority. This classical file API is completely bonkers from a security perspective.

So we already have capabilities, what we really need to do is to stop adding all of this insecurity! The developers of the E language actually showed that this could be done by making a capability secure subset of Java called Joe-E, which removed the parts of Java and the standard library that exposed ambient authority or rights amplification patterns. Most Java code could run unmodified.

And as for whether capability security will ever be adopted elsewhere, it already has been! WASM's WASI has capability security in its core design, because capability security is exactly what you need for good isolation and virtualization, which are the core properties WASM needs.

> The OP has a good criticism of why this is a bad idea. It's an old idea, mostly from LISP land, where early systems saved the whole LISP environment state. Source control? What's that?

Symbolics Genera can save (incremental and complete) images (-> "Worlds"). The image tracks all the sources loaded into it. The sources/files/docs/... of the software is stored on a central (or local) file server.

I can for example start an initial world and load it with all the wanted software in the various versions I want. Maybe I save a new world from that.

I can also start an pre-loaded world and incrementally update the software: write patches, create new minor/major versions, load patches and updates from the central server, install updates from distributions, ... Maybe save new worlds.

The "System Construction Tool" tracks what code is loaded in what version from where.

As for "capabilities", I'm not sure I fully understand how that is advantageous to the convention of passing the helper function ("capability") as an argument to the "capable" function.

For instance, in Zig, you can see that a function allocates memory (capability) because it requires you to pass an allocator that it can call!

I'd like to see if others are more creative than me!

That's pretty much how it plays out, as I understand it.

The trick is making sure that that object is the Only possible way to do the thing. And making more features like that, for example Networking, or File I/O, etc

In Zig it's conventional to pass an allocator, but any code can end run around the convention by reaching for page_allocator or c_allocactor behind your back. Capabilities upgrade that convention into a guarantee.
> Value Database

> Smalltalk and another esoteric programming environment I used for a while called Frontier had an idea of a persistent data store environment. Basically, you could set global.x = 1, shut your program down, and start it up again, and it would still be there.

Frontier! I played with that way back when on the Mac. Fun times.

But as for programming language with integrated database... MUMPS! Basically a whole language and environment (and, in the beginning, operating system) built around a built-in global database. Any variable name prefixed with ^ is global and persistent, with a sparse multi-dimensional array structure to be able to organize and access the variables (e.g. ^PEOPLE(45,"firstname") could be "Matthew" for the first name of person ID 45). Lives on today in a commercial implementation from Intersystems, and a couple Free Software implementations (Reference Standard M, GT.M, and the GT.M fork YottaDB). The seamless global storage is really nice, but the language itself is truly awful.

Image persistence was one of the cool ideas of Smalltalk. And in practice, one of the biggest drawbacks. Cruft and old values accumulated steadily, with very little way to find and eliminate them. Transient execution has some cons. But on the pro side, every run starts from a "clean slate."
This may fall in the "you think you do, but you don't category", but I've always wanted a Smalltalk (or similar, not that picky) with a persistent virtual memory.

That is, the VM is mapped to a backing file, changes persisted automatically, no "saving", limited by drive space (which, nowadays, is a lot). But nowadays we also have vast memory space to act as a page cache and working memory.

My contrived fantasy use case was having a simple array name "mail", which an array containing all of my email messages (in email object, of course). Naturally as you get more mail, the array gets longer. Also, as you delete mail, then the array shifts. It's no different, roughly, than the classic mbox format, save it's not just text, its objects.

You can see if you delete a email, from a large (several GBs), there would be a lot of churn. That implies maybe it's not a great idea to use that data structure, but that's not the point. You CAN use that data structure if you like (just like you can use mbox if you like).

Were it to be indexed, that would be done with parallel data structures (trees or hashes or whatever).

But this is all done automagically. Just tweaks to pages in working memory backed by the disk using the virtual memory manager. Lots and lot of potential swapping. C'est la vie, no different from anything else. This what happens when you map 4TB into a 16GB work space.

The problem with such a system, is how fragile is potentially is. Corrupt something and it happily persists that corruption, wrecking the system. You can't reboot to fix it.

Smalltalk suffers from that today. Corrupt the image (oops, did I delete the Object become: method again?), and its gone for good. This is mitigated by having backup images, and the changelist to try to bring you back to the brink but no further.

I'm guessing a way to do that in this system is to use a copy on write facility. Essentially, snapshot the persistent store on each boot (or whatever), and present a list of previous snapshot at start up.

Given the structure of a ST VM you'd like to think this is not that dreadful to work up. I'd like to think a paper napkin implementation PoC would be possible, just to see what it's like. One of those things were the performance isn't really that great, but the modern systems are so fast, we don't really notice it in human terms.

But I do think it would be interesting.

Have you looked at Pharo? Their git integration makes it relatively easy to export and backup parts of your main image, and to pull the things back into a fresher one once you mess up.
Smalltalk suffers from mis-information —

> oops, did I delete the Object become: method again?), and its gone for good.

And then you admit actually it's not gone for good because if you created the method that will be recorded in the changes.log file and if it was a provided method that will still be in the provided sources file.

https://cuis-smalltalk.github.io/TheCuisBook/The-Change-Log....

> with very little way to find and eliminate them.

The best Smalltalk these days is GlamorousToolkit: https://gtoolkit.com/

It has a sort of git in it, so you can easily "rollback" your image to previous states. So going back and forth in history is trivial.

> you can easily "rollback" your image to previous states.

Sounds very interesting. Does it support multi-developer merging and/or rebasing of changes?

I believe it's just a git repo behind the scenes. Not sure if the UI exposes those things as I never used that in multi-developer scenarios! Give it a go and see.
And in practice, both.

Save image... for short-term convenience; build clean every week from archived text files.

----

1984 "Smalltalk-80 The Interactive Programming Environment" page 500

"At the outset of a project involving two or more programmers: Do assign a member of the team to be the version manager. … The responsibilities of the version manager consist of collecting and cataloging code files submitted by all members of the team, periodically building a new system image incorporating all submitted code files, and releasing the image for use by the team. The version manager stores the current release and all code files for that release in a central place, allowing team members read access, and disallowing write access for anyone except the version manager."

https://rmod-files.lille.inria.fr/FreeBooks/TheInteractivePr...

So the "version manager" is a human git repository!
40 years ago, yes!

In the same way that "network" had recently been sneaker-net.

The MUMPS database is wild. When I was working in MUMPS, it was so easy and fun to whip up an internal tool to share with my coworkers. You don't have to give any special thought at all to persistence, so you're able to stay in the flow of thinking about your business logic.

But as you said, the language itself is almost unbearable to use.

I had a professor who is responsible for a lot of the more "modern" MUMPS stuff (lets be real, MUMPS is OLD!). Guy was pretty unbearable too.
TADS, an OOP language + VM for interactive fiction, has this "value database" model. Once loaded into memory, the compiled image can be updated with values stored in a separate save file. The compiled image itself could store updated values as well.

In fact, it does this during a "preinit" stage that runs immediately after compilation. Once all preinit code finishes executing, the compiled image is overwritten with the updated state. The language includes a "transient" keyword to permit creating objects that should not be stored.

This same mechanism permits in-memory snapshots, which are used for the game's UNDO feature. No need to rewind or memento-ize operations, just return to a previous state.

It's not a general-purpose mechanism. After all, the language is for building games with multiple player-chosen save files, and to permit restarting the game from a known Turn 0 state.

Starlark, a variant of Python, can be thought of as semi dynamic: all mutation in each file happens once, single threaded, and then that file and all its data structures are frozen so downstream files can use it in parallel

A lot of "staged" programs can be thought of as semi dynamic as well, even things like C++ template expansion or Zig comptime: run some logic up front, freeze it, then run the rest of the application later

Google’s build system uses Starlark definition files for this reason. Very easy to write flexible configurations for each project and module but building is of course very parallel.
My wild idea is that I'd like to see a modern "high-level assembler" language that doesn't have a callstack. Just like in the olden days, all functions statically allocate enough space for their locals. Then, combine this with some semi-convenient facility for making sure that local variables for a given function always fit into registers; yes, I admit that I'm strange when I say that I dream of a language that forces me to do manual register allocation. :P But mostly what I want to explore is if it's possible to create a ""modern"" structured programming language that maps cleanly to assembly, and that provides no optimization backend at all, but has enough mechanical sympathy that it still winds up fast enough to be usable.
> all functions statically allocate enough space for their locals.

Would you still have distinct activation records per call or forfeit the ability to have reentrant functions and recursion?

That's one of the main reasons to move to dynamic (as in a call stack) allocation of your activation records versus a single static allocation per function.

In this hypothetical language I'm assuming that recursion is unsupported and that if threading is supported at all, then each thread has its own copy of every function's locals (or at least every function that can be called concurrently; structured concurrency might be leveraged to prove that some functions don't need to be reentrant, or maybe you just chuck a mutex in each function prologue and YOLO). However, while enforcing that basic recursion is forbidden isn't too difficult (you make the language statically-typed, all names lexically-scoped, and don't support forward declarations), it does probably(?) mean that you also lose first-class functions and function pointers, although I haven't thought deeply about that.
I think lambdas or function pointers can be possible if they assume all of the scope of where they are called rather than where they are declared, that would prevent them from allowing recursion through forward declarations.

It would be awkward to work with since you'd have to be aware of all of the eventual caller scopes rather than your current local scope when defining it.

I suppose it would be like macros instead of true functions.

Why would you like to have this language? Is it about control over the execution? About better ways to personally optimize? Or just intellectual pleasure? Or is it about reliving the olden days of assembly language programming but with a modern conveniences?
I would simply find pleasure in being able to understand basically every level of the stack. For a RISC architecture, it's not too hard to get a grasp on how it works. Likewise for a simple-enough programming language. The problem(?) is that in between these two is an opaque black box--the optimization backend, which I feel I have no hope of understanding. So instead I wonder if it's possible to have a "safe" (safer than C) and "high-level" (more abstractive than C) language that is still useful and semi-performant, and I'm wondering how much ergonomics would need to be sacrificed to get there. It's a thought experiment.
Have you thought about what happens if you want to read and parse a file? Do you declare the maximum filesize you want to support and statically allocate that much memory?
No, you have a scoped pointer to dynamically allocated memory; when the scoped pointer is destroyed/cleaned up/released at the end of the function, it releases the allocated memory.
I'm not intending to imply that the language I'm describing can't support heap-allocated memory; Rust shows us that it's even possible to do so without having to manually deallocate, if you're okay with a single-ownership discipline (which is a rather simple analysis to implement, as long as you don't also want a borrow checker along for the ride). Instead, this is about trying to make a language that makes it easy to keep locals in registers/cache, rather than relying on the compiler backed to do register allocation and hoping that your CPU can handle all that cache you're thrashing.
A useful purpose for such a thing is in certain embedded, hard-real-time, or mission-critical scenarios.

Many such programming environments need strict control over stack sizes to avoid any possibility of stack overflow.

I had a similar notion a few years back, thinking about a somewhat wider range of "scoped guarantees". The compiler would compute things such as the maximum stack usage of a function, and this would "roll up" to call sites automatically. This could also be used to enforce non-usage of certain dangerous features such as locks, global flags, or whatever.

> modern "high-level assembler" language that doesn't have a callstack

PIC16 has a hardware stack of only a few return addresses, and therefore imposes the "all register allocation is static" + "no recursion" that you're asking for.

I agree about relational languages. It's absurd when I think that SQL and Datalog came from the same foundations of relational calculus. It's just so much lost expressive power.

I really like what PRQL [1] did, at least it makes table operations easily chainable. Another one that comes to mind is Datomic [2].

[1]: https://prql-lang.org/

[2]: https://docs.datomic.com/peer-tutorial/query-the-data.html

Uh... given the beauty of relational algebra I don't understand how we ended up with the ugly mess of sql.
Here you go: https://www.red-gate.com/simple-talk/opinion/opinion-pieces/...

"RM: What was key to SQL becoming the standard language for relational databases in the mid- 1980s? Was all down to good marketing?

CJD: In other words, why did SQL became so popular? Especially given all its faults? Well, I think this is rather a sorry story. I said earlier that there has never been a mainstream DBMS product that’s truly relational. So the obvious question is: Why not? And I think a good way for me to answer your questions here is to have a go at answering this latter question in their place, which I’ll do by means of a kind of Q&A dialog. Like this:

    Q:
        Why has no truly relational DBMS has ever been widely available in the marketplace?
    A:
        Because SQL gained a stranglehold very early on, and SQL isn’t relational. 
    Q:
        Why does SQL have such a stranglehold? 
    A:
        Because SQL is “the standard language for RDBMSs.” 
    Q:
        Why did the standard endorse SQL as such and not something else-something better? 
    A:
        Because IBM endorsed SQL originally, when it decided to build what became DB2. IBM used to be more of a force in the marketplace than it is today. One effect of that state of affairs was that-in what might be seen as a self-fulfilling prophecy-competitors (most especially Relational Software Inc., which later became Oracle Corp.) simply assumed that SQL was going to become a big deal in the marketplace, and so they jumped on the SQL bandwagon very early on, with the consequence that SQL became a kind of de facto standard anyway. 
    Q:
        Why did DB2 support SQL? 
    A:
        Because (a) IBM Research had running code for an SQL prototype called System R and (b) the people in IBM management who made the decision to use System R as a basis on which to build DB2 didn’t understand that there’s all the difference in the world between a running prototype and an industrial strength product. They also, in my opinion, didn’t understand software (they certainly didn’t understand programming languages). They thought they had a bird in the hand. 
    Q:
        Why did the System R prototype support SQL? 
    A:
        My memory might be deficient here, but it’s my recollection that the System R implementers were interested primarily in showing that a relational-or “relational”-DBMS could achieve reasonable performance (recall that “relational will never perform” was a widely held mantra at the time). They weren’t so interested in the form or quality of the user interface. In fact, some of them, at least, freely admitted that they weren’t language designers as such. I’m pretty sure they weren’t all totally committed to SQL specifically. (On the other hand, it’s true that at least one of the original SQL language designers was a key player in the System R team.) 
    Q:
        Why didn’t “the true relational fan club” in IBM-Ted and yourself in particular-make more fuss about SQL’s deficiencies at the time, when the DB2 decision was made? 
    A:
        We did make some fuss but not enough. The fact is, we were so relieved that IBM had finally agreed to build a relational-or would-be relational-product that we didn’t want to rock the boat too much. At the same time, I have to say too that we didn’t realize how truly awful SQL was or would turn out to be (note that it’s much worse now than it was then, though it was pretty bad right from the outset). But I’m afraid I have to agree, somewhat, with the criticism that’s implicit in the question; that is, I think I have to admit that the present mess is partly my fault."
Discussed in HN (probably posted many times): https://news.ycombinator.com...
I know the story of System R really well.

It was a breakthrough system is almost all aspects. It defined what dbs would look like both internally and externally for decades.

In many ways SQL is like C: good for what authors wanted it to be but severe consequences much later.

But the history doesn't care. SQL (and C) still have many-many years ahead of them.

> Why has no truly relational DBMS has ever been widely available in the marketplace?

Postgres was "truly relational" for a significant portion of its life before finally losing the battle with the SQL virus. There is probably no DMBS more widely available. Granted, it wasn't widely used until the SQL transition.

> SQL isn’t relational.

This is key. Relations are too complicated for the layman, the one who is paying for it, to understand. Tables are more in tune to what is familiar to them. The hardcore math/software nerds might prefer relationality, but they aren't the ones negotiating multi-million dollar contracts with Oracle/IBM.

I remember when Postgres moved to SQL. People started billing it as being Oracle, but free. That got non-technical manager attention. Without that marking success appealing to the layman, I expect nobody would be using it today.

I have spent a lot of time trying to understand how we ended up with SQL. Best I can determine, we got SQL because it isn't relational, it is tablational. Tables are a lot easier than relations to understand for the layman, and they successfully pushed for what they were comfortable with, even if to the chagrin of technical people.
I explore the idea with https://tablam.org (relational + array). I even toyed with making relational queries to make types:

    data Person = User ?except .password ?select .name, .id + Customer ?select .name, .customer_id AS .id
So the types are in full sync when changes on the schema happen. And all of this is type safe.
I was struggling with doing interesting things with the semantic web circa 2007 and was thinking "OWL sucks" and looking at Datalog as an alternative. At that time Datalog was an obscure topic and hard to find information about it. 10 years later it was big.

(Funny after years of searching I found somebody who taught me how to do really complex modelling in OWL DL but from reading the literature I'm pretty sure the average PhD or prof in the field has no idea.)

> I found somebody who taught me how to do really complex modelling in OWL DL

Is there any resource you could recommend for that?

I wrote up what I learned an a technical report that got sent to the editors at ISO a month or so ago and ought to appear pretty soon. Look up my profile and send me a note.
I like a lot of these ideas.

"Semi-dynamic" is one of the most common architectures there is for large & complex systems. AAA games are usually written in a combination of C++ and a scripting language. GNU Emacs is a Lisp application with a custom interpreter that is optimized for writing a text editor. Python + C is a popular choice as well as Java + Groovy or Clojure, I've even worked with a Lua + FORTRAN system.

I also think "parsers suck". It should be a few hundred lines at most, including the POM file, to add an "unless" statement to the Java compiler. You need to (1) generate a grammar which references the base grammar and adds a single production, (2) create a class in the AST that represents the "unless" statement and (3) add an transformation that rewrites

   unless(X) {...} -> if(!X) {...}
You should be able to mash up a SQL grammar and the Java grammar so you can write

   var statement = <<<SELECT * FROM page where id=:pageId>>>;
this system should be able to export a grammar to your IDE. Most parser generators are terribly unergonomic (cue the event-driven interface of yacc) and not accessible to people who don't have a CS education (if you need a bunch of classes to represent your AST shouldn't these get generated from your grammar?) When you generate a parser you should get an unparser. Concrete syntax trees are an obscure data structure but were used in obscure RAD tools in the 1990s that would let you modify code visually and make the kind of patch that a professional programmer would write.

The counter to this you hear is that compile time is paramount and there's a great case for that in large code bases. (I had a system with a 40 minute build) Yet there's also a case that people do a lot of scripty programming and trading compile time for a ergonomics can be a win (see Perl and REBOL)

I think one goal in programming languages is to bury Lisp the way Marc Anthony buried Caesar. Metaprogramming would be a lot more mainstream if it was combined with Chomksy-based grammars, supported static typing, worked with your IDE and all that. Graham's On Lisp is a brilliant book (read it!) that left me disappointed in the end because he avoids anything involving deep tree transformations or compiler theory: people do much more advanced transformations to Java bytecodes. It might be easier to write those kind of transformations if you had an AST comprised of Java objects instead of the anarchy of nameless tuples.+

Totally agree that programming languages are a bit stagnant, with most new features being either trying to squeeze a bit more correctness out via type systems (we're well into diminishing returns here at the moment), or minor QoL improvements. Both are useful and welcome but they aren't revolutionary.

That said, here's some of the feedback of the type you said you didn't want >8)

(1) Function timeouts. I don't quite understand how what you want isn't just exceptions. Use a Java framework like Micronaut or Spring that can synthesize RPC proxies and you have things that look and work just like function calls, but which will throw exceptions if they time out. You can easily run them async by using something like "CompletableFuture.supplyAsync(() -> proxy.myCall(myArgs))" or in Kotlin/Groovy syntax with a static import "supplyAsync { proxy.myCall(myArgs) }". You can then easily wait for it by calling get() or skip past it. With virtual threads this approach scales very well.

The hard/awkward part of this is that APIs are usually defined these days in a way that doesn't actually map well to standard function calling conventions because they think in terms of POSTing JSON objects rather than being a function with arguments. But there are tools that will convert OpenAPI specs to these proxies for you as best they can. Stricter profiles that result in more idiomatic and machine-generatable proxies aren't that hard to do, it's just nobody pushed on it.

(2) Capabilities. A language like Java has everything needed to do capabilities (strong encapsulation, can restrict reflection). A java.io.File is a capability, for instance. It didn't work out because ambient authority is needed for good usability. For instance, it's not obvious how you write config files that contain file paths in systems without ambient authority. I've seen attempts to solve this and they were very ugly. You end up needing to pass a lot of capabilities down the stack, ideally in arguments but that breaks every API ever designed so in reality in thread locals or globals, and then it's not really much different to ambient authority in a system like the SecurityManager. At least, this isn't really a programming language problem but more like a standard library and runtime problem.

(3) Production readiness. The support provided by app frameworks like Micronaut or Spring for things like logging is pretty good. I've often thought that a new language should really start by taking a production server app written in one of these frameworks and then examining all the rough edges where the language is mismatched with need. Dependency injection is an obvious one - modern web apps (in Java at least) don't really use the 'new' keyword much which is a pretty phenomenal change to the language. Needing to declare a logger is pure boilerplate. They also rely heavily on code generators in ways that would ideally be done by the language compiler itself. Arguably the core of Micronaut is a compiler and it is a different language, one that just happens to hijack Java infrastructure along the way!

What's interesting about this is that you could start by forking javac and go from there, because all the features already exist and the work needed is cleaning up the resulting syntax and semantics.

(4) Semi-dynamic. This sounds almost exactly like Java and its JIT. Java is a pretty dynamic language in a lot of ways. There's even "invokedynamic" and "constant dynamic" features in the bytecode that let function calls and constants be resolved in arbitrarily dynamic ways at first use, at which point they're JITd like regular calls. It sounds very similar to what you're after and performance is good despite the dynamism of features like lazy loading, bytecode generated on the fly, every method being virtual by default etc.

(5) There's a library called Permazen that I think gets really close to...

PL/SQL is an abomination of a language. It’s easily the worst example you could have given.
Actually it is my favourite stored procedured programming language, nothing else comes close to it in capabilities, or tooling.

Transact SQL is a close second, thanks to the tooling, but not as great as language, still better than the alternatives.

pgSQL has the plus of being similar to PL/SQL, but it is neither as feature rich, nor in tooling.

Everything else is even worse.

Do you actually mean that sincerely or is this just another occasion that you’re taking an opposing view just for giggles?

Because given your past comments favouring Java and my experience with both those languages, I’m very surprised you’d pick PL/SQL over SQL/JRT.

I really mean that, all those approaches using Perl, Java, .NET as stored procedures never gained much love among DBAs or the market in general, and I always favour approaches where I am not the only person on the building that can change something.

Also those approaches lack the end to end tooling, so you're basically back at printf debugging.

That’s a fair point though it’s more of a practical argument when the topic was more academic discussion with regards to language design.

But I do agree that practical considerations are important when making architectural decisions.

This is actually quite relevant to language design, too many people still get lost on discussing about grammar and language semantics, forgeting about everything else that is quite relevant why chose language A over language B.
(comment deleted)
Ironic you say that because usually we have the opposite positions with me asking you to have a little pragmatism.

I’ll save this post and share it with you the next time a Go thread comes up. ;)

Share at will, as professional, I am the first to chose C or Go if that is what the customer requires.

It is the customer and their happiness to the service that matters, not what programming languages I enjoy using.

Something that more people should actually think about, instead of proposing rewrites and then leaving when it isn't fun any longer.

On that much we do agree
The issue with stuff like SQL/JRT is that often stored procs are just running a few SQL statements in a row, the stored procs are being used for security or latency optimization but not because there's some truly complex logic. And if you want a language that has the basics like variables, loops, conditionals, data structures etc but which also makes issuing SQL statements natural, then it's hard to beat PL/SQL.
You might be interesting in looking at the Lima programming language: http://btetrud.com/Lima/Lima-Documentation.html . It has ideas that cover some of these things. For example, it's intended to operate with fully automatic optimization. This assumption allows shedding lots of complexity that arises from needing to do the same logical thing in multiple ways that differ in their physical efficiency characteristics. Like instead of having 1000 different tree classes, you have 1 and optimisers can then look at your code and decide what available tree structures make most sense in each place. Related to your async functions idea, it does provide some convenient ways of handling these things. While functions are just normal functions, it has a very easy way to make a block of async (using "thread") and provides means of capturing async errors that result from that.
> Some Lisps may be able to do all this, although I don’t know if they quite do what I’m talking about here; I’m talking about there being a very distinct point where the programmer says “OK, I’m done being dynamic” for any given piece of code.

In Common Lisp there are tricks you can pull like declaring functions in a lexical scope (using labels or flet) to remove their lookup overhead. But CL is generally fast enough that it doesn't really matter much.

You can declaim inline a toplevel function. That doesn't necessarily mean that it will be integrated into callers. Among the possible effects is that the dynamism of reference can be culled away. If a function A calls B where B is declaimed inline then A can be compiled to assume that B definition. (Such that if B is redefined at run-time, A can keep calling the old B, not going through the #'B function binding lookup.).

I seem to remember that Common Lisp compilers are allowed to do this for functions that are in the same file even if they are not declaimed inline. If A and B are in the same file, and B is not declaimed notinline (the opposite of inline), then A can be translated to assume the B definition.

So all your helper functions in a Lisp module are allowed to be called more efficiently, not having to go through the function binding of the symbol.

Interesting that E is cited under “capabilities”, but not under “loosen up the functions”. E’s eventual-send RPC model is interesting in a number of ways. If the receiver is local then it works a bit like a JavaScript callback in that there’s an event loop driving execution; if it’s remote then E has a clever “promise pipelining” mechanism that can hide latency. However E didn’t do anything memorable (to me at least!) about handling failure, which was the main point of that heading.

For “capabilities” and “A Language To Encourage Modular Monoliths”, I like the idea of a capability-secure module system. Something like ML’s signatures and functors, but modules can’t import, they only get access to the arguments passed into a functor. Everything is dependency injection. The build system determines which modules are compiled with which dependencies (which functors are passed which arguments).

An existing “semi-dynamic language” is CLOS, the Common Lisp object system. Its metaobject protocol is designed so that there are clear points when defining or altering parts of the object system (classes, methods, etc.) at which the result is compiled, so you know when you pay for being dynamic. It’s an interesting pre-Self design that doesn’t rely on JITs.

WRT “value database”, a friend of mine used to work for a company that had a Lisp-ish image-based geospatial language. They were trying to modernise its foundations by porting to the JVM. He had horror stories about their language’s golden image having primitives whose implementation didn’t correspond to the source, because of decades of mutate-in-place development.

The most common example of the “value database” or image-based style of development is in fact your bog standard SQL database: DDL and stored procedures are very much mutate-in-place development. We avoid the downsides by carefully managing migrations, and most people prefer not to put lots of cleverness into the database. The impedance mismatch between database development by mutate-in-place and non-database development by rebuild and restart is a horribly longstanding problem.

As for “a truly relational language”, at least part of what they want is R style data frames.

We have built something that hits on points 1, 3, 5, and 7 at https://reboot.dev/ ... but in a multi-language framework (supporting Python and TypeScript to start).

The end result is something that looks a lot like distributed, persistent, transactional memory. Rather than explicit interactions with a database, local variable writes to your state are transactionally persisted if a method call succeeds, even across process/machine boundaries. And that benefits point 7, because transactional method calls compose across team/application boundaries.

[1] Loosen Up The Functions [3] Production-Level Releases [5] Value Database [7] A Language To Encourage Modular Monoliths

This seems to be similar to Azure Durable Functions:

https://learn.microsoft.com/en-us/azure/azure-functions/dura...

They are related, for sure. But one of the biggest differences is that operations affecting multiple Reboot states are transactional, unlike Azure's "entity functions".

Because multiple Azure entity functions are not updated transactionally, you are essentially always implementing the saga pattern: you have to worry about cleaning up after yourself in case of failure.

In Reboot, transactional function calls automatically roll back all state changes if they fail, without any extra boilerplate code. Our hypothesis is that that enables a large portion of an application to skip worrying about failure entirely.

Code that has side-effects impacting the outside world can be isolated using our workflow mechanism (effectively durable execution), which can themselves be encapsulated inside of libraries and composed. But we don't think that that is the default mode that developers should be operating in.

Could you expand on this part?

> Code that has side-effects impacting the outside world can be isolated using our workflow mechanism (effectively durable execution

Sounds very interesting!

I have been thinking about something like this for a new PL, and many kinds of side effect can actually be reversed, as if it never happened.

I have also read that exceptions can complicate control flow, disallowing some optimizations - but if they are transactional, then we can just add their reverse to the supposedly already slow error path, and enjoy our performance boost!

Every method in Reboot has a type: reader, writer, transaction, or workflow. Our retry semantics are such that any method can always be retried from the top, but for different reasons:

In readers, no state changes are possible. And in writers and transactions, retry/abort is always safe because no state changes occur until the method completes successfully.

In workflows, retry is always safe, and is in fact required due to the primitives we use to implement durable execution (we will publish more docs on this soon!). The workflow retries durably until it eventually completes, one way or another.

That means that a workflow is always the right spot to execute an external side effect: if a reader/writer/transaction want to execute a side effect, they do so by spawning a task, which is only actually spawned if the method completes successfully. And we do "effect validation" (effectively: running your method twice!) to make it very hard to write a side effect in the wrong place.

https://docs.reboot.dev/develop/side_effects has some more details on our approach to side effects, but feel free to follow up in our discord too: https://discord.com/invite/cRbdcS94Nr

----

> I have also read that exceptions can complicate control flow, disallowing some optimizations - but if they are transactional, then we can just add their reverse to the supposedly already slow error path, and enjoy our performance boost!

Somewhat...! When you write a transaction method in Reboot, code that fails with an exception cannot have had a side effect on the outside world, and all state changes will vanish if the transaction aborts. So there is never any need to clean something up, unless you are using exceptions to implement control flow.

The section about language support for modular monoliths reminds me of John Lakos's "Large-Scale C++ Software Design", which focuses on the physical design/layout of large C++ projects to enforce interfaces and reduce coupling and compilation time. Example recommendations include defining architecture layers using subdirectories and the PImpl idiom. It's pretty dated (1996, so pre-C++98), but still a unique perspective on an overlooked topic.

https://www.amazon.com/dp/0201633620

Folks, this is not a process that converges. We've now had 60 years of language design, use and experience. We're not going to get to an ideal language because there are (often initially hidden) tradeoffs to be made. Everyone has a different idea of which side of each tradeoff should be taken. Perhaps in the future we can get AI to generate and subsequently re-generate code, thereby avoiding the need to worry too much about language design (AI doesn't care that it constantly copies/pastes or has to refactor all the time).
Practically it has kinda converged, though not necessarily on a global optimum. Most coding is done in relatively few languages.
It's bimodal. Most of the world uses one paradigm of programming (declarative programming via Excel and SQL), while developers use another paradigm (imperative programming via Python/C/C++/Javascript et al.).
I'll throw another idea here I've been thinking from a time now.

Most languages have a while construct and a do-while.

  while(condition){block};
  do{block}while(condition);
The while is run as

    ...
  start:
    condition
    branch-if-false > end
    block
    branch-always > start
  end:
    ...
And the do-while switches the order:

    ...
  start:
    block
    condition
    branch-if-true > start
    ...
The issue with the while is that more often than not you need to do some preparations before the condition. So you need to move that to a function, or duplicate it before and inside the loop. Do-while doesn't help, since with that you can't do anything after the condition. The alternative is a while(true) with a condition in the middle.

  while(true){
    prepare;
    if(!check) break;
    process
  }
But what if there was a language construct for this? Something like

  do{prepare}while(condition){process}
Is there a language that implements this somehow? (I'm sure there is, but I know no one)

The best thing is that this construct can be optimized in assembly perfectly:

    ...
    jump-always > start
  after:
    process
  start:
    prepare
    condition
    branch-if-true > after
    ...
At what point are you just doing async and coroutines?
Interesting idea, but once you add scoping:

  do {
      let value = prepare();
  } while (value.is_valid) {
      process(value);
  }
Can the second block of the do-while see `value` in its lexical scope? If yes, you have this weird double brace scope thing. And if no, most non-trivial uses will be forced to fall back to `if (...) break;` anyway, and that's already clear enough imo.
The scope should be unique, yes. In your example value should be visible.

Your are right about the word double braces, but I can't think of an alternate syntax other than just removing the braces around the while. But in that case it may seem odd to have a keyword that can only be used inside a specific block...wich is basically a macro for a if(.)break; Maybe I'm too used to the c/java syntax, maybe with a different way of defining blocks?

(comment deleted)
That seems more like a programmer expectations issue than something fundamental. Essentially, you have "do (call some function that returns a chunk of state) while (predicate that evaluates the state) ..."

Hard to express without primitives to indicate that, maybe.

Let me FTFY:

  do {
      let value = prepare();
      while (value.is_valid) {
         process(value);
  }
>Can the second block of the do-while see `value` in its lexical scope? If yes, you have this weird double brace scope thing

As long as it's documented and expected, it's not weird.

The scope then is the whole "do-while" statement, not the brace.

I don't write a lot of while loops so this is just a bit unfamiliar to me, but I'm not really understanding how this isn't the same as `do{block}while(condition);`? Could you give a simple example of what kind of work `prepare` is doing?
Think of a producer (a method that returns data each time you request one, like reading a file line by line or extracting the top of a queue for example) that you need to parse and process until you find a special element that means "stop".

Something like

  do{
    raw_data=produce.get()
    data=parse(raw_data)
  }while(data!=STOP){
    process(data)
  }
I'm aware this example can be trivially replaced with a while(data=parse(producer.get())){process(data)} but you are forced to have a method, and if you need both the raw and parsed data at the same time, either you mix them into a wrapper or you need to somehow transfer two variables at the same time from the parse>condition>process

A do-while here also has the same issue, but in this case after you check the condition you can't do any processing afterwards (unless you move the check and process into a single check_and_process method...which you can totally do but again the idea is to not require it)

i often want to write:

  do {
    offset = tell(filehandle)
  } while(line = readline(filehandle))
  {
    print "line starts at offset: ", offset
  }
You mean like a shell's while-do-done? It's just about allowing statements as the conditions, rather than just a single expression. Here's an example from a repl I wrote:

  repl_prompt="${repl_prompt-repl$ }"
  while
    printf "%s" "$repl_prompt"
    read -r line
  do
    eval "$line"
  done
  echo
The `printf` is your `prepare`.

This should also be doable in languages where statements are expressions, like Ruby, Lisp, etc.

Here's a similar Ruby repl:

  while (
    print "repl> "
    line = gets
  ) do
    result = eval line
    puts result.inspect
  end
  puts
Exactly, here you are basically keeping it as a while with a condition but allowing it to be any code that at the end returns a boolean, although you need to make sure that variables defined in that block can be used in the do part.

Sidenote: I wasn't aware that shell allows for multiple lines, good to know!

Not quite the same but almost feels like the BEGIN block in awk.
I suspect you'll love Common Lisp's LOOP: https://gigamonkeys.com/book/loop-for-black-belts

Example:

    (loop repeat 8
               ;; init x, then decide how to "prepare" next  iteration
               for x = 0 then (+ (* 2 x) 1)
               collect x)

    (0 1 3 7 15 31 63 127)
You can insert a condition check in the middle, of course:

    (loop repeat 8
               for x = 0 then (+ (* 2 x) 1)
               ;; but stop if x gets too big
               while (< x 100)
               collect x)

    (0 1 3 7 15 31 63)
And much, much more. It's the ultimate loop construct.
actually, according to the LOOP syntax, the REPEAT clause has to follow the FOR clause...
Just a silly example, but it does work on SBCL at least.
A bunch of things work in implementations, while but are not standard conforming.
Don't forget about the `prog*` family.

---

    (prog1 foo bar*)
Evaluates foo, then bar(s), and returns the result of evaluating foo and discards the results of bar(s).

Useful if `foo` is the condition and you need to perform some change to it immediately after, eg:

    (while (prog1 (< next prev) (setq prev next)) ...)
---

    (prog2 foo bar baz*)
Evaluates foo, then bar, then baz(s) (if present), returns the result of evaluating bar and discards the results of evaluating foo and baz(s).

Might be what GP wants. `foo` is the preparation, `bar` is the condition`, and `baz` can be some post-condition mutation on the compared value. Not too dissimilar to

    for (pre, cond, post) {}
With `prog2` you could achieve similar behavior with no built in `for`:

    (while (prog2 pre cond post) ...)
---

    (progn foo*)
Evaluate each foo in order, return the result of evaluating the last element of foo and discard all the others.

`progn` is similar to repeated uses of the comma operator in C, which GP has possibly overlooked as one solution.

    while (prepare, condition) { process }
Learning to embrace the LOOP construct has been an experience, for me. Same with the FORMAT abilities. It is amazing how much hate they both get, for how capable they both are.
I think we have a similar way of thinking. I once wrote a blog post about a for loop extension (based on Golang for illustration) [0].

  values := []string{"hello", "world"}
  for v, value := range values {
    fmt.Printf("%s", value);
  }
  inter {
    fmt.Printf(",")
  }
  before {
    fmt.Printf("[")
  }
  after {
    fmt.Println("]")
  }
  empty {
    fmt.Println("(nothing found)")
  }
[0] https://lukas-prokop.at/articles/2024-04-24-for-loop-extensi...
Eiffel has the loop structure.

from <initialization statements> until <termination condition> loop <group of statements> end

In some way it's the dual of break, in that you want to jump into the middle of the loop, while break is to jump out of it.

Let's rewrite the loop this way, with 'break' expanded to 'goto':

  while (true) {
    prepare...
    if (!cond) goto exitpoint;
    process...
  }
  exitpoint:
The dual would be:

  goto entrypoint;
  do {
    process...
  entrypoint:
    prepare...
  } while(cond);
Both constructs need two points: where the jump begins and where it lands. The 'break' is syntactic sugar that removes the need to specify the label 'exitpoint'. In fact with 'break' the starting point is explicit, it's where the 'break' is, and the landing point is implicit, after the closing '}'.

If we want to add the same kind of syntactic sugar for the jump-in case, the landing point must be explicit (no way for the compiler to guess it), so the only one we can make implicit is the starting point, that is where the 'do' is.

So we need: a new statement, let's call it 'entry', that is the dual of 'break' and a new semantic of 'do' to not start the loop at the opening '{' but at 'entry'.

  do {
    process...
    entry;
    prepare...
  } while (cond);
Is it more readable than today's syntax? I don't know...
When we are on subject of loops... I'd love to have 'else' block for loops that runs when the loop had zero iterations.
Not the same thing (although I thought it was), according to the Python docs, but related:

https://docs.python.org/3/reference/compound_stmts.html

See sections 8.3, the for statement, and 8.2, the while statement.

Yeah, `while...else` in Python does the wrong thing. Executes `else` block when the loop finished normally (not through `break`).

Scala for example has a `breakable {}` block that lets you indicate where you should land after a `break`

    breakable {
      while condition {
        // loop body
        if otherCondition then break;
        // rest of the body
      }
      // body of pythonic else block
    } // you land here if you break
However I have no idea how to implement the kind of `else` I described in any language without checking the condition twice.
In Scala you can do:

    while { prepare; condition } 
    do { process }
This runs all 3 in order every iteration but quits if condition evaluates to false. It just uses the fact that value of a block is the value of the last expression in the block.

Scala has a lot of syntax goodies although some stuff is exotic. For example to have a 'break' you need to import it and indicate where from exactly you want to break out of.

C-style for-loop is kinda sorta this. Although the "prepare" part has to be an expression rather than a statement, given that you have the comma operator and ?: you can do a lot there even in C. In C++, you can always stick a statement in expression context by using a lambda. So:

    for ([]{
        /*prepare*/
    }(); /*condition*/;) {
        /*body*/
    }
However, the most interesting take on loops that I've seen is in Sather, where they are implemented on top of what are, essentially, coroutines, with some special facilities that make it possible to exactly replicate the semantics of the usual `while`, `break` etc in this way: https://www.gnu.org/software/sather/docs-1.2/tutorial/iterat...
The C language construct for that is 'goto'.
Yeah, but the parent wants a non brain-damaged too-general construct
What if loops are a design mistake?

Look at C: it has 5 loop-related keywords (4.5; 'break' is two-timing in 'switch') yet it is still not enough.

Ada has had something similar and very flexible since from the 80s ... like:

  loop
    Get(Current_Character);
  exit when Current_Character = '*';
    Echo(Current_Character);
  end loop;
There's not that much new under the prog lang sun :(

    do {
        Get(Current_Character);
        if (Current_Character == '*') break;
        print(Current_Character);
    } while (true);
I don't see why this needs a new construct in languages that don't already have it. It's just syntactic sugar that doesn't actually save any work. The one with the specialized construct isn't really any shorter and looks pretty much the same. Both have exactly one line in the middle denoting the split. And both lines look really similar anyway.
PowerShell can process 0..n input objects from the pipeline using BEGIN {...} PROCESS {...} END {...} blocks.

I find this so incredibly useful, that I miss it from other languages.

Something related that I've noticed with OO languages such as Java is that it tends to result in "ceremony" getting repeated n-times for processing n objects. a well-designed begin-process-end syntax for function calls over iterables would be amazing. This could apply to DB connection creation, security access checks, logging, etc...

Well, I am in a process of making a language where general loops will look like

    loop
        prepare;
        while check;
        process;
    end;
I also think you'd enjoy Knuth's article "Structured Programming with go to Statements" [0]. It's the article that gave us the "premature optimization is the root of all evil" quote but it's probably the least interesting part of it. Go read it, it has a several sections that discuss looping constructs and possible ways to express it.

[0] https://pic.plover.com/knuth-GOTO.pdf

For semi-dynamic language, Julia definitely took the approach of being a dynamic language that can be (and is) JITed to excellent machine code. I personally have some larger projects that do a lot of staged programming and even runtime compilation of user-provided logic using Julia. Obviously the JIT is slower to complete than running a bit of Lua or whatever, but the speed after that is phenomenal and there’s no overhead when you run the same code a second time. It’s pretty great and I’d love to see more of that ability in other languages!

Some of the other points resonate with me. I think sensible dynamic scoping would be an easy way to do dependency injection. Together with something like linear types you could do capabilities pretty smoothly, I think. No real reason why you couldn’t experiment with some persistent storage as one of these dependencies, either. Together with a good JIT story would make for a good, modular environment.

Oh and Zig is another option for allowing injections that are checked when used at a call site rather than predefined through interfaces.

AFAIK it doesn’t have closures (it’s too C-like) so you need to use methods for all your (implicit) interfaces, but that’s okay…

I think the “exemplars” could be automatically yoinked from documentation and tests and existing usage of the function in the code base. Work needs to be done on the IDE front to make this accessible to the user.

Julia is kind of Dylan's revenge, even if it doesn't take over the whole world, it is already great if it gets its own corner, and from the looks of it that is going alright.
Well OP, are you me? everything you listed is also in my short wishlist for a programming language (well except for the value database, once to you have first class relational tables in your language, persistence can be tied to the table identity, doesn't need to be implicit).

Capabilities and dynamic scoping for "modularisation" nicely lead to implicit variables instead of truly global dynamically scoped variables. Implicit variables also probably work well to implement effect systems which means well behaved asyncs.

Edit: other features I want:

- easy embedding in other low level languages (c++ specifically)

- conversely, easy to embed functions written in another language (again c++).

- powerful, shell-like, process control system (including process trees and pipelines), including across machines.

- built-in cooperative shared memory concurrency, and preemptive shared nothing.

- built-in distributed content addressed store

I guess I want Erlang :)