Skip to content

Latest commit

 

History

History
494 lines (364 loc) · 11.7 KB

File metadata and controls

494 lines (364 loc) · 11.7 KB

A Dream of a Machine

(or, why yet another programming language)

Stephen Leach, Sept 2025


A dream of a machine, perfect in every respect

"Not far from here, by a white sun, behind a green star, lived the Steelypips, illustrious, industrious, and they hadn't a care: no spats in their vats, no rules, no schools, no gloom, no evil influence of the moon, no trouble from matter or antimatter - for they had a machine, a dream of a machine, with springs and gears and perfect in every respect. And they lived with it, and on it, and under it, and inside it, for it was all they had - first they saved up all their atoms, then they put them all together, and if one didn't fit, why they chipped at it a bit, and everything was just fine."

Stanisław Lem, The Cyberiad


Focus on features that are unlikely to be familiar

Qualifiers const, val, var and friends Exceptions
Procedures, functions and finesses Concatenative semantics
Queries Machines
Autoloading Synchronous coroutines
Pluggable syntax Symbols
Capsules Elements
Configuration Series & Chains

Qualifiers

It is all about const



Deep immutability is the central feature of Nutmeg, embracing the essential difference between functional and imperative programming.

Qualifiers const, val and var

  • var, an assignable variable that can have any value
    • var x := List(1, 2, 3)
  • val, an unassignable variable that can have any value
    • val y := List(1, 2, 3)
  • const, an unassignable variable that can only have a recursively immutable value
    • const z := [1, 2, 3]

Snapshots and Freezing

  • All datatypes come in immutable and mutable 'flavours' (shallow)
    • VarList("abc", "xyz") vs. List("abc", "xyz")
  • You can freeze any mutable datatype and it becomes immutable
    • my_list := VarList("abc", "xyz").freeze()
  • You can deep-freeze it and it becomes const (deep immutability)
    • const my_list := VarList("abc", "xyz").freeze(--deep)
  • You can take a snapshot of it (a deeply immutable copy)
val x := VarList()
const x1 := x.snapshot()  // x1 is const
x.Add(1, 2, 3)            // but x is not

Procedures

It's all about the finesse

Finesses are procedures that can be safely invoked by functions and preserve all functional programming guarantees.

Procedures

def rev(val L):
    n := len(L)
    for i in [0 ..< n div 2]:
        L[i], L[n-i] <-- L[n-i], L[i]
    end
end

Functions

[function]
def rev(const L):
   L.tail.rev ++ [L.head]
end

Finesse - functional on the outside, crunchy on the inside

[finesse]
def rev(x):
    L := x.snapshot(--mutable)
    n := len(L)
    for i in [0 ..< n div 2]:
        L[i], L[n-i] <-- L[n-i], L[i]
    end
    return(L.freeze)
end

Query

Iteration is query-solving

Q ::= P := E
    | P in E
    | V from E
    | Q where E
    | Q && Q
    | Q // Q
    | Q while E
    | Q until E

Expressions vs Queries

An expression is a unit-of-code that is interpreted as meaning "evaluation attempts to generate a value"

b**2 - 4*a*c

A query is a unit-of-code that is interpreted as meaning "evaluation attempts to bind variables to values"

discriminant := b**2 - 4*a*c
d := sqrt(discriminant)
a2 := 2 * a
[root1, root2] := [(-b+d)/a2, -b+d)/a2]   ### Pattern matching

Loops

x in [1, 2, 3, 4, 5] ### A query with 5 possible solutions

### for QUERY do STATEMENTS endfor
for x in L do
    println("A solution is:", x)
endfor

Conditionals

query where expression

if [x, y, …] := problem.solutions where x != y then
   println("There are at least two different roots")
   println("x: \(x), y: \(y)")
else
   println("No solution pairs exist")
end
if [x, y, …] := problem.solutions where x != y then
###             ^^^^query^^^^^^^^ where ^expression

Nested Loops and Early exit

for x in range(1, 100) && y in range(1, 100) 
    where x + y == 50 
    while x < y 
    until x > 20 
do
    println("Found pair: (", x, ", ", y, ")")
endfor

Only One Solution Needed?

if x in range(1, 100) && y in range(1, 100) 
    where x + y == 50 
    while x < y 
    until x > 20 
then
    println("Found pair: (", x, ", ", y, ")")
endif

Autoloading

Resources are program

Where do you put your program's data files? How do you find them? How do you load them? Autoloading eliminates all these issues by equating resources with variables.

def generate_images_dropdown():
    for (k, v) in wallpapers.items():
        MenuItem(k, _ => ShowImage(v))
    endfor.Menu
enddef

For directory structure:

wallpapers.map
├── bufalo.gif
├── tiger.gif
├── anenome.gif
└── aardvark.gif
this.nutmeg

Pluggable Syntax

Bring your own parser

