Motoko style guidelines
Overview
To increase readability and uniformity of Motoko source code, the style guide provides suggestions for formatting Motoko sources and other basic conventions.
Layout
Spacing
Put spaces around arithmetic operators, except to visually group sub-expressions of more tightly binding operators.
let z = - 2*x + 3*y + 4*(x*x + y*y);
Put spaces around comparison operators, Boolean operators, and assignment operators.
4 + 5 <= 5 + 4;
not (a or b and not c);
v := 0;
v += 1;Put spaces around '='.
var v = 0;
let r = { a = 1; b = 2 };Analogously, put spaces around
:
.var v : Nat = 0;
func foo(x : Nat, y : Nat) : Nat { x + y }
func bar((x, y) : (Nat, Nat)) : Nat { x + y }
let w = 1 ^ 0xff : Nat16;Rationale: ':' is to declarations what '=' is to definitions. Moreover, the left-hand of a type annotation may generally be an arbitrary complex expression or pattern.
Put a space after a comma or semicolon, but not before.
let tuple = (1, 2, 3);
let record = { a = 1; b = 2; c = 3 };Put spaces inside braces, unless they are a simple variant or record.
func f() { 0 };
f({ a = 1; b = 2; c = 3 });
f({a = 1; b = 2}); // okay as well
type Vec3D = { x : Float; y : Float; y : Float };
type Order = { #less; #equal; #more };
type Order = {#less; #equal; #more}; // okay as well
type Proc = {h : Nat; w : Nat} -> {#ok; #fail};Put spaces inside brackets if they stretch multiple lines.
foo(
firstArgument,
( longTupleComponent, anotherLongExpression,
moreLongExpression
),
[ 1, 2, 3,
4, 5, 6,
],
{ field1 = 4; field2 = 5;
field3 = 6;
}
);Put a space between statement keywords and their operands.
if (f()) A else B;
for (x in xs.vals()) { ... };
switch (compare(x, y)) {
case (#less) { A };
case (_) { B };
}
assert (x < 100);
await (async (0));Do not put a space between a function or variant tag and its argument tuple or around a generic type parameter list.
type Pair<X> = (X, X);
type Id = <X>(X) -> X;
let ok = #ok(5);
func id<X>(x : X) : X { x };
id<Nat>(5);Put a space between a function and its argument if it is not a tuple or parenthesized expression (see parentheses) or a record used as a named parameter list (see picking types).
sin 0.0;
g [1, 2, 3];
f{arg1 = 0; arg2 = 0};
Rationale: g[1]
in particular will be misparsed as an indexing operation.
Do not put a space around access operators like
.
,?
,!
, or index brackets.foo(bar).baz[5]().boo;
foom(?(bam()! + 1));
Line breaks
Pick a fixed right margin for lines and break definitions or expressions. 80 still is considered a good limit by many.
let sum = a + b + 2*c + d +
e + f + g + h + i + k +
l + m + n + o + p;
// Or:
let sum =
a + b + 2*c + d + e +
f + g + h + i + k + l +
m + n + o + p;Rationale: Among other reasons, this style of formatting:
Avoids code being hidden to the right in a window.
Avoids random line breaks in side-by-side diffs. For example, as shown by GitHub or similar code review tools.
Allows prettier display on paper, web sites, or other media.
Break lines after an operator.
a + b + c +
d + f;
foo(bar, baz).
boo();When breaking function definitions or calls with long argument lists, put each argument on a separate line.
Also, consider using records for long parameter lists, see picking types.
func someFunction(
arg1 : FirstType,
arg2 : SecondType,
anotherArg : Nat,
yetAnother : [Type],
func : Nat -> Nat,
) : Nat {
...
};
someFunction(
veryLongArgumentExpression,
anotherVeryLongArgumentExpression,
3,
aNestedFunctionCall(
alsoWithLongArguments,
andMoreSuchArguments,
),
moreLongishArgument,
);Rationale: This prevents overlooking an argument when reading code and avoids re-breaking lines when changing one of the expressions.
Indentation
Each level of indentation should be 2 spaces.
actor A {
public func f() {
return;
}
}Rationale: There may be a lot of nesting. Using only 2 spaces avoids wasting screen estate.
Indentation should not depend on the lexical contents of previous lines.
In particular, do not vertically align indentation with inner characters from previous lines.
let x = someFunction(
arg1, arg2, arg3, arg4, arg5); // Do this.
let x = someFunction(arg1, arg2, arg3,
arg4, arg5); // Or this.
let x =
someFunction(arg1, arg2, arg3, arg4, arg5); // Or this.
let x = someFunction( // Or this.
longArg1,
longArg2,
longArg3,
longArg4,
longArg5,
);
// COUNTER EXAMPLE!
let x = someFunction(arg1, arg2, arg3,
arg4, arg5); // DO NOT DO THIS!Rationale: There are many problems with vertical alignment, for example:
It wastes a lot of horizontal space.
It creates wildly inconsistent indentation levels that obfuscate the structure of the code.
It can produce realignment churn when changing a line, which, even when automated by editors, inflates and obfuscates diffs.
It completely breaks with variable-width fonts.
Rule of thumb: there should be no indentation that is not a multiple of 2.
Do not use tabs.
Rationale: The interpretation of tabs varies wildly across tools and they get lost or are displayed incorrectly in many contexts, such as web pages, diffs, etc.
Grouping
Separate complex multi-line definitions with empty lines. One-liners can be put on consecutive lines.
func foo() {
// This function does a lot of interesting stuff.
// It's definition takes multiple lines.
}
func boo() {
// This is another complicated function.
// It's definition also takes multiple lines.
}
func add(x : Nat, y : Nat) { return x + y };
func mul(x : Nat, y : Nat) { return x * y };Separate logic groups of definitions with two empty lines. Add a one-line comment as a "section header" for each group.
// A very large class
class MuffleMiff(n : Nat) {
// Accessors
public func miffMuff() : Text {
...
}
public func sniffMiff() : Nat {
...
}
// Mutators
public func clearMurk() {
...
}
public func addMuff(name : Text) {
...
}
// Processing
public func murkMuffle(param : List<Gnobble>) {
...
}
public func transformSneezler() {
...
}
// Internal State
var miffCount = 0;
var mabbleMap = Map<Nat, Text>();
}
Comments
Use line comments (
//…
). Use block comments (/* … */
) only when commenting in the middle of a line or for commenting out pieces of code during development.// The following function runs the current
// pallaboom on a given snibble. It returns
// suitable plexus if it can.
func paBoom(s : Snibble) : Handle<Plexus> {
let puglet = initPugs(s.crick, 0 /* size */, #local);
/* Don't do the odd stuff yet...
...
...
*/
return polyfillNexus(puglet); // for now
}Rationale: Line comments make it easier to insert, remove or swap individual lines.
Put short comments explaining a single line at the end of the line, separated by at least 2 spaces.
paBoom(getSnibble())); // create new snibble
Put multi-line comments before a line of code, with the same indentation as the code it is describing.
func f() {
// Try to invoke the current pallaboom with
// the previous snibble. If that succeeds,
// we have the new plexus; if not, complain.
let plexusHandle = paBoom(getSnibble()));
}Capitalize comments that are on separate lines. Use a proper full stop for sentences.
Punctuation
Semicolons
Motoko uniformly requires a semicolon to separate expressions or local declarations in a block, regardless of whether the preceding declaration ends in a closing '}'.
Rationale: This is unlike other C-style languages, which tend to have rather ad-hoc rules.
Put a semicolon after the last expression in a block, unless the whole block is written on a single line.
Similarly for types.
// No ; needed before closing } on same line
type Vec3D = {x : Float; y : Float; z : Float};
type Result<A> = {#ok : A; #error : Text};
func add(x : Nat, y : Nat) : Nat { return x + y };
// End last case with ;
type Address = {
first : Text;
last : Text;
street : Text;
nr : Nat;
zip : Nat;
city : Text;
};
type Expr = {
#const : Float;
#add : (Expr, Expr);
#mul : (Expr, Expr);
};
func eval(e : Expr) : Float {
switch (e) {
case (#const(x)) { x };
case (#add(e1, e2)) { eval(e1) + eval(e2) };
case (#mul(e1, e2)) { eval(e1) * eval(e2) };
};
}Rationale: Consistently ending lines with semicolon simplifies adding, removing, or swapping lines.
Braces
Put braces around function bodies,
if
orcase
branches, and loop bodies, unless they appear nested as an expression and only contain a single expression.func f(x) { f1(x); f2(x) };
let abs = if (v >= 0) v else -v;
let val = switch (f()) { case (#ok(x)) x; case (_) 0 };
func succ(x : Nat) : Nat = x + 1;Use "C-style" layout for braced sub-expressions stretching multiple lines.
func f() {
return;
};
if (cond) {
foo();
} else {
bar();
};
switch (opt) {
case (?x) {
f(x);
};
case (null) {};
};
Parentheses
Motoko supports "parenless" style, meaning that parentheses are optional in most places, such as function parameter lists, or statement operands, when they enclose an expression that either is bracketed already. For example, a tuple, object, or array, or a simple constant or identifier.
type Op = Nat -> Nat;
let a2 = Array.map<Nat, Nat>(func x { x + 1 }, a);
let y = f x;
let z = f {};
let choice = if flag { f1() } else { f2() };
switch opt {
case null { tryAgain() };
case _ { proceed() };
};Avoid overuse of parenless style.
In particular, do not omit parentheses and braces on statements at the same time.
// COUNTER EXAMPLES!
let choice = if flag x + y else z; // DO NOT DO THIS!
switch val {
case 0 f(); // DO NOT DO THIS!
case n n + 1; // OR THIS!
};Rationale: Omitting both at the same time makes the code harder to read, since there is less visual clue how it groups.
Similarly, do not omit parentheses around function parameters if the function also has type parameters.
// COUNTER EXAMPLE!
foo<Nat> 0; // DO NOT DO THIS!Omit parentheses around argument types of a function type with a single argument and no type parameters.
But do not omit them around when functions or classes also have type parameters.
type Inv = Nat -> Nat;
type Id = <T>(T) -> T;
type Get = <X>(C<X>) -> X;
// COUNTER EXAMPLE!
type Get = <X>C<X> -> X; // DO NOT DO THIS!
Miscellaneous
Use
_
to group digits in numbers.Group by 3 digits in decimal numbers and by 4 in hexadecimal notation.
let billion = 1_000_000_000;
let pi = 3.141_592_653_589_793_12;
let mask : Nat32 = 0xff00_ff0f;
Naming
Style
Use
UpperCamelCase
for type names (including classes or type parameters), module names, and actor names.Use
lowerCamelCase
for all other names, including constants and variant fields.module MoreMuff {
type FileSize = Nat;
type Weekday = {#monday; #tuesday; #wednesday};
type Pair<X> = (X, X);
class Container<X, Y>() { ... };
func getValue<Name>(name : Name) : Pair<Name> { ... };
let zero = 0;
let pair = getValue<Text>("opus");
var nifty : Nat = 0;
object obj { ... };
actor ServerProxy { ... };
};Rationale: The general convention is upper case for "static" entities like types and lower case for "dynamic" values. Modules and actors are fairly static and can export types. Objects usually don’t export types and tend to be used mostly as dynamic values.
Spell acronyms as regular words.
type HttpHeader = ...;
func getUrl() { ... };
let urlDigest = ...;Do not use identifier names that start with an underscore
_
, except to document that a variable in a pattern is intentionally unused.let (width, _color, name) = rumpler();
... // _color is not used here
func foo(x : Nat, _futureFlag : Bool) { ... };Rationale: A type checker can warn about unused identifiers, which can be suppressed by explicitly prepending
_
to its name to document intention.This aligns with the use of the keyword
_
for pattern wildcards.
Conventions
The name of functions returning a value should describe that value.
Avoid redundant
get
prefixes.dict.size();
list.first();
sum(array);The name of functions performing side effects or complex operations should describe that operation.
dict.clear();
dict.set(key, value);
let result = traverse(graph);The name of predicate functions returning
Bool
should use anis
orhas
prefix or a similar description of the tested property.class Set<X>() {
public func size() : Nat { ... };
public func add(x : X) { ... };
public func remove(x : X) { ... };
public func isEmpty() : Bool { ... };
public func contains(x : X) : Bool { ... };
};Functions converting to or from a type
X
are namedtoX
andfromX
, respectively, if the source, resp. target, is either the object the function is a method of, or the primary type of the module this function appears in.In classes or objects, use a name ending with
_
to distinguish private variables from getters.class Cart(length_ : Nat) {
var width_ = 0;
public func length() : Nat { return length_ };
public func width() : Nat { return width_ };
}Rationale: In Motoko, functions are first-class values, so functions and other value identifiers share the same name space.
Identifiers with a leading
_
should not be used for private state, since that indicates an unused name (see style).Use longer, more descriptive names for global or public identifier or ones with large scope, and short names for local ones with small scope.
It is fine to use single character identifiers when there is nothing interesting to say, especially when using the same naming scheme consistently.
func map(x : Nat, y : Nat) : Nat { x + y };
func eval(e : Expr) : Nat {
let n =
switch (e) {
case (#neg(e1)) { - eval(e1) };
case (#add(e1, e2)) { eval(e1) + eval(e2) };
case (#mul(e1, e2)) { eval(e1) * eval(e2) };
};
Debug.print(n);
return n;
};Rationale: Contrary to popular belief, overly chatty local names can decrease readability instead of increasing it, by increasing the noise level.
In suitable cases, use plural form for describing a collection of items, such as a list or array.
This also works for short names.
func foreach<X>(xs : [X], f : X -> ()) {
for (x in xs.vals()) { f(x) }
}
Types
Type annotations
Put type annotations on definitions that involve fixed-width numeric types, to disambiguate the type of overloaded arithmetic operators and constants.
let mask : Nat32 = 0xfc03_ff00;
let pivot : Nat32 = (size + 1)/2;
let vec : [Int16] = [1, 3, -4, 0];Use floating point constants to enforce type
Float
without an extra annotation. Similarly, use an explicit+
sign to produce a positive value of typeInt
instead ofNat
, if desired.let zero = 1.0; // type Float
let offset = +1; // type IntSimilarly, put inline type annotations on arithmetic expressions with types other than
Nat
orInt
.if (x & mask == (1 : Nat32)) { ... };
The need to annotate constants in cases like this is a short-coming of Motoko’s type system that we hope to address soon.
An annotation is not needed on function arguments, since their type is usually inferred from the function. The only exception is when that argument has generic type and the type arguments have been omitted.
func foo(len : Nat32, vec : [Nat16]) { ... };
func bar<X>(x : X) { ... };
foo(3, [0, 1, 2]);
bar<Nat16>(0);
bar(0 : Nat16);Put type annotations on mutable variables, unless their type is obvious.
var name = "Motoko";
var balance = 0;
func f(i : Int) {
var j = i;
};
var balance : Int = 0;
var obj : Class = foo();Rationale: Due to subtyping, inferring the type from the initialization would not necessarily deduce the intended type. For example,
balance
would have typeNat
without the annotation, ruling out assignments of integers.Put type annotations on all public fields in a class.
class C(init_ : Nat) {
public let init : Nat = init_;
public var count : Nat = 0;
}Omit return type annotations of functions when the type is
()
.func twiceF() { f(); f() }; // no need to write ": ()"
Omit type annotations on functions when they are passed as arguments.
Array.map<Nat, Nat>(func n {n + 1}, a);
Put type annotations on definitions that involve numeric types other than
Nat
orInt
, to resolve the overloading between arithmetic operators and constants.let mask : Nat32 = 0xfc03_ff00;
let offset : Nat32 = size + 1;
Picking types
Use
Nat
for any integral value that cannot be negative.Use fixed-width
NatN
orIntN
only when storing many values and space usage matters, when bit-fiddling requires the low-level interpretation of a number as a vector of bits or when matching types imposed by external requirements, such as other canisters.Avoid proliferation of option types, and therefore
null
.Limit their use to as small a scope as possible. Rule out the
null
case and use non-option types wherever possible.Consider using records instead of tuples when there are more than 2 or 3 components. Records are just simple objects with named fields.
Note that record types need not be declared but can be used in place.
func nodeInfo(node : Node) : {parent : Node; left : Node; right : Node} { ... }
Consider using variants instead of
Bool
to represent binary choices.Note that variant types need not be declared but can be used in place.
func capitalization(word : Text) : {#upper; #lower} { ... }
Where possible, use return type
()
for functions whose primary purpose is to mutate state or cause other side effects.class Set<X>() {
public func add(x : X) { ... };
public func remove(x : X) { ... };
...
};Consider using a record (an object with just data) as argument for long parameter lists.
func process({seed : Float; delta : Float; data : [Record]; config : Config}) : Thing {
...
};
process{config = Config(); data = read(); delta = 0.01; seed = 1.0};Rationale: This expresses named parameters. This way, arguments can be freely reordered at the call site and callers are prevented from accidentally passing them in the wrong order.
Higher-order functions, such as functions that take a callback argument, should put the function parameter last.
Rationale: Makes call sites more readable, and in the absence of currying, there is no point in putting the function first, like you often would in functional languages.
Do not use sentinel values, such as
-1
, to represent invalid values.Use the option type instead.
func lookup(x : key) : ?Nat { ... }
Data is immutable in Motoko unless explicitly stated otherwise.
Use mutability types and definitions (
var
) with care and only where needed.Rationale: Mutable data cannot be communicated or share across actors. It is more error-prone and much more difficult to formally reason about, especially when concurrency is involved.
Features
Statements
Use
for
loops instead ofwhile
loops for iterating over a numeric range or a container.for (i in Iter.range(1, 10)) { ... };
for (x in array.vals()) { ... };Rationale: For loops are less error-prone and easier to read.
Use
if
orswitch
as expressions where appropriate.func abs(i : Int) : Int { if (i < 0) -i else i };
let delta = switch mode { case (#up) +1; case (#dn) -1 };Motoko requires that all expressions in a block have type
()
, in order to prevent accidentally dropped results.Use
ignore
to explicitly drop results. Do not useignore
when it’s not needed.ignore async f(); // fire of a computation
Motoko allows to omit the
return
at the end of a function, because a block evaluates to its last expression.Use this when a function is short and in "functional" style, that is, the function does not contain complex control flow or side effects.
Use explicit
return
at the end when the function contains otherreturn
statements or imperative control flow.func add(i : Nat, j : Nat) : Nat { i + j };
func foo(a : Float, b : Float) : Float {
let c = a*a + b*b;
c + 2*c*c;
};
func gcd(i : Nat, j : Nat) : Nat {
if (j == 0) i else gcd(j, i % j);
};
func gcd2(i : Nat, j : Nat) : Nat {
var a = i;
var b = j;
while (b > 0) {
let c = a;
a := b;
b := c % b;
};
return a;
};
Objects and records
Use the short-hand object syntax
{x1 = e1; … ; xN = eN}
when using objects as simple records, i.e., data structures with no private state and no methods.Use
object
when creating singleton objects.Limit the use of objects to records where possible.
Rationale: Only records can be sent as message parameters or results and can be stored in stable variables. Objects with methods are also more expensive to create and represent in memory.
Use full objects only as a means for encapsulating state or behavior.
Classes
Use
class
to create multiple objects of the same shape.Name classes after their conceptual functionality, not their implementation, except when having to distinguish multiple different implementations of the same concept. For example,
OrderedMap
vsHashMap
).Classes are both type definitions and factory functions for objects.
Do not use classes unless both these roles are intended; use plain type aliases or functions returning an object in other cases.
Do not overuse classes.
Use a module defining a plain type and functions on it where appropriate. Use classes only as a means for encapsulating state or behavior.
Rationale: Objects with methods have disadvantages over simple record types with separate functions (see above).
If values of a class are meant to be sendable (shared), the class needs to provide a pair of
share
/unshare
methods that convert to/from a sharable representation, for example, as a record.For immutable classes it may seem more natural to make
unshare
a kind of static function. However, even for immutable ones it may depend on constructor arguments (such as an ordering function), so that the a pattern likeMap(compareInt).unshare(x)
seems appropriate.For the time being, avoid overloading classes with too many methods, since that is currently expensive.
Restrict to a sufficiently small set of canonical methods and make less essential ones that can be implemented on top of those into functions in the enclosing module.
Use modules for "static" classes or methods.
Modules
Use
module
to group definitions, including types, and create a name spae for them.Where applicable, name modules after the main type or class they implement or provide functions for.
Limit each module to a single main concept/type/class or closely entangled family of concepts/types/classes.