wende/efx
{ "createdAt": "2019-04-09T17:22:04Z", "defaultBranch": "master", "description": "First-class effects for Elixir", "fullName": "wende/efx", "homepage": "", "language": "Elixir", "name": "efx", "pushedAt": "2019-04-28T20:13:00Z", "stargazersCount": 16, "topics": [], "updatedAt": "2025-10-07T10:02:13Z", "url": "https://github.com/wende/efx"}Efx - First class side effects for Elixir
Section titled “Efx - First class side effects for Elixir”Experimental Elixir language extension adding support for First class effects. Otherwise known as algebraic effects[1][2]
Marking effects
Section titled “Marking effects”eff print() :: {IO.puts/1}def print() do IO.puts "Something"endDefining own effects
Section titled “Defining own effects”defeffect Example.print()Now this is an effect and can be reasoned about on its own. It also is an alias to itself and all the underlying effects. What it means is that used like
eff fun() :: {Example.print/0}
Where Example.print() contains effects IO.puts/1 and IO.read/1
What is really represents is {Example.print/1, IO.puts/1, IO.read/1}
And each and everyone of these effects can be captured independently
Think of effects as call to functions that are identified as effect-full
Capturing effects
Section titled “Capturing effects”Effects can be captured using a dedicated syntax handle do...catch...end
handle do Example.print()catch # Here we can replace our call with anything different. Let's forbid our IO.puts # from printing any passwords IO.puts(arg1) -> if arg1 =~ "password" do :ok else IO.puts(arg1) end
# But lets get rid of all writes whatsoever IO.write(arg) -> :okendSaid handler purifies the body of the selected effect, but also augments it with all effects mentioned in the handler’s body. Because we don’t know what values the function can take we always assume the most pessimistic eventuality.
Hence, in this example results of our code block are purified from the IO.write/1 effect; but not purified of IO.puts/1 effect (because it can still be emitted under specific circumstances)
Effect inference
Section titled “Effect inference”To maintain a controllable system in which the effects are values derived from the code it’s imperative to be able to check correctness of effect annotations. Due to [almost] all of the information available at compile time, it is possible to implement an algorithm similar to Hindley-Milner type inference to determine all of the effects and verify their corresponding annotations. For the sake of simplicity it is referred as event-checking in the rest of the documentation.
Example of a compile-time effect-checking error
Section titled “Example of a compile-time effect-checking error”eff print() :: {} # <- This indicates pure computationdef print() do IO.puts "Something"end
# Efx: Compile error at ./lib/example.ex:1# Effect annotation for function Example.print/1 suggest it's a pure function.
# 1 defmodule Example do# 2 eff print :: {}# 3 def print() do
# But the definition says its effects are {IO.puts/1}Example of compile-time effect-checking inference
Section titled “Example of compile-time effect-checking inference”def print() do File.read "file" |> IO.putsend
eff test() :: {IO.puts/1}def test() do print()end
# Efx: Compile error at ./lib/example.ex:8# Effect annotation for function Example.print/1 suggest it emits# {IO.puts/1}
# 7# 8 eff test :: {}# 9 def test() do
# But the definition says its effects are {IO.puts/1, File.write/1}Explicit Effect control
Section titled “Explicit Effect control”When you require a part of code to be explicitly effect controlled out of your codebase you can use effects do...end construct. Which asserts all of the code is handled by Efx.
Example
Section titled “Example” effects do
test "Make sure the code is pure" do # This construct will be effect-checked even though out of main project's source-files IO.puts "A" end
endendDifficulties
Section titled “Difficulties”Inference of functions sent as messages
Section titled “Inference of functions sent as messages”Erlang’s processes system allows to send an anonymous function as a regular value to the process via Process.send/2. This proves impossible to figure out effects of a function at compile time if it receives an effect-full function via message passing.