Skip to content
Playground

Syntax Style

Use Yulang's free-paren style for ordinary code. Function application, colon application, indentation, and user-defined operators carry much of the structure that parentheses would carry in C-like languages.

This page defines the preferred forms and the places where whitespace changes the parse.

Prefer Colon for a Large Final Argument

Use : when the final argument is a full expression or a block. This keeps deeply nested calls from turning into nested parentheses.

yulang
say: "hello"

format:
    my name = "Yulang"
    "hello, {name}"

run_console:
    my answer = ask()
    say answer
yulang
-- Prefer this shape.
say: format: greeting name

-- Use parentheses only when the inner colon expression must be grouped.
say (format: greeting name)

f x: body means that x is an ordinary argument and body is the colon argument. It parses like f x (body), not like (f x:) body.

yulang
f x: g y z

Chain Single-Expression Colon Blocks Inline

When a : block's entire body is a single expression, keep that expression in the call spine instead of dropping each call to a new indented block. This rule applies only to blocks whose body is one expression. Keep ordinary indented blocks when the body has several statements, local bindings, or substantial if/else, case, catch, or handler branches.

yulang
-- Prefer this shape for nested single-expression bodies.
say: run_console: ask()

-- Avoid expanding each single expression into its own block.
say:
    run_console:
        ask()

Whitespace after : is also a style signal. Use the spaced form when each step should read as its own call.

yulang
say: run_console: ask()

Omit the space when the chain should read as one fused operation, similar to function composition.

yulang
say:run_console:ask()

Both forms are colon application. The choice is about how tightly the chain should read.

Whitespace Is Syntax

Yulang has three call-like forms that look similar but parse differently.

yulang
f(x)    -- C-style call
f (x)   -- ML-style application of the parenthesized expression x
f: x    -- colon application

The same rule applies to indexing.

yulang
xs[0]   -- index
xs [0]  -- apply xs to the list [0]

Symbols make the whitespace rule especially visible.

yulang
f:foo   -- colon application: f applied to foo
f :foo  -- ML application: f applied to the symbol :foo

When the meaning depends on this distinction, insert the space deliberately. Do not use f:foo as a compact spelling for passing a symbol.

Newlines End ML Application

Whitespace application is line-oriented. A newline stops the current ML application chain, unless another syntax form such as an indented colon block continues it.

yulang
f x y

f:
    x
    y

Use : or a grouped expression when an argument expression should continue across a line boundary. Inside parentheses, a bare newline at the same grouping level can be read as another tuple/group item; indent continuation lines when the expression is meant to continue.

yulang
f:
    g x
    h y

f (g
    x)

Keep Method-Style Calls Receiver-First

Prefer receiver-first dot calls for operations that conceptually belong to the left-hand value. Field and method selection use dot syntax; selection itself is not a call, and the selected value is applied by the usual call syntax.

yulang
xs.len
xs.map f
text.splice(range 1 3, "bc")

Prefer module::name for constructors, effect operations, and names that are primarily module exports.

yulang
path_err::not_found "/x"
std::control::nondet::each xs

Do not add parentheses solely to enable a trailing dot call. If the receiver does not naturally stand on the left, restructure the call instead.

yulang
-- Prefer this.
say: 1 + 2

-- Reconsider this when the parentheses only exist for `.say`.
(1 + 2).say

Parentheses are still fine when they express real grouping or disambiguation. This rule only applies to parentheses whose only purpose is making a dot call syntactically possible.

Use the Pipe for Left-to-Right Data Flow

The pipeline operator is |. It passes the left-hand value as the first argument to the right-hand call spine.

yulang
1 | add 2    -- add 1 2

xs
    | map f
    | filter pred

| is left-associative and binds weaker than ordinary infix operators.

yulang
a + b | f    -- (a + b) | f

Use Indentation for Blocks

Indented blocks are the normal style for multi-line bodies.

yulang
my total xs =
    my start = 0
    fold add start xs

Braces are useful when the block is small or needs to stay inside another expression.

yulang
my inc = \x -> { x + 1 }

The last expression of a block is the block value.

Prefer Header Patterns for Functions

Bindings use patterns on the left-hand side. When the head is a name, following patterns become curried function arguments. Prefer this direct header style for small functions.

yulang
my add x y = x + y
my area { width = 1, height = 2 } = width * height

Use an explicit lambda when the function value itself is the subject of the expression.

