Skip to content
Playground

Cookbook ​

Task-oriented recipes for everyday Yulang. Most sections pair an idiomatic form with a brief note; the config, file-editing, log, and larger-example sections develop longer examples. For deeper background, follow the cross-references into the language reference.

The examples lean on Yulang's parens-light style — bare application f x y, colon application f: ..., and indented blocks — rather than C-style calls.

Define a value and a function ​

yulang
my answer = 42
my greet name = "hello, " + name
greet "Yulang"

my makes a private binding. Arguments after the name become curried function parameters.

Functions

Make a function public ​

yulang
pub double x = x + x

pub exports the binding from the current module. our exposes it through the containing companion (typical for methods).

Modules

Pattern-match with case ​

yulang
my describe n =
    case n:
        0 -> "zero"
        x if x < 0 -> "negative"
        _ -> "positive"

Arms are tried top-to-bottom. A guard uses if after the pattern.

Pattern Matching

Attach a method to a struct ​

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

with: opens the struct's companion module. The receiver p stands for the value .norm2 is called on.

Structs & Roles

Implement a role for a type ​

yulang
impl Add point:
    our a.add b = point { x: a.x + b.x, y: a.y + b.y }

A role declares an interface; an impl provides the methods for a specific type. The receiver a (and second argument b here) match the role's method signatures.

Structs & Roles

Loop with an early break ​

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

sub: opens an early-return scope. return value exits the innermost sub:. Inside for, last, next, and redo control the loop itself.

Control Flow

Use a mutable local with $x / &x ​

yulang
{
    my $count = 0
    &count = $count + 1
    &count = $count + 1
    $count
}

my $x = … introduces a local mutable binding. $x reads, &x = value writes. Internally these compile to a small handled var effect, so they fit the effect system rather than escaping it.

Read a text file ​

yulang
my text = read_text "data.txt"
text.say

str widens to path, so a string literal is enough for ordinary paths. Filesystem errors are raised as io_err in the effect row. Wrap them only when the caller needs a value-level result.

yulang
my wrapped = io_err::wrap: read_text "data.txt"
case wrapped:
    result::ok text -> text
    result::err _ -> ""

std::io::file

Read a config file ​

The config, file-editing, and log examples show everyday scripting tasks with small source files under examples/config-file-text/.

This abridged example reads a simple config file with method-form string APIs, trimming both sides of = before converting the port:

yulang
my parse_setting(clean: str): opt (str, str) = case clean.split_once "=":
    just (key, value) -> just (key.trim, value.trim)
    nil -> nil

my read_config(path: str): (int, list (str, str), int, int) =
    my $port = 0
    my $entries = []
    for line in (std::io::file::read_text path).split "\n":
        my clean = line.trim
        if clean.starts_with "#":
            ()
        else:
            case parse_setting clean:
                just (key, value) ->
                    if key == "port":
                        case value.to_int:
                            just parsed -> &port = parsed
                            nil -> ()
                    &entries = $entries + [(key, value)]
                nil -> ()
    ($port, $entries, 0, 0)

Full example: config_read.yu.

Edit a text file by line ​

For durable line edits, open the file as a text ref and update line views. This abridged example writes a temp copy first, so reruns do not dirty the tracked sample:

yulang
use std::control::nondet::*
use std::control::var::*
use std::text::str::*

std::io::file::write_text path (std::io::file::read_text source)

my &doc = std::io::file::text path
((&doc.lines.each).update \line ->
    line.replace_once "todo:" "done:"
).list

$doc.say

Full example: file_edit.yu.

Summarize logs with parser patterns ​

Parser patterns also work well for small log summaries. This abridged example captures the GET payload, filters /api paths with a guard, and parses the captured milliseconds:

yulang
use std::text::parse::*

my api_ms(line: str): opt int = case line:
    ~"GET {rest = ..}" if rest.starts_with "/api" -> case rest.split_once " took ":
        just (_, tail) -> case tail.split_once " ms":
            just (ms, _) -> ms.to_int
            nil -> nil
        nil -> nil
    _ -> nil

Full example: log_stats.yu.

Set up optional arguments ​

yulang
my area {width = 1, height = 2} = width * height

area { width: 3 }
area { width: 3, height: 4 }
area {}

A record pattern with defaults gives every field optional behavior at the call site. Defaults evaluate left-to-right and may reference earlier fields.

Functions

Raise and catch a typed error ​

