Getting Started
Installation
Add nix-effects as a flake input:
{
inputs.nix-effects.url = "github:kleisli-io/nix-effects";
outputs = { nix-effects, nixpkgs, ... }:
let
fx = nix-effects.lib;
in {
# fx.types, fx.run, fx.send, fx.bind, fx.effects, fx.stream ...
};
}
Or import directly without flakes:
let fx = import ./path/to/nix-effects { lib = nixpkgs.lib; };
in ...
Your first type
Define a type with fx.types.refined:
let
inherit (fx.types) Int refined;
Port = refined "Port" Int (x: x >= 1 && x <= 65535);
in {
# Kernel decision procedure — fast boolean check
ok = Port.check 8080; # true
bad = Port.check 99999; # false
# Effectful validate — runs through the trampoline, produces blame context
result = fx.run (Port.validate 99999)
fx.effects.typecheck.collecting [];
# result.state = [ { context = "Port"; message = "Expected Port, got int"; ... } ]
}
Your first dependent type
One field's type depends on another field's value — this is a genuine dependent type, checked by the MLTT proof-checking kernel:
let
inherit (fx.types) Bool Int String ListOf DepRecord refined;
FIPSCipher = refined "FIPSCipher" String
(x: builtins.elem x [ "AES-256-GCM" "AES-192-GCM" "AES-128-GCM" "AES-256-CBC" "AES-128-CBC" ]);
ServiceConfig = DepRecord [
{ name = "fipsMode"; type = Bool; }
{ name = "cipherSuites"; type = self:
if self.fipsMode then ListOf FIPSCipher else ListOf String; }
];
in {
ok = ServiceConfig.checkFlat { fipsMode = true; cipherSuites = [ "AES-256-GCM" ]; }; # true
bad = ServiceConfig.checkFlat { fipsMode = true; cipherSuites = [ "3DES" ]; }; # false
}
Your first effect
Write a computation, then choose the handler:
let
inherit (fx) pure bind run;
inherit (fx.effects) get put;
# Double the state
doubleState = bind get (s: bind (put (s * 2)) (_: pure s));
result = run doubleState fx.effects.state.handler 21;
# result.value = 21 (old state returned)
# result.state = 42 (state doubled)
in result
Running the showcase
The repo includes a working end-to-end demo:
git clone https://github.com/kleisli-io/nix-effects
cd nix-effects
# Valid config — build succeeds
nix build .#cryptoService
# Invalid config (3DES in FIPS mode) — caught at eval time
nix build .#buggyService
# error: Type errors in ServiceConfig:
# - List[FIPSCipher][3]: "3DES" is not a valid FIPSCipher
# Run all tests
nix flake check
The kernel behind every type
Every .check call runs the MLTT type-checking kernel. When you write
Port.check 8080, the kernel elaborates 8080 into a term,
type-checks it, and returns a boolean. This is not a separate system —
it is what .check does.
You can also write verified implementations using HOAS combinators:
let
H = fx.types.hoas;
v = fx.types.verified;
# Write a function in HOAS, kernel type-checks it, extract as Nix function
succ = v.verify (H.forall "x" H.nat (_: H.nat))
(v.fn "x" H.nat (x: H.succ x));
in
succ 5 # 6 — a certified Nix function
The kernel checks the implementation against its type at nix eval time.
If the types don't match, you get an error before anything builds.
See the Kernel Architecture chapter for the
full pipeline.
What's in the box
The fx attrset is the entire public API:
| Namespace | Contents |
|---|---|
fx.pure, fx.bind, fx.send, fx.map, fx.seq | Freer monad kernel |
fx.run, fx.handle | Trampoline interpreter |
fx.adapt, fx.adaptHandlers | Handler composition |
fx.types.* | Type system (primitives, constructors, dependent, refinement, universe) |
fx.types.hoas | HOAS surface combinators for the kernel |
fx.types.elaborateType, etc. | Elaboration bridge: fx.types ↔ kernel |
fx.types.verified | Convenience combinators for writing verified implementations |
fx.effects.* | Built-in effects (state, error, reader, writer, acc, choice, conditions, typecheck, linear) |
fx.stream.* | Effectful lazy sequences |