1SubML strips away the separation between modules and values in an ML-like language. Modules become ordinary records, module types turn into record types with existential type members. This unification lets developers pass modules to functions, return them from conditionals, or store them in data structures. No dedicated module syntax clutters the language.
The compiler handles structural subtyping, global type inference, higher-rank polymorphism, existentials, higher-kinded types (without partial application), recursive types, and worst-case polynomial-time type checking. It compiles to JavaScript for browser execution and offers a web playground for instant testing—no setup required.
Why This Approach Matters
Traditional ML-family languages like OCaml split modules from values, creating two languages to learn and debug. 1SubML merges them, reducing cognitive load and errors from phase distinctions. Modules as first-class citizens enable runtime decisions: pick an implementation based on conditions, swap them in data structures, or abstract over them with functors that look like regular functions.
This shines for extensible systems. Consider counters with hidden state:
alias Counter = { type s; new: s; increment: (s, int) -> s; get: s -> int };
Implementations coerce to this type via :>, hiding internals:
let simple_counter = ({ new = 0; increment = fun (n, inc) -> n + inc; get = fun n -> n; } :> Counter);
A “stepping” version tracks extra state. Feed either to count_to, a first-class functor. Runtime if-expressions select modules dynamically. Implications? Simpler plugins, safer abstraction without separate compilation phases. In security-critical code, existentials enforce information hiding rigorously.
Arbitrary-precision integers handle big computations effortlessly. Fibonacci 999 or 1000 computes instantly in the playground, unlike fixed-precision limits in many langs.
Hands-On Examples
Fibonacci shows recursion and mutation:
let fib = fun n -> (
let r = {mut n; mut a = 1; mut b = 1};
loop if r.n <= 1 then `Break r.a
else (r.n <- r.n - 1; let old_a = r.a; r.a <- r.a + r.b; r.b <- old_a; `Continue ())
);
Pattern matching on variants computes areas cleanly:
let area = fun shape ->
match shape with
| `Circle {rad} -> rad *. rad *. 3.1415926
| `Rect {length; height} -> length *. height;
print "area =", area `Circle {rad = 5.0};
print "area =", area `Rect {height = 4.; length = 2.5};
These run in the browser playground at the project's site. Test type checking, execution, and tweaks immediately.
Building and Limitations
The compiler builds with Rust. Install lalrpop first, then:
lalrpop compiler_lib/src/grammar.lalrpop
cargo build --release
Run with ./target/release/cli myfile.ml. Outputs JavaScript or checks types.
Skepticism warranted: it's experimental. No mention of garbage collection details, optimization status, or ecosystem. Polynomial-time checking avoids exponential blowups in some Hindley-Milner variants, but real-world scaling unproven. Higher-kinded types lack partial app, limiting some abstractions. Still, as a research vehicle, it probes unification's viability.
Why care now? Languages like Rust grapple with modularity; 1SubML's model could inspire cleaner designs. For crypto or finance devs needing bigints and strong types, the playground offers low-risk exploration. Production? Wait for maturity, but track it—unified languages might reshape how we compose systems securely and scalably.