Gleam for Elm users
Hello Elm type magicians!
Hello Elm type magicians!
Elm and Gleam have similar goals of providing a robust and sound type system with a friendly and approachable set of features.
They have some differences in their output and focus. Where Elm compiles to JavaScript, Gleam compiles to both Erlang and JavaScript, and where Elm is best suited for front-end browser based applications, Gleam targets back-end and front-end application development.
Another area in which Elm and Gleam differ is around talking to other languages. Elm does not provide user-defined foreign function interfaces for interacting with JavaScript code and libraries. All communication between Elm and JavaScript has to go through the Elm ports. In contrast to this, Gleam makes it easy to define inferfaces for using Erlang or JavaScript code and libraries directly and has no concept of ports.
In Elm, single line comments are written with a --
prefix and multiline comments are written with {- -}
and {-| -}
for documentation comments.
-- Hello, Joe!
{- Hello, Joe!
This is a multiline comment.
-}
{-| Determine the length of a list.
length [1,2,3] == 3
-}
length : List a -> Int
length xs =
foldl (\_ i -> i + 1) 0 xs
In Gleam comments are written with a //
prefix.
// Hello, Joe!
Comments starting with ///
are used to document the following function, constant, or type definition. Comments starting with ////
are used to document the current module.
//// This module is very important.
/// The answer to life, the universe, and everything.
const answer: Int = 42
There are no multiline comments in Gleam.
In Elm, you assign variables in let-blocks and you cannot re-assign variables within a let-block.
You also cannot create a variable with the same name as a variable from a higher scope.
let
size = 50
size = size + 100 -- Compile Error!
in
Gleam has the let
keyword before its variable names. You can re-assign variables and you can shadow variables from other scopes. This does not mutate the previously assigned value.
let size = 50
let size = size + 100
let size = 1
Both Elm and Gleam will check the type annotation to ensure that it matches the type of the assigned value. They do not need annotations to type check your code, but you may find it useful to annotate variables to hint to the compiler that you want a specific type to be inferred.
In Elm, type annotations are optionally given on the line above the variable assignment. They can be provided in let-blocks but it frequently only provided for top level variables and functions.
someList : List Int
someList = [1, 2, 3]
In Gleam type annotations can optionally be given when binding variables.
let some_list: List(Int) = [1, 2, 3]
In Elm, constants can be defined at the top level of the module like any other value and exported if desired and reference from other modules.
module Hikers exposing (theAnswer)
theAnswer: Int
theAnswer =
42
In Gleam constants can be created using the const
keyword.
const the_answer = 42
pub fn main() {
the_answer
}
Gleam constants can be referenced from other modules.
// in file other_module.gleam
pub const the_answer: Int = 42
import other_module
fn main() {
other_module.the_answer
}
In Elm, functions are defined as declarations that have arguments, or by assigning anonymous functions to variables.
sum x y =
x + y
mul =
\x y -> x * y
mul 3 2
-- 6
Gleam’s functions are declared using a syntax similar to Rust or JavaScript. Gleam’s anonymous functions are declared using the fn
keyword.
pub fn sum(x, y) {
x + y
}
let mul = fn(x, y) { x * y }
mul(1, 2)
In Elm, functions can optionally have their argument and return types annotated. These type annotations will always be checked by the compiler and throw a compilation error if not valid. The compiler will still type check your program using type inference if annotations are omitted.
sum : number -> number -> number
sum x y = x + y
mul : number -> number -> Bool -- Compile error
mul x y = x * y
All the same things are true of Gleam though the type annotations go inline in the function declaration, rather than above it.
pub fn add(x: Int, y: Int) -> Int {
x + y
}
pub fn mul(x: Int, y: Int) -> Bool {
x * y // compile error, type mismatch
}
Elm has no built-in way to label arguments. Instead it would standard for a function to expect a record as an argument in which the field names would serve as the argument labels. This can be combined with providing a ‘defaults’ value of the same record type where callers can override only the fields that they want to differ from the default.
defaultOptions =
{ inside = defaultString
, each = defaultPattern,
, with = defaultReplacement
}
replace opts =
doReplacement opts.inside opts.each opts.with
replace { each = ",", with = " ", inside = "A,B,C" }
replace { defaultOptions | inside = "A,B,C,D" }
In Gleam arguments can be given a label as well as an internal name.
pub fn replace(inside string, each pattern, with replacement) {
go(string, pattern, replacement)
}
replace(each: ",", with: " ", inside: "A,B,C")
There is no performance cost to Gleam’s labelled arguments as they are optimised to regular function calls at compile time, and all the arguments are fully type checked.
In Elm, the module
keyword allows to create a module. Each module maps to a single file. The module name must be explicitly stated and must match the file name.
module One exposing (identity)
identity : a -> a
identity x =
x
A Gleam file is a module, named by the file name (and its directory path). There is no special syntax to create a module. There can be only one module in a file.
// in file one.gleam
pub fn identity(x) {
x
}
// in file main.gleam
import one // if `one` was in a folder called `lib` the import would be `lib/one`
pub fn main() {
one.identity(1)
}
In Elm, exports are handled at the top of the file in the module declaration as a list of names.
module Math exposing (sum)
-- this is public as it is in the export list
sum x y =
x + y
-- this is private as it isn't in the export list
mul x y =
x * y
In Gleam, constants & functions are private by default and need the pub
keyword to be public.
// this is public
pub fn sum(x, y) {
x + y
}
// this is private
fn mul(x, y) {
x * y
}
In Elm, expressions can be grouped using let
and in
.
view =
let
x = 5
y =
let
z = 4
t = 3
in
z + (t * 5) -- Parenthesis are used to group arithmetic expressions
in
y + x
In Gleam braces {
}
are used to group expressions.
pub fn main() {
let x = {
print(1)
2
}
let y = x * { x + 10 } // braces are used to change arithmetic operations order
y
}
Both Elm and Gleam support Int
and Float
as separate number types.
Elm has a built-in number
concept that allows it to treat Int
and Float
generically so operators like +
can be used for two Int
values or two Float
values though not for an Int
and a Float
.
Operators in Gleam are not generic over Int
and Float
so there are separate symbols for Int
and Float
operations. For example, +
adds integers together whilst +.
adds floats together. The pattern of the additional .
extends to the other common operators.
Additionally, underscores can be added to both integers and floats for clarity.
const one_million = 1_000_000
const two_million = 2_000_000.0
In both Elm and Gleam all strings support unicode. Gleam uses UTF-8 binaries. Elm compiles to JavaScript which uses UTF-16 for its strings.
Both languages use double quotes for strings.
"Hellø, world!"
Strings in Elm are combined using the ++
operator and functions like String.append
and String.concat
:
greeting =
"Hello, " ++ "world!"
birthdayWishes =
String.append "Happy Birthday, " person.name
holidayWishes =
String.concat [ "Happy ", holiday.name, person.name ]
"Hellø, world!"
Similar to Elm, you can combine strings, for that Gleam has the operator <>
and also functions like string.append
and string.concat
in the standard library.
let happy_new_year_wishes =
"Happy New Year " <> person.name
let birthday_wishes =
string.append(to: "Happy Birthday ", suffix: person.name)
let holiday_wishes =
string.concat([ "Happy ", holiday.name, person.name ])
Tuples are very useful in both Elm and Gleam as they’re the only collection data type that allows mixed types in the collection.
In Elm, tuples are limited to only 2 or 3 entries. It is recommended to use records when needing larger numbers of entries.
myTuple = ("username", "password", 10)
(_, password, _) = myTuple
There is no limit to the number of entries in Gleam tuples, but records are still recommended as giving names to fields adds clarity.
let my_tuple = #("username", "password", 10)
let #(_, password, _) = my_tuple
Records are used to define and create structured data.
In Elm, you can declare records using curly braces containing key-value pairs:
person =
{ name = "Alice"
, age = 43
}
The type of the record is derived by the compiler. In this case it would be { name : String, age : number }
.
Records can also be created using a type alias name as a constructor.
Record fields can be accessed with a dot syntax:
greeting person =
"Hello, " ++ person.name ++ "!"
Records cannot be updated because they are immutable. However, there is a special syntax for easily creating a new record based on an existing record’s fields:
personWithSameAge = { person | name = "Bob" }
In Gleam, you cannot create records without creating a custom type first.
type Person {
Person(name: String, age: Int)
}
let person = Person(name: "Alice", age: 43)
Record fields can be accessed with a dot syntax:
greeting = string.concat(["Hello, ", person.name, "!"])
Records cannot be updated because they are immutable. However, there is a special syntax for easily creating a new record based on an existing record’s fields:
let person_with_same_age = Person(..person, name: "Bob")
Both Elm and Gleam support lists. All entries have to be of the same type.
Elm has a built-in syntax for lists and the cons
operator (::
) for adding a new item to the head of a list.
list = [2, 3, 4]
anotherList = 1 :: list
yetAnotherList = "hello" :: list // compile error, type mismatch
Gleam also has a built-in syntax for lists and its own spread operator (..
) for adding elements to the front of a list.
let list = [2, 3, 4]
let list = [1, ..list]
let another_list = [1.0, ..list] // compile error, type mismatch
The standard library provides the gleam/list module for interacting with lists. Functions like list.head
return an Option
value to account for the possibility of an empty list.
Dict in Elm and Dict in Gleam have similar properties and purpose.
In Gleam, dicts can have keys and values of any type, but all keys must be of the same type in a given dict and all values must be of the same type in a given dict.
Like Elm, there is no dict literal syntax in Gleam, and you cannot pattern match on a dict.
import Dict
Dict.fromList [ ("key1", "value1"), ("key2", "value2") ]
Dict.fromList [ ("key1", "value1"), ("key2", 2) ] -- Compile error
import gleam/dict
dict.from_list([#("key1", "value1"), #("key2", "value2")])
dict.from_list([#("key1", "value1"), #("key2", 2)]) // Type error!
As Gleam does not treat integers and floats generically, there is a pattern of an extra .
to separate Int
operators from Float
operators.
Operator | Elm | Gleam | Notes |
---|---|---|---|
Equal | == |
== |
In Gleam both values must be of the same type |
Not equal | /= |
!= |
In Gleam both values must be of the same type |
Greater than | > |
> |
In Gleam both values must be ints |
Greater than | > |
>. |
In Gleam both values must be floats |
Greater or equal | >= |
>= |
In Gleam both values must be ints |
Greater or equal | >= |
>=. |
In Gleam both values must be floats |
Less than | < |
< |
In Gleam both values must be ints |
Less than | < |
<. |
In Gleam both values must be floats |
Less or equal | <= |
<= |
In Gleam both values must be ints |
Less or equal | <= |
<=. |
In Gleam both values must be floats |
Boolean and | && |
&& |
In Gleam both values must be bools |
Boolean or | || |
|| |
In Gleam both values must be bools |
Add | + |
+ |
In Gleam both values must be ints |
Add | + |
+. |
In Gleam both values must be floats |
Subtract | - |
- |
In Gleam both values must be ints |
Subtract | - |
-. |
In Gleam both values must be floats |
Multiply | * |
* |
In Gleam both values must be ints |
Multiply | * |
*. |
In Gleam both values must be floats |
Divide | // |
/ |
In Gleam Both values must be ints |
Divide | / |
/. |
In Gleam both values must be floats |
Modulo | remainderBy |
% |
In Gleam both values must be ints |
Concatenate | ++ |
<> |
|
Pipe | |> |
|> |
Gleam’s pipe will try piping into the first position or Elm style as the only argument to a function, using whichever type checks. |
Elm uses type aliases to define the layout of records. Gleam uses Custom Types to achieve a similar result.
Gleam’s custom types allow you to define a collection data type with a fixed number of named fields, and the values in those fields can be of differing types.
type alias Person =
{ name : String
, age : Int
}
person = Person "Jake" 35
name = person.name
Gleam’s custom types can be used in much the same way. At runtime, they have a tuple representation and are compatible with Erlang records.
type Person {
Person(name: String, age: Int)
}
let person = Person(name: "Jake", age: 35)
let name = person.name
Both Elm and Gleam have a similar concept of custom types. These allow you to list out the different states that a particular piece of data might have.
The following example might represent a user in a system:
type User
= LoggedIn String -- A logged in user with a name
| Guest -- A guest user with no details
You must use a case-expression to interact with the contents of a value that uses a custom type:
getName : User -> String
getName user =
case user of
LoggedIn name ->
name
Guest ->
"Guest user"
A custom type with a single entry can be used to help create opaque data types for your module’s API if only the type and not the single constructor is exported.
type User {
LoggedIn(name: String) // A logged in user with a name
Guest // A guest user with no details
}
Like in Elm, you must use a case-expression to interact with the contents of a value that uses a custom type.
fn get_name(user) {
case user {
LoggedIn(name) -> name
Guest -> "Guest user"
}
}
In Gleam, a custom type with a single entry that has fields of its own fills the role of type alias
in Elm.
In order to create an opaque data type, you can use the opaque
keyword.
Neither Gleam nor Elm have a concept of ‘null’ in their type system. Elm uses Maybe
to handle this case. Gleam uses a similar approach called Option
.
In Elm, Maybe
is defined as:
type Maybe a
= Just a
| Nothing
In Gleam, Option
is defined as:
pub type Option(a) {
Some(a)
None
}
The standard library provides the gleam/option module for interacting with Option
values.
Neither Gleam nor Elm have exceptions and instead represent failures using the Result
type.
Elm’s Result
type is defined as:
type Result error value
= Ok value
| Err error
In Gleam, the Result
type is defined in the compiler in order to support helpful warnings and error messages.
If it were defined in Gleam, it would look like this:
pub type Result(value, reason) {
Ok(value)
Error(reason)
}
The standard library provides the gleam/result module for interacting with Result
values.
Similar to the Elm function Result.andThen
, the Gleam standard library includes a result.try function that allows for chaining together functions that return Result
values. This can be used in conjuntion with the use
keyword to allow for early returns from a function. A use
expression will take the value from the passed in Result
and treat the rest of the function body as the function that should be called if the Result
is Ok
. If the passed in Result
is an Error
, the rest of the function body will not get called and the Error
will be immediately returned.
let a_number = "1"
let an_error = "ouch"
let another_number = "3"
use int_a_number <- try(parse_int(a_number)) // parse_int(a_number) returns Ok(1) so int_a_number is bound to 1
use attempt_int <- try(parse_int(an_error)) // parse_int(an_error) returns an Error and will be returned
use int_another_number <- try(parse_int(another_number)) // never gets executed
Ok(int_a_number + attempt_int + int_another_number) // never gets executed
Elm has syntax for if-expressions for control flow based on boolean values.
description =
if value then
"It's true!"
else
"It's not true."
Gleam has no built-in if-expression syntax and instead relies on matching on boolean values in case-expressions to provide this functionality:
let description =
case value {
True -> "It's true!"
False -> "It's not true."
}
description // => "It's true!"
Both Gleam and Elm support case-expressions for pattern matching on values including custom types.
getName : User -> String
getName user =
case user of
LoggedIn name ->
name
Guest ->
"Guest user"
fn get_name(user) {
case user {
LoggedIn(name) -> name
Guest -> "Guest user"
}
}
Pattern matching on multiple values at the same time is supported:
case x, y {
1, 1 -> "both are 1"
1, _ -> "x is 1"
_, 1 -> "y is 1"
_, _ -> "neither is 1"
}
Guard expressions can also be used to limit when certain patterns are matched:
case xs {
[a, b, c] if a == b && b != c -> "ok"
_other -> "ko"
}
For more information and examples, see the case expressions entry in the Gleam language tour.
Elm is a pure language so all side-effects, eg. making an HTTP request, are managed by the command system. This means that functions for making HTTP requests return an opaque command value that you return to the runtime, normally via the update function, in order to execute the request.
Gleam is not a pure language and so does not have a command system for managing side-effects. Any function can directly perform side effects and where necessary will manage success and failure using the Result
type or other more specific custom types.
Elm programs compile to JavaScript and primarily allow you to talk to JavaScript via ports. Elm does not have an accessible foreign function interface for calling JavaScript directly from Elm code. Only core modules can do that. Ports provide a message-passing interface between the Elm application and JavaScript. It is very safe. It is almost impossible to cause runtime errors in your Elm code by passing incorrect values to or from ports. This makes Elm a very safe language with very good guarantees against runtime exceptions but at the cost of some friction when the developer wants to interact with JavaScript.
Gleam provides syntax for directly calling Erlang functions. The developer specifies the types for the Erlang function and the compiler assumes those types are accurate. This means less friction when calling Erlang code but also means less of a guarantee of safety as the developer might get the types wrong.
Gleam also provides a similar syntax for calling JavaScript functions via wrapper modules you provide.
Functions that call Erlang or JavaScript code directly use the @external
attribute and use strings to refer to the module/function to call.
It is possible to call functions provided by other languages on the Erlang Virtual Machine but only via the Erlang name that those functions end up with.
@external(erlang, "rand", "uniform")
pub fn random_float() -> Float
// Elixir modules start with `Elixir.`
@external(erlang, "Elixir.IO", "inspect")
pub fn inspect(a) -> a
@external(erlang, "calendar", "local_time")
@external(javascript, "./my_package_ffi.mjs", "now")
pub fn now() -> DateTime
For more information and examples, see the Externals entry in the Gleam language tour.
Elm has ‘The Elm architecture’ baked into the language and the core modules. Generally speaking, all Elm applications follow the Elm architecture. Elm is generally focused on writing front-end browser applications and the architecture serves it well.
Gleam does not have a set architecture. It is not focused on making front-end browser applications and so does not share the same constraints. As it compiles to Erlang, Gleam application architecture is influenced by Erlang approaches to building distributed, fault-tolerant systems including working with OTP. In order to create a type-safe version of the OTP approach, Gleam has its own gleam/otp library.
Elm packages are installed via the elm install
command and are hosted on package.elm-lang.org.
All third-party Elm packages are written in pure Elm. It is not possible to publish an Elm package that includes JavaScript code unless you are in the core team. Some packages published under the elm
and elm-explorations
namespaces have JavaScript internals.
Gleam packages are installed via the gleam add
command and are hosted on hex.pm with their documentation on hexdocs.pm.
All Gleam packages can be published with a mix of Gleam and Erlang code. There are no restrictions on publishing packages with Erlang code or that wrap Erlang libraries.
The Elm compiler is written in Haskell and distributed primarily via npm. The core libraries are written in a mix of Elm and JavaScript.
The Gleam compiler is written in Rust and distributed as precompiled binaries or via some package managers. The core libraries are written in a mix of Gleam and Erlang.
Gleam has the concept of ‘atoms’ inherited from Erlang. Any value in a type definition in Gleam that does not have any arguments is an atom in the compiled Erlang code.
There are some exceptions to that rule for atoms that are commonly used and have types built-in to Gleam that incorporate them, such as Ok
, Error
and booleans.
In general, atoms are not used much in Gleam, and are mostly used for booleans, Ok
and Error
result types, and defining custom types.
Custom types in Elm can be used to achieve similar things to atoms.
type Alignment
= Left
| Centre
| Right
type Alignment {
Left
Centre
Right
}
To aid debugging, Elm has a Debug.toString()
function:
import Debug
Debug.toString [1,2] == "[1,2]"
To aid debugging, Gleam has a string.inspect()
function:
import gleam/string
string.inspect([1, 2, 3]) == "[1, 2, 3]"