At the moment of writing F# has 3.9k stars vs Gleam has 17.6k but let’s compare them a bit shall we?
|> WHY?
Comparing a hedgehog 🦔 with a starfish ⭐️ at first sight seems like comparing apple with pears, but le me explain more in this brief article: they actually have quite some feats and “genes” in common!
What is F#?
What is Gleam?
Gleam, a new statically typed language in the Erlang/BEAM “actor” ecosystem, brings functional programming to the world of highly concurrent, fault-tolerant systems, and also targets javascript / nodejs as possible runtime.
|> Quick |> Similarities |> Differences
- Targeting a backend runtime (diff: .NET vs BEAM)
- Garbage Collected (GC) memory
- Strongly Typed both have great automatic type inference!
- Targeting JS + TS(natively with
external
in Gleam vs Fable in F#) - Actor system and error kernel support (native in Gleam on BEAM, somewhat via Akka/Orleans in .NET or some implementation using mailbox in F# for simple message processing)
let
keyword for binding (expression based langs, gleam via{ }
)- pipelines
|>
(instead of only function application) - sum (unions / complex) and product types (tuples, records) in both languages! Algebraic Data Types
- great pattern matching on ADTs , tuples and lists
- unit
()
type (Nil
in Gleam), when expressions return nothing/empty! ///
(F#) and////
(Gleam) for api docs- immutability by default (Gleam is strict, F# allows
mutable
keyword and<-
assignment operator, can be handy) - Functional and declarative languages: Gleam is purely functional in this regard: Gleam has no for loops for example, only recursion! F# is FP first, but allows also OOP.
- Option and Result modules!
- Gleam has no null values! F# tries to evade it with Option and
?
nullable ref support (NET9+) from .NET but you can still encounter null in interop with libraries and C# code in F#, so you need to handle it. - Gleam has no exceptions! F# has exceptions and try/catch/finally as part of .NET but you can decide to adopt the Result pattern and ROP, as both Result and Option are part of the standard F# library.
- F# allows
rec
code (and even tail recursion check via attribute) but also for loops, iterators (and computation expressions for collections ~ list comprehensions like python!) - F# allows for object oriented on top of .NET and interops with C#, So in F# is possible to create classes, interfaces and objects.
<>
is string concat in Gleam but+
in F#, equality in F# is=
and==
in Gleam, unequality is<>
in F# and!=
in gleam.- gleam has not yet (atm of writing) added support for string interpolation (minor)
|> Ecosystem (.NET vs Beam)
We have seen in the first picture that Gleam has quite much more stars than F# but let’s compare now their respective ecosystem and libraries
|> A Bit More on Gleam and its Types
Gleam’s type system is inspired by ML, but it’s not exactly an ML type system. It shares some similarities, such as:
- Static Typing: Like ML, Gleam uses static types, meaning that types are checked at compile time.
- Algebraic Data Types (ADTs): Gleam supports ADTs, which are common in ML-family languages. You can define custom types with tagged unions, which are similar to how ML handles data types.
- Automatic Type Inference: Gleam, unlike most common strongly typed languages has awesome automatic type inference (~ like ML languages, Ocaml, Elm, F#). This means that gleam programs does NOT require explicit type annotations for even for function signatures, unlike e.g. rust or typescript or C#/java/scala. Both F# and Gleam have an amazing type inference! — thanks to Eli Dowling for the mention.
However, Gleam diverges from ML in a few ways:
- Generics and type variables: Generics in gleam work in a slightly different fashion, using type variables (see above)
- Immutability by Design: Like Rust, Gleam leans heavily on immutability, but Rust provides more flexibility with mutable data structures under strict borrowing rules. Gleam sticks to immutability in a more simplified, functional manner.
|> Preparation
F#: Setting Up .NET Development
To develop in F#, you’ll need to have the .NET SDK installed, as F# is fully integrated into the .NET ecosystem (you will get F#, C# and VB), if you are using VsCode or Vim you can check the Ionide extension for editing F# files.
brew install dotnet-sdk
dotnet --version
dotnet fsi
> "hello world!";;
> #quit;;
Gleam: Setting Up for Gleam Development
if you are using vscode or vim you can find gleam extensions to support your favourite editor.
brew install erlang
erl -version
brew install gleam
|> Creating a New Project
F#: Setting Up with .NET CLI
dotnet new console -lang F# -o MyFSharpApp
cd MyFSharpApp
dotnet run
Gleam: Setting Up with Gleam CLI
gleam new my_gleam_project
cd my_gleam_project
gleam build
gleam run
|> Project Files
Gleam .toml
Gleam uses gleam.toml
for project metadata. It's lightweight and readable:
name = "my_app"
version = "0.1.0"
description = "An example Gleam app"
[dependencies]
gleam_stdlib = "~> 0.30"
my_other_project = { path = "../my_other_project" }
F# .fsproj
(xml)
F# and .NET uses an XML-based .fsproj
file to define the project (.csproj in the case of C# , the two are 99% identical), a bit more verbose but does the job pretty well too.
Note: F# project files need to include source code explicitly as F# does not allow for cyclic references and the source file ordering must be respected. You can choose to see this as a limitation or as a feature. Most F# devs see this as a feature for code correctness, plus it makes it easy to spot entry points and dependencies, just going up or down in the project tree structure.
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net8.0</TargetFramework>
</PropertyGroup>
<ItemGroup>
<Compile Include="Functions.fs" />
<Compile Include="Program.fs" /><!-- last file contains the entry point -->
</ItemGroup>
<ItemGroup>
<PackageReference Include="Newtonsoft.Json" Version="13.0.1" />
</ItemGroup>
</Project>
|> Syntax Similarities Between F# and Gleam
Both F# and Gleam emphasize simplicity and expressiveness in their syntax, making it easier for developers to write concise and maintainable code. While F# integrates deeply with .NET and Gleam compiles to BEAM bytecode, they share several functional programming concepts:
1. Immutability and Variables
In both languages, immutability is a core principle, with values typically defined as constants rather than mutable variables.
- F# uses the
let
keyword to declare immutable values:
let x = 42
// in F# we always have expression bindings, last value is result
let greet =
printfn "saying hello..."
"Hello, World!"
- Similarly, Gleam uses the
let
keyword as well:
let x = 42
// expression binding, last value is result
let greet = {
io.debug("saying hello...")
"Hello, World!"
}
- In both languages, there is a strong focus on immutability. While in Gleam it is mandatory, in F# mutability can be made explicit when developers know what they are doing or for optimization purposes, a bit like Rust, using the mutable keyword.
let mutable x = 0
x <- x + 1 // changing a mutable variable using the assignment operator
2. Pattern Matching and ADTs
Pattern matching is a powerful feature in both F# and Gleam, making code more expressive and allowing exaustive handling of different cases for union types.
- In F#, pattern matching is done using the
match
expression:
let describeNumber n =
match n with
| 0 -> "Zero"
| 1 -> "One"
| _ -> "Many"
- Gleam follows a similar structure with its own
case
expression:
fn describe_number(n) {
case n {
0 -> "Zero"
1 -> "One"
_ -> "Many"
}
}
Both languages provide great ways to match union/sum ADT values, F# allows also for Active patterns, for Gleam not sure atm of writing.
Let’s see now how to declare record and complex types in Gleam
type Person {
Person(name: String, age: Int)
}
type Employee {
Manager(emp: Person, level: Int)
Engineer(emp: Person, progr_lang: String)
Empl(emp: Person)
}
in F# records and unions can be easilly declared as well with the same keyword type
but following the more usual ML/Elm/OCAML synthax.
type Person = { Name: string ; Age: int }
type Employee =
| Manager of (emp: Person) * (level: int)
| Engineer of (emp: Person) * (progrLang: string)
| Empl of Person
3. Functions as First-Class Citizens
Both F# and Gleam treat functions as first-class citizens, allowing them to be passed around as values and used in higher-order functions.
- In F#, defining a simple function looks like this:
// curried (default)
let add a b =
a + b
// tupled args
let add2 (a,b) =
a + b
- In Gleam, it’s similarly straightforward but we have to use the
fn
keyword andpub
if we want to expose a function from a module
pub fn add(a, b) {
a + b
}
// anonymous fn
let add2 = fn(a,b) { a + b }
- both languages support higher order functions, and expression bindings. F# has a simpler synthax and all functions are curried by default, and supports both tupled and curried parameters.
4. Pipelines |>
One of the most beloved features in functional languages is the ability to pipe data through a series of transformations. Both F# and Gleam allow this with similar syntax.
- F# uses the
|>
operator for pipelining:
let result =
[1; 2; 3]
|> List.map (fun x -> x * 2)
|> List.filter (fun x -> x > 3)
- Gleam uses the
|>
operator in the same way:
let result =
[1, 2, 3]
|> list.map(fn(x) { x * 2 })
|> list.filter(fn(x) { x > 3 })
This makes it easy to build complex transformations in a clear, readable way in both languages.
|> Differences Between F# and Gleam
Despite their functional roots, F# and Gleam differ significantly in type inference and generics, concurrency, and somewhat in error management.
Let’s break down some of the key differences.
|> Type Systems and Type Inference
F# and Gleam both have strong, static type systems, they slightly differ in their approach to type inference but both offer automatic inference.
- F# Type Inference: In F#, the type system is powerful and often does not require explicit type signatures (full ML like). The compiler can infer types automatically, allowing you to omit type annotations in most cases:
let add a b = a + b
- Here, F# automatically infers that both
a
andb
are integers without needing to specify the types explicitly. This makes F# code concise and readable, particularly for simple functions. types can be added when required to help the compiler like(a : int) (b: int) : int =
Gleam’s Automatic Type inference:
- Similarly, Gleam also does not rquire explicit type annotations for function declarations, resulting in great readability and mantainability and ease of refactor.
fn add(a,b){
a + b
}
In both languages type annotations can be added optionally when needed.
|> Concurrency Models
Concurrency is one of the areas where F# and Gleam differ dramatically due to their ecosystems.
- F# Task-based Asynchronous Model: F# leverages .NET’s
async/await
model for concurrency. Tasks are the primary abstraction for asynchronous operations, and you can write asynchronous code using thetask
(hot) computation expression, the nativeasync
(cold) ce is also available and it possibly inspired async/await in C# before its time. The use oftask
is suggested where .NET interop or performance optimizations might be needed, so i set it here as the default.
open System.Threading.Tasks
task {
let! result = someAsyncTask()
return result
}
- In F#, tasks represent async operations that may take time to complete, usually I/O or side-effects, and you can chain them with
let!
orand!(parallel)
inside atask
block. This makes concurrency in F# highly efficient and integrates with the .NET ecosystem’s powerful multithreading capabilities.
Gleam and Erlang’s Concurrency Model:
Gleam benefits from Erlang’s battle-tested concurrency model, which is built around lightweight processes and the actor model. In Erlang (and therefore Gleam), concurrency isn’t managed with threads or tasks but with processes that communicate via message passing.
- The “let it crash” philosophy means that instead of catching errors and trying to recover within the process, Erlang embraces fault tolerance by isolating failures to individual processes. These processes can fail and be restarted without affecting the entire system, which is crucial for building resilient, distributed applications:
% Erlang example of spawning processes and sending messages spawn
(fun() -> receive Message ->
io:format("Received ~p~n",
[Message]) end).
- F# and .NET offers libraries for implementing the Actor model such as Akka.NET and Orleans, and more. in F# is possible to implement concurrent message passing using the
FSharp.Control.MailboxProcessor
class and some rudimentary forms of “actor” routines or supervision.
|> Targeting JS
$ gleam run --target javascript
Both Gleam and F# can target JS / node.js, Gleam natively via cli
and .toml
project vs F# requires Fable. Gleam can target with External other BEAM libraries and ecosystem (Elixir, Erlang) or JS/TS, and F# can target instead a wide range of languages via Fable, atm of writing:
- JS/TS
- Python:
- Rust
- Dart
|> External / Import for target languages
Fable External Declarations
Assume we needed to use some pre-existing js function like the following when targeting js in gleam.
// The `now` function in `./my_package_ffi.mjs` looks like this:
export function now() {
return new Date();
}
Here is how you would make this JS function usable from F# via Fable, via the Import
attribute:
open Fable.Core
open Fable.Core.JsInterop
// Define a type with no constructors
type DateTime = obj
// Declare an external function that creates an instance of the type
[<Import("now", from="./my_package_ffi.mjs")>]
let now: unit -> DateTime = jsNative
Fable supports also other languages, like Python and other languages via separate packages, for example:
// https://www.compositional-it.com/news-blog/f-with-python-pt-2-tensorflow-binding/
[<ImportAll("tensorflow")>]
let tensorflow : ITensorFlow = nativeOnly
Gleam External Declarations
Gleam’s external functions and external types allow us to import and use this non-Gleam code. An external type is one that has no constructors. Gleam doesn’t know what shape it has or how to create one, it only knows that it exists. An external function is one that has the
@external
attribute on it, directing the compiler to use the specified module function as the implementation, instead of Gleam code. — src: Gleam.run
pub type DateTime
@external(erlang, "calendar", "local_time")
@external(javascript, "./my_package_ffi.mjs", "now")
pub fn now() -> DateTime
pub fn main() {
io.debug(now())
}
Error Handling
Error handling also differs between the two languages.
- F# Exception Handling: F# adopts the Result pattern (Railway Oriented Programming / ROP) as guideline but allows the traditional exception-handling model of the .NET ecosystem as well. You can use
try
/with
blocks to catch exceptions or functional constructs like theResult
type for safer error management to prevent exception bubbling.
let safeDivide a b =
if b = 0 then
None
else
Some (a / b)
- Gleam’s Functional Error Handling: Gleam general philosophy is “let it fail” typical of Erlang/Actor systems. Exceptions are avoided entirely, using tagged unions like
Result
andOption
to handle errors explicitly only when required. Gleam has like Go the concept of panic to fail with unexpected errors.
fn safe_divide(a: Int, b: Int) -> Result(Int, String) {
if b == 0 {
// actually gleam can! but is not relevant here...
Error("Cannot divide by zero")
} else {
Ok(a / b)
}
}
|> THE END
In essence, F# and Gleam each shine in their own unique ways in the realm of functional programming!
- F# is a powerful player in the .NET ecosystem, offering advanced features like type inference and computation expressions. It’s perfect for developers looking to build robust applications while leveraging the extensive libraries of .NET.
- Gleam is the nimble newcomer from the BEAM world, prioritizing simplicity and concurrency. Its focus on immutability and fault tolerance makes it an excellent choice for building resilient distributed systems.
Whether you’re drawn to F#’s rich functionality or Gleam’s lightweight design, both languages offer exciting opportunities to explore functional programming.
Choose your adventure and let the coding journey begin! 🎉 |> thank you for your time!
References (some)
- https://tour.gleam.run/
- https://try.fsharp.org/
- https://tour.gleam.run/
- https://exercism.org/tracks/fsharp
- https://exercism.org/tracks/gleam
- https://gleam.run/
- https://dotnet.microsoft.com/en-us/languages/fsharp
- https://www.codemag.com/article/1707051/Writing-Concurrent-Programs-Using-F
- https://fsharp.org/
- https://fable.io/
- https://zaid-ajaj.github.io/Feliz/
- https://github.com/dotnet/orleans
- https://gleam.run/getting-started/installing/#installing-erlang
- https://gleam.run/writing-gleam/command-line-reference/
- https://learn.microsoft.com/en-us/dotnet/fsharp/tools/fsharp-interactive/
- https://learn.microsoft.com/en-us/dotnet/core/tools/
- https://www.compositional-it.com/news-blog/calling-python-from-f-with-fable/
- https://www.compositional-it.com/news-blog/f-with-python-pt-2-tensorflow-binding/
- https://fable.io/docs/python/features.html