Perhaps the first aspect of any programming language that becomes unbearable is syntax. It is the quirks and cute ideas that first become unbearable. As far back as 1990 we were committed to BYO parsers.

BYO Parser

The first challenge in learning a programming language is the surface syntax. Often distinctive and quirky e.g. E1 ? E2 : E3 Although there is a relationship between the language and its syntax, it is often very weak. We resolved to make it possible for any programmer to completely replace the initial syntax ("common") by bringing their own parser.

Pre-reqs

The internal abstract syntax tree has to be easy to understand, easy to create and easy to render UNIX filter: Writeable in any language/toolkit and invoked by a command AST in XML (Or JSON post-2010)

echo 'x + 1' | nutmeg parse prints:

{
   "arguments": {
       "body": [
           {
               "kind": "id",
               "name": "x",
               "reftype": "get"
           },
           {
               "kind": "int",
               "value": "1"
           }
       ],
       "kind": "seq"
   },
   "kind": "syscall",
   "name": "+"
}

echo 'x + 1' | monogram -f xml prints:

 <operator name="+" syntax="infix">
   <apply kind="parentheses" separator="undefined">
     <identifier name="f" />
     <arguments>
       <identifier name="x" />
     </arguments>
   </apply>
   <number value="1" />
 </operator>

Capsules

It's all about thread safety

The root problem of threads as a mechanism for concurrency is shared access to mutable memory. Nutmeg tasks strictly enforce access to mutable memory so it is thread local.

Capsule Classes

[capsule]
class Counter(const initial=0):
    var ^n := initial

    def ^get(const k):
        sleep(ms=10)
        ^n
        ^n <- ^n + k
    end
endclass

All parameters and return values are const (or "green").

Capsules are free to create mutable store internally. Internal store is completely isolated.

Tasks and Futures with forcing on-demand

### Create a task.
counter := Task(Counter)

### Invoke a method and get a future.
n := counter.Get()
for _ in [0..<2] do println(n, --debug); sleep(ms=20) endfor
<future _>
<future 0>

println(n+100)   ### Implicit forcing
100

Task

counter := Task(Counter)

### f(% _, x, _ %) is partial application and shorthand for a lambda form:
###
###    fn (tmp1, tmp2) => f( tmp1, x, tmp2 ) endfn
###
let
    new_worker := Worker(% counter %) 
in
    worker1 := Task(new_worker)
    worker2 := Task(new_worker)
end

Functions and finesses counts as capsules

You don't need to do anything special to a function or finesse to run in a Task

[capsule]
class EquivalentCapsuleClass(F):
    def ^run(args…):
        F(args…)
    end
end

microsft c# async

No async/await

Coffee cup = PourCoffee()
Console.WriteLine("Coffee is ready")

eggs := FryEggs(%2%).RunAsTask
hashBrown := FryHashBrowns(%3%).RunAsTask
toast := ToastBread(%4%).RunAsTask

ApplyButter(toast)  ### Implicit await
ApplyJam(toast)
Console.WriteLine("Toast is ready")

Juice oj = PourOJ();
Console.WriteLine("Oj is ready");

eggs.Await() ### Wait if needed (a bit pointless)
Console.WriteLine("Eggs are ready");
hashBrown.Await() ### Equally pointless
Console.WriteLine("Hash browns are ready");

Whereas in C#:

Coffee cup = PourCoffee();
Console.WriteLine("Coffee is ready");

Task<Egg> eggsTask = FryEggsAsync(2);
Task<HashBrown> hashBrownTask = FryHashBrownsAsync(3);
Task<Toast> toastTask = ToastBreadAsync(2);

Toast toast = await toastTask;
ApplyButter(toast);
ApplyJam(toast);
Console.WriteLine("Toast is ready")

Juice oj = PourOJ();
Console.WriteLine("Oj is ready");

Egg eggs = await eggsTask;
Console.WriteLine("Eggs are ready");
HashBrown hashBrown = await hashBrownTask;
Console.WriteLine("Hash browns are ready");

Configuration

It's just program

Why do configuration files even exist? Because programs need to be parameterised over elaborate datatypes. So why do we specify datatypes in a funny limited syntax when we have a full programming language available?

Safety, Simplicity and Transparency

Sphinxdocs is a very widely used document generation system, the configuration file is conf.py, which is written in normal Python But it can execute arbitrary code, not safe to share or even make available to customers Might take unbounded time to execute, so ReadTheDocs cannot just trust it No type-checking No example generation

[configuration]

The [configuration] attribute marks a const variable whose value can be overridden by a separate Nutmeg script at load time

[configuration]
database_url := "localhost:5432"

Hence the compiler knows these variables and their types!

Advantages

  • Safe - script execution is resource limited (time and memory) and, being functional, unable to affect the rest of the system
  • Natural to exploit auto-loading
  • Easy to type-check
    • nutmeg config validate my_config.script.nutmeg
  • Easy to create
    • nutmeg config generate > example.script.nutmeg