As you're reading this guide, you're encouraged to play around with the examples to get a feel for the language.
When you see a icon below an example, clicking it will open the example in the Koto Playground, where you can run the code and see what happens as you make changes.
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.
1
# -> 1
1 + 1
# -> 2
-1 - 10
# -> -11
3 * 4
# -> 12
9 / 2
# -> 4.5
12 % 5
# -> 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
1 + 2 * 3 + 4
# -> 11
# With parentheses, the additions are performed first
(1 + 2) * (3 + 4)
# -> 21
Booleans are declared with the true
and false
keywords, and combined using
the and
and or
operators.
true and false
# -> false
true or false
# -> true
Booleans can be negated with the
not
operator.
not true
# -> false
not false
# -> true
Values can be compared for equality with the
==
and !=
operators.
1 + 1 == 2
# -> true
99 != 100
# -> true
The null
keyword is used to declare a value of type Null
,
which indicates the absence of a value.
null
# -> null
In boolean contexts (such as logical operations), null
is treated as being
equivalent to false
. Every other value in Koto evaluates as true
.
not null
# -> true
null or 42
# -> 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
x
# -> 42
# Replace the existing value of `x`
x = true
x
# -> true
Compound assignment operators are also available. For example,
x *= y
is a simpler way of writing x = x * y
.
a = 100
a += 11
# -> 111
a
# -> 111
a *= 10
# -> 1110
a
# -> 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
# -> [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
# -> [1] 2 + 2: 4
x
# -> 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]
x[0]
# -> 99
x[1]
# -> null
x[2] = false
x[2]
# -> 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
x
# -> [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]
b
# -> [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
x
# -> (100, true, -1)
Parentheses can be used for grouping to avoid ambiguity.
(1, 2, 3), (4, 5, 6)
# -> ((1, 2, 3), (4, 5, 6))
Access tuple elements by index using square brackets, starting from
0
.
x = false, 10
# -> (false, 10)
x[0]
# -> false
x[1]
# -> 10
y = true, 20
# -> (true, 20)
x, y
# -> ((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)
b
# -> (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
()
# -> null
# A comma inside parentheses creates a tuple
(,)
# -> ()
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
x
# -> ([1, 2, 3], [99, 5, 6])
Strings in Koto contain a sequence of UTF-8 encoded characters,
and can be declared using '
or "
quotes.
'Hello, World!'
# -> Hello, World!
"Welcome to Koto 👋"
# -> Welcome to Koto 👋
Strings can start on one line and finish on another.
'This is a string
that spans
several lines.'
# -> This is a string
# -> that spans
# -> several lines.
Strings can be joined together with the
+
operator.
'a' + 'Bc' + 'Def'
# -> aBcDef
Variables can be easily included in a string by surrounding them with {}
curly
braces.
xyz = 123
'The value of xyz is {xyz}'
# -> 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.
'2 plus 3 is {2 + 3}.'
# -> 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
{}
braces.
The maximum value is \u{10ffff}
.\xNN
: ASCII character
\x
.'\{\'\"}'
# -> {'"}
'Hi \u{1F44B}'
# -> 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."
foo
# -> 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\'.'
# -> This string has to escape its 'single quotes'.
print "This string contains unescaped 'single quotes'."
# -> This string contains unescaped 'single quotes'.
Individual bytes of a string can be accessed via indexing with []
braces.
'abcdef'[3]
# -> d
'xyz'[1..]
# -> 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.'
# -> 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.'#
# -> This string contains "both" 'quote' types.
print r##'This string also includes a '#' symbol.'##
# -> 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!'
hi()
# -> Hello!
add = |x, y| x + y
add(50, 5)
# -> 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
f(2, 3, 4)
# -> 234
The parentheses for arguments when calling a function are optional and can be ommitted in simple expressions.
square = |x| x * x
square 8
# -> 64
add = |x, y| x + y
add 2, 3
# -> 5
# Equivalent to square(add(2, 3))
square add 2, 3
# -> 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
f -1
# -> 42
If a value isn't provided to
return
, then the returned value is null
.
f = |n|
return
n * n
f 123
# -> null
The pipe 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.
x = multiply 2, square add 1, 3
x
# -> 32
# Parentheses don't help all that much...
x = multiply(2, square(add(1, 3)))
x
# -> 32
# Piping allows for a left-to-right flow of results.
x = add(1, 3) >> square >> multiply 2
x
# -> 32
# Call chains can also be broken across lines.
x = add 1, 3
>> square
>> multiply 2
x
# -> 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
m.oranges
# -> 99
...or using block syntax with indented entries:
m =
apples: 42
oranges: 99
lemons: 63
m.apples
# -> 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}
a.foo
# -> 99
# Assign a new instance of the map to `z`.
z = a
# Modifying the data via `z` is reflected in `a`.
z.foo = 'Hi!'
a.foo
# -> Hi!
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.
bar = 'hi!'
m = {foo: 42, bar}
m.bar
# -> hi!
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}!'
m.hello 'World'
# -> Hello, World!
m.bye 'Friend'
# -> Bye, Friend!
self
is a special identifier that refers to the instance of the container in
which the function is contained.
self
allows functions to access and modify data from the map,
enabling object-like behaviour.
m =
name: 'World'
say_hello: || 'Hello, {self.name}!'
m.say_hello()
# -> Hello, World!
m.name = 'Friend'
m.say_hello()
# -> 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
c
# -> {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
m.'true'
# -> 42
m.key99
# -> 99
Although map keys are typically strings, other value types can be used as keys by using the map.insert and map.get functions.
Map keys are typically strings, but any immutable value can be used as a map key.
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
string.to_lowercase 'HELLO'
# -> hello
# Return the first element of the list
list.first [99, -1, 3]
# -> 99
Values in Koto automatically have access to their corresponding core modules via
.
access.
'xyz'.to_uppercase()
# -> XYZ
['abc', 123].first()
# -> abc
(7 / 2).round()
# -> 4
{apples: 42, pears: 99}.contains_key 'apples'
# -> 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'
# -> 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'
# -> odd
...and multi-line using indented blocks:
x = 24
if x < 0
print 'negative'
else if x > 24
print 'no way!'
else
print 'ok'
# -> 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
x, x
# -> (3, 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
foo, foo
# -> (33, 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)
fib 7
# -> 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.
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}'
# -> 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
(10, 11, 12, 13, 14, 15)
.each |n| fizz_buzz n
.to_tuple()
# -> ('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.
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}"
# -> Starts with 'a', followed by 'b', then 4 others
Koto includes several ways of evaluating expressions repeatedly in a loop.
while
loops continue to repeat while a condition is true.
x = 0
while x < 5
x += 1
x
# -> 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()
# -> 3
# -> 2
# -> 1
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
x
# -> 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
y
# -> 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
y
# -> 25
for
loops are repeated for each element in a sequence,
such as a list or tuple.
for n in [10, 20, 30]
print n
# -> 10
# -> 20
# -> 30
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
# -> 1
# -> 2
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()
i.next()
# -> IteratorOutput(10)
i.next()
# -> IteratorOutput(20)
i.next()
# -> 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)
i.next()
# -> IteratorOutput(!)
i.next()
# -> IteratorOutput(!)
i.next()
# -> 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
x.next()
# -> IteratorOutput(4)
x.next()
# -> IteratorOutput(5)
x.next()
# -> null
for
for
loops accept any iterable value as input, including adapted iterators.
for x in 'abacad'.keep |c| c != 'a'
print x
# -> b
# -> c
# -> 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
# -> 20
# -> --
# -> 30
# -> --
# -> 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.
[1, 2, 3]
.each |n| n * 2
.to_tuple()
# -> (2, 4, 6)
(1, 2, 3, 4)
.keep |n| n % 2 == 0
.each |n| n * 11
.to_list()
# -> [22, 44]
Multiple assignments can be performed in a single expression by separating the variable names with commas.
a, b = 10, 20
a, b
# -> (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
y, x
# -> (2, 1)
Unpacking works with any iterable value, including adapted iterators.
a, b, c = [1, 2, 3, 4, 5]
a, b, c
# -> (1, 2, 3)
x, y, z = 'a-b-c'.split '-'
x, y, z
# -> ('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]
a, b, c
# -> (-1, -2, null)
x, y, z = 42
x, y, z
# -> (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
# -> ('foo', 42)
# -> ('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()
x.next()
# -> IteratorOutput(1)
x.next()
# -> IteratorOutput(2)
x.next()
# -> 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
make_generator(0).to_tuple()
# -> (1, 2, 3)
make_generator(10)
.keep |n| n % 2 == 1
.to_list()
# -> [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
(1, 2, 3, 4, 5)
.each |n| n * 10
.every_other() # Skip over every other value in the iterator chain
.to_list()
# -> [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
r = 10..20
# -> 10..20
r.start()
# -> 10
r.end()
# -> 20
r.contains 20
# -> false
..=
creates an inclusive range, which includes the end of the range.
# Create a range from 10 to 20, including 20
r = 10..=20
# -> 10..=20
r.contains 20
# -> 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..
r.start()
# -> 10
r.end()
# -> null
# Create an unbounded range up to and including 100
r = ..=100
r.start()
# -> null
r.end()
# -> 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
# -> 1
# -> 2
# -> 3
(0..5).to_list()
# -> [0, 1, 2, 3, 4]
Ranges can be used to create a slice of a container's data.
x = (10, 20, 30, 40, 50)
x[1..=3]
# -> (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
y = x[3..6]
# -> def
a = [1, 2, 3]
# When a list is sliced, the sliced elements get copied into a new list
b = a[0..2]
# -> [1, 2]
b[0] = 42
# -> 42
a[0]
# -> 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()
z[..2]
# -> ('H', 'ë')
z[2..]
# -> ('l', 'l', 'ø')
Interpolated string expressions can be formatted using formatting options similar to Rust's.
Inside an interpolated expression, options are provided after a :
separator.
'{number.pi:𝜋^8.2}'
# -> 𝜋𝜋3.14𝜋𝜋
A minimum width can be specified, ensuring that the formatted value takes up at least that many characters.
foo = "abcd"
'_{foo:8}_'
# -> _abcd _
The minimum width can be prefixed with an alignment modifier:
<
- left-aligned^
- centered>
- right-alignedfoo = "abcd"
'_{foo:^8}_'
# -> _ 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
'_{x:8}_'
# -> _ 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
'_{x:~<8}_'
# -> _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
'{x:06}'
# -> 0001.2
A maximum width for the interpolated expression can be specified following a
.
character.
foo = "abcd"
'{foo:_^8.2}'
# -> ___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
'{x:.4}'
# -> 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
my_function 2
# -> 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
f(), f(), f()
# -> (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
f(), f(), f()
# -> (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
# -> (1, null, null)
f 1, 2
# -> (1, 2, null)
f 1, 2, 3
# -> (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
# -> (42, -2, -3)
f 99, 100
# -> (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()
# -> -1
f false
# -> 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
# -> 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]
f x
# -> 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])
f x
# -> 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)
f x
# -> 16
A name can be added to ellipses to assign the unpacked elements.
f = |(first, others...)| first * others.sum()
x = (10, 1, 2, 3)
f x
# -> 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
f [100, 10, 1]
# -> 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}
my_map
.keep |(key, _value)| key.starts_with 'foo'
.to_tuple()
# -> (('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
(a + b).data
# -> 30
(a - b).data
# -> -10
a *= b
a.data
# -> 200
All of the binary arithmetic and logic operators (*
, <
, >=
, etc) can be
implemented following this pattern.
Additionally, the following metakeys can also be defined:
@negate
The @negate
metakey overrides the negation operator.
foo = |n|
data: n
@negate: || foo -self.data
x = -foo(100)
x.data
# -> -100
@size
and @[]
The @size
metakey defines how the object should report its size,
while the @[]
metakey defines what values should be returned when indexing is
performed on the object.
If @size
is implemented, then @[]
should also be implemented.
The @[]
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 by both single indices and ranges should be
supported.
foo = |data|
data: data
@size: || size self.data
@[]: |index| self.data[index]
x = foo (100, 200, 300)
size x
# -> 3
x[1]
# -> 200
# Unpack the first two elements in the argument and multiply them
multiply_first_two = |(a, b, ...)| a * b
multiply_first_two x
# -> 20000
# Inspect the first element in the object
match x
(first, others...) then 'first: {first}, remaining: {size others}'
# -> first: 100, remaining: 2
@||
The @||
metakey defines how the object should behave when its called as a
function.
foo = |n|
data: n
@||: ||
self.data *= 2
self.data
x = foo 2
x()
# -> 4
x()
# -> 8
@iterator
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
(foo 0).to_tuple()
# -> (1, 2, 3)
(foo 100).to_list()
# -> [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.
@next
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
foo(10, 15).to_tuple()
# -> (10, 11, 12, 13, 14)
@next_back
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
foo
.skip 3 # 0, 1, 2
.reversed()
.take 3 # 2, 1, 0
.to_tuple()
# -> (2, 1, 0)
@display
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})'
foo 42
# -> Foo(42)
x = foo -1
"The value of x is '{x}'"
# -> The value of x is 'Foo(-1)'
@type
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"
koto.type (foo 42)
# -> Foo
@base
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'
dog('Fido').speak()
# -> Woof! My name is Fido!
cat('Smudge').speak()
# -> Meow! My name is Smudge!
@meta
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
x.hello
# -> Hello!
print x.get_info()
# -> -1 is negative
print map.keys(x).to_tuple()
# -> ('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})"
(foo 10) + (foo 20)
# -> 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"
# -> Caught an error
# -> ...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}'"
# -> Caught an error: '!Error!'
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'
# -> An assertion failed
try
assert_eq 'hello', 'goodbye'
catch error
print 'An assertion failed'
# -> An assertion failed
Tests can be organized by collecting @test
functions in an object.
The tests can then be run manually with
test.run_tests
.
For automatic testing, see the description of exporting @tests
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'
# -> Testing addition
# -> Test complete
# -> Testing subtraction
# -> Test complete
# -> About to fail
# -> 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.
import
Items from modules can be brought into the current scope using import
.
from list import last
from number import abs
x = [1, 2, 3]
last x
# -> 3
abs -42
# -> 42
Multiple items from a single module can be imported at the same time.
from tuple import contains, first, last
x = 'a', 'b', 'c'
first x
# -> a
last x
# -> c
contains x, 'b'
# -> 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
list_first [1, 2]
# -> 1
tuple_first (3, 2, 1)
# -> 3
export
export
is used to add values to the current module's exports map.
Single values can be assigned to and exported at the same time:
##################
# my_module.koto #
##################
export say_hello = |name| 'Hello, {name}!'
##################
##################
from my_module import say_hello
say_hello 'Koto'
# -> 'Hello, Koto!'
When exporting multiple 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'
@tests
and @main
A module can export a @tests
object containing @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 exported @tests
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'
@tests =
@test hello_world: ||
print 'Testing...'
assert_eq (say_hello 'World'), 'Hello, World!'
##################
##################
from my_module import say_hello
# -> Testing...
# -> Successfully initialized `my_module`
say_hello 'Koto'
# -> '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
.