· 5 min read

Grain meets you halfway

Trying Grain convinced me that functional language design becomes approachable when algebraic data types feel ordinary, mutation is explicit, and WebAssembly stays a deployment detail.

Van Gogh painting of a golden wheat field swaying beneath swirling clouds, with dark cypresses and olive trees at its edge.
Vincent van Gogh, Public domain

I tried Grain because I wanted to see whether a small functional language could make WebAssembly feel like a place to write normal programs. It does, mostly. What I didn't expect was how relaxed the functional side would be.

Algebraic data types, pattern matching, immutable bindings, and inferred types sit next to braces, named arguments, loops, early returns, and mutation when you ask for it. You can model your domain on day one, and when you need to count through an array you write a loop like everyone else.

The type system arrives quietly#

My first useful program was input validation. The code names the good and bad outcomes directly:

cart.gr
module Cartfrom "number" include Numberenum Quantity {  Count(Number),  Invalid(String),}let parseQuantity = input => {  match (Number.parseInt(input, radix=10)) {    Ok(value) when value > 0 => Count(value),    Ok(_) => Invalid("quantity must be positive"),    Err(_) => Invalid("quantity must be a number"),  }}

The compiler infers the function's type, and anyone reading the code still sees the contract. Grain's custom enum types carry data, pattern matching takes them apart, and Option and Result are built from the same machinery as your own types, so there's no separate convention to memorize.

The surface helps more than I expected. parseInt takes a named argument, functions are arrows with blocks, and annotations are there when you want them without the compiler demanding any. It reads like a normal program that happens to have a serious type system underneath.

Even Number is looser than I assumed. Grain's built-in type reference says it covers arbitrary-size integers, floats, and rationals, with fixed-width numbers kept separate for when representation matters. I expected a Wasm language to pull every value toward the machine; instead you move toward the machine only when you have a reason to.

Loops without a lecture#

Functional languages tend to lose people in the boring twenty percent of a program. The data model goes fine, and then a small accumulator turns into a detour about recursion and which combinator a serious person would use. In Grain you write the loop:

totals.gr
module Totalsfrom "array" include Arraylet sum = values => {  let mut total = 0  for (let mut i = 0; i < Array.length(values); i += 1) {    total += values[i]  }  total}

Bindings are immutable by default, so mut marks the two names that change, and the language gives you ordinary loops. None of this weakens the enum from the last section; the type system keeps doing its job while the local algorithm takes whatever shape reads best.

I can swap the loop for a fold when the fold explains the operation better, but I don't have to do it to prove anything. Purity is a tool, not a dress code.

WebAssembly stays at the boundary#

Grain compiles directly to WebAssembly, but you would hardly know it from the source. One CLI runs your program, emits Wasm, formats code, generates docs, and starts the language server. The target only really shows up when you ask for a production build:

Build commandssh
grain cart.grgrain compile --release cart.grgrain compile --release --elide-type-info cart.gr

The release profile adds size and speed optimizations. Eliding type information shrinks the module further, at the cost of records and enum variants printing less helpfully. I'm fine with that kind of leak — it's a tradeoff with a name, and you pay it once, at build time.

Early means early#

The language feels coherent enough that it's tempting to assume the platform around it is finished too. It isn't, and Grain says so — its own documentation calls the language early and rapidly changing. Version 0.7.2 shipped on February 8, 2026, and the 0.7 release before it changed arrow syntax and parts of the grammar, and introduced a new object-file format. Read examples against the compiler you actually installed.

Some rough edges are concrete. The packaged compiler guide warns that a project's first build takes around ten seconds, because the JavaScript compiler writes the runtime and standard library into your project; building the native compiler is the faster path. Ten seconds is fine for an experiment. In a tight edit cycle I'd feel it every time.

Tooling is further along than I expected — there's a formatter and a language server, and recent releases added inlay types, document symbols, and multiple-error display. Dependencies are the younger part of the story: as far as I can tell, the proposal for a Cargo-like package manager named Silo is still an open issue.

So for now the bet stays small. Grain fits self-contained Wasm tools, experiments, teaching — anything with a modest dependency surface. That suits me. A language doesn't have to win anything to be worth an evening, and this one had me modeling failure states with real enums a few hours after I installed it. That's further than most languages meet me.