Style guide

Table of contents

  1. Introduction
  2. Encoding
  3. Line endings
  4. Line length
  5. Indentation
  6. Literals
  7. Naming
    1. Let constants
    2. Argument names
    3. Predicates
    4. Traits
    5. Conversion methods
  8. Defining methods
  9. Parentheses
  10. Message chains
  11. Binary expressions
  12. Keyword arguments
  13. Comments
  14. Imports
  15. Modules
  16. Blocks
  17. Error handling
  18. Implementing traits

Introduction

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 should be encoded in UTF-8.

Line endings

Unix (\n) line endings should be used at all times.

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 it makes it less readable.

Indentation

Inko source code should be indented using 2 spaces per indentation level, not tabs. Tabs are displayed inconsistently across different mediums, potentially making source code harder to read. By using spaces only we also prevent the accidental mixing of tabs and spaces.

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

Opening curly braces are placed on the same line as the expression that precedes it:

# Good
[10, 20, 30].each do (number) {

}

# Bad
[10, 20, 30].each do (number)
{

}

Literals

Array and HashMap literals should not include whitespace after the open tag and before the closing tag:

# Good
[10, 20, 30]

%['a': 10, 'b': 20]

# Bad
[ 10, 20, 30 ]

%[ 'a': 10, 'b': 20 ]

When a literal does not fit on a single line, place every value on a separate line, and use a trailing comma for the last value:

# Good
[
  10,
  20,
  30,
]

%[
  'a': 10,
  'b': 20,
]

# Bad
[
  10,
  20,
  30
]

%[
  'a': 10,
  'b': 20
]

Naming

Constants use PascalCase for naming, such as ByteArray and String:

# Good
object AddressFormatter {}

# Bad
object Addressformatter {}

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

# Methods

# Good
def to_string {}

# Bad
def toString {}

# Arguments

# Good
def write_bytes(bytes) {}

# Bad: "val" is not a meaningful name.
def write_bytes(val) {}

# Variables

# Good
let home_address = 'Foo Street'
let @home_address = 'Foo Street'

# Bad
let homeAddress = 'Foo Street'
let @homeAddress = 'Foo Street'

Let constants

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

# Good
let FIRST_DAY_OF_WEEK = 'Monday'

# Bad
let FirstDayOfWeek = '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 ?:

# Good
def allowed? -> Boolean {
  # ...
}

# Bad
def 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.

Defining methods

Methods are defined using the def keyword. If a method does not take any arguments, leave out the parentheses:

# Good
def example {}

# Bad
def 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:

def example(
  foo,
  bar,
) {

}

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

def example(
  foo,
  bar,
) !! ErrorType -> ReturnType {

}

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

def example(
  foo,
  bar,
)
!! 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.

def example!(A, B)(foo: A, bar: B) {

}

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

def 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 omit parentheses when sending a message. When sending a message without arguments, leave out the parentheses:

# Good
[10, 20, 30].first

# Bad
[10, 20, 30].first()

When passing one or more arguments, include parentheses:

# Good
'hello'.slice(0, 1)

# Bad
'hello'.slice 0, 1

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

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

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

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

# Good
test.group('This is a test group') do (g) {

}

# Bad
test.group('This is a test group', do (g) {

})

# Also bad
test.group 'This is a test group', do (g) {

}

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:

# Good
some_object.some_message_name(
  10,
  20,
  30,
)

# Bad
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

Binary expressions

When sending a message to the result of a binary expression, place the message on the next line and indent it with two space:

# Bad
(10 > 5).if_true {
  # ...
}

# Good
10 > 5
  .if_true {
    # ...
  }

Inko will parse both examples the same way, but the second example saves us from having to wrap the expression in parentheses.

Keyword arguments

When passing a single argument, prefer the use of positional arguments:

# Good
[10, 20, 30].remove_at(0)

# Also fine, though a bit redundant.
[10, 20, 30].remove_at(index: 0)

When passing multiple arguments, use keyword arguments:

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

# Bad: we have no idea what our arguments mean.
'hello'.slice(0, 2)

Keyword arguments may be left out when using a DSL, such as the one provided by std::test, and it's clear enough what the meaning of the arguments are:

# Good
test.group 'This is the description of the group', do (g) {

}

# Bad: the use of keyword arguments is a bit redundant here.
test.group name: 'This is the description of the group', body: do (g) {

}

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 work is being performed.

When documenting a type, constant, or method, the first line of the comment should be a short summary. This summary should be roughly one sentence 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.
object Person {

}

For types and methods, use ## instead of #. Modules should be documented using #!:

#! This is the documentation of the entire module. Just like other comments, it
#! can span multiple lines as long as every line starts with a #!

## A Person can be used for storing details of a single person, such as their
## name and address.
object Person {
  def init(name: String) {
    ## The name of the person.
    let @name = name
  }
}

Imports

Imports should be placed at the top of a module, in alphabetical order unless a specific order is required. If this is the case, this requirement should be documented using a regular comment to prevent accidental reordering of the imports:

# Good
import std::fs::file
import std::stdio::stdout

# Bad: not in alphabetical order
import std::stdio::stdout
import std::fs::file

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

# Good
import std::fs::file::(self, FilePath)

# Bad
import std::fs::file::(FilePath, self)

Modules

When defining a module, items defined in it should come in the following order:

  1. Types and constants.
  2. Module methods.
  3. Code to run when the module is imported.

Example:

object Person {
  def init(name: String) {
    let @name = name
  }
}

def create_person(name: String) -> Person {
  Person.new(name)
}

let person = create_person('Alice')

You can deviate from this guideline if a different order is required.

Blocks

When a closure does not take any arguments, leave out the do keyword:

# Good
let block = { 10 }

# Bad: `do` is redundant
let block = do { 10 }

Lambdas always require the lambda keyword, otherwise they will be inferred as a closure.

The do and lambda keywords should be followed by a single space:

# Good
let block = do (number) { number }

# Bad
let block = do(number) { number }

The return type of a block should not be specified unless required otherwise:

# Good
[10, 20, 30].each do (number) {

}

# Bad: the compiler can just infer the return type for us.
[10, 20, 30].each do (number) -> Integer {
  10
}

When defining a block before using it, specify the argument types unless you want them to be dynamically typed:

# Good
let block = do (number: Integer) { number }

# Technically fine if we're OK with "number" being of type Dynamic, but the
# compiler won't protect us from using the argument in the wrong way.
let block = do (number) { number }

Error handling

If a try-else expression is simple enough, omit the use of curly braces:

try some_expression else do_something_else

If the try expression is complex, or the else body contains multiple expressions, use curly braces for both:

try {
  some_expression
} else {
  do_something_else
  do_more_work_here
}

In this case else is placed on the same line as the closing curly brace of the try expression.

If else argument goes on the same line as else:

try {
  some_expression
} else (error) {
  do_something_else
  do_more_work_here
}

Implementing traits

When defining an object, it is preferred to immediately implement any desired traits:

# Good
object Person impl ToString {}

# Bad
object Person {}

impl ToString for Person {}

If the object ... impl line is too long to fit on a single line, place every trait name on their own line like so:

object Person impl
  Foo,
  Bar,
  Baz {

}

When implementing a trait for a previously defined object, use the impl ... for syntax:

impl ToString for Person {

}