A rendered version of this document can be found here.
See the neighbouring readme for an explanation of the
print!
and check!
commands used in the following example.
Koto programs contain a series of expressions that are evaluated by Koto's runtime.
As an example, this simple script prints a friendly greeting.
name = 'World'
print 'Hello, {name}!'
Single-line comments start with a #
.
# This is a comment, everything until the end of the line is ignored.
Multi-line comments start with #-
and end with -#
.
#-
This is a
multi-line
comment.
-#
Numbers and arithmetic are expressed in a familiar way.
print! 1
check! 1
print! 1 + 1
check! 2
print! -1 - 10
check! -11
print! 3 * 4
check! 12
print! 9 / 2
check! 4.5
print! 12 % 5
check! 2
Arithmetic operations follow the conventional order of precedence. Parentheses can be used to group expressions as needed.
# Without parentheses, multiplication is performed before addition
print! 1 + 2 * 3 + 4
check! 11
# With parentheses, the additions are performed first
print! (1 + 2) * (3 + 4)
check! 21
Booleans are declared with the true
and false
keywords, and combined using
the and
and or
operators.
print! true and false
check! false
print! true or false
check! true
Booleans can be negated with the not
operator.
print! not true
check! false
print! not false
check! true
Values can be compared for equality with the ==
and !=
operators.
print! 1 + 1 == 2
check! true
print! 99 != 100
check! true
The null
keyword is used to declare a value of type Null
,
which indicates the absence of a value.
print! null
check! null
In boolean contexts (such as logical operations), null
is treated as being
equivalent to false
. Every other value in Koto evaluates as true
.
print! not null
check! true
print! null or 42
check! 42
Values are assigned to named identifiers with =
, and can be freely reassigned.
Named values like this are known as variables.
# Assign the value `42` to `x`
x = 42
print! x
check! 42
# Replace the existing value of `x`
x = true
print! x
check! true
Compound assignment operators are also available.
For example, x *= y
is a simpler way of writing x = x * y
.
a = 100
print! a += 11
check! 111
print! a
check! 111
print! a *= 10
check! 1110
print! a
check! 1110
The debug
keyword allows you to quickly display a value while working on a
program.
It prints the result of an expression, prefixed with its line number and the original expression as a string.
x = 10 + 20
debug x / 10
check! [2] x / 10: 3.0
When using debug
, the displayed value is also the result of the expression,
which can be useful if you want to quickly get feedback during development.
x = debug 2 + 2
check! [1] 2 + 2: 4
print! x
check! 4
Lists in Koto are created with []
square brackets and can contain a mix of
different value types.
Access list elements by index using square brackets, starting from 0
.
x = [99, null, true]
print! x[0]
check! 99
print! x[1]
check! null
x[2] = false
print! x[2]
check! false
Once a list has been created, its underlying data is shared between other instances of the same list. Changes to one instance of the list are reflected in the other.
# Assign a list to x
x = [10, 20, 30]
# Assign another instance of the list to y
y = x
# Modify the list through y
y[1] = 99
# The change to y is also reflected in x
print! x
check! [10, 99, 30]
The +
operator allows lists to be joined together, creating a new list that
contains their concatenated elements.
a = [98, 99, 100]
b = a + [1, 2, 3]
print! b
check! [98, 99, 100, 1, 2, 3]
Tuples in Koto are similiar to lists, but are designed for sequences of values that have a fixed structure.
Unlike lists, tuples can't be resized after creation, and values that are contained in the tuple can't be replaced.
Tuples are declared with a series of expressions separated by commas.
x = 100, true, -1
print! x
check! (100, true, -1)
Parentheses can be used for grouping to avoid ambiguity.
print! (1, 2, 3), (4, 5, 6)
check! ((1, 2, 3), (4, 5, 6))
You can access tuple elements by index using square brackets, starting from 0
.
print! x = false, 10
check! (false, 10)
print! x[0]
check! false
print! x[1]
check! 10
print! y = true, 20
check! (true, 20)
print! x, y
check! ((false, 10), (true, 20))
The +
operator allows tuples to be joined together,
creating a new tuple containing their concatenated elements.
a = 1, 2, 3
b = a + (4, 5, 6)
print! b
check! (1, 2, 3, 4, 5, 6)
An empty pair of parentheses in Koto resolves to null
.
If an empty tuple is needed then use a single ,
inside parentheses.
# An empty pair of parentheses resolves to null
print! ()
check! null
# A comma inside parentheses creates a tuple
print! (,)
check! ()
While tuples have a fixed structure and its contained elements can't be replaced, mutable value types (like lists) can be modified while they're contained in tuples.
# A Tuple containing two lists
x = ([1, 2, 3], [4, 5, 6])
# Modify the second list in the tuple
x[1][0] = 99
print! x
check! ([1, 2, 3], [99, 5, 6])
Strings in Koto contain a sequence of UTF-8 encoded characters,
and can be declared using '
or "
quotes.
print! 'Hello, World!'
check! Hello, World!
print! "Welcome to Koto 👋"
check! Welcome to Koto 👋
Strings can start on one line and finish on another.
print! 'This is a string
that spans
several lines.'
check! This is a string
check! that spans
check! several lines.
Strings can be joined together with the +
operator.
print! 'a' + 'Bc' + 'Def'
check! aBcDef
Variables can be easily included in a string by surrounding them with {}
curly
braces.
xyz = 123
print! 'The value of xyz is {xyz}'
check! The value of xyz is 123
Including variables in a string this way is known as string interpolation.
Simple expressions can also be interpolated using the same syntax.
print! '2 plus 3 is {2 + 3}.'
check! 2 plus 3 is 5.
Strings can contain the following escape codes to define special characters,
all of which start with a \
.
\n
: Newline\r
: Carriage Return\t
: Tab\'
: Single quote\"
: Double quote\\
: Backslash\{
: Interpolation start\u{NNNNNN}
: Unicode character- Up to 6 hexadecimal digits can be included within the
{}
braces. The maximum value is\u{10ffff}
.
- Up to 6 hexadecimal digits can be included within the
\xNN
: ASCII character- Exactly 2 hexadecimal digits follow the
\x
.
- Exactly 2 hexadecimal digits follow the
print! '\{\'\"}'
check! {'"}
print! 'Hi \u{1F44B}'
check! Hi 👋
The end of a line can be escaped with a \
, which will skip the
newline and any leading whitespace on the next line.
foo = "This string \
doesn't contain \
newlines."
print! foo
check! This string doesn't contain newlines.
Both single '
and double "
quotes are valid for defining strings in Koto
and can be used interchangeably.
A practical reason to choose one over the other is that the alternate quote type can be used in a string without needing to use escape characters.
print 'This string has to escape its \'single quotes\'.'
check! This string has to escape its 'single quotes'.
print "This string contains unescaped 'single quotes'."
check! This string contains unescaped 'single quotes'.
Individual bytes of a string can be accessed via indexing with []
braces.
print! 'abcdef'[3]
check! d
print! 'xyz'[1..]
check! yz
Care must be taken when using indexing with strings that could contain
non-ASCII data.
If the indexed bytes would produce invalid UTF-8 data then an
error will be thrown. To access unicode characters see string.chars
.
When a string contains a lot of special characters, it can be preferable to use a raw string.
Raw strings ignore escape characters and interpolated expressions, providing the raw contents of the string between its delimiters.
Raw strings use single or double quotes as the delimiter, prefixed with an r
.
print r'This string contains special characters: {foo}\n\t.'
check! This string contains special characters: {foo}\n\t.
For more complex string contents, the delimiter can be extended using up to 255
#
characters after the r
prefix,
print r#'This string contains "both" 'quote' types.'#
check! This string contains "both" 'quote' types.
print r##'This string also includes a '#' symbol.'##
check! This string also includes a '#' symbol.
Functions in Koto are created using a pair of vertical bars (||
),
with the function's arguments listed between the bars.
The body of the function follows the vertical bars.
hi = || 'Hello!'
add = |x, y| x + y
Functions are called with arguments contained in ()
parentheses.
The body of the function is evaluated and the result is returned to the caller.
hi = || 'Hello!'
print! hi()
check! Hello!
add = |x, y| x + y
print! add(50, 5)
check! 55
A function's body can be an indented block, where the last expression in the body is evaluated as the function's result.
f = |x, y, z|
x *= 100
y *= 10
x + y + z
print! f(2, 3, 4)
check! 234
The parentheses for arguments when calling a function are optional and can be ommitted in simple expressions.
square = |x| x * x
print! square 8
check! 64
add = |x, y| x + y
print! add 2, 3
check! 5
# Equivalent to square(add(2, 3))
print! square add 2, 3
check! 25
Something to watch out for is that whitespace is important in Koto, and because
of optional parentheses, f(1, 2)
is not the same as f (1, 2)
. The former
is parsed as a call to f
with two arguments, whereas the latter is a call to
f
with a tuple as the single argument.
When the function should be exited early, the return
keyword can be used.
f = |n|
return 42
# This expression won't be reached
n * n
print! f -1
check! 42
If a value isn't provided to return
, then the returned value is null
.
f = |n|
return
n * n
print! f 123
check! null
The arrow operator (->
) can be used to pass the result of one function to
another, working from left to right. This is known as function piping,
and can aid readability when working with a long chain of function calls.
add = |x, y| x + y
multiply = |x, y| x * y
square = |x| x * x
# Chained function calls can be a bit hard to follow for the reader.
print! x = multiply 2, square add 1, 3
check! 32
# Parentheses don't help all that much...
print! x = multiply(2, square(add(1, 3)))
check! 32
# Piping allows for a left-to-right flow of results.
print! x = add(1, 3) -> square -> multiply 2
check! 32
# Call chains can also be broken across lines.
print! x = add 1, 3
-> square
-> multiply 2
check! 32
Maps in Koto are containers that contain a series of entries with keys that correspond to associated values.
The .
dot operator returns the value associated with a particular key.
Maps can be created using inline syntax with {}
braces:
m = {apples: 42, oranges: 99, lemons: 63}
# Get the value associated with the `oranges` key
print! m.oranges
check! 99
...or using block syntax with indented entries:
m =
apples: 42
oranges: 99
lemons: 63
print! m.apples
check! 42
Once a map has been created, its underlying data is shared between other instances of the same map. Changes to one instance are reflected in the other.
# Create a map and assign it to `a`.
a = {foo: 99}
print! a.foo
check! 99
# Assign a new instance of the map to `z`.
z = a
# Modifying the data via `z` is reflected in `a`.
z.foo = 'Hi!'
print! a.foo
check! Hi!
A map's entries are maintained in a consistent order, representing the sequence in which its entries were added.
You can access map entries by index using square brackets, starting from 0
.
The entry is returned as a tuple containing the key and its associated value.
m = {apples: 42, oranges: 99, lemons: 63}
print! m[1]
check! ('oranges', 99)
Entries can also be replaced by assigning a key/value tuple to the entry's index.
m = {apples: 42, oranges: 99, lemons: 63}
m[1] = ('pears', 123)
print! m
check! {apples: 42, pears: 123, lemons: 63}
Koto supports a shorthand notation when creating maps with inline syntax. If a value isn't provided for a key, then Koto will look for a value in scope that matches the key's name, and if one is found then it will be used as that entry's value.
hi, bye = 'hi!', 'bye!'
print! m = {hi, x: 42, bye}
check! {hi: 'hi!', x: 42, bye: 'bye!'}
Maps can store any type of value, including functions, which provides a convenient way to group functions together.
m =
hello: |name| 'Hello, {name}!'
bye: |name| 'Bye, {name}!'
print! m.hello 'World'
check! Hello, World!
print! m.bye 'Friend'
check! Bye, Friend!
self
is a special identifier that refers to the instance of the container in
which the function is contained.
In maps, self
allows functions to access and modify data from the map,
enabling object-like behaviour.
m =
name: 'World'
say_hello: || 'Hello, {self.name}!'
print! m.say_hello()
check! Hello, World!
m.name = 'Friend'
print! m.say_hello()
check! Hello, Friend!
The +
operator allows maps to be joined together, creating a new map that
combines their entries.
a = {red: 100, blue: 150}
b = {green: 200, blue: 99}
c = a + b
print! c
check! {red: 100, blue: 99, green: 200}
Map keys are usually defined and accessed without quotes, but they are stored in the map as strings. Quotes can be used if a key needs to be defined that would be otherwise be disallowed by Koto syntax rules (e.g. a keyword, or using characters that aren't allowed in an identifier). Quoted keys also allow dynamic keys to be generated by using string interpolation.
x = 99
m =
'true': 42
'key{x}': x
print! m.'true'
check! 42
print! m.key99
check! 99
Map keys are typically strings, but any immutable value can be used as a map key by using the map.insert and map.get functions.
The immutable value types in Koto are strings, numbers,
booleans, ranges, and null
.
A tuple is also considered to be immutable when its contained elements are also immutable.
The Core Library provides a collection of fundamental functions and values for working with the Koto language, organized within modules.
# Get the size of a string
print! string.to_lowercase 'HELLO'
check! hello
# Return the first element of the list
print! list.first [99, -1, 3]
check! 99
Values in Koto automatically have access to their corresponding core modules
via .
access.
print! 'xyz'.to_uppercase()
check! XYZ
print! ['abc', 123].first()
check! abc
print! (7 / 2).round()
check! 4
print! {apples: 42, pears: 99}.contains_key 'apples'
check! true
The documentation for the Core library (along with this guide) are
available in the help
command of the Koto CLI.
Koto's prelude is a collection of core library items that are automatically
made available in a Koto script without the need for first calling import
.
The modules that make up the core library are all included by default in the prelude. The following functions are also added to the prelude by default:
print 'io.print is available without needing to be imported'
check! io.print is available without needing to be imported
Koto includes several ways of producing values that depend on conditions.
if
expressions come in two flavours; single-line:
x = 99
if x % 2 == 0 then print 'even' else print 'odd'
check! odd
...and multi-line using indented blocks:
x = 24
if x < 0
print 'negative'
else if x > 24
print 'no way!'
else
print 'ok'
check! ok
The result of an if
expression is the final expression in the branch that gets
executed.
x = if 1 + 1 == 2 then 3 else -1
print! x
check! 3
# Assign the result of the if expression to foo
foo = if x > 0
y = x * 10
y + 3
else
y = x * 100
y * y
print! foo
check! 33
switch
expressions can be used as a cleaner alternative to
if
/else if
/else
cascades.
fib = |n|
switch
n <= 0 then 0
n == 1 then 1
else (fib n - 1) + (fib n - 2)
print! fib 7
check! 13
match
expressions can be used to match a value against a series of patterns,
with the matched pattern causing a specific branch of code to be executed.
Patterns can be literals or identifiers. An identifier will accept any value,
so they're often used with if
conditions to refine the match.
print! match 40 + 2
0 then 'zero'
1 then 'one'
x if x < 10 then 'less than 10: {x}'
x if x < 50 then 'less than 50: {x}'
x then 'other: {x}'
check! less than 50: 42
The _
wildcard match can be used to match against any value
(when the matched value itself can be ignored),
and else
can be used for fallback branches.
fizz_buzz = |n|
match n % 3, n % 5
0, 0 then "Fizz Buzz"
0, _ then "Fizz"
_, 0 then "Buzz"
else n
print! (10, 11, 12, 13, 14, 15)
.each |n| fizz_buzz n
.to_tuple()
check! ('Buzz', 11, 'Fizz', 13, 14, 'Fizz Buzz')
List and tuple entries can be matched against by using parentheses,
with ...
available for capturing the rest of the sequence.
print! match ['a', 'b', 'c'].extend [1, 2, 3]
('a', 'b') then "A list containing 'a' and 'b'"
(1, ...) then "Starts with '1'"
(..., 'y', last) then "Ends with 'y' followed by '{last}'"
('a', x, others...) then
"Starts with 'a', followed by '{x}', then {size others} others"
unmatched then "other: {unmatched}"
check! Starts with 'a', followed by 'b', then 4 others
The ?
operator can be used to short-circuit expression chains where null
might be encountered as an intermediate value. The ?
checks the current value
in the expression chain and if null
is found then the chain is short-circuited
with null
given as the expression's result.
This makes it easier to check for null
when you want to avoid runtime errors.
info = {town: 'Hamburg', country: 'Germany'}
# `info` contains a value for 'town', which is then passed to to_uppercase():
print! info.get('town')?.to_uppercase()
check! HAMBURG
# `info` doesn't contain a value for 'state',
# so the `?` operator short-circuits the expression, resulting in `null`:
print! info.get('state')?.to_uppercase()
check! null
# Without the `?` operator an intermediate step is necessary:
country = info.get('country')
print! if country then country.to_uppercase()
check! GERMANY
Multiple ?
checks can be performed in an expression chain:
get_data = || {nested: {maybe_string: null}}
print! get_data()?
.get('nested')?
.get('maybe_string')?
.to_uppercase()
check! null
Koto includes several ways of evaluating expressions repeatedly in a loop.
for
loops are repeated for each element in a sequence,
such as a list or tuple.
for n in [10, 20, 30]
print n
check! 10
check! 20
check! 30
while
loops continue to repeat while a condition is true.
x = 0
while x < 5
x += 1
print! x
check! 5
until
loops continue to repeat until a condition is true.
z = [1, 2, 3]
until z.is_empty()
# Remove the last element of the list
print z.pop()
check! 3
check! 2
check! 1
continue
skips the remaining part of a loop's body and proceeds with the next repetition of the loop.
for n in (-2, -1, 1, 2)
# Skip over any values less than 0
if n < 0
continue
print n
check! 1
check! 2
Loops can be terminated with the break
keyword.
x = 0
while x < 100000
if x >= 3
# Break out of the loop when x is greater or equal to 3
break
x += 1
print! x
check! 3
A value can be provided to break
, which is then used as the result of the loop.
x = 0
y = while x < 100000
if x >= 3
# Break out of the loop, providing x + 100 as the loop's result
break x + 100
x += 1
print! y
check! 103
loop
creates a loop that will repeat indefinitely.
x = 0
y = loop
x += 1
# Stop looping when x is greater than 4
if x > 4
break x * x
print! y
check! 25
The elements of a sequence can be accessed sequentially with an iterator,
created using the .iter()
function.
An iterator yields values via .next()
until the end of the sequence is
reached, when null
is returned.
i = [10, 20].iter()
print! i.next()
check! IteratorOutput(10)
print! i.next()
check! IteratorOutput(20)
print! i.next()
check! null
The iterator
module contains iterator generators like
once
and repeat
that generate output values
lazily during iteration.
# Create an iterator that repeats ! twice
i = iterator.repeat('!', 2)
print! i.next()
check! IteratorOutput(!)
print! i.next()
check! IteratorOutput(!)
print! i.next()
check! null
The output of an iterator can be modified using adaptors from the
iterator
module.
# Create an iterator that keeps any value above 3
x = [1, 2, 3, 4, 5].keep |n| n > 3
print! x.next()
check! IteratorOutput(4)
print! x.next()
check! IteratorOutput(5)
print! x.next()
check! null
for
loops accept any iterable value as input, including adapted iterators.
for x in 'abacad'.keep |c| c != 'a'
print x
check! b
check! c
check! d
Iterator adaptors can be passed into other adaptors, creating iterator chains that act as data processing pipelines.
i = (1, 2, 3, 4, 5)
.skip 1
.each |n| n * 10
.keep |n| n <= 40
.intersperse '--'
for x in i
print x
check! 20
check! --
check! 30
check! --
check! 40
Iterators can also be consumed using functions like
.to_list()
and .to_tuple()
,
allowing the output of an iterator to be easily captured in a container.
print! [1, 2, 3]
.each |n| n * 2
.to_tuple()
check! (2, 4, 6)
print! (1, 2, 3, 4)
.keep |n| n % 2 == 0
.each |n| n * 11
.to_list()
check! [22, 44]
Multiple assignments can be performed in a single expression by separating the variable names with commas.
a, b = 10, 20
print! a, b
check! (10, 20)
If there's a single value being assigned, and the value is iterable, then it gets unpacked into the target variables.
my_tuple = 1, 2
x, y = my_tuple
print! y, x
check! (2, 1)
Unpacking works with any iterable value, including adapted iterators.
a, b, c = [1, 2, 3, 4, 5]
print! a, b, c
check! (1, 2, 3)
x, y, z = 'a-b-c'.split '-'
print! x, y, z
check! ('a', 'b', 'c')
If the value being unpacked doesn't contain enough values for the assignment,
then null
is assigned to any remaining variables.
a, b, c = [-1, -2]
print! a, b, c
check! (-1, -2, null)
x, y, z = 42
print! x, y, z
check! (42, null, null)
Unpacking can also be used in for
loops, which is particularly useful when
looping over the contents of a map.
my_map = {foo: 42, bar: 99}
for key, value in my_map
print key, value
check! ('foo', 42)
check! ('bar', 99)
Generators are iterators that are made by calling generator functions,
which are any functions that contain a yield
expression.
The generator is paused each time yield
is encountered,
waiting for the caller to continue execution.
my_first_generator = ||
yield 1
yield 2
x = my_first_generator()
print! x.next()
check! IteratorOutput(1)
print! x.next()
check! IteratorOutput(2)
print! x.next()
check! null
Generator functions can accept arguments like any other function, and each time they're called a new generator is created.
As with any other iterable value, the iterator
module's functions
are made available to generators.
make_generator = |x|
for y in (1, 2, 3)
yield x + y
print! make_generator(0).to_tuple()
check! (1, 2, 3)
print! make_generator(10)
.keep |n| n % 2 == 1
.to_list()
check! [11, 13]
Generators can also serve as iterator adaptors by modifying the output of another iterator.
Inserting a generator into the iterator
module makes it available
in any iterator chain.
# Make an iterator adaptor that yields every
# other value from the adapted iterator
iterator.every_other = ||
n = 0
# When the generator is created, self is initialized with the previous
# iterator in the chain, allowing its output to be adapted.
for output in self
# If n is even, then yield a value
if n % 2 == 0
yield output
n += 1
print! (1, 2, 3, 4, 5)
.each |n| n * 10
.every_other() # Skip over every other value in the iterator chain
.to_list()
check! [10, 30, 50]
Ranges of integers can be created with ..
or ..=
.
..
creates a non-inclusive range,
which defines a range up to but not including the end of the range.
# Create a range from 10 to 20, not including 20
print! r = 10..20
check! 10..20
print! r.start()
check! 10
print! r.end()
check! 20
print! r.contains 20
check! false
..=
creates an inclusive range, which includes the end of the range.
# Create a range from 10 to 20, including 20
print! r = 10..=20
check! 10..=20
print! r.contains 20
check! true
If a value is missing from either side of the range operator then an unbounded range is created.
# Create an unbounded range starting from 10
r = 10..
print! r.start()
check! 10
print! r.end()
check! null
# Create an unbounded range up to and including 100
r = ..=100
print! r.start()
check! null
print! r.end()
check! 100
Bounded ranges are declared as iterable,
so they can be used in for loops and with the iterator
module.
for x in 1..=3
print x
check! 1
check! 2
check! 3
print! (0..5).to_list()
check! [0, 1, 2, 3, 4]
Ranges can be used to create a slice of a container's data.
x = (10, 20, 30, 40, 50)
print! x[1..=3]
check! (20, 30, 40)
For immutable containers like tuples and strings, slices share the original value's data, with no copies being made.
For mutable containers like lists, creating a slice makes a copy of the sliced portion of the underlying data.
x = 'abcdef'
# No copies are made when a string is sliced
print! y = x[3..6]
check! def
a = [1, 2, 3]
# When a list is sliced, the sliced elements get copied into a new list
print! b = a[0..2]
check! [1, 2]
print! b[0] = 42
check! 42
print! a[0]
check! 1
When creating a slice with an unbounded range, if the start of the range if ommitted then the slice starts from the beginning of the container. If the end of the range is ommitted, then the slice includes all remaining elements in the container.
z = 'Hëllø'.to_tuple()
print! z[..2]
check! ('H', 'ë')
print! z[2..]
check! ('l', 'l', 'ø')
Koto is a primarily a dynamically typed language, however in more complex programs you might find it beneficial to add type checks.
These checks can help in catching errors earlier, and can also act as documentation for the reader.
One way to add type checks to your program is to use the
type
function, which returns a value's type as a string.
x = 123
assert_eq (type x), 'Number'
Checking types this way is rather verbose, so Koto offers type hints as a more ergonomic alternative.
You can declare variables with type hints using a let
expression.
If a value is assigned that doesn't match the declared type then an error will be thrown.
let x: String = 'hello'
print! x
check! hello
let a: Number, _, c: Bool = 123, x, true
print! a, c
check! (123, true)
Type hints can also be added to for
loop arguments.
The type will be checked on each iteration of the loop.
for i: Number, s: String in 'abc'.enumerate()
print i, s
check! (0, 'a')
check! (1, 'b')
check! (2, 'c')
Function arguments can also be given type hints, and the type of the
return value can be checked with the ->
operator.
f = |s: String| -> Tuple
s.to_tuple()
print! f 'abc'
check! ('a', 'b', 'c')
For generator functions, the ->
type hint is used to check
the generator's yield
expressions.
g = || -> Number
yield 1
yield 2
yield 3
print! g().to_tuple()
check! (1, 2, 3)
Type hints can be used in match
patterns to check the type of the a value.
Rather than throwing an error, if a type check fails then the next
match pattern will be attempted.
print! match 'abc'
x: Tuple then x
x: String then x.to_tuple()
check! ('a', 'b', 'c')
The Any
type will result in a successful check with any value.
print! let x: Any = 'hello'
check! hello
The Callable
type hint will accept functions, or any value that can behave
like a function.
let say_hello: Callable = || 'hello'
print! say_hello()
check! hello
The Indexable
type hint will accept any value that supports []
indexing.
add_first_two = |x: Indexable| x[0] + x[1]
print! add_first_two (100, 99, -1)
check! 199
The Iterable
type is useful when any iterable value can be accepted.
let a: Iterable, b: Iterable = [1, 2], 3..=5
print! a.chain(b).to_tuple()
check! (1, 2, 3, 4, 5)
Interpolated string expressions can be formatted using formatting options similar to Rust's.
Inside an interpolated expression, options are provided after a :
separator.
print! '{number.pi:𝜋^8.2}'
check! 𝜋𝜋3.14𝜋𝜋
A minimum width can be specified, ensuring that the formatted value takes up at least that many characters.
foo = "abcd"
print! '_{foo:8}_'
check! _abcd _
The minimum width can be prefixed with an alignment modifier:
<
- left-aligned^
- centered>
- right-aligned
foo = "abcd"
print! '_{foo:^8}_'
check! _ abcd _
All values are left-aligned if an alignment modifier isn't specified, except for numbers which are right-aligned by default.
x = 1.2
print! '_{x:8}_'
check! _ 1.2_
The alignment modifier can be prefixed with a character which will be used to
fill any empty space in the formatted string (the default character being
).
x = 1.2
print! '_{x:~<8}_'
check! _1.2~~~~~_
For numbers, the minimum width can be prefixed with 0
, which will pad the
number to the specified width with zeroes.
x = 1.2
print! '{x:06}'
check! 0001.2
A maximum width for the interpolated expression can be specified following a
.
character.
foo = "abcd"
print! '{foo:_^8.2}'
check! ___ab___
For numbers, the maximum width acts as a 'precision' value, or in other words, the number of decimal places that will be rendered for the number.
x = 1 / 3
print! '{x:.4}'
check! 0.3333
Functions in Koto have some advanced features that are worth exploring.
When a variable is accessed in a function that wasn't declared locally, then it gets captured by copying it into the function.
x = 1
my_function = |n|
# x is assigned outside the function,
# so it gets captured when the function is created.
n + x
# Reassigning x here doesn't modify the value
# of x that was captured when my_function was created.
x = 100
print! my_function 2
check! 3
This behavior is different to many other languages, where captures are often taken by reference rather than by copy.
It's also worth noting that captured variables will have the same starting value each time the function is called.
x = 99
f = ||
# Modifying x only happens with a local copy during a function call.
# The value of x at the start of the call matches when the value it had when
# it was captured.
x += 1
print! f(), f(), f()
check! (100, 100, 100)
To modify captured values, use a container (like a map) to hold on to mutable data.
data = {x: 99}
f = ||
# The data map gets captured by the function,
# and its contained values can be modified between calls.
data.x += 1
print! f(), f(), f()
check! (100, 101, 102)
When calling a function, any missing arguments will be replaced by null
.
f = |a, b, c|
print a, b, c
f 1
check! (1, null, null)
f 1, 2
check! (1, 2, null)
f 1, 2, 3
check! (1, 2, 3)
Missing arguments can be replaced with default values by using or
.
f = |a, b, c|
print a or -1, b or -2, c or -3
f 42
check! (42, -2, -3)
f 99, 100
check! (99, 100, -3)
or
will reject false
, so if false
would be a valid input then a
direct comparison against null
can be used instead.
f = |a|
print if a == null then -1 else a
f()
check! -1
f false
check! false
A variadic function can be created by appending ...
to the
last argument.
When the function is called any extra arguments will be collected into a tuple.
f = |a, b, others...|
print "a: {a}, b: {b}, others: {others}"
f 1, 2, 3, 4, 5
check! a: 1, b: 2, others: (3, 4, 5)
Functions that expect containers as arguments can unpack the contained elements directly in the argument declaration by using parentheses.
# A function that sums a container with three contained values
f = |(a, b, c)| a + b + c
x = [100, 10, 1]
print! f x
check! 111
Any container that supports indexing operations (like lists and tuples) with a matching number of elements will be unpacked, otherwise an error will be thrown.
Unpacked arguments can also be nested.
# A function that sums elements from nested containers
f = |((a, b), (c, d, e))|
a + b + c + d + e
x = ([1, 2], [3, 4, 5])
print! f x
check! 15
Ellipses can be used to unpack any number of elements at the start or end of a container.
f = |(..., last)| last * last
x = (1, 2, 3, 4)
print! f x
check! 16
A name can be added to ellipses to assign the unpacked elements.
f = |(first, others...)| first * others.sum()
x = (10, 1, 2, 3)
print! f x
check! 60
The wildcard _
can be used to ignore function arguments.
# A function that sums the first and third elements of a container
f = |(a, _, c)| a + c
print! f [100, 10, 1]
check! 101
If you would like to keep the name of the ignored value as a reminder,
then _
can be used as a prefix for an identifier. Identifiers starting with
_
can be written to, but can't be accessed.
my_map = {foo_a: 1, bar_a: 2, foo_b: 3, bar_b: 4}
print! my_map
.keep |(key, _value)| key.starts_with 'foo'
.to_tuple()
check! (('foo_a', 1), ('foo_b', 3))
Value types with custom behaviour can be defined in Koto through the concept of objects.
An object is any map that includes one or more metakeys
(keys prefixed with @
), that are stored in the object's metamap.
Whenever operations are performed on the object, the runtime checks its metamap
for corresponding metakeys.
In the following example, addition and subtraction operators are overridden for
a custom Foo
object:
# Declare a function that makes Foo objects
foo = |n|
data: n
# Overriding the addition operator
@+: |other|
# A new Foo is made using the result
# of adding the two data values together
foo self.data + other.data
# Overriding the subtraction operator
@-: |other|
foo self.data - other.data
# Overriding the multiply-assignment operator
@*=: |other|
self.data *= other.data
self
a = foo 10
b = foo 20
print! (a + b).data
check! 30
print! (a - b).data
check! -10
a *= b
print! a.data
check! 200
All of the binary arithmetic and logic operators (*
, <
, >=
, etc) can be
implemented following this pattern.
Additionally, the following metakeys can also be defined:
The @negate
metakey overrides the negation operator.
foo = |n|
data: n
@negate: || foo -self.data
x = -foo(100)
print! x.data
check! -100
The @size
metakey defines how the object should report its size,
while the @index
metakey defines what values should be returned when indexing is
performed on the object.
If @size
is implemented, then @index
should also be implemented.
The @index
implementation can support indexing by any input values that make
sense for your object type, but for argument unpacking to work correctly the
runtime expects that indexing should be supported for both single indices and also ranges.
foo = |data|
data: data
@size: || size self.data
@index: |index| self.data[index]
x = foo ('a', 'b', 'c')
print! size x
check! 3
print! x[1]
check! b
Implementing @size
and @index
allows an object to participate in argument unpacking.
foo = |data|
data: data
@size: || size self.data
@index: |index| self.data[index]
x = foo (10, 20, 30, 40, 50)
# Unpack the first two elements in the value passed to the function and multiply them
multiply_first_two = |(a, b, ...)| a * b
print! multiply_first_two x
check! 200
# Inspect the first element in the object
print! match x
(first, others...) then 'first: {first}, remaining: {size others}'
check! first: 10, remaining: 4
The @index_mut
metakey defines how the object should behave when index-assignment is used.
The given value should be a function that takes an index as the first argument, and the value to be assigned as the second argument.
foo = |data|
data: data
@index: |index| self.data[index]
@index_mut: |index, value| self.data[index] = value
x = foo ['a', 'b', 'c']
x[1] = 'hello'
print! x[1]
check! hello
The @call
metakey defines how the object should behave when its called as a
function.
foo = |n|
data: n
@call: ||
self.data *= 2
self.data
x = foo 2
print! x()
check! 4
print! x()
check! 8
The @iterator
metakey defines how iterators should be created when the object
is used in an iterable context.
When called, @iterator
should return an iterable value that will then be used
for iterator operations.
foo = |n|
# Return a generator that yields the three numbers following n
@iterator: ||
yield n + 1
yield n + 2
yield n + 3
print! (foo 0).to_tuple()
check! (1, 2, 3)
print! (foo 100).to_list()
check! [101, 102, 103]
Note that this key will be ignored if the object also implements @next
,
which implies that the object is already an iterator.
The @next
metakey allows for objects to behave as iterators.
Whenever the runtime needs to produce an iterator from an object, it will first
check the metamap for an implementation of @next
, before looking for
@iterator
.
The @next
function will be called repeatedly during iteration,
with the returned value being used as the iterator's output.
When the returned value is null
then the iterator will stop producing output.
foo = |start, end|
start: start
end: end
@next: ||
if self.start < self.end
result = self.start
self.start += 1
result
else
null
print! foo(10, 15).to_tuple()
check! (10, 11, 12, 13, 14)
The @next_back
metakey is used by
iterator.reversed
when producing a reversed
iterator.
The runtime will only look for @next_back
if @next
is implemented.
foo =
n: 0
@next: || self.n += 1
@next_back: || self.n -= 1
print! foo
.skip 3 # 0, 1, 2
.reversed()
.take 3 # 2, 1, 0
.to_tuple()
check! (2, 1, 0)
The @display
metakey defines how the object should be represented when
displaying the object as a string.
foo = |n|
data: n
@display: || 'Foo({self.data})'
print! foo 42
check! Foo(42)
x = foo -1
print! "The value of x is '{x}'"
check! The value of x is 'Foo(-1)'
The @type
metakey takes a string as a value which is used when checking the
value's type, e.g. with koto.type
foo = |n|
data: n
@type: "Foo"
print! koto.type (foo 42)
check! Foo
Objects can inherit properties and behavior from other values,
establishing a base value through the @base
metakey.
This allows objects to share common functionality while maintaining their own
unique attributes.
In the following example, two kinds of animals are created that share the
speak
function from their base value.
animal = |name|
name: name
speak: || '{self.noise}! My name is {self.name}!'
dog = |name|
@base: animal name
noise: 'Woof'
cat = |name|
@base: animal name
noise: 'Meow'
print! dog('Fido').speak()
check! Woof! My name is Fido!
print! cat('Smudge').speak()
check! Meow! My name is Smudge!
The @meta
metakey allows named metakeys to be added to the metamap.
Metakeys defined with @meta
are accessible via .
access,
similar to regular object keys
, but they don't appear as part of the object's
main data entries when treated as a regular map.
foo = |n|
data: n
@meta hello: "Hello!"
@meta get_info: ||
info = match self.data
0 then "zero"
n if n < 0 then "negative"
else "positive"
"{self.data} is {info}"
x = foo -1
print! x.hello
check! Hello!
print x.get_info()
check! -1 is negative
print map.keys(x).to_tuple()
check! ('data')
Metamaps can be shared between objects by using
Map.with_meta
, which helps to avoid inefficient
duplication when creating a lot of objects.
In the following example, behavior is overridden in a single metamap, which is then shared between object instances.
# Create an empty map for global values
global = {}
# Define a function that makes a Foo object
foo = |data|
# Make a new map that contains `data`,
# and then attach a shared copy of the metamap from foo_meta.
{data}.with_meta global.foo_meta
# Define some metakeys in foo_meta
global.foo_meta =
# Override the + operator
@+: |other| foo self.data + other.data
# Define how the object should be displayed
@display: || "Foo({self.data})"
print! (foo 10) + (foo 20)
check! Foo(30)
Errors can be thrown in the Koto runtime, which then cause the runtime to stop execution.
A try
/ catch
expression can be used to catch any thrown errors,
allowing execution to continue.
An optional finally
block can be used for cleanup actions that need to
performed whether or not an error was caught.
x = [1, 2, 3]
try
# Accessing an invalid index will throw an error
print x[100]
catch error
print "Caught an error"
finally
print "...and finally"
check! Caught an error
check! ...and finally
throw
can be used to explicity throw an error when an exceptional condition
has occurred.
throw
accepts strings or objects that implement @display
.
f = || throw "!Error!"
try
f()
catch error
print "Caught an error: '{error}'"
check! Caught an error: '!Error!'
Type hints can also be used in try
expressions to implement different
error handling logic depending on the type of error that has been thrown.
A series of catch
blocks can be added to the try
expression, each catching
an error that has a particular type.
The final catch
block needs to not have a type check so that it can catch
any errors that were missed by the other blocks.
f = || throw 'Throwing a String'
try
f()
catch n: Number
print 'An error occurred: {n}'
catch error: String
print error
catch other
print 'Some other error occurred: {other}'
check! Throwing a String
Koto includes a simple testing framework that help you to check that your code is behaving as you expect through automated checks.
The core library includes a collection of assertion functions in the
test
module,
which are included by default in the prelude.
try
assert 1 + 1 == 3
catch error
print 'An assertion failed'
check! An assertion failed
try
assert_eq 'hello', 'goodbye'
catch error
print 'An assertion failed'
check! An assertion failed
Tests can be organized by collecting @test
functions in a map.
The tests can then be run manually with test.run_tests
.
For automatic testing, see the description of exporting @test
functions in the
following section.
basic_tests =
@test add: || assert_eq 1 + 1, 2
@test subtract: || assert_eq 1 - 1, 0
test.run_tests basic_tests
For setup and cleanup operations shared across tests,
@pre_test
and @post_test
metakeys can be implemented.
@pre_test
will be run before each @test
, and @post_test
will be run after.
make_x = |n|
data: n
@+: |other| make_x self.data + other.data
@-: |other| make_x self.data - other.data
x_tests =
@pre_test: ||
self.x1 = make_x 100
self.x2 = make_x 200
@post_test: ||
print 'Test complete'
@test addition: ||
print 'Testing addition'
assert_eq self.x1 + self.x2, make_x 300
@test subtraction: ||
print 'Testing subtraction'
assert_eq self.x1 - self.x2, make_x -100
@test failing_test: ||
print 'About to fail'
assert false
try
test.run_tests x_tests
catch _
print 'A test failed'
check! Testing addition
check! Test complete
check! Testing subtraction
check! Test complete
check! About to fail
check! A test failed
Koto includes a module system that helps you to organize and re-use your code when your program grows too large for a single file.
Items from modules can be brought into the current scope using import
.
from list import last
from number import abs
x = [1, 2, 3]
print! last x
check! 3
print! abs -42
check! 42
Multiple items from a single module can be imported at the same time.
from tuple import contains, first, last
x = 'a', 'b', 'c'
print! first x
check! a
print! last x
check! c
print! contains x, 'b'
check! true
Imported items can be renamed using as
for clarity or to avoid conflicts.
from list import first as list_first
from tuple import first as tuple_first
print! list_first [1, 2]
check! 1
print! tuple_first (3, 2, 1)
check! 3
export
is used to add values to the current module's exports map.
Values can be assigned to and exported at the same time:
##################
# my_module.koto #
##################
export hello, goodbye = 'Hello', 'Goodbye'
export say_hello = |name| '{hello}, {name}!'
##################
# other.koto #
##################
from my_module import say_hello, goodbye
say_hello 'Koto'
check! 'Hello, Koto!'
print! goodbye
check! Goodbye
To add a type check to an exported value, use a let
expression:
export let foo: Number = -1
When exporting a lot of values, it can be convenient to use map syntax:
##################
# my_module.koto #
##################
# Define some local values
a, b, c = 1, 2, 3
# Inline maps allow for shorthand syntax
export { a, b, c, foo: 42 }
# Map blocks can also be used with export
export
bar: 99
baz: 'baz'
A module can export @test
functions, which will be automatically run after
the module has been compiled and initialized.
Additionally, a module can export a @main
function.
The @main
function will be called after the module has been compiled and
initialized, and after any exported @test
functions have been successfully run.
Note that because metakeys can't be assigned locally,
the use of export
is optional when adding entries to the module's metamap.
##################
# my_module.koto #
##################
export say_hello = |name| 'Hello, {name}!'
@main = || # Equivalent to export @main =
print '`my_module` initialized'
@test hello_world = ||
print 'Testing...'
assert_eq (say_hello 'World'), 'Hello, World!'
##################
# other.koto #
##################
from my_module import say_hello
check! Testing...
check! Successfully initialized `my_module`
say_hello 'Koto'
check! 'Hello, Koto!'
When looking for a module, import
will look for a .koto
file with a matching
name, or for a folder with a matching name that contains a main.koto
file.
e.g. When an import foo
expression is run, then a foo.koto
file will be
looked for in the same location as the current script,
and if foo.koto
isn't found then the runtime will look for foo/main.koto
.