moonbit-bestpractice
MoonBit Coding Standards
1. Documentation
- Use
///|for documentation comments on top-level definitions (functions, structs, enums, traits, tests). This is the output format ofmoon fmt. - Ensure public APIs are documented.
2. Naming Conventions
- Types (Structs, Enums) & Traits: PascalCase (e.g.,
Position,GeoJSONObject,ToBBox). - Abbreviations: Abbreviations other than those at the beginning (such as JSON or ID) should generally be all uppercase (e.g.
FromJSON,JSONToMap). - Functions & Methods: snake_case (e.g.,
from_json,to_geometry,boolean_point_in_polygon). - Variables & Parameters: snake_case (e.g.,
coordinates,shifted_poly). - No Abbreviations: Do not abbreviate variable names unless they are common abbreviations (e.g.,
id,json). Avoidpforpoint,lsforline_string, etc. - Collections: Use the
_arraysuffix for array arguments/variables instead of plural names (e.g.,polygon_arrayinstead ofpolygons). - Constructors: Always define
fn newinside the struct body. Factory functions usenew_hoge/from_hogenaming — never::new.
3. Idioms & Best Practices
3.1 Constructors & Instance Initialization
-
Default constructor: The
fn newdeclaration inside the struct body is the constructor definition itself — do NOT write a separatefn StructName::new(...)implementation outside.fn newsupportsraise, so validation logic should be placed innewto ensure instances are always in a valid state.OK:
struct MyStruct { x : Int y : Int fn new(x~ : Int, y~ : Int) -> MyStruct }NG:
fn MyStruct::new(x~ : Int, y~ : Int) -> MyStruct { { x, y } } -
Factory functions: Define separate static functions with names like
new_hoge,from_hoge, etc. Never name them::new. Factory functions must generate values via thenewconstructor (StructName(...)is equivalent toStructName::new(...)):struct Rect { x : Double y : Double width : Double height : Double // Validation in new ensures all Rect instances have valid size fn new(x~ : Double, y~ : Double, width~ : Double, height~ : Double) -> Rect raise } // Conversion: create from a different representation fn Rect::from_corners(x1~ : Double, y1~ : Double, x2~ : Double, y2~ : Double) -> Rect { Rect(x=x1, y=y1, width=x2 - x1, height=y2 - y1) } // Specific state: create a type with optional fields in a predetermined state fn Rect::new_unit(x~ : Double, y~ : Double) -> Rect { Rect(x~, y~, width=1.0, height=1.0) } -
Initialization: Struct literal syntax (
StructName::{...}) should ONLY be used strictly within constructor functions likenew. External code must use the constructor syntax (StructName(...)). -
Updating: Use dedicated update functions/methods to modify values.
-
Struct Update Syntax: Avoid using Struct Update Syntax (e.g.,
{ ..base, field: value }) whenever possible, as it may bypass validation logic or constraints. -
Ignore Usage: Use proper pipeline style when ignoring return values:
expr |> ignore.
3.2 Error Handling
Use the raise effect for functions that can fail instead of returning Result types for synchronous logic.
Defining error types:
suberror DivError { DivError(String) }
suberror E3 {
A
B(String)
C(Int, loc~ : SourceLoc)
}
suberror uses enum-like constructor syntax. The older suberror A B syntax is deprecated. Always use suberror A { A(B) } with explicit constructors.
Raising errors:
- Custom error:
raise DivError("division by zero") - Generic failure:
fail("message")— convenience function that raisesFailuretype with source location
Function signatures:
fn div(x : Int, y : Int) -> Int raise DivError { ... }
fn f() -> Unit raise { ... }
fn add(a : Int, b : Int) -> Int noraise { a + b }
raise CustomError: function may raise a specific error typeraiseorraise Error: function may raise any errornoraise: function guaranteed not to raise
Handling errors:
try div(42, 0) catch {
DivError(msg) => println(msg)
} noraise {
v => println(v)
}
let a = div(42, 0) catch { _ => 0 }
let res = try? (div(6, 0) * div(6, 3))
try! div(42, 0)
try { expr } catch { pattern => handler } noraise { v => ... }: full error handlinglet a = expr catch { _ => default }: simplified inline catchtry? expr: convert toResult[T, Error]try! expr: panic on error
Error polymorphism:
Use raise? for higher-order functions that conditionally throw:
fn[T] map(
array : Array[T],
f : (T) -> T raise?
) -> Array[T] raise? { ... }
When f is noraise, map is also noraise. When f raises, map raises.
Best practices:
- Prefer
raiseeffect overResultfor synchronous code - In tests, let errors propagate or use
guardto assert success
3.3 Pattern Matching & Guards
-
Avoid
iffor Validation: Useguardinstead:guard array.length() > 0 else { raise Error("Empty") } -
Use
guardfor assertions, early returns, or unwrapping:-
General Code: Always provide an explicit fallback using
else:guard hoge is Hoge(fuga) else { raise fail("Message") } -
Tests: Use
guardwithout fallback for better readability:guard feature.geometry is Some(@geojson.Geometry::Polygon(poly))
-
-
Use
matchfor exhaustive handling of Enums. -
Use Labeled Arguments/Punners (
~) in patterns and constructors when variable names match field names:match geometry { Polygon(coordinates~) => coordinates }
3.4 Functions
-
Anonymous Functions: Prefer arrow syntax
args => bodyoverfnkeywordfn(args) { body }. -
Local
fnannotation: Localfndefinitions must explicitly annotateraise/asynceffects. Inference for localfnis deprecated. Arrow functions are unaffected.fn outer() -> Unit raise { fn local_fn() -> Unit raise { fail("err") } let arrow_fn = () => { fail("err") } }
3.5 Structs & Enums
-
Enum Wrapping Structs: Define independent Structs for each variant, then wrap them in the Enum. Use the same name for the Variant and the Struct.
pub struct Point { ... } pub enum Geometry { Point(Point) MultiPoint(MultiPoint) } derive(Debug, Eq) -
Delegation: When implementing traits for such Enums, pattern match on
selfand delegate to the inner struct's method. -
Prefer distinct Structs for complex data wrapped in Enums if polymorphic behavior is needed.
-
Standard traits:
Debug,Eq,Compare,ToJson,FromJson,Hash(note: types containingJsoncannot deriveHash). -
Debugtrait: Always deriveDebuginstead ofShow.DebugreplacesShowas the standard trait for structural formatting. Usesdebug_inspect()for output.struct MyStruct { ... } derive(Debug, Eq) -
Constructor qualified names: Built-in constructors require qualified names (e.g.,
Failure::Failure). Constructors with arguments cannot be used as higher-order functions; use a wrapper:let f = x => Some(x)
3.6 Traits
-
Definition Notation:
pub trait ToGeoJSON { to_geojson(Self) -> GeoJSON } -
Performance Overrides: Always override default trait methods if a more efficient implementation is possible for the specific type.
-
Implementation Rules:
- Default Implementation: If a method's return value is not
Selfand it can be implemented solely using other methods, provide a default implementation. - Trait Object Delegation: When implementing a super-trait for a type that also implements a sub-trait, define the logic as a static function on the sub-trait's object and relay to it.
- Direct Implementation: If delegation is not possible or creates circular dependencies, implement the method directly on the type.
- Default Implementation: If a method's return value is not
3.7 Cascade Operator
x..f() is equivalent to { x.f(); x } — for methods returning Unit.
let result = StringBuilder::new()
..write_char('a')
..write_object(1001)
..write_string("abcdef")
.to_string()
Enables chaining mutable operations without modifying return types. Compiler warns if result is ignored.
3.8 Range Syntax
a..<b: exclusive upper bound (increasing)a..<=b: inclusive upper bound (increasing) — replaces deprecateda..=ba>..b: exclusive lower bound (decreasing)a>=..b: inclusive lower bound (decreasing)
for i in 0..<10 { ... }
for i in 10>=..0 { ... }
3.9 Loop nobreak
Replaces old loop else. Executes when the loop condition becomes false.
let r = while i > 0 {
if cond { break 42 }
i = i - 1
} nobreak {
7
}
break must provide a value matching the nobreak return type.
3.10 declare Keyword
Similar to Rust's todo! macro. Use declare before function definitions to indicate specification-only declarations. Missing implementations generate warnings (not errors).
declare fn add(x : Int, y : Int) -> Int
4. Performance Optimization
-
Lazy evaluation with Iterator: For array processing where the size is unknown or potentially large, prefer
iter()for lazy evaluation to avoid intermediate array allocations. This is especially effective for fold operations likeminimum()/maximum():let min_x = coords.iter().map(c => c.x()).minimum().unwrap() -
Flattening: Use
flatten()instead of manual loops withappendwhen merging nested collections.
5. Toolchain
moon.pkgDSL replaces deprecatedmoon.pkg.json.
6. Testing
See MoonBit Testing Standards for detailed testing guidelines.