Tour
A short walk through Yulang's main features. Every example here runs in the Playground as-is. We go in the order "run something" → "data and behavior" → "control flow" → "effects" → "errors".
Basics
1 + 2A bare expression at the top level is evaluated and its value is shown. The playground feels like a script: write, run.
my double x = x + x
double 21my f x = ... is a binding whose left-hand side is a name followed by argument patterns — like OCaml's let f x = ... or Haskell's f x = .... For more arguments, just keep going: my add x y = x + y.
Visibility uses three keywords:
my— private, visible only inside the current scopeour— public in the enclosing companion modulepub— exported out of the module
Structs
struct point { x: int, y: int } with:
our p.norm2 = p.x * p.x + p.y * p.y
point { x: 3, y: 4 } .norm2struct declares a nominal record type, similar to Rust's or OCaml's. The twist is with:, which lets you attach methods in the same declaration. The receiver p stands for the value .norm2 is called on. Convenient when "this type comes with this behavior" and a full class would be overkill.
Optional arguments
A record pattern with defaults gives you named optional arguments for free.
my area {width = 1, height = 2} = width * height
area { width: 3 }
area {}
area { width: 3, height: 4 }Defaults evaluate left-to-right, and later fields may reference earlier ones:
my f {a = 1, b = a + 1, c = b + 1} = (a, b, c)
f {} // (1, 2, 3)
f { a: 10 } // (10, 11, 12)Feels like Python keyword arguments with defaults, Ruby hash splats, or TypeScript's function f({a = 1} = {}). The types are inferred — no annotations needed.
Mutable bindings and references
my $x = ... introduces a mutable binding. $x reads, &x = v writes:
my $x = 10
&x = $x + 1
$xThe $ and & sigils are inherited from Perl/Raku. Bindings default to immutable; mutation has to ask for a sigil, so "state starts here" jumps out when you skim code.
Fields and indices work the same way:
my $xs = [2, 3, 4]
&xs[1] = 6
$xsUnder the hood these compile to a small var effect, which means mutable state shows up in the type and cannot silently escape its binder. Not a "variable visible from anywhere" — a "read/write port opened right here".
Nondeterminism
each xs from std::undet picks one element of xs. .list runs the search and collects every result:
(each [1, 2, 3] + each [4, 5, 6]).listThis says "pick one from [1,2,3], pick one from [4,5,6], add them; give me all combinations." Same idea as Prolog, Haskell's list monad, or a SQL cross join — embedded into ordinary expressions.
Infinite ranges work too. Constrain later choices from earlier ones, and .once returns the first success:
{
my a = each 1..
my b = each a<..
my c = each b<..
guard: a * a + b * b == c * c
(a, b, c)
} .onceYou declare "I want a Pythagorean triple" and the program goes and finds one.
Junctions
all and any lift comparisons over collections.
if all [1, 2, 3] < any [2, 3, 4]:
1
else:
0Compare this to the Ruby spelling you would otherwise need — [1,2,3].all? { |x| [2,3,4].any? { |y| x < y } }. In Yulang the comparison stays right where you read it, because if accepts an effectful condition.
Effects
act declares an effect interface. Operations are called like ordinary functions, and catch handles them:
act console:
our read: () -> int
our ask() = console::read()
our run_console(action: [console] 'a): 'a = catch action:
console::read(), k -> run_console(k 42)
run_console:
ask()ask() has type [console] int — "returns int, may perform the console effect." run_console handles the operation with catch, and the result type loses [console].
The k in a handler arm is the continuation. Calling k value resumes the captured computation at the point where the operation was invoked. That one mechanism gives you swappable test handlers, loops, early return, exceptions — all of them are just choices of what to do with k.
Loops and early exit
for x in xs: iterates over anything that implements Fold (lists, ranges, …). sub: opens an early-return scope, and return value exits the nearest sub::
sub:
for x in 0..:
if x == 5: return x
0return is not a parser-only keyword. The prelude exports return as an operator built on the sub effect's return operation. last, next, and redo work the same way — they call the loop's effect operations. "Early exit", "break", and "continue" are applications of the same effect machinery, not special syntax.
Errors
error is sugar that produces an enum and a set of effect operations in one go:
error fs_err:
not_found path
denied path
invalid_path pathfs_err::not_found "path" reads either as a data constructor or as a throwing operation, depending on context:
my err: fs_err = fs_err::not_found "/x" // value
fs_err::not_found "/x" // raises [fs_err]Aggregate into a wider error with from:
error io_err:
fs from fs_errErrors stay named in the effect row, so "what can go wrong" is visible in the type. There is intentionally no Display-erased anyhow wrapper — close errors into a result value with wrap when a value-level view is what you need.
Comments
// regular line comment
-- doc comment
---
multi-line doc comment
----- is a doc comment, not an ordinary one. Use // for casual notes.
Where to go next
- Cookbook — task-oriented recipes
- Pitfalls — surprises and how to dodge them
- Cheat Sheet — one-page syntax map
- Reference — feature-by-feature detail