Skip to content

A small library for handling side effects! Particularly promises in Gleam. Inspired by Elm and Lustre's approach to effect handling.

License

Notifications You must be signed in to change notification settings

ethanthoma/effect

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

18 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Effect

effectual computations made easy

meme

Table of Contents

  1. Overview
  2. Example
  3. Installation
  4. Usage

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}
import effect/promise as effect_promise

pub type Error {
  UriParse
  Fetch(fetch.FetchError)
}

pub fn main() {
  let google: Effect(String, Error) = {
    use uri <- effect.from_result_map_error(
      uri.parse("https://www.google.com"),
      fn (_) { UriParse }, // replacing the error
    )
    use req <- effect.from_result_map_error(
      request.from_uri(uri),
      fn (_) { UriParse }
    )
    use resp <- effect_promise.from_promise_result(fetch.send(req), Fetch)
    use text <- effect_promise.from_promise_result(
      fetch.read_text_body(resp),
      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
  // Effect(Int, early)
  let eff_ok = effect.continue(42)
  // Effect(msg, String)
  let eff_err = 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, TooSmallError) = 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)
    // fun fact, I added this func to std
    let log: Float = float.exponential(f)
  }

  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_promise_result.

import gleam/fetch.{type FetchBody, type FetchError}
import gleam/javascript/promise.{type Promise}
import gleam/string

import effect.{type Effect}
import effect/promise as effect_promise

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_readme()
    // get an effect from a promise(result)
    use fetch_body: Response(FetchBody) <- effect_promise.from_promise_result(
      prom,
      Fetch, // map the error
    )

    // read the response from FetchBody to String
    let prom: Promise(Result(Response(String), FetchError)) =
      fetch.read_text_body(fetch_body)

    use text: Response(String) <- effect_promise.from_promise_result(
      prom,
      // map the 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))
    _ -> io.println("Failed: other error")
  }
}

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, effect.Nothing) = {
    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.

About

A small library for handling side effects! Particularly promises in Gleam. Inspired by Elm and Lustre's approach to effect handling.

Resources

License

Stars

Watchers

Forks