yulang
my mapper = \f xs -> xs.map f

Record patterns with defaults are the idiomatic way to spell small optional named arguments.

yulang
my box { width = 1, height = width } = width * height

box {}
box { width: 3 }

Defaults are evaluated left-to-right, so later defaults may refer to earlier fields.

Keep case and catch Arms Vertical

Inline branches are useful for tiny expressions, but pattern-heavy code reads better with one arm per line.

yulang
act console:
    our write: str -> ()

case value:
    nil -> fallback
    just x -> x

catch action:
    console::write text, k -> k ()
    value -> value

Use guard clauses on the arm that owns the condition.

yulang
case n:
    x if x < 0 -> "negative"
    _ -> "non-negative"

Put Extensions in with: Blocks

struct, enum, act, error, role, and type ... with: declarations create or extend a companion namespace. Put methods and nearby implementation details in the with: block when they conceptually belong to the declared thing.

yulang
type str with:
    our s.splice r insert = std::text::str::splice s r insert

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

This keeps receiver-style APIs close to the type or effect that owns them.

with: is also useful as an expression-local extension point. Put helper bindings next to the expression that uses them when they are not part of a type's public companion API.

yulang
loop initial with:
    our loop state =
        if done state:
            state
        else:
            loop: step state

Put Constraints Near the Binding That Needs Them

Use where at the point where a type variable needs a role constraint. Do not push constraints into unrelated helper bindings merely to make a call typecheck. The constraint belongs at the boundary whose behavior depends on the role.

yulang
my double(x: 'a): 'a =
    where 'a: Add
    x + x

Treat Operators as Imported Syntax

Operators are not all parser builtins. A module can define and export prefix, infix, suffix, nullfix, and lazy infix operators. Put public operator declarations near the top of a module or in a prelude-like module, so downstream files can import the syntax before they are parsed.

yulang
-- Near the top of the exporting module.
pub infix(+) 6.0.0 6.0.0 = add
pub lazy infix(and) 2.0.0 2.0.0 = \a -> \b -> ...
pub prefix(return) 1.0.0 = \value -> value

Word operators such as return, last, next, and redo follow the same operator model as symbolic operators. They should not be treated as magic parser exceptions in user code.

Use Lazy Operators for Short-Circuiting

Short-circuiting is written as lazy operator syntax rather than as a special case of the evaluator.

yulang
pub lazy infix(and) 2.0.0 2.0.0 = \a -> \b ->
    if a():
        b()
    else:
        false

Both operands are provided as thunks, so the body decides whether to force each side. This makes and/or ordinary library-defined syntax with lazy evaluation behavior.

Add Type Annotations at Boundaries

Local code normally relies on inference.

yulang
my id(x) = x

Add annotations where they communicate a public contract, reduce ambiguity, or make an intended cast boundary explicit.

yulang
pub my id(x: 'a): 'a = x

my result: result str io_err = io_err::wrap:
    read_text path

Type variables are written as sigil identifiers such as 'a; they do not need a separate binder in ordinary function declarations.

Prefer Explicit State Syntax

Use explicit reference syntax to make mutable or local-reference behavior visible.

yulang
my incremented =
    my $count = 0
    &count = $count + 1
    $count

Keep ordinary my bindings immutable-looking.

Comments and Docs Are Different

Use // and /* ... */ for ordinary comments.

yulang
// local note

/* longer note */

Use -- and --- ... --- only for documentation comments. They are parsed as documentation syntax and may be kept by tooling.

yulang
-- Documents the next declaration.

---
Longer documentation block.
---

Summary

  • Prefer whitespace application and : over nested parentheses.
  • Chain nested single-expression : blocks inline; keep real multi-statement blocks indented.
  • Use say: run_console: ask() for visible steps and say:run_console:ask() for an intentionally fused chain.
  • Use parentheses when grouping is the point, not as default punctuation.
  • Avoid parentheses whose only job is enabling a trailing dot call.
  • Prefer my f x y = ... for ordinary function bindings.
  • Use record-pattern defaults for small optional named arguments.
  • Keep pattern-heavy case and catch arms vertical.
  • Put methods, attached impls, and expression-local helpers in the nearest natural with: block.
  • Remember that f(x) and f (x) are different.
  • Remember that f:foo and f :foo are different.
  • Put exported operator syntax where importers can see it before parsing.
  • Use indentation for real blocks and braces for compact inline blocks.

Yulang