Skip to content

Style guide

This guide documents the best practises to follow when writing Inko source code, such as what indentation method to use, and when to use keyword arguments.

Encoding

Inko source files must be encoded in UTF-8. The compiler does not support reading source files using a different encoding.

Line endings

Unix (\n) line endings must be used at all times. Windows newlines (\r) are not supported.

Line length

Lines should be hard wrapped at 80 characters per line. It's OK if a line is a few characters longer, but only if wrapping the line makes it less readable. For example, if a line's length is dominated by a string, then it's OK to keep that on a single line.

Indentation

Inko source code should be indented using 2 spaces per indentation level, not tabs. Different programs use different widths for tabs (sometimes with no way of changing this), potentially making source code harder to read. By using spaces only we prevent the accidental mixing of tabs and spaces, and ensure Inko code always looks consistent.

Inko relies heavily on blocks, which can lead to lots of indentation levels. Using 4 spaces per indentation level would consume too much horizontal space, so we use 2 spaces instead.

Place opening curly braces on the same line as the expression that precedes them:

if foo {

}

This applies to all expressions, such as if, try, while, etc.

Naming

Constants use PascalCase, such as ByteArray and String:

class AddressFormatter {}

Methods, local variables, instance attributes, and arguments all use snake_case for naming, such as to_string and write_bytes:

Let constants

Constants defined using let use SCREAMING_SNAKE_CASE, such as DAY_OF_WEEK or NUMBER:

let FIRST_DAY_OF_WEEK = 'Monday'

Argument names

Arguments should use human readable names, such as address. Avoid the use of abbreviations such as num instead of number. Every argument is a keyword argument, and the use of abbreviations can make it harder for a reader to figure out what the meaning of an argument is.

Predicates

When defining a method that returns a Boolean, end the method name with a ?:

fn allowed? -> Boolean {
  # ...
}

This removes the need for prefixing your method names with is_, such as is_allowed.

Traits

Traits should be a given a clear name such as ToArray or Index. Don't use the pattern of [verb]-ble such as Enumerable or Iterable.

Conversion methods

Methods that convert one type into another should be prefixed with to_, followed by a short name of the type. Examples include to_array, to_string, to_coordinates, etc. If a value is moved into another type, use into_ as the prefix instead.

Defining methods

If a method does not take any arguments, leave out the parentheses:

fn example {}

If a method definition does not fit on a single line, place every argument on a separate line, followed by a comma. The last argument should also be followed by a comma:

fn example(
  foo: A,
  bar: B,
) {

}

If a throw or return type is given, place them on the same line as the closing parenthesis, if possible:

fn example(
  foo: A,
  bar: B,
) !! ErrorType -> ReturnType {

}

If this doesn't fit, place both types on their own line, at the same indentation level as the closing parenthesis:

fn example(
  foo: A,
  bar: B,
)
!! ErrorType
-> ReturnType {

}

In all cases it's best to avoid code like this.

Type arguments should be placed on the same line as the method name.

fn example[A, B](foo: A, bar: B) {

}

If this doesn't fit, the same rules apply as used for regular arguments:

fn example[
  A,
  B,
](foo: A, bar: B) {

}

Again, such code is best avoided, as it can be a bit hard to read.

Parentheses

Inko allows you to leave out the parentheses when a method doesn't take any arguments, or when only a single argument is provided and it's a closure.

When calling a method without arguments, leave out the parentheses:

[10, 20, 30].pop

If the only argument is a closure, leave out the parentheses:

[10, 20, 30].each fn (number) {
  # ...
}

If there are multiple arguments, and the last one is a block, use parentheses and place the closure outside them:

t.test('This is a test') fn (t) {

}

When the number of arguments don't fit on a single line, place every argument on their own line like so:

some_object.some_message_name(
  10,
  20,
  30,
)

When spreading arguments across multiple lines, end the last argument with a comma:

some_object.some_message_name(
  10,
  20,
  30,
)

By using a trailing comma, adding a new argument arguments is easier as you don't need to first add a comma to the current last argument, before adding a new argument. When removing lines this also leads to smaller diffs.

Message chains

When chaining multiple messages together that don't fit on a single line, place every message on a separate line:

foo
  .bar
  .baz

Named arguments

The use of named arguments is recommended whenever this enhances the readability of code. Take the following code for example:

'hello'.slice(0, 2)

Looking at this code, it's not clear what the values 0 and 2 are used for. Using keyword arguments this becomes obvious:

'hello'.slice(start: 0, length: 2)

When passing variables with the same name as the arguments, you can leave out named arguments:

# This is redundant.
'hello'.slice(start: start, length: length)

# This is fine.
'hello'.slice(start, length)

Comments

Comments should be used to describe intent, provide examples, and explain certain decisions that might not be obvious to the reader. Comments should not be used to explain what the code does in its literal sense.

When documenting a type, constant, or method, the first line of the comment should be a short summary. This summary should be about one sentence long and describe the purpose of the item. For example:

# A Person can be used for storing details of a single person, such as their
# name and address.
class Person {

}

When documenting a module, start the comment on the first line of the module, before any imports:

# The documentation of the module goes here.
import std::stdio::STDOUT

Imports

Imports should be placed at the top of a module, in alphabetical order:

import std::fs::file
import std::stdio::stdout

The symbols imported from a module should also be listed in alphabetical order. If self is imported, it should come first:

import std::fs::file::(self, ReadOnlyFile)

Class literals

Constructing an instance of a class uses the following syntax:

TypeName { @attribute = 'value' }

When constructing an instance, place the expression on a single line if it fits:

Person { @name = 'Alice', @age = 32 }

If it doesn't fit, put every attribute assignment on a separate line:

Person {
  @name = 'Alice',
  @age = 32
}

If a class doesn't define any attributes, construct your instance as follows:

Person {}