Navigation

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);