Comparing |> Gleam ⭐️ |> F#🦔

jkone27
12 min readSep 24, 2024

--

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 externalin 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

.NET vs Erlang ecosystem comparison from chatgpt…

|> A Bit More on Gleam and its Types

Gleam automatic type inference is 🙌 awesome like true ML langs

Gleam’s type system is inspired by ML, but it’s not exactly an ML type system. It shares some similarities, such as:

  1. Static Typing: Like ML, Gleam uses static types, meaning that types are checked at compile time.
  2. 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.
  3. 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:

Generic functions in gleam using type variables
  • 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 and pub 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 and b 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 the task (hot) computation expression, the native async (cold) ce is also available and it possibly inspired async/await in C# before its time. The use of task 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! or and!(parallel) inside a task 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 the Result 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 and Option 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)

--

--

Responses (1)