Published 31 Oct, 2020 by Louis Pilfold
It’s halloween and time for another Gleam release! This time it’s an extra special release as it also includes the first version of Gleam’s type safe actor system, compatible with Erlang’s OTP.
Mutual recursion
Historically functions in Gleam had to be defined in a module prior to being used.
pub fn main() {
helper() // Compile error! Function `helper` not found
}
fn helper() {
0
}
This has proven to be an annoyance as even though our code should compile (all the required functions have been defined in the module) the compiler won’t succeed unless we carefully maintain a specific order of functions in the module.
This limitation also forced the programmer to write any helper functions at
the top of the file, before the functions that make use of them. Many people
like to write their code in the opposite order with their entry-point at the
top (such as a main
function), and then helper functions below. That way
the module reads top-to-bottom, introducing new functions in the order they
are used.
Lastly it also meant that two functions could not call each other, making useful techniques such as mutual recursion impossible.
With this release statements in a module can be written in any order, and they still don’t require any type annotations to be fully type checked.
pub fn barry() {
io.println("To you!")
paul()
}
pub fn paul() {
io.println("To me!")
barry()
}
That’s the only major change to the language this time. For a full list of changes see the compiler changelog.
Next up, Gleam’s new type safe actor system!
Gleam OTP
Gleam’s actor system is built with a few primary goals:
- Full type safety of actors and messages.
- Be compatible with Erlang’s OTP actor framework.
- Provide fault tolerance and self-healing through supervisors.
- Have equivalent performance to Erlang’s OTP.
Two notable non-goals are that we are not explicitly supporting the BEAM’s hot code upgrades, and we do not provide type safety for distributed computing where two BEAM nodes on different computers send messages to each other.
The core concepts of Gleam OTP are processes, channels, actors, and supervisors. Let’s take a look at each of them.
Processes
The core concurrency primitive in Gleam OTP is the process, which is a lightweight green thread implemented by the BEAM virtual machine. These processes are extremely cheap to create and run, and a single BEAM instance can happily run millions of processes at the same time. It’s highly sophisticated preemptive scheduler will ensure they make full use of all the cores of the CPU, and no malicious or buggy process can block others from running.
There’s a wealth of information online about the BEAM’s processes so we won’t cover it all here, but know that Gleam’s actor system performs well thanks to decades of excellent work by the Erlang community.
Use processes in Gleam we import the gleam/otp/process
module which
provides functions for creating and working with them.
import gleam/io
import gleam/otp/process
pub fn main() {
process.start(fn() {
io.println("Hello from the new process!")
})
io.println("Hello from the parent process!")
}
In this code snippet we create a new process using the
process.start
function, before printing a message to
the console.
The newly created process also prints a message, but because the two processes run asynchronously (and likely run on different CPU cores) we don’t know which will be printed first. It could be the new one first, or it could be the parent, and it could be different each time the program runs.
One important thing to note is that there’s no need for an async/await
syntax or callbacks. Asynchronous code in Gleam looks exactly the same to
regular synchronous code and doesn’t require any special types such as
futures or promises.
This should hopefully feel familiar to people familiar with concurrent programming in languages such as Erlang, Elixir, and Go.
Channels
It’s not good enough to merely be able to create processes, we need processes to be able to communicate and cooperate. Erlang and Gleam don’t have mutable state so locks and shared mutable state is not an option- instead Gleam uses channels.
Channels allow one process to send a message to another in a type safe fashion, and they may be familiar to Go and Rust programmers.
A channel is made of a Sender
, which messages as sent into,
and a Receiver
, which messages are pulled out of by the
channel owner process.
import gleam/io
import gleam/otp/process
pub fn main() {
// Create a new channel owned by the current process
let tuple(sender, receiver) = process.new_channel()
process.start(fn() {
io.println("Hello from the new process!")
// Send a message to the parent process
process.send(sender, "Done")
})
// Receive a message from the child, waiting up to 100ms
process.receive(receiver, 100) // This returns Ok("Done")
io.println("Hello from the parent process!")
}
Here the code has been updated to use a channel to coordinate the processes. The parent process creates a channel and waits for the child to send a message over the channel before printing. Because of this we know that the child process will now always print to the console before the parent does.
Here we’re just using a simple string message to synchronize processes but any type we want can be sent over channel, enabling sharing of data in concurrent programs.
The Gleam compiler can full type check channels. Because the message sent
with the sender is a String
the compiler knows messages pulled from the
receiver must also be strings and will print a helpful error with any program
that doesn’t use these messages correctly.
Actors
In programs written in Erlang using OTP we tend to not use raw processes
frequently, instead we use a higher level abstraction called gen_server
that builds upon a process to provide additional features making it more
suited to being used in a long-lived Erlang application. Gleam is similar,
though our higher level abstraction is called actor
and
differs in design to gen_server
in order to provide type safety.
import gleam/io
import gleam/otp/actor
import gleam/otp/process.{Sender}
pub type Message {
Request(reply_channel: Sender(String))
}
pub fn main() {
let actor = actor.start(0, handle_message)
// Send a message to the child and wait for a response
process.call(actor, Request, 100) // This returns Ok("Done")
io.println("Hello from the parent process!")
}
fn handle_message(msg: Message, state) {
io.println("The actor got a message")
process.send(msg.reply_channel, "Done")
actor.Continue(state)
}
Here the code has been adapted to use an actor rather than a low-level
process, and instead of explicitly creating a new channel we use the
call
function. This function is similar to Erlang’s
gen_server:call
in that it provides a way to send a message to a process
and then wait until a reply is received.
Since an actor is being used here rather than a raw process it doesn’t shut
down after running the function once, instead the handler function is called
once per message received and the actor continues to run until either it is
requested to shut down or it returns actor.Stop
from its handler function.
Any of OTP’s debug or system messages sent to the actor are automatically handled by the actor implementation, while they would need to be manually handled when using a raw process.
Supervisors
Supervisors are special actors that start other actors and then monitor them to ensure they are healthy. If one of the children crashes the supervisor restarts it along with any younger children, restoring the program to a healthy state now that any transient problems have ended.
If the problem is not transient and the child continues to crash then the supervisor will shut down and then the supervisor’s supervisor can attempt a restart now that more possibly invalid or corrupt state has been reset.
By using this supervision functionality OTP applications can achieve high reliability by shedding state in an incremental fashion until the program self-heals. This is kind-of similar to how Kubernetes and other container orchestrators handle failure in microservices, though faster and more precise thanks to it being integrated at all levels of the program.
import gleam/otp/supervisor.{add, returning, worker}
import my_app/monitoring
import my_app/database
import my_app/web
pub fn main() {
supervisor.start(fn(children) {
children
|> add(worker(database.start))
|> add(worker(monitoring.start))
|> add(worker(web.start))
})
}
Here we have a supervisor for a pretend Gleam web application. It has 3
children of the type worker
, a database connection, an actor responsible
for monitoring, and a HTTP handling actor. If we were adding a supervisor
child we could use the supervisor
function instead of worker
.
Going forward
Gleam OTP v0.1 is the result of approaching 2 years of research and implementation, mostly focusing on how to strike a good balance between type safety and compatibility with existing Erlang OTP patterns.
Rather than build on top of Erlang’s supervisor
and gen_server
Gleam OTP
has a small core written in Erlang which implements the Receiver
half of
channels, and the rest is implemented in Gleam. Thanks to this we can have
greater confidence in the viability of the core abstractions as they have
been sufficient to implement the rest of Gleam OTP in a type safe fashion.
These APIs (particularly supervisor) are experimental and open to breaking changes as we discover new ways to improve them, but we’ve reach a point where we can start writing programs using it.
If you write something using Gleam OTP please do get in touch and let us know how you find it!
Discord chat
Lastly, we’ve got a new home for the community! The Gleam IRC channel has been replaced by the Gleam Discord chat server to great success. Since opening we’ve seen a big increase in activity and lots of new exciting Gleam projects. If you’d like to join click here!.
Try it out
If you want to try out the new version of Gleam head over to the installation page. I’d love to hear how you find it and get your feedback so Gleam can continue to improve.
Want to view some existing Gleam projects? Head on over to the awesome-gleam list. Looking for something to build in Gleam? Check out the suggestions tracker.
Supporting Gleam
If you would like to help make strongly typed programming on the Erlang virtual machine a production-ready reality please consider sponsoring Gleam via the GitHub Sponsors program.
This release would not have been possible without the support of all the people who have sponsored and contributed to it, so a huge thank you to them.
Lastly, a huge thank you to the contributors to and sponsors of Gleam since last release!
- Adam Bowen
- Adam Mokan
- Al Dee
- Arian Daneshvar
- Arno Dirlam
- Bangash
- Ben Myles
- Bernardo Amorim
- Bruno Dusausoy
- Bruno Michel
- Chris Young
- Christian Meunier
- Christian Wesselhoeft
- clangley
- Clever Bunny LTD
- Cole Lawrence
- Connor Schembor
- Dan Mueller
- Dave Lucia
- David McKay
- eeeli24
- Eric Meadows-Jönsson
- Erik Terpstra
- Florian Kraft
- Guilherme Pasqualino
- Hendrik Richter
- Herdy Handoko
- human154
- Ian González
- Ingmar Gagen
- Isac Sund
- Ivar Vong
- James MacAulay
- Jechol Lee
- John Palgut
- José Valim
- Lars Lillo Ulvestad
- Lars Wikman
- Leandro Cesquini Pereira
- mario
- Mario Vellandi
- Mark Markaryan
- Mark Molnar
- Mark Spink
- Markus
- Markus Hechenberger
- Matthew Cheely
- Michael Bausano
- Michael Jones
- Michał Łępicki
- Mike Janger
- Mike Roach
- Milad
- Nick Reynolds
- Parker Selbert
- Pete Jodo
- Peter
- Raphael Megzari
- René Klačan
- RJ Dellecese
- Robin Mattheussen
- Sam Aaron
- Santi Lertsumran
- Sasan Hezarkhani
- Sascha Wolf
- Saša Jurić
- Scott Wey
- Sean Jensen-Grey
- Sebastian Porto
- Shane Sveller
- sharno
- Shritesh Bhattarai
- Simone Vittori
- Sven Marquardt
- Terje Bakken
- Tim Buchwaldt
- Tom Whatmore
- Tristan Sloughter
- Tyler Wilcock
- Wojtek Mach
Thanks for reading! Have fun! 💜