One of the young programming languages I look at from afar and root for is Gleam. It is a statically typed language for BEAM, written in Rust. Similarly to Rust, if focuses on catching many potential bugs at compilation time and providing useful error messages, so it's easy to fix them. Since it's targeting BEAM, it can easily interop with other BEAM languages, such as Elixir or Erlang. And this fact allows to overcome some issues that languages in their infancy stage usually have.
My recent thought revolve around using Gleam to model the core business logic, while letting Elixir do the heavy application work around it. Let's face the facts: Phoenix is great, Ecto is great, ExUnit is great... It will take years for language such as Gleam to develop ecosystem similar to Elixir. On the other hand, I find modelling business logic with types easier and more maintainable than without them. And Elixir typespecs have their issues:
- Dialyzer error messages are not very friendly, it usually takes me couple minutes to figure out what they really mean.
- Types are optional and there seems to be almost-consensus to write typespecs for public functions only. However, in my experience, because of that, bugs happen a lot in private functions, which are neither typespec'd, nor unit tested.
- Dialyzer simply does not catch everything. In fact sometimes I'm surprised at the things it does not catch.
In Gleam, all these issues are addressed: types are mandatory in every function, the compiler checks everything and error messages are quite informative. For example:
error: Type mismatch
┌─ /home/katafrakt/dev/poligon/gleamixir_test/src/booking.gleam:21:5
│
21 │ _ -> Error(InvalidRoomNumber)
│ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
This case clause was found to return a different type than the previous
one, but all case clauses must return the same type.
Expected type:
Bool
Found type:
Result(a, BookingError)
So I decided to have a look at how the interop between Gleam and Elixir works and if it could be useful. To set things us, I used mix_gleam Hex package from Gleam team, which allows you to compile Gleam as part of Mix compilation. I just followed instructions in the readme and everything works. So, time to write the first "business logic" in Gleam.
To test things, I decided to code a fragment of super simple booking system for my imaginary estate. It's quite small - only has three rooms. But still, you can book them, if the details are valid. Here's the relevant code:
// src/booking.gleam
pub type Booking {
Booking(surname: String, room_no: Int, number_of_people: Int)
}
pub type BookingError {
InvalidRoomNumber
WrongNumberOfPeople
}
// Unfortunately, Gleam does not have default arguments nor multiple dispatch,
// so we have to somehow bypass that by creating two pseudo-constructors
pub fn new2(surname: String, room_no: Int) -> Booking {
new3(surname, room_no, 1)
}
pub fn new3(surname: String, room_no: Int, number_of_people: Int) -> Booking {
Booking(surname, room_no, number_of_people)
}
pub fn validate_booking(booking: Booking) -> Result(Booking, BookingError) {
case booking.room_no {
1 | 2 | 5 -> validate_capacity(booking)
_ -> Error(InvalidRoomNumber)
}
}
fn validate_capacity(booking: Booking) -> Result(Booking, BookingError) {
case room_capacity(booking.room_no) >= booking.number_of_people {
True -> Ok(booking)
False -> Error(WrongNumberOfPeople)
}
}
fn room_capacity(room_no: Int) -> Int {
case room_no {
1 -> 2
2 | 5 -> 4
}
}
It's not exactly Elixir, but I guess you can get what's going on, as it's quite idiomatic.
When compiled, Gleam modules are available like Erlang modules. So I can write this, for example:
iex(1)> :booking.new2("Smith", 1)
{:booking, "Smith", 1, 1}
As it turns out, Gleam types, when returned to Elixir, become a tuple with first element being a type name and the following elements field values. This works both ways, so I can pipe the result into another Gleam function.
iex(2)> :booking.new2("Smith", 1) |> :booking.validate_booking()
{:ok, {:booking, "Smith", 1, 1}}
iex(3)> :booking.new2("Smith", 3) |> :booking.validate_booking()
{:error, :invalid_room_number}
iex(4)> :booking.new3("Smith", 5, 6) |> :booking.validate_booking()
{:error, :wrong_number_of_people}
This looks pretty good. A nice thing is that Gleam's Result
type maps elegantly into Elixir's {:ok, value}
or {:error, message}
convention.
Maybe I didn't see a lot, but it brings me close to the conclusion that yes, Gleam can be mixed with Elixir in parts where full static typing is desirable and integration between the two languages goes pretty seamless. I would have to prepare a larger example to check it, but potentially looks good.
A few things I don't necessarily like in Gleam (or find missing):
- Completely unlike Elixir, it relies heavily on filenames - i.e. name of the file is the name of the resulting module. This is not necessarily bad, but I prefer less coupling between the filesystem and the code.
- No support for multiple dispatch or default arguments forces me to write weird things like
new2
andnew3
pseudo-constructors. Perhaps in real example it wouldn't be so problematic though, as probably I'd be passing all the data at once. - I was surprised that sometimes the compiler wasn't able to infer the type, even though it should be possible.
- The standard library at the moment does not include things like dates at all, although of course I can define relevant logic myself without a lot of problems.
Bonus content: Accessing namespaced Gleam modules
I wrote above that module names accessible to Elixir are named based on the filename of the file where Gleam code is written. It was pretty straightforward when I did it in src/booking.gleam
files and could access via :booking.new()
, but what if I had a file in a subdirectory? I checked it by creating a src/transport/trains.gleam
file and couldn't really find a way to call it from Elixir. I was close to the conclusion that it's impossible and you can only write publicly exported code in top-level namespace, but then I found how to list all the atoms defined for the application:
iex(1)> :application.get_key(:gleamixir_test, :modules)
{:ok, [GleamixirTest, :booking, :transport@trains]}
iex(2)> :transport@trains.example()
{:train, {:engine, "EU07", 160},
[
{:train_car, "Passenger double-decker", 160, "001"},
{:train_car, "Postal", 120, "005"},
{:train_car, "Passenger double-decker", 160, "002"},
{:train_car, "Restaurant", 160, "600"}
]}
iex(3)> :transport@trains.example() |> :transport@trains.max_speed()
{:ok, 120}
So, in short, you use @
-sign as a namespace separator.
And this is the code of trains
module if you're interested:
import gleam/list
import gleam/int
pub type Train {
Train(engine: Engine, cars: List(TrainCar))
}
pub type Engine {
Engine(class: String, max_speed: Int)
}
pub type TrainCar {
TrainCar(car_type: String, max_speed: Int, serial_number: String)
}
pub fn example() {
Train(
engine: Engine("EU07", 160),
cars: [
TrainCar("Passenger double-decker", 160, "001"),
TrainCar("Postal", 120, "005"),
TrainCar("Passenger double-decker", 160, "002"),
TrainCar("Restaurant", 160, "600"),
],
)
}
pub fn max_speed(train: Train) {
[
train.engine.max_speed,
..list.map(train.cars, fn(c: TrainCar) { c.max_speed })
]
|> list.sort(by: int.compare)
|> list.head()
}