Introducing Nanyx

Posted on

For the last few years I’ve been quietly building a programming language. It’s called Nanyx, and I think it’s finally at the point where it’s worth talking about.

Nanyx is a statically-typed, functional-first language that compiles to WebAssembly. If you’ve used F#, Elm or OCaml you’ll recognise a lot of it: Hindley-Milner type inference, immutable values, exhaustive pattern matching, and no null. But it also tries hard to be approachable if you’re coming from TypeScript or Python, and it has a few ideas of its own.

Here’s a taste:

module main
type Shape =
| #circle(radius: float)
| #rectangle(width: float, height: float)
def area: Shape -> float = {
| #circle(radius) -> 3.14159 * radius * radius
| #rectangle(width, height) -> width * height
}
@entry
def main = {
[#circle(radius = 1.0), #rectangle(width = 2.0, height = 3.0)]
\map(area)
\sum
\println
}

The ideas I care most about

Pipes everywhere. The \ operator sends the value on the left into the function on the right, so a chain of transformations reads in the order it actually runs. Combined with shorthand lambdas like { .age >= 18 }, most data-shuffling code ends up as a short, readable pipeline.

Tags instead of enums, options and exceptions. A tag like #notFound is both a value and its own type. Tags combine into unions, carry payloads, and the compiler insists you match every case. There’s no single Option type: any union containing #some(a) is option-like, and the standard library helpers leave your other tags alone. Errors are just more tags in the return type, so they’re impossible to forget.

Contexts for effects. A function declares the contexts it needs in its type, such as [Console] for printing. That makes side effects visible at a glance, and the same mechanism doubles as typeclass-style constraints. Mutation works the same way: values are immutable unless you’re inside a memory { } block, and nothing mutable can escape it, so a function can be imperative inside and pure outside.

No classes. Behaviour is attached to a type by name, so point.length, point \length and Point.length(point) are all the same definition. Operators are attached definitions too.

Workflows. Libraries can add language-like constructs without compiler support. A block after a builder is rewritten into continuation-passing calls, which is how async/await, try result chaining, sequence builders and query DSLs are all plain library code.

The philosophy

I wanted a language that feels small. Braces always mean a function. Every function takes exactly one argument. There’s no overloading, no currying by default, and no arbitrary unions like string | int, each for reasons written up in the docs.

The compiler emits errors but never warnings, because linting belongs in pluggable analyzers rather than in the thing that decides whether your program runs. And a debug build still runs in the face of type errors, so you can exercise the parts of your program that do type check while you fix the parts that don’t.

Where it’s at

Nanyx is under active development and the language is still changing, so expect rough edges. The compiler is written in F# and lives on GitHub, along with a VS Code extension and a set of examples.

If any of this sounds interesting, have a read of the docs, try the examples, and tell me what you think. I’ll be writing more about individual features here over the coming months.

Leave a Reply