The zebra puzzle

hello nixperts :vulcan_salute:,

i tried to naively translate microkanren
to solve the zebra puzzle in nix.
very important for packaging (/s).

flake.nix
{
  description = "solve zebra puzzle with microkanren in nix";

  inputs = {
    nixpkgs.url =
      "github:NixOS/nixpkgs/10069ef4cf863633f57238f179a0297de84bd8d3";
  };

  outputs = { self, nixpkgs, ... }: {
    microkanren = with nixpkgs.lib;
      # http://webyrd.net/scheme-2013/papers/HemannMuKanren2013.pdf
      let
        # logic variables
        mkVar = int: { __Var = int; };
        isVar = term: term ? __Var;
        eqVar = a: b: a == b;

        # arrays (instead of pairs)
        isArray = term: (isList term) && (term != [ ]);

        # substitutions
        occurs-check = var: val: sub:
          let v = walk val sub;
          in if isVar v then
            eqVar v var
          else if isArray v then
            occurs-check var (head v) sub || occurs-check var (tail v) sub
          else
            false;
        extendSub = var: val: sub:
          if occurs-check var val sub then
            false
          else
            sub // { ${toString var.__Var} = val; };
        emptySub = { };

        # unification
        walk = term: sub:
          let
            lookup = var: sub: sub ? "${toString var.__Var}";
            binding = if isVar term then lookup term sub else false;
          in if binding then walk (sub."${toString term.__Var}") sub else term;
        unify = A: B: sub:
          let
            a = walk A sub;
            b = walk B sub;
          in if (isVar a) && (isVar b) && (eqVar a b) then
            sub
          else if (isVar a) then
            extendSub a b sub
          else if (isVar b) then
            extendSub b a sub
          else if (isArray a) && (isArray b) then
            let
              headSub = unify (head a) (head b) sub;
              tailSub = unify (tail a) (tail b) headSub;
            in (if headSub != false then tailSub else false)
          else if (a == b) then
            sub
          else
            false;

        # logic stream
        emptyState = {
          sub = emptySub;
          counter = 0;
        };
        isEmptyStream = x: x == { };
        isImmatureStream = x: x ? force;
        delay = x: { force = x; };

        mzero = { };
        unit = state: {
          head = state;
          tail = mzero;
        };
        mplus = s1: s2:
          if (isEmptyStream s1) then
            s2
          else if (isImmatureStream s1) then
            delay (mplus s2 s1.force)
          else {
            inherit (s1) head;
            tail = mplus s1.tail s2;
          };
        bind = stream: goal:
          if (isEmptyStream stream) then
            mzero
          else if (isImmatureStream stream) then
            delay (bind stream.force goal)
          else
            mplus (goal stream.head) (bind stream.tail goal);

        # single constraint
        eqq = a: b: state:
          let sub = unify a b state.sub;
          in if sub != false then
            unit {
              inherit sub;
              inherit (state) counter;
            }
          else
            mzero;

        # core goals
        disj-i = g1: g2: state: mplus (g1 state) (g2 state);
        conj-i = g1: g2: state: bind (g1 state) g2;
        conj = goals:
          if (length goals) == 1 then
            head goals
          else
            conj-i (head goals) (conj (tail goals));
        disj = goals:
          if (length goals) == 1 then
            head goals
          else
            disj-i (head goals) (disj (tail goals));
        conde = clauses: disj (map (clause: conj clause) clauses);
        fresh = f: state:
          let
            vars = foldlAttrs (acc: n: v: {
              counter = acc.counter + 1;
              vars = acc.vars // { ${n} = mkVar acc.counter; };
            }) {
              counter = state.counter;
              vars = { };
            } (functionArgs f);
          in (conj (f vars.vars)) {
            inherit (state) sub;
            inherit (vars) counter;
          };

        # stream helpers
        pull = stream:
          if isImmatureStream stream then pull stream.force else stream;
        take-all = Stream:
          let stream = pull Stream;
          in if stream == { } then
            [ ]
          else
            [ stream.head ] ++ (take-all stream.tail);
        take = num: Stream:
          if num == 0 then
            [ ]
          else
            let stream = pull Stream;
            in if isEmptyStream stream then
              [ ]
            else
              [ stream.head ] ++ (take (num - 1) stream.tail);

        # reify
        mkReify = vars: states: map (reify-state-vars vars) states;
        reify-state-vars = vars: state:
          let mkVal = n: walk-h (mkVar n) state.sub;
          in (foldlAttrs (acc: n: v:
            let val = mkVal acc.counter;
            in {
              counter = acc.counter + 1;
              vars = acc.vars // {
                ${n} = walk-h val (reify-sub val emptySub);
              };
            }) {
              counter = 0;
              vars = { };
            } vars).vars;
        reify-sub = v: sub:
          let
            val = walk v sub;
            name = reify-name (length (attrNames sub));
          in if isVar val then
            extendSub val name sub
          else if isArray val then
            reify-sub (tail val) (reify-sub (head val) sub)
          else
            sub;
        reify-name = n: "__${toString n}";
        walk-h = v: sub:
          let val = walk v sub;
          in if isVar val then
            val
          else if isArray val then
            [ (walk-h (head val) sub) ] ++ (walk-h (tail val) sub)
          else
            val;

        # run
        solve = all-or-num: goals:
          let stream = (fresh goals) emptyState;
          in mkReify (functionArgs goals) (if all-or-num == "all" then
            take-all stream
          else
            take all-or-num stream);

        # helper goals
        facto = vals: X: disj (map (x: eqq x X) vals);
        conso = H: T: P: (eqq ([ H ] ++ [ T ]) P);
        fail = state: mzero;
        succeed = unit;

      in {
        # goals
        inherit fresh conde conj disj eqq facto conso fail succeed;
        # interface
        inherit solve;
      };

    zebra = with self.microkanren;
      # https://en.wikipedia.org/wiki/Zebra_Puzzle
      let
        color = [ "red" "blue" "green" "ivory" "yellow" ];
        nationality = [ "englishman" "spaniard" "ukrainian" "norweigian" "japanese" ];
        drink = [ "coffee" "tea" "milk" "orange juice" "water" ];
        smoke = [ "old gold" "kools" "chesterfields" "lucky strike" "parliaments" ];
        pet = [ "dog" "snails" "fox" "horse" "zebra" ];

        next = A: B:
          conde [
            [ (eqq A 1) (eqq B 2) ]
            [ (eqq A 2) ((facto [ 1 3 ]) B) ]
            [ (eqq A 3) ((facto [ 2 4 ]) B) ]
            [ (eqq A 4) ((facto [ 3 5 ]) B) ]
            [ (eqq A 5) (eqq B 4) ]
          ];
        right = A: B:
          conde [
            [ (eqq A 1) ((facto [ 2 3 4 5 ]) B) ]
            [ (eqq A 2) ((facto [ 3 4 5 ]) B) ]
            [ (eqq A 3) ((facto [ 4 5 ]) B) ]
            [ (eqq A 4) (eqq B 5) ]
            [ (eqq A 5) fail ]
          ];

        houseNumber = O: H:
          fresh ({ C, N, D, S, P }: [
            (eqq H [ O C N D S P ])
          ]);
        houseColor = C: H:
          fresh ({ O, N, D, S, P }: [
            (eqq H [ O C N D S P ])
          ]);
        houseNat = N: H:
          fresh ({ O, C, D, S, P }: [
            (eqq H [ O C N D S P ])
          ]);
        houseSmoke = S: H:
          fresh ({ O, C, N, D, P }: [
            (eqq H [ O C N D S P ])
          ]);
        housePet = P: H:
          fresh ({ O, C, N, D, S }: [
            (eqq H [ O C N D S P ])
          ]);
        houseDrink = D: H:
          fresh ({ O, C, N, S, P }: [
            (eqq H [ O C N D S P ])
          ]);

        membero = r: X: Tuple:
          fresh ({ A1, A2, A3, A4, A5, H1, H2, H3, H4, H5 }: [
            (eqq Tuple [ H1 H2 H3 H4 H5 ])
            (r A1 H1)
            (r A2 H2)
            (r A3 H3)
            (r A4 H4)
            (r A5 H5)
            (disj [ (eqq A1 X) (eqq A2 X) (eqq A3 X) (eqq A4 X) (eqq A5 X) ])
          ]);
        unique = set: r: array: conj (map (x: membero r x array) set);

        block = B:
          fresh ({ H1, H2, H3, H4, H5 }: [
            (eqq B [ H1 H2 H3 H4 H5 ])
            (houseNumber 1 H1)
            (houseNumber 2 H2)
            (houseNumber 3 H3)
            (houseNumber 4 H4)
            (houseNumber 5 H5)
          ]);

        require = Block: H:
          fresh ({ H1, H2, H3, H4, H5 }: [
            (eqq Block [ H1 H2 H3 H4 H5 ])
            (disj [ (eqq H H1) (eqq H H2) (eqq H H3) (eqq H H4) (eqq H H5) ])
          ]);

        puzzle = B:
          conj [
            (block B)
            # The Englishman lives in the red house.
            (fresh ({ O, D, S, P }: [
                (require B [ O "red" "englishman" D S P ])
            ]))
            # The Spaniard owns the dog.
            (fresh ({ O, C, D, S }: [
                (require B [ O C "spaniard" D S "dog" ])
            ]))
            # Coffee is drunk in the green house.
            (fresh ({ O, N, S, P }: [
              (require B [ O "green" N "coffee" S P ])
            ]))
            # The Ukrainian drinks tea.
            (fresh ({ O, C, N, S, P }: [
              (require B [ O C "ukrainian" "tea" S P ])
            ]))
            # The green house is immediately to the right of the ivory house.
            (fresh ({ O1, O2, N1, N2, D1, D2, S1, S2, P1, P2 }: [
              (require B [ O1 "ivory" N2 D2 S2 P2 ])
              (require B [ O2 "green" N1 D1 S1 P1 ])
              (next O1 O2)
              (right O1 O2)
            ]))
            # The Old Gold smoker owns snails.
            (fresh ({ O, C, N, D, S, P }: [
              (require B [ O C N D "old gold" "snails" ])
            ]))
            #  Kools are smoked in the yellow house.
            (fresh ({ O, C, N, D, S, P }: [
              (require B [ O "yellow" N D "kools" P ])
            ]))
            # Milk is drunk in the middle house.
            (fresh ({ O, C, N, D, S, P }: [
              (require B [ 3 C N "milk" S P ])
            ]))
            # The Norwegian lives in the first house.
            (fresh ({ O, C, N, D, S, P }: [
              (require B [ 1 C "norweigian" D S P ])
            ]))
            # The man who smokes Chesterfields lives in the house next to the man with the fox.
            (fresh ({ O1, O2, C1, C2, N1, N2, D1, D2, S1, S2, P1, P2 }: [
              (require B [ O1 C1 N1 D1 "chesterfields" P1 ])
              (require B [ O2 C2 N2 D2 S1 "fox" ])
              (next O1 O2)
            ]))
            # Kools are smoked in the house next to the house where the horse is kept.
            (fresh ({ O1, O2, C1, C2, N1, N2, D1, D2, S1, S2, P1, P2 }: [
              (require B [ O1 C1 N1 D1 "kools" P1 ])
              (require B [ O2 C2 N2 D2 S1 "horse" ])
              (next O1 O2)
            ]))
            # The Lucky Strike smoker drinks orange juice.
            (fresh ({ O, C, N, D, S, P }: [
              (require B [ O C N "orange juice" "lucky strike" P ])
            ]))
            # The Japanese smokes Parliaments.
            (fresh ({ O, C, N, D, S, P }: [
              (require B [ O C "japanese" D "parliaments" P ])
            ]))
            # The Norwegian lives next to the blue house.
            (fresh ({ O1, O2, C1, C2, N1, N2, D1, D2, S1, S2, P1, P2 }: [
              (require B [ O1 C1 "norweigian" D1 S1 P1 ])
              (require B [ O2 "blue" N2 D2 S2 P2 ])
              (next O1 O2)
            ]))

            (unique color houseColor B)
            (unique nationality houseNat B)
            (unique drink houseDrink B)
            (unique smoke houseSmoke B)
            (unique pet housePet B)
          ];
      in { solution = solve "all" ({ X }: [ (puzzle X) ]); };
  };
}

nix eval .#zebra.solution --json | jq

thought it was fun so sharing.
happy hacking.

7 Likes

Looks like this has the potential to be added to my Nix benchmarking suite (which now only includes hanoi)

1 Like

Perhaps you could also include advent of code solutions people have published.

@Aleksanaa In the long run, it would be nice to upstream such benchmarks to cppnix and other implementations. Is this something that you plan on doing? I’ve thought a lot about coming up with something like llvm-test-suite, but for nix language.

Now the status quo for cppnix is that changes that affect the evaluator do pretty much ad-hoc testing. Last I looked Lix also had a pretty rudimentary suite of benchmarks that boiled down to doing a search over nixpkgs with or without GC.