yulang
my path = std::text::path::of_bytes (std::text::str::to_bytes "/tmp/data")

catch read_text path:
    io_err::not_found _, _ -> "(missing)"
    value -> value

fail e is short for e.throw. catch arms name the operation directly, so errors are always caught by their concrete name.

Errors

Close an error effect into a result ​

yulang
my path = std::text::path::of_bytes (std::text::str::to_bytes "/tmp/data")

my wrapped = io_err::wrap: read_text path
case wrapped:
    result::ok text -> text
    result::err err -> err.show

E::wrap runs the thunk, catches the matching error effect, and returns a result value. The err side is Display-able through the auto-generated impl.

Errors → wrap

Aggregate several errors into one ​

yulang
pub error app_err:
    file from io_err
    parse from parse_err

my read_and_parse path =
    app_err::up:
        my text = read_text path
        parse_json text

from makes a wider error type wrap narrower ones. app_err::up lifts the narrower errors into app_err inside the block.

Errors → from aggregation

Handle a custom effect ​

yulang
pub act log:
    pub put: str -> ()

my run_into_strings(action: [log; 'e] 'a): ['e] ('a, list str) =
    my $entries = []
    my result = catch action:
        log::put msg, k ->
            &entries = $entries + [msg]
            k ()
        v -> v
    (result, $entries)

catch removes the handled operation from the row. Each arm receives the payload and a continuation k. Calling k value resumes the captured computation.

Effects

Search nondeterministically ​

yulang
use std::control::nondet::*

(each [1, 2, 3] + each [10, 20]).list

each xs picks one element of xs nondeterministically. .list runs the computation and collects every result. .once, .logic, and friends produce other shapes.

std::control::nondet

Effectful boolean conditions ​

yulang
if all [1, 2, 3] < any [2, 3, 4]:
    "found at least one"
else:
    "no overlap"

Yulang's if accepts an effectful boolean condition through std::control::junction. all xs and any xs express "every / some element of xs".

Define a cast between two types ​

yulang
struct user_id { raw: int }

cast(x: user_id): int = x.raw
cast(x: int): user_id = user_id { raw: x }

my id: user_id = 7
my raw: int = id

cast(x: A): B = body registers a conversion rule from A to B. The compiler applies the rule at expected-type boundaries such as annotations and function arguments.

Casts

Larger examples ​

Combining the recipes above, you can already write small but real programs. Every one of these runs in the Playground as-is.

A tiny expression evaluator ​

enum plus a recursive function is enough for a calculator:

yulang
enum expr =
    num int
    | add (expr, expr)
    | mul (expr, expr)

my eval e =
    case e:
        expr::num n -> n
        expr::add (a, b) -> eval a + eval b
        expr::mul (a, b) -> eval a * eval b

eval (expr::add (expr::num 2, expr::mul (expr::num 3, expr::num 4)))
// → 14

Carrying a tuple as a variant payload (add (expr, expr)) is the straightforward way to express multi-argument constructors.

Filtering a list with recursion ​

List spread patterns plus recursion give you a natural filter:

yulang
my is_even n =
    std::int::eq (std::int::sub n (std::int::mul (std::int::div n 2) 2)) 0

my keep_evens xs =
    case xs:
        [] -> []
        [x, ..rest] ->
            my r = keep_evens rest
            if is_even x:
                [x] + r
            else:
                r

keep_evens [1, 2, 3, 4, 5, 6, 7, 8]
// → [2, 4, 6, 8]

Aggregating in a loop ​

A local my $x = ... is the natural way to thread a value through a loop:

yulang
{
    my $best = 0
    for y in [3, 1, 4, 1, 5, 9, 2, 6, 5, 3]:
        if y > $best: &best = y
    $best
}
// → 9

$best does not escape the block — the type system makes sure mutable state stays in the scope where it was introduced.

each plus guard lets you describe what you want and have the runtime find it:

yulang
use std::control::nondet::*

{
    my a = each 1..20
    my b = each a..20
    my c = each b..20
    guard: a * a + b * b == c * c
    (a, b, c)
}.list
// → [(3, 4, 5), (5, 12, 13), (6, 8, 10), (8, 15, 17), ...]

Swap .list for .once to stop after the first success. Passing each choice into the range of the next prunes the search early.

See also ​

  • Tour — a walk through the same features in narrative form.
  • Reference — the full language reference.
  • Syntax Style — when and how to drop parentheses.

Yulang