Handler-Swap Validation
The computation is built once from a typed record validator. Only the handler changes. That makes the effect boundary visible: collecting accumulates every validation error, logging records each check, and strict aborts at the first failure.
Define a typed boundary
The type describes a network configuration with positive numeric
fields. badConfig intentionally violates multiple checks.
Pos = refined "Pos" Int (x: x > 0);
Network = Record {
hostName = String;
port = Pos;
interfaces = ListOf (Record { name = String; mtu = Pos; });
};
badConfig = {
hostName = "kleisli.io";
port = (-1);
interfaces = [
{ name = "eth0"; mtu = (-50); }
{ name = 42; mtu = 1500; }
{ name = "eth2"; mtu = "big"; }
];
};
Swap the handler
Network.validate badConfig returns an effectful computation. The
same computation can be interpreted by collecting, logging, or strict
handlers without rebuilding the validator.
comp = Network.validate badConfig;
runWith = handlers: state: fx.handle { inherit handlers state; } comp;
collecting =
let
r = runWith fx.effects.typecheck.collecting [ ];
line = e: " ${renderPath e} :: expected ${e.typeName}, got ${e.actual}";
in
"${toString (builtins.length r.state)} error(s):\n"
+ builtins.concatStringsSep "\n" (map line r.state);
logging =
let
r = runWith fx.effects.typecheck.logging [ ];
line = e: " ${if e.passed then "pass" else "fail"} ${renderPath e} : ${e.typeName}";
in
builtins.concatStringsSep "\n" (map line r.state);