Effect

Table of Contents
Overview
A small library for handling side effects! Particularly promises in Gleam.
Inspired by Lustre’s Effects and effect-ts.
This library allows you to treat async operations and potential failures as first-class effectful computations, which can be composed before finally being executed.
The motivation was to create an API for dealing with promises from gleam_promises.
Example
import gleam/fetch
import gleam/io
import gleam/javascript/promise
import gleam/result
import gleam/string
import effect.{type Effect}
pub type Error {
UriParse
Fetch(fetch.FetchError)
}
pub fn main() {
let google: Effect(String, Error) = {
use uri <- effect.from_result(
uri.parse("https://www.google.com") |> result.replace_error(UriParse),
)
use req <- effect.from_result(
request.from_uri(uri) |> result.replace_error(UriParse),
)
use resp <- effect.from_box(fetch.send(req), promise.map)
use resp <- effect.from_result(resp |> result.map_error(Fetch))
use text <- effect.from_box(fetch.read_text_body(resp), promise.map)
use text <- effect.from_result(text |> result.map_error(Fetch))
text.body |> effect.continue
}
use res: Result(String, Error) <- effect.perform(google)
case res {
Ok(body) -> body |> io.println
Error(e) -> e |> string.inspect |> io.println_error
}
}
Further documentation can be found at https://hexdocs.pm/effect.
Installation
gleam add effect@2
Usage
Below are demonstrations of common usages demonstrating on how to create and compose effects,
handle failures, work with promises, and actually perform
the effect.
Basic Construction
import gleam/int
import gleam/io
import effect.{type Effect}
pub fn main() {
// Succeed / Fail shortcuts
let eff_ok: Effect(Int, early) = effect.continue(42)
let eff_err: Effect(msg, String) = effect.throw("Oops!")
// Combine
let eff: Effect(Int, String) = {
case int.random(2) |> int.is_even {
True -> eff_ok
False -> eff_err
}
}
// Perform
use res: Result(Int, String) <- effect.perform(eff)
case res {
Ok(num) -> io.println("Got: " <> int.to_string(num))
Error(err) -> io.println("Error: " <> err)
}
}
Chaining Effects
Use then
and map
to sequence operations, only continuing on success, or short-circuiting on error:
import gleam/float
import gleam/int
import gleam/io
import effect.{type Effect}
type TooSmallError {
TooSmallError
}
pub fn main() {
let computation: Effect(Int, TooSmallError) = {
let effect: Effect(Int, early) = effect.continue(10)
use num: Int <- effect.then(effect)
case num < 5 {
True -> effect.throw(TooSmallError)
False -> effect.continue(num * 2)
}
}
let computation: Effect(Float, TooSmallError) = {
use num: Int <- effect.map(computation)
let f: Float = int.to_float(num)
let log: Float = float.exponential(f) // fun fact, I added this func to std
}
use res: Result(Float, TooSmallError) <- effect.perform(computation)
case res {
Ok(n) -> io.println("Final result: " <> float.to_string(n))
Error(TooSmallError) -> io.println("Error: num is too small!")
}
}
Handling Promises
You can convert a promise.Promise(Result(a, e))
into an Effect(a, e)
using
from_box
and promise.map
.
import gleam/fetch.{type FetchBody, type FetchError}
import gleam/javascript/promise.{type Promise}
import gleam/string
import effect.{type Effect}
type Error {
Fetch(fetch.FetchError)
}
fn fetch_data() -> Promise(Result(FetchBody, FetchError)) {
let assert Ok(uri) = uri.parse("https://www.google.com")
let assert Ok(req) = request.from_uri(uri)
fetch.send(req)
}
fn main() {
let eff: Effect(String, Error) = {
// fetch from google
let prom = fetch_data()
use res: Result(FetchBody, FetchError) <- effect.from_box(prom, promise.map)
use fetch_body: FetchBody <- effect.from_result(res |> result.map_error(Fetch))
// read the response from FetchBody to String
let prom: Promise(Result(String, FetchError)) = fetch.read_text_body(fetch_body)
use text: Result(String, FetchError) <- effect.from_box(prom, promise.map)
use text: String <- effect.from_result(text |> result.map_error(Fetch))
// return just the body
text.body |> effect.continue
}
use res: Result(String, Error) <- effect.perform(eff)
case res {
// Server Data
Ok(body) -> io.println(body)
// Network Error
Error(Fetch(msg)) -> io.println("Failed: " <> string.inspect(msg))
}
}
Pure Effects
Sometimes you don’t need the early
return path and only need to operate on the
happy path. The perform
function returns a Result
type but there’s a pure
alternative:
import gleam/int
import gleam/io
import effect.{type Effect}
fn main() {
let eff: Effect(Int, early) = {
use a <- effect.from(5)
use b <- effect.from(2)
a * b |> effect.continue
}
use num: Int <- effect.pure(eff)
num |> int.to_string |> io.println
}
For more, be sure to checkout the test cases.