Notes on using Sesterl
06 Jun 2021 (last edited on 20 Dec 2021)
Sesterl is a new statically typed programming language for the BEAM (the Erlang virtual machine).
Installation
To install Sesterl, we can download a release build for Ubuntu or MacOS from the Releases page on the Sesterl GitHub Repository.
$ unzip sesterl-0-2-1-ubuntu-latest.zip
Archive: sesterl-0-2-1-ubuntu-latest.zip
inflating: sesterl
$ chmod +x sesterl
$ mv sesterl ~/.local/bin/
$ sesterl --version
v0.2.1
On other systems sesterl has to be compiled from source.
“Hello World” with rebar3
Sesterl can generate a rebar3 config file for a project from a sesterl.yaml
file.
To generate a rebar3
config from the above file, we can run:
$ sesterl config .
output written on '/home/michal/hello_sesterl/./rebar.config'.
A rebar3 project also needs an *.app.src
file in the src/
directory:
Sesterl source files have a .sest
file extension. The syntax is similar to Standard ML or OCaml.
To compile a project, we can run:
$ rebar3 do sesterl compile, compile
===> Fetching rebar_sesterl (from {git,"https://github.com/gfngfn/rebar_sesterl_plugin.git",
{branch,"master"}})
===> Analyzing applications...
===> Compiling rebar_sesterl
===> Verifying dependencies...
===> Compiling Sesterl programs (command: "sesterl build ./ -o _generated") ...
parsing '/home/michal/projects/hello_sesterl/src/some_file.sest' ...
type checking '/home/michal/projects/hello_sesterl/src/some_file.sest' ...
output written on '/home/michal/projects/hello_sesterl/./_generated/HelloSesterl.Hello.erl'.
output written on '/home/michal/projects/hello_sesterl/./_generated/sesterl_internal_prim.erl'.
===> Analyzing applications...
===> Compiling hello_sesterl
_generated/sesterl_internal_prim.erl:8:14: Warning: variable 'Arity' is unused
===> Verifying dependencies...
===> Analyzing applications...
===> Compiling hello_sesterl
_generated/sesterl_internal_prim.erl:8:14: Warning: variable 'Arity' is unused
And to execute code from the above module from the Erlang shell:
$ rebar3 shell
===> Verifying dependencies...
===> Analyzing applications...
===> Compiling hello_sesterl
Erlang/OTP 24 [erts-12.0.2] [source] [64-bit] [smp:6:6] [ds:6:6:10] [async-threads:1] [jit]
Eshell V12.0.2 (abort with ^G)
1> 'HelloSesterl.Hello':my_hello().
<<"Hello, world!">>
ok
Yay!
Sesterl standard library
Sesterl standard library is just a rebar3 package, that can be added as a git dependency to the configuration yaml file:
1
2
3
4
5
6
7
8
dependencies:
- name: "sesterl_stdlib"
source:
type: "git"
repository: "https://github.com/gfngfn/sesterl_stdlib"
spec:
type: "branch"
value: "master"
Rebar3 config needs to be regenerated with sesterl config .
, and on next compilation we’ll be able to refer to modules from the stdlib (but only through what is defined in its main Stdlib module):
1
2
3
4
5
module Hello = struct
val my_hello() =
let list = Stdlib.Binary.to_list("Hello, world!") in
print_debug(list)
end
1
2
3
4
5
6
7
8
module Hello = struct
module Binary = Stdlib.Binary
val my_hello() =
let list = Binary.to_list("Hello, world!") in
print_debug(list)
end
1
2
3
4
5
6
7
8
module Hello = struct
open Stdlib.Binary
val my_hello() =
let list = to_list("Hello, world!") in
print_debug(list)
end
Bonus: “Hello World” without rebar3
When compiling the above example with sesterl
, we get two Erlang source files as a result:
$ sesterl build without_rebar3.sest -o _generated
parsing '/home/michal/hello_sesterl/without_rebar3.sest' ...
type checking '/home/michal/hello_sesterl/without_rebar3.sest' ...
output written on '/home/michal/hello_sesterl/_generated/Hello.erl'.
output written on '/home/michal/hello_sesterl/_generated/sesterl_internal_prim.erl'.
$ ls _generated
Hello.erl sesterl_internal_prim.erl
The Hello.erl
file contains the 'Hello'
erlang module, and sesterl_internal_prim.erl
(“prim” as in “primitives”) contains a module with a few functions to provide some of basic functionality of the language (e.g. Erlang’s send wrapped in a function). This module will not be available in our escript program unless we add code to load it.
To make the escript executable we have to add one dummy line to the beginning of the erlang source file, and then we can run it with escript -c
(the -c
argument tells escript to compile the module first):
$ (echo "% additional line for escript to work" && cat _generated/Hello.erl) > tmpfile && mv tmpfile _generated/Hello.erl
$ escript -c _generated/Hello.erl
_generated/Hello.erl:7:8: Warning: variable 'S11Args' is unused
% 7|
% | ^^^^^^
Hello, world!
Hurray!
Separate types without tags
Something similar to “newtypes” or phantom types (?) - separate, incompatible types that have the same run-time representation (without boxing or tagging) - can be achieved with the Sesterl’s type system (just like OCaml’s):
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
module Hello = struct
module Username :> sig
type t :: o
val from_binary : fun(binary) -> t
end = struct
type t = binary
val from_binary(x) = x
end
module Hostname :> sig
type t :: o
val from_binary : fun(binary) -> t
end = struct
type t = binary
val from_binary(x) = x
end
val do_something_with_username(x : Username.t) = x
val main() =
let a = Hostname.from_binary("example.com") in
do_something_with_username(a)
end
/*(*
! [Type error] file 'some_file.sest', line 23, characters 31-32:
this expression has type
Hello.Hostname.t
but is expected of type
Hello.Username.t
*)*/
Or if we want to reuse the interface signature and implementation:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
module Hello = struct
signature ID = sig
type t :: o
val from_binary : fun(binary) -> t
end
module BinaryID = struct
type t = binary
val from_binary(x) = x
end
module Username :> ID = BinaryID
module Hostname :> ID = BinaryID
val do_something_with_username(x : Username.t) = x
val main() =
let a = Hostname.from_binary("example.com") in
do_something_with_username(a)
end
/*(*
! [Type error] file 'some_file.sest', line 22, characters 31-32:
this expression has type
Hello.Hostname.t
but is expected of type
Hello.Username.t
*)*/
The below example doesn’t work (doesn’t throw an error), because Sesterl ignores the unused parameter from the type and assumes they are the same thing:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
module Hello = struct
type id<$a> = binary
type hostname =
| Hostname
type username =
| Username
val do_something_with_username(x : id<username>) = x
val hostname_from_binary(x : binary) : id<hostname> = x
val main() =
let a = hostname_from_binary("example.com") in
do_something_with_username(a)
end
/*(* No error even though we'd like to see one here! *)*/