Tour
This page is a short walk through Yulang's main features. Every example here runs in the Playground as-is. The tour starts with basics, structs, optional arguments, parser patterns, mutable bindings and references, and nondeterminism. It then covers junctions, effects, loops and early exit, errors, and comments before pointing to the next pages.
Basics
Start with one expression:
1 + 2The Playground evaluates bare top-level expressions and shows only the last root value. Call say to display an earlier value. It feels like a script: write, run.
A binding can define a function:
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, continue the same pattern: my add x y = x + y.
Visibility uses three keywords:
my— private, visible only inside the current scope;doubleandareause itour— public in the enclosing companion module;.norm2and the effect operations use itpub— exported out of the module; writepub double x = ...when another module should import the earlierdouble
Structs
A with: block attaches a method in the same struct declaration:
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. This form is 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
say: area { width: 3 }
say: area {}
say: area { width: 3, height: 4 }These calls print 6, 2, and 12 in order.
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.
Parser patterns
Parser patterns let a case arm match and capture pieces of a string directly. ~"..." is the compact form; rule { ... } gives the same matcher room for guards and named parser pieces.
use std::text::parse::*
my route = \line -> case line:
~"get :key" -> "GET " + key
~"set :key {v = ..}" -> "SET " + key + " = " + v
rule { id = word } if id.starts_with "a" -> "user " + id
_ -> "unknown"
(route "get color").say
(route "set color deep-blue").say
(route "alice").say
(route "???").sayThe four calls print GET color, SET color = deep-blue, user alice, and unknown.
Mutable bindings and references
my $x = ... introduces a mutable binding. $x reads, &x = v writes:
{
my $x = 10
&x = $x + 1
$x
}The $ 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
$xs
}Under 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::control::nondet 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, can 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 path_err:
not_found path
denied path
invalid_path pathpath_err::not_found "path" reads either as a data constructor or as a throwing operation, depending on context:
error path_err:
not_found path
denied path
invalid_path path
my err: path_err = path_err::not_found "/x" // value
path_err::not_found "/x" // raises [path_err]The annotation gives the first line a path_err value. Without that value context, the second line performs the [path_err] effect.
from aggregates one error into a wider one:
error path_err:
not_found path
denied path
invalid_path path
error app_err:
path from path_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
Yulang has line comments and documentation 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