MoonBit Coding Standards
Related Skills
- moonbit-docs — MoonBit language reference (syntax, types, functions, methods, deriving). Use for language questions and debugging.
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 guard without 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:
1. Default Implementation: If a method's return value is not Self and it can be implemented solely using other methods, provide a default implementation. 2. 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. 3. Direct Implementation: If delegation is not possible or creates circular dependencies, implement the method directly on the type.
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) -> Int4. 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.