Skip to content
Playground

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:

yulang
1 + 2

The 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:

yulang
my double x = x + x
double 21

my 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; double and area use it
  • our — public in the enclosing companion module; .norm2 and the effect operations use it
  • pub — exported out of the module; write pub double x = ... when another module should import the earlier double

Structs

A with: block attaches a method in the same struct declaration:

yulang
struct point { x: int, y: int } with:
    our p.norm2 = p.x * p.x + p.y * p.y

point { x: 3, y: 4 } .norm2

struct 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.

yulang
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:

yulang
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.

yulang
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 "???").say

The 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:

yulang
{
    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:

yulang
{
    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:

yulang
(each [1, 2, 3] + each [4, 5, 6]).list

This 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:

yulang
{
    my a = each 1..
    my b = each a<..
    my c = each b<..

    guard: a * a + b * b == c * c

    (a, b, c)
} .once

You declare "I want a Pythagorean triple" and the program goes and finds one.

Junctions

all and any lift comparisons over collections.

yulang
if all [1, 2, 3] < any [2, 3, 4]:
    1
else:
    0

Compare 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:

yulang
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::

yulang
sub:
    for x in 0..:
        if x == 5: return x
    0

return 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:

yulang
error path_err:
    not_found path
    denied path
    invalid_path path

path_err::not_found "path" reads either as a data constructor or as a throwing operation, depending on context:

yulang
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:

yulang
error path_err:
    not_found path
    denied path
    invalid_path path

error app_err:
    path from path_err

Errors 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:

yulang
// 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

Yulang