This is a document describing the preferred practices for writing code for the Pony standard library. Other projects are free to use the same style, but you are free to use any style in other projects. The following conventions are meant to ensure consistency and readability of Pony code. Any of the following rules may be broken in situations where the recommended practice would diminish readability.
The maximum line length is 80 columns.
No trailing whitespace at the end of lines.
Use 2 spaces for indentation, no hard tabs.
actor Main
new create(env: Env) =>
env.out.print("Hello, world!")
-
One blank line is placed between top-level declarations such as classes, primitives, actors, etc.
-
There is no blank lines between the class declaration and its first member or documentation.
-
Fields have no blank lines between them.
-
Functions are separated from other members by one line.
// OK class Foo let bar: Bar let baz: Baz fun foo() => // ... fun bar() => // ... // OK class Foo """ Some documentation """ let bar: Bar fun foo() => // ... // OK class Foo fun foo() => // ... // Not OK class Foo let bar: Bar let baz: Baz fun foo() => // ...
-
Blank lines between one-line functions or primitives may be omitted if they are related.
primitive Red fun apply(): U32 => 0xFF0000FF primitive Green fun apply(): U32 => 0x00FF00FF primitive Blue fun apply(): U32 => 0x0000FFFF type Color is (Red | Green | Blue)
-
Use spaces around binary operators and after commas, colons, and semicolons. Do not put spaces around brackets or parentheses.
let x: USize = 3 x = x + (5 * x) (let a, let b) = ("a", "b") let ns = [as USize: 1; 2; 3] fun foo(i: USize, bar: USize = 0) foo(x where bar = y)
-
The
not
operator is surrounded by spaces, but the-
operator is not followed by a space.if not x then -a end
-
Lambda expressions follow the rules above except that a space is placed before the closing brace only if the lambda is on one line.
// OK {(a: USize, b: USize): USize => a + b } // OK {(a: USize, b: USize): USize => a + b } // Not OK {(a: USize, b: USize): USize => a + b} // Not OK { (a: USize, b: USize): USize => a + b } // Not OK {(a: USize, b: USize): USize => a + b }
-
Normal rules apply to Lambda types.
// OK type foo is {(USize, USize): USize} box // Not OK type foo is {(USize, USize): USize } box // Not OK type foo is { (USize, USize): USize } box
Align long union and intersection types with each type on a separate line starting with the (
, |
, or &
symbols. In this case, every symbol must be separated by a single space or a newline.
// OK
type Signed is (I8 | I16 | I32 | I64 | I128)
// OK
type Signed is
( I8
| I16
| I32
| I64
| I128 )
// OK
type Signed is
( I8
| I16
| I32
| I64
| I128
)
// Not OK
type Signed is ( I8
| I16
| I32
| I64
| I128 )
// Not OK
type Signed is
(I8
|I16
|I32
|I64
|I128)
Multiline arrays generally have a newline between each entry, though logical exceptions exist (such as tables).
// OK
let ns = [as USize: 1; 2; 3]
// OK
let strs = ["a"; "b"; "c"]
// OK
let ns =
[ as USize:
1
2
3 ]
// OK
let ns =
[ as USize:
1
2
3
]
// Not OK
let ns =
[as USize:
1
2
3
]
// Not OK
let ns = [ as USize:
1
2
3
]
// Not OK
let ns = [ as USize:
1
2
3 ]
-
Control structures are aligned so that their keywords (if, else, for, try, end, etc.) are at the same indentation level.
if cond1 then e1 elseif cond2 then e2 else e3 end if cond1 then e1 elseif cond2 then e2 else e3 end for name in ["Bob"; "Fred"; "Sarah"].values() do env.out.print(name) end
-
Small expressions may be placed on a single line.
if cond then e1 else e2 end
-
Match expression cases are not indented.
match (x, y) | (None, _) => "none" | (let s: String, 2) => s + " two" | (let s: String, 3) => s + " three" | (let s: String, let u: U32) if u > 14 => s + " other big integer" | (let s: String, _) => s + " other small integer" else "something else" end match alive | true => Debug.err("Beep boop!") this.forever() | false => Debug.err("Ugah!") None end
-
Match expressions may not be on a single line.
// OK match x | let n: U8 => n.string() else "0" end // Not OK match x | let n: U8 => n.string() else "0" end
Assignment of a multiline expression is indented below the =
symbol.
// OK
var x =
if friendly then
"Hello"
else
false
end
// OK
let output =
recover String(
file_name.size()
+ file_linenum.size()
+ file_linepos.size()
+ msg.size()
+ 4)
end
// Not OK
var x = if friendly then
"Hello"
else
false
end
-
Partial functions are denoted with a
?
separated from other symbols by one space.// OK fun foo(): Foo^ ? => // ... // OK fun foo() ? => // ... // Not OK fun foo(): Foo^? => // ... // Not OK fun foo()? => // ...
-
Multiline function parameters are each listed on a separate line. The return type preceded by a
:
is placed at the same indentation level followed by a question mark (if the function is partial). The=>
is placed on a separate line between the parameter list and the function body at the same indentation level as thefun
keyword. These rules also apply to FFI function declarations.// OK fun find( value: A!, offset: USize = 0, nth: USize = 0, predicate: {(box->A!, box->A!): Bool} val = {(l: box->A!, r: box->A!): Bool => l is r }) : USize ? => // ... // OK fun tag _init(typ': ValueType, default': (Value | None)) : (ValueType, Value, Bool) ? => // ... // OK @pony_asio_event_create[AsioEventID]( owner: AsioEventNotify, fd: U32, flags: U32, nsec: U64, noisy: Bool) // Not OK fun find( value: A!, offset: USize = 0, nth: USize = 0, predicate: {(box->A!, box->A!): Bool} val = {(l: box->A!, r: box->A!): Bool => l is r }) : USize ? => // ...
-
No spaces are placed around the
.
symbol, but the.>
symbol is spaced like an infix operator.// OK Iter[I64](input.values()).skip(2).next() // OK output .> append(file_name) .> append(":") .> append(msg) // Not OK Iter[I64](input . values()) . skip(2) . next() // Not OK output.>append(file_name).>append(":").>append(msg)
-
Explicit partial calls are postfixed with '?' operator without whitespace in between.
// OK [as U8: 0; 1; 1].apply(0)? // not OK let newVal = myHashMap.insert("key", "value") ?
-
Multiple function calls should generally be aligned so that each call is placed on a separate line indented below the initial object.
// OK Iter[I64](input.values()) .take_while({(x: I64): Bool ? => error }) .collect(Array[I64])) // OK output .> append(file_name) .> append(":") .> append(msg) // Not OK Iter[I64](input.values()).take_while({(x: I64): Bool ? => error }) .collect(Array[I64])) // Not OK output .>append(file_name) .>append(":") .>append(msg)
-
Function arguments mostly follow the same rules as arguments in method declarations. However, all arguments may be placed on the following line with an additional level of indentation if all arguments would fit on that line. Otherwise, arguments must be placed on individual lines. These rules also apply to FFI calls. A
where
keyword and the following arguments may all exist on their own line.// OK h.assert_true(Iter[I64](input.values()).any(is_positive)) // OK Iter[String].chain( [input0.values(); input1.values(); input0.values()].values()) // OK h.assert_eq[String]( " fmt ", Format("fmt", FormatDefault, PrefixDefault, -1, 9, AlignCenter)) // OK buf.append(Format.int[U32]( high where fmt = FormatHexBare, width = 4)) // Not OK @pony_asio_event_create(this, fd, AsioEvent.read_write_oneshot(), 0, true) // Not OK options.upsert(o.spec().name(), o, {(x, n) => x._append(n) })?
Multiline type parameters are each listed on a separate line. Type constraints will start on the next line beginning with the is
keyword. Complex type constraints follow the rules described in Type Aliases.
// OK
type Map[
K: (mut.Hashable val & Equatable[K]),
V: Any #share]
is HashMap[K, V, mut.HashEq[K]]
// OK
class Flags[
A: Flag[B] val, B: (Unsigned & Integer[B] val) = U64]
is Comparable[Flags[A, B] box]
// OK
type BinaryHeapPriority[A: Comparable[A] #read]
is ( _BinaryHeapPriority[A]
& (MinHeapPriority[A] | MaxHeapPriority[A]) )
// OK
fun ref zip4[B, C, D, E](
i2: Iterator[B],
i3: Iterator[C],
i4: Iterator[D],
i5: Iterator[E])
: Iter[(A, B, C, D, E)]^
=>
// OK
fun div_checked[
T: (SignedInteger[T, U] val & Signed),
U: UnsignedInteger[U] val](
x: T,
y: T)
: (T, Bool)
=>
// Not OK
primitive HashEq64
[A: (Hashable64 #read & Equatable[A] #read)] is HashFunction64[A]
// Not OK
class Flags[A: Flag[B] val,
B: (Unsigned & Integer[B] val) = U64]
is Comparable[Flags[A, B] box]
// Not OK
class val _MapCollisions[K: Any #share, V: Any #share,
H: mut.HashFunction[K] val]
// Not OK
fun mod_checked
[T: (SignedInteger[T, U] val & Signed), U: UnsignedInteger[U] val](
x: T,
y: T)
: (T, Bool)
=>
-
CamelCase
is used for types andsnake_case
is used for field, function, and variable names. -
Acronyms (HTTP, JSON, XML) in types are uppercase.
// OK primitive URLEncode // OK type TCPConnectionAuth is (AmbientAuth | NetAuth | TCPAuth | TCPConnectAuth) // OK let url = URL.valid("https://www.ponylang.io/") // Not OK class JsonDoc
-
The file name of Pony source files should be based on the name of the principal type defined in that file.
- The principal type in a file is the type that makes up the bulk of the significant lines of code in the file or is conceptually more important or fundamental than all other types in the file. For example, if a file defines a trait type and a group of small class types that all provide that trait, then the trait type should be considered the principal type.
- If there are multiple types defined in the file which all have equal significance and a shared name prefix, then the shared prefix should be used as the principal type name. For example, a file that defines
PacketFoo
,PacketBar
, andPacketBaz
types should usePacket
as the principal type name, even if noPacket
type is defined. - If there are multiple significant types defined in the file which do not have a shared name prefix, then this should be taken as a hint that these types should probably be defined in separate files instead of together in one file.
-
The file name should be directly derived from the principal type name using a consistent reproducible scheme of case conversion.
- The file name should be the "snake case" version of the principal type name. That is, each word in the principal type name (as defined by transitions from lowercase to uppercase letters) should be separated with the underscore character (
_
) and lowercased to generate the file name. For example, a file that defines theContentsLog
type should be namedcontents_log.pony
. - If the principal type is a private type (its name beginning with an underscore character), then the file name should also be prefixed with an underscore character to highlight the fact that it defines a private type. For example, a file that defines the
_ClientConnection
type should be named_client_connection.pony
. - If the principal type name contains an initialism (a sequence of uppercase letters with no lowercase letters between them), then the entire initialism should be considered as a single word when converting to snake case. Note that if there is another word following the initialism, its first letter will also be uppercase, but should not be considered part of the sequence of uppercase letters that form the initialism. For example, a file that defines the
SSLContext
type should be namedssl_context.pony
.
- The file name should be the "snake case" version of the principal type name. That is, each word in the principal type name (as defined by transitions from lowercase to uppercase letters) should be separated with the underscore character (
Public functions and types must include a triple-quoted docstring unless it is self-explanatory to anyone with the most basic knowledge of the domain. Markdown is used for formatting. The """
tokens are placed on their own lines, even when the entire docstring could fit on a single line with the """
tokens.
primitive Format
"""
Provides functions for generating formatted strings.
* fmt. Format to use.
* prefix. Prefix to use.
* prec. Precision to use. The exact meaning of this depends on the type,
but is generally the number of characters used for all, or part, of the
string. A value of -1 indicates that the default for the type should be
used.
* width. The minimum number of characters that will be in the produced
string. If necessary the string will be padded with the fill character to
make it long enough.
* align. Specify whether fill characters should be added at the beginning or
end of the generated string, or both.
* fill: The character to pad a string with if is is shorter than width.
"""
Single line comments will have exactly one space between the //
token and the beginning of the comment message. This single space may only be omitted if the comment only contains valid Pony code. Single line comments that continue a previous comment have the same spacing requirements. No such restrictions are placed on multiline comments between the /*
and */
tokens.
// if `data` is 1 cacheline or less in size
// copy it into the existing `_current` array
// to coalesce multiple tiny arrays
// into a single bigger array