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.
say: "hello"
format:
my name = "Yulang"
"hello, {name}"
run_console:
my answer = ask()
say answer-- 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.
f x: g y zChain 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.
-- 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.
say: run_console: ask()Omit the space when the chain should read as one fused operation, similar to function composition.
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.
f(x) -- C-style call
f (x) -- ML-style application of the parenthesized expression x
f: x -- colon applicationThe same rule applies to indexing.
xs[0] -- index
xs [0] -- apply xs to the list [0]Symbols make the whitespace rule especially visible.
f:foo -- colon application: f applied to foo
f :foo -- ML application: f applied to the symbol :fooWhen 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.
f x y
f:
x
yUse : 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.
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.
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.
path_err::not_found "/x"
std::control::nondet::each xsDo not add parentheses solely to enable a trailing dot call. If the receiver does not naturally stand on the left, restructure the call instead.
-- Prefer this.
say: 1 + 2
-- Reconsider this when the parentheses only exist for `.say`.
(1 + 2).sayParentheses 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.
1 | add 2 -- add 1 2
xs
| map f
| filter pred| is left-associative and binds weaker than ordinary infix operators.
a + b | f -- (a + b) | fUse Indentation for Blocks
Indented blocks are the normal style for multi-line bodies.
my total xs =
my start = 0
fold add start xsBraces are useful when the block is small or needs to stay inside another expression.
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.
my add x y = x + y
my area { width = 1, height = 2 } = width * heightUse an explicit lambda when the function value itself is the subject of the expression.
my mapper = \f xs -> xs.map fRecord patterns with defaults are the idiomatic way to spell small optional named arguments.
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.
act console:
our write: str -> ()
case value:
nil -> fallback
just x -> x
catch action:
console::write text, k -> k ()
value -> valueUse guard clauses on the arm that owns the condition.
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.
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.yThis 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.
loop initial with:
our loop state =
if done state:
state
else:
loop: step statePut 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.
my double(x: 'a): 'a =
where 'a: Add
x + xTreat 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.
-- 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 -> valueWord 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.
pub lazy infix(and) 2.0.0 2.0.0 = \a -> \b ->
if a():
b()
else:
falseBoth 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.
my id(x) = xAdd annotations where they communicate a public contract, reduce ambiguity, or make an intended cast boundary explicit.
pub my id(x: 'a): 'a = x
my result: result str io_err = io_err::wrap:
read_text pathType 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.
my incremented =
my $count = 0
&count = $count + 1
$countKeep ordinary my bindings immutable-looking.
Comments and Docs Are Different
Use // and /* ... */ for ordinary comments.
// local note
/* longer note */Use -- and --- ... --- only for documentation comments. They are parsed as documentation syntax and may be kept by tooling.
-- 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 andsay: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
caseandcatcharms vertical. - Put methods, attached impls, and expression-local helpers in the nearest natural
with:block. - Remember that
f(x)andf (x)are different. - Remember that
f:fooandf :fooare different. - Put exported operator syntax where importers can see it before parsing.
- Use indentation for real blocks and braces for compact inline blocks.