Introduction
Komodo is a programming language to test ideas in a fast and easy way. It is ideal for problems with basic discrete structures, like numbers and words. Komodo tries to make operating with these entities as easy as possible while minimizing the amount of code needed to achieve a successful implementation. Komodo is a scripting language regarding its flexibility, but it is a pretty specific language with a lot of assumptions about how things should be. Komodo is an experimental language, and some of its features are there because I had a personal preference for them.
Komodo draws inspiration from Picat, SETL and the Wolfram Language (I blame my university homework for the last one). It definitely has influence from (wait for it) Python and JavaScript, which are languages that I used for several years before learning more programming paradigms. Komodo is in part a result of my frustrations with these languages.
Komodo is unstable (I just add what I want) and in active development. Here is the source code.
This document is a guide to show how to use Komodo.
Tip: You can search for terms in this document using the 🔍 icon at the top of the page, or pressing the
s
key.
Installation
Currently, you can install Komodo downloading binaries or building from source.
Download binaries
There are pre-compiled binaries for x86-64 Linux here. If this is not your architecture, you will have to build from source.
You can easily download Komodo with these commands:
wget https://github.com/danilopedraza/komodo/releases/download/v0.1.0/komodo
chmod +x komodo
mv komodo $HOME/.local/bin
Build from source
You can build and install Komodo using the Rust infrastructure. Komodo is not published in crates.io, so you will have to clone the GitHub repository. To run the following commands you must have Rust installed.
git clone https://github.com/danilopedraza/komodo.git
cd komodo/core
cargo build --release
chmod +x target/release/komodo
cp target/release/komodo $HOME/.local/bin
Now you should be able to use Komodo from the command line with the komodo
command.
Alternatively, you can try Komodo by running cargo run --all-features
in the komodo/core
directory.
Using Komodo
Using the REPL
To start the REPL, you just have to type komodo
in the terminal. It will show you something like this:
>>>
You can type a Komodo expression, and it will evaluate it. Let's try something simple:
>>> "Hello, world!"
"Hello, world!"
You can do arithmetic:
>>> 2 + 2
4
You can store values with a let
expression:
>>> let a := 1 + 0.5
1.5
>>> a + 0.5
2.0
Executing files
The Komodo interpreter can read files provided by you. You just have to type komodo <path>
. Create a file called fib.komodo
and put the following in it:
let fib(0) := 0
let fib(1) := 1
let fib(n) := fib(n - 1) + fib(n - 2)
println(fib(10)) # 55
Now, in the same directory where the file is, type komodo fib.komodo
.
VSCode Extension
There is a VSCode extension available for Komodo. Right now, it just adds syntax highlighting. It is published in the VS Marketplace and OpenVSX, so you can easily search it in VSCode and VSCodium.
Code examples
If you like to learn by doing or looking at examples, you can find some in the code repository and Rosetta Code.
Programming with Komodo
In this chapter, we will see the basic properties of Komodo and how to use them to build programs.
Core elements
What is Komodo good at?
The aim of Komodo is to easily express experimental solutions to problems.
What does experimental mean here? They are probably not complete nor correct solutions. When people solve a problem, they usually get to the solution iteratively. They start by some intuition-based guess, and tweak until it satisfies all of the problem's constraints. Komodo wants to make this process easier, making assumptions that are reasonable and offering tools that are convenient.
What is Komodo bad at?
Everything else. We also have to confess that, currently, Komodo is painfully slow. This will probably hurt the user experience and limit the use cases of the language.
Expression-oriented
Komodo is an expression-oriented language, which means that almost everything that you write returns something.
For example, languages like Python or JavaScript have a clear distinction between statements (pieces of code that declare something) and expressions (pieces of code that describe a value). JavaScript has statements like for
, if
, and let
, and expressions like 5
or () => null
. You can't do something like let a = (let b = 5);
, because the value in the right does not describe a value, and the point of the let
statement is to save a value!
In Komodo, we think of this differently. Every piece of code that you write returns something. For example, the expression let a := 1
returns 1, and the expression let chr := 'x'
returns 'x'
.
Komodo does have some statements, but they are a few.
With pattern matching
Pattern matching is the main mechanism to write rules in Komodo. You can use it when declaring functions. For example, this code computes the 10th Fibonacci number:
let fib(0) := 0
let fib(1) := 1
let fib(n) := fib(n - 1) + fib(n - 2)
fib(10) # 55
This code gives the last element of a list:
let last([val]) := val
let last([first|tail]) := last(tail)
last([1, 2, 3]) # 3
The [first|tail]
expression represents a list whose first element is first
and tail
is a list with the rest. This syntax exists in languages like Prolog, Erlang, and Picat.
With weak typing
komodo does not enforce type rules, so you can pass any type to a function. This feature is motivated for two reasons:
-
Komodo does not have mechanisms like polymorphism. Weak typing (with pattern matching) allows to (kinda) solve the same problem that polymorphism solves.
-
It enriches pattern matching! And invites you, dear programmer, to use it.
Caching/memoization by default
You can save the result of certain function calls, to avoid repetitive calculations. This is very useful when writing recursive functions. Take the fibonacci example again:
let fib(0) := 0
let fib(1) := 1
let fib(n) := fib(n - 1) + fib(n - 2)
fib(10) # 55
The recursive call repeats a lot of calculations because is calling the same recursive function two times, and they will do the same (leading to exponential growth on the number of calls!). With large enough inputs (something like fib(50)
), the program will be unacceptably slow. We can fix this just by adding a word:
let fib(0) := 0
let fib(1) := 1
let memoize fib(n) := fib(n - 1) + fib(n - 2)
fib(10) # 55
Adding memoize
before defining a function will save the results computed from that call, so they can used later, when the function is called with the same arguments again.
Appendix
This chapter contains reference material you may find useful when learning and using Komodo.
Builtin types
Komodo has a few built-in types:
-
Integers: Signed, arbitrary precision integers. You can write them in decimal form, or the usual prefixed binary (
0x
), octal (0o
) or hex (0x
) form. -
Decimals: Signed, arbitrary precision decimal numbers. You can write them in decimal form with a dot.
-
Fractions: Signed, arbitrary precision fractions, made with Integers. You can write them like this:
1 // 2
. -
Characters: Old-fashioned ASCII characters. You can write them like this:
'x'
, wherex
is a representation of some ASCII value. (Support for escaped characters is pretty bad right now!) -
Strings: A bunch of characters, ordered. You can write them like this:
"Hello, world!"
.Note: Many programming languages use the built-in list type to define strings. Komodo does not do this. The string type is completely different and defined independently in the interpreter.
-
Lists: An ordered collection of anything. You can write them in two ways:
- By extension:
[1, 2, 4, 8]
- By comprehension:
[ 2**k : k in 0..4 ]
Both of these examples describe the same thing! I should also remind you that the elements of a list can be of different type. This is valid:
[1, '2', "3"]
.Note: You probably noticed the
0..4
expression. This is a range, and it behaves exactly like you expect: It goes from 0 to 3 (it always excludes the last number). - By extension:
-
Tuples: An ordered collection of anything. Wait... That's the same as lists! Yes. There are differences, though. You can't do operations with tuples just like you can with lists, and you can't compose them like we did above with lists. You can write them like this:
('1', 11, "111")
. -
Sets: An unordered and extendable collection of anything. You can write them like this:
{ 1, 2, 3 }
. -
Functions: Pieces of code that receive values and return a value. There are two ways of writing them:
- Named, with patterns:
let f(0) := 25 let f(n) := f(n - 1) + 1
- Anonymously:
n -> let x = n + 2 x*x
With this syntax, the last expression (i.e
x*x
) is returned as the result. Of course, this is an expression and you can save it to a value, or execute it in place like this:(x -> x * 2)(1)
.Note: You may have noticed that we used parenthesis to denote a series of steps. This is the only case where something that could be a tuple will be interpreted in another way.
-
Dictionaries: A collection of key-value pairs. There are no restrictions on the keys or the values. You can use anything at the same time! You can write them like this:
{"a" => 5, [] => 'b'}
.
Data structures
Apart from the primitive types, Komodo has three main data structures: Lists, Sets and Dictionaries.
List
Lists are ordered, possibly mutable and non-growable. They can be written exhaustively:
let evens := [0, 2, 4]
Or by comprehension:
let evens := [2*i for i in 0..3]
You can create a new list by putting a new element at the beggining of an existing list:
[5|[1, 2]] = [5, 1, 2]
They can be concatenated with each other:
[1, 2, 3] + [4, 5, 6] = [1, 2, 3, 4, 5, 6]
And multiplied:
[1, 3]*2 = [1, 3, 1, 3]
Set
Sets are unoredered, inmutable and non-growable. They can be written exhaustively:
let Z3 := {0, 1, 2}
Or by comprehension:
let Z3 := {i for i in 0..3}
They can be joined (with the addition operator):
{1, 2} + {1, 3} = {1, 2, 3}
Dictionary
Dictionaries are unordered, inmutable collections of key-value pairs. The pairs can be anything. They can be defined exhaustively:
let dict := {
5 => 7,
"1" => 1,
[] => 0
}
Control flow structures
Komodo includes some control flow structures from imperative languages.
if
These are expressions, and always are complete.
if 5 % 2 = 0 then
"5 is even"
else
"5 is odd"
for
Although these are expressions, they are not meant to return anything. They always return an empty tuple ()
.
for i in 0..5 do
let x := i*i
println(x)
Keywords
The following list contains all the keywords that Komodo uses. You can't name anything with them. Komodo is a small language, so it's not that bad of a problem to remember these.
as
: to define aliases of imported modulesdo
: part of thefor
loop syntaxelse
: part ofif
expressionsfalse
: Boolean false literalfor
: loop over itemsfrom
: for importing some names from modulesif
: forif
expressionsimport
: for importing modulesin
: operator for membershiplet
: declare an inmutable valuethen
: part ofif
expressionstrue
: Boolean true literalvar
: declare a mutable value
Operators and symbols
Infix operators
This list contains all the infix operators, an example of how the operator would be used, a short explanation, and its precedence. An operator with a higher precedence will be grouped before an operator with less precedence.
Operator | Example | Explanation | Precedence |
---|---|---|---|
in | a in [] | Membership operator | 1 |
.. | 0..10 | Range generation | 2 |
|| | true || false | Logic OR | 3 |
&& | true && false | Logic AND | 4 |
> | 1 > 0 | "Greater" comparison | 5 |
>= | 1 >= 0 | "Greater or equal" comparison | 5 |
< | 1 < 0 | "Less" comparison | 5 |
<= | 1 <= 0 | "Less or equal" comparison | 5 |
/= | 9 /= 9 | Negated equality | 5 |
= | 'a' = 'a' | Equality | 5 |
^ | 2 ^ 3 | Bitwise XOR | 6 |
& | 1 & 0 | Bitwise AND | 7 |
<< | 1 << 2 | Left shift | 8 |
>> | 8 >> 2 | Right shift | 8 |
- | 8 - 3 | Arithmetic substraction | 9 |
+ | 1 + 1 | Arithmetic addition | 9 |
/ | 1 / 2 | Arithmetic division | 10 |
% | 5 % 7 | Division remainder | 10 |
* | 5 * 7 | Arithmetic multiplication | 10 |
** | 2 ** 5 | Exponentiation | 11 |
Prefix operators
This list contains all the prefix operators, an example of how the operator would be used, and a short explanation.
Operator | Example | Explanation |
---|---|---|
~ | ~1 | Bitwise negation |
! | !false | Logic negation |
- | -5 | Additive inverse |