Inko 0.9.0 released
Merry Christmas! Inko 0.9.0 has been released, with a new manual, fewer dependencies, the removal of nullable types, support for generators, pattern matching, and much more!
Table of contents
- A brand new manual
- A new network IO poller
- Fewer dependencies for building libffi
- Reduced memory usage of the Ruby compiler
- A new "check" option
- Changes to constructing objects
- Panics for the [] operator
- Receiving messages with a timeout is now a separate method
- First steps towards a self-hosting compiler
- Nullable types are replaced with an Option type
- Writing iterators using generators
- Pattern matching is now available
- Object is now a trait
- Importing methods from modules
- Loop methods are now in a separate module
- Garbage collection in process threads
- Documentation fixes
- Other changes
For the full list of changes, take a look at the changelog.
If you would like to support the development of Inko, please donate to Inko on Open Collective or via GitHub Sponsors.
A brand new manual
The old Inko manual that was part of the website is replaced with a new manual based on mkdocs. The manual is now part of the main Inko repository, instead of the website repository. This makes it easier to contribute documentation changes. These changes also allow us to version the manual in the future, though we aren't quite there yet.
The new manual is found here.
A new network IO poller
Inko's network IO poller is now built on top of the excellent polling Rust crate. This allows us to support a wider range of platforms in the future, and reduces the amount of code we have to maintain ourselves.
Fewer dependencies for building libffi
In version 0.8.0 we started using a new version of the libffi Rust crate, reducing the number of dependencies necessary to built the Inko VM. As the libffi changes were not yet released, we had to use a patch to make use of these changes. A new version of libffi has since been released, removing the need for these patches.
You can now also configure Inko to use a system installation of libffi instead of building libffi from source. For more information, take a look at our Building from source guide in the manual.
Reduced memory usage of the Ruby compiler
In commit fb80d4 we reduced the memory usage of the Ruby compiler by about 20%, making it easier to run Inko in memory constrained environments such as Docker containers.
A new "check" option
You can now run inko build --check
to check if an Inko program contains any
errors, without building the program. This makes it easier to integrate Inko
into your text editor/IDE of choice. For example, we have an open pull
request to add Inko support to
the popular Vim plugin ALE.
Changes to constructing objects
Starting with Inko 0.9.0, the static method new
is no longer defined
automatically, and the instance method init
is no longer used to initialise an
object after its allocation. Instead, Inko objects can now be constructed
directly. For example, take this object:
object Person {
@name: String
def init(name: String) {
@name = name
}
}
Before version 0.9.0, you could create an instance of this object like so:
Person.new('Alice')
Starting with 0.9.0, you need to use the following pattern instead:
object Person {
@name: String
static def new(name: String) -> Self {
Person { @name = name }
}
}
Here Person { @name = name }
creates a new instance of the Person
object,
and sets the @name
attribute to the name
argument. All attributes an object
defines must be assigned when using this syntax.
Since typing the object name can get tedious, you can also use Self
like so:
object Person {
@name: String
static def new(name: String) -> Self {
Self { @name = name }
}
}
Do note that the pattern Self { ... }
is only available in static methods; it
can't be used in instance methods.
These changes bring several benefits:
- Developers have greater control over what methods are defined for their objects
- It's now impossible to use a partially initialised object
- The compiler no longer needs to automatically generate methods, simplifying the implementation
- Using variable arguments to initialise an object is now easier, as
the
init
instance method used before didn't support variable arguments - You can use different static methods to initialise the object in different
ways, instead of being forced to funnel everything through a single
init
instance method
Various built-in types, such as Integer
and Array
can't be initialised using
this syntax; instead you must use the new
static method these types define.
Panics for the [] operator
The []
operator now panics whenever its used with an undefined index. This
applies to Array.[]
, ByteArray.[]
, and Map.[]
. We believe this to be a
safer and better default compared to returning a nullable or Option
type.
If you don't want a panic, you can use the get
method these types define,
which returns an Option
type (more on this below).
Receiving messages with a timeout is now a separate method
The method std::process.receive
is split into two methods: receive
and
receive_timeout
. This cleans up the internals a bit, and makes it more obvious
what behaviour is expected when receiving messages.
First steps towards a self-hosting compiler
We have merged the first steps towards a self-hosting compiler. This includes various changes to the parser, a type-checker, pattern matching support, and various compiler changes.
Originally we had intended to merge these changes when we have a working self-hosting compiler. Over time we realised there is still a lot of work to do, and keeping multiple branches in sync (with lots of changes between these branches) is a challenge. To make development easier, we have merged what we have so far.
The self-hosting code merged so far is still a work in progress. For example, it's not yet able to infer closure argument types. We will continue development on the self-hosting compiler in the coming months.
In spite of being a work in progress, the work has proven invaluable; even revealing several soundness issues the Ruby compiler failed to detect.
Nullable types are replaced with an Option type
Inko used nullable types (technically "nillable" as Inko used Nil
instead of
NULL
) for optional values. In 0.9.0 this has been replaced with the use of an
Option
type.
While nullable types may seem like a good solution, they are difficult to support without running into soundness issues. For example, the following code used to compile but result in a runtime error:
def add(numbers: Array!(?Integer)) {
numbers.push(Nil)
}
let numbers: Array!(Integer) = Array.new(10)
add(numbers)
numbers[1] + 5 # This will error, because it's Nil instead of an integer
This would compile as Integer
is compatible with ?Integer
, and error because
our code isn't expecting the Array to contain a Nil
.
Nullable also require more work from the user when dealing with output that may
be explicitly set to Nil
. For example, using iterators required the use of two
methods: one to check if a new value is available, and one to get the value.
Lastly, nullable types make composition difficult. For example, we can't define
a generic map
method for all nullable types in Inko, as a ?T
and a T
are
the same types at runtime (assuming the ?T
is not Nil
).
Using an Option
type solves these problems, as Option!(T)
and T
are
fundamentally different types, both which exist at runtime.
For these reasons, nullable types have been removed in Inko 0.9.0 and replaced
with the use of an Option
type. To make working with these types easier, you
can use ?T
to signal an optional T
. This is just syntax sugar for
Option!(T)
, but cuts down the amount of times you have to type the word
"Option" in your code.
The introduction of an Option
type also makes it easier to write iterators, as
we no longer need two methods to use an iterator. Instead, iterators now define
a next
method that returns an Option!(T)
, with a None
value signalling the
end of the iterator.
Nil
still exists, but is only used in cases where it makes sense (e.g. when a
method omits its return type).
Writing iterators using generators
Inko now supports generators, drastically simplifying the process of writing
iterators. Generators are limited to methods, and require you to annotate the
method with the type that it yields. Generators also implement the Iterator
trait, allowing you to use them as regular iterators.
As an example, we can define an Integer
generator like so:
def generator => Integer {
yield 10
yield 20
}
let gen = generator
gen.next # => Option(10)
gen.next # => Option(20)
gen.next # => None
Here yield
yields a value, then continues where it left off when the generator
is resumed.
Combined with the introduction of Option
types, this makes writing external
iterators a breeze. For example, this is how Map.iter
is implemented in Inko
0.8.1:
def iter -> Iterator!(Pair!(K, V)) {
let mut index = 0
let mut found: ?Pair!(K, V) = Nil
let max = @buckets.length
Enumerator.new(
while: {
{
(index < max).and { found.nil? }
}.while_true {
found = @buckets[index]
index += 1
}
found != Nil
},
yield: {
let pair = found!
found = Nil
pair
}
)
}
And here is the implementation for version 0.9.0:
def iter => Pair!(K, V) {
@buckets.each do (pair_opt) {
pair_opt.let do (pair) { yield pair }
}
}
That's a big difference!
Pattern matching is now available
Pattern matching has been covered in previous articles, and is available in Inko 0.9.0. You can read up all about it in the new Pattern matching manual.
Object is now a trait
While Inko didn't expose inheritance through the language/syntax, it relied on
it internally as all objects were an instance of Object
. In Inko 0.9.0,
Object
is now a trait that is implemented automatically for instances of
user-defined objects. The Any
trait has been turned into a compile-time only
type, much like the Never
type. This type can't be used for much on its own,
and instead must be casted or pattern-matched into a more useful type.
An important detail is that the Object
trait is only implemented for
instances of user-defined objects. It's not implemented for types such as
traits and modules. This means the following is not valid:
object Person {}
# invalid because Person itself doesn't implement Object
Person.if_true { ... }
The following is valid, because an instance of Person
does implement the
Object
trait:
Person {}.if_true { ... }
These changes mean inheritance is no longer relied upon. This allows us to work towards a more efficient memory representation of objects, and more efficient (and simpler) method lookups.
As part of these changes, the Hash
and Equal
traits are no longer
implemented automatically.
Importing methods from modules
You can now import individual methods from a module:
# module foo.inko
def foo -> Integer {
42
}
# module bar.inko
import foo::(foo)
foo # => 42
Importing methods requires the use of parentheses (just as is necessary when
importing multiple symbols), as the compiler otherwise thinks it has to import
the module foo::foo
. This restriction may be lifted in the future.
Loop methods are now in a separate module
Loop methods such as loop
and while_true
are no longer defined on the
Block
type, as this leads to soundness issues. For example, in Inko 0.8.1 this
code would type-check:
do (number) { number > 10 }.while_true { foo }
This is unsound because the implementation of while_true
never passes any
arguments to its receiver, resulting in a runtime error.
This can't be solved without making the type system overly complex. To work
around that, we introduce a new module for loops: std::loop
. This module
defines three methods: while
, loop
, and repeat
. As while
and loop
are
commonly used, these are exposed using the prelude, removing the need to import
them manually.
This all combined means you now write loops as follows:
import std::stdio::stdout
let mut i = 0
while({ i < 10 }) {
stdout.print(i)
i += 1
}
Garbage collection in process threads
Garbage collection is now performed in the same thread that was running the process before triggering garbage collection, instead of a separate thread being used. This reduces the garbage collection time as we no longer need to move data across threads, reduces the total number of threads necessary, and simplifies the garbage collection implementation. Parallel tracing is still performed using a pool of threads.
Documentation fixes
Matheus Richard fixed several typos in our documentation. Thanks Matheus!
Other changes
Lots of other changes are included in this release, so be sure to check out the changelog for this release.
If you'd like to follow the development of Inko, consider joining our Matrix chat room or our Reddit community.