Skip to content

Commit c66811a

Browse files
Add a condp implementation
1 parent ddf3607 commit c66811a

File tree

2 files changed

+282
-0
lines changed

2 files changed

+282
-0
lines changed

kanren/condp.py

Lines changed: 167 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,167 @@
1+
from itertools import tee
2+
from typing import Mapping, Optional, Sequence, Tuple, Union
3+
4+
from cons import car, cdr
5+
from toolz import interleave
6+
from unification import reify
7+
from unification.utils import transitive_get as walk
8+
9+
from .core import lall, lconj_seq, ldisj_seq
10+
11+
12+
def collect(s: Mapping, f_lists: Optional[Sequence] = None):
13+
"""A function that produces suggestions (for `condp`) based on the values of
14+
partially reified terms.
15+
16+
This goal takes a list of suggestion function, variable pairs lists and
17+
evaluates them at their current, partially reified variable values
18+
(i.e. ``f(walk(x, s))`` for pair ``(f, x)``). Each evaluated function should
19+
return ``None``, a string label in a corresponding `condp` clause, or the
20+
string ``"use-maybe"``.
21+
22+
Each list of suggestion functions is evaluated in order, their output is
23+
concatenated, and, if the output contains a ``"use-maybe"`` string, the
24+
next list of suggestion functions is evaluated.
25+
26+
Parameters
27+
==========
28+
s
29+
miniKanren state.
30+
f_lists
31+
A collection of function + variable pair collections (e.g.
32+
``[[(f0, x0), ...], ..., [(f, x), ...]]``).
33+
"""
34+
if isinstance(f_lists, Sequence):
35+
# TODO: Would be cool if this was lazily evaluated, no?
36+
# Seems like this whole thing would have to become a generator
37+
# function, though.
38+
ulos = ()
39+
# ((f0, x0), ...), ((f, x), ...)
40+
for f_list in f_lists:
41+
f, args = car(f_list), cdr(f_list)
42+
_ulos = f(*(walk(a, s) for a in args))
43+
ulos += _ulos
44+
if "use-maybe" not in _ulos:
45+
return ulos
46+
return ulos
47+
else:
48+
return ()
49+
50+
51+
def condp(global_sugs: Tuple, branches: Union[Sequence, Mapping]):
52+
"""A goal generator that produces a `conde`-like relation driven by
53+
suggestions potentially derived from partial miniKanren state values.
54+
55+
From [1]_.
56+
57+
Parameters
58+
==========
59+
global_sugs
60+
A tuple containing tuples of suggestion functions and their
61+
logic variable arguments. Each suggestion function is evaluated
62+
using the reified versions of its corresponding logic variables (i.e.
63+
their "projected" values). Each suggestion function is expected to
64+
return a tuple of branch labels corresponding to the keys in
65+
`branches`.
66+
branches
67+
Sequence or mapping of string labels--for each branch in a conde-like
68+
goal--to a tuple of goals pairs.
69+
70+
71+
.. [1] Boskin, Benjamin Strahan, Weixi Ma, David Thrane Christiansen, and Daniel
72+
P. Friedman, "A Surprisingly Competitive Conditional Operator."
73+
74+
"""
75+
if isinstance(branches, Mapping):
76+
branches_: Sequence = tuple(branches.items())
77+
else:
78+
branches_ = branches
79+
80+
def _condp(s):
81+
global_los = collect(s, global_sugs)
82+
yield from ldisj_seq(lconj_seq(g) for k, g in branches_ if k in global_los)(s)
83+
84+
return _condp
85+
86+
87+
def collectseq(branch_s: Mapping, f_lists: Optional[Sequence] = None):
88+
"""A version of `collect` that takes a `dict` of branches-to-states.
89+
90+
Parameters
91+
==========
92+
branch_s
93+
Branch labels to miniKanren state/replacements dictionaries.
94+
f_lists
95+
A collection of function + variable pair collections (e.g.
96+
``[[(f0, x0), ...], ..., [(f, x), ...]]``).
97+
"""
98+
if isinstance(f_lists, Sequence):
99+
ulos = ()
100+
for f_list in f_lists:
101+
f, args = f_list
102+
_ulos = f({k: reify(args, s) for k, s in branch_s.items()})
103+
ulos += _ulos
104+
if "use-maybe" not in _ulos:
105+
return ulos
106+
return ulos
107+
else:
108+
return ()
109+
110+
111+
def condpseq(branches: Union[Sequence[Sequence], Mapping]):
112+
r"""An experimental version of `condp` that passes branch-state-reified
113+
maps to branch-specific suggestion functions.
114+
115+
In other words, each branch-specific suggestion function is passed a `dict`
116+
with branch-label keys and the its function arguments are reified against
117+
the state resulting from said branch.
118+
119+
.. note::
120+
121+
Only previously evaluated branches will show up in these `dict`\s, so
122+
branch order will determine the information available to each suggestion
123+
function.
124+
125+
Parameters
126+
==========
127+
branches
128+
Ordered map or a sequence of sequences mapping string labels--for each
129+
branch in a `conde`-like goal--to a tuple starting with a single
130+
suggestion function followed by the branch goals.
131+
132+
"""
133+
if isinstance(branches, Mapping):
134+
branches_: Sequence = tuple(branches.items())
135+
else:
136+
branches_ = branches
137+
138+
def _condpseq(s, __bm=branches_):
139+
__bm, local_items = tee(__bm)
140+
141+
# Provide each branch-specific suggestion function a copy of the state
142+
# after the preceding branch's goals have been evaluated.
143+
def f(items):
144+
los = set()
145+
branch_s = {}
146+
for k, goals_branch_sugs in local_items:
147+
# Branch suggestions can be `None` and all branch
148+
# goals will be added.
149+
branch_sugs = car(goals_branch_sugs)
150+
goals = cdr(goals_branch_sugs)
151+
152+
if branch_sugs:
153+
# We only expect one suggestion function per-branch.
154+
branch_sugs = (branch_sugs,)
155+
los |= set(collectseq(branch_s or {k: s}, branch_sugs))
156+
157+
if branch_sugs is None or k in los:
158+
# TODO: Refactor!
159+
a, b = tee(lall(*goals)(s))
160+
branch_s[k] = next(a)
161+
yield b
162+
163+
branch_s.setdefault(k, None)
164+
165+
yield from interleave(f(local_items))
166+
167+
return _condpseq

tests/test_condp.py

Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
1+
from cons import car, cons
2+
from cons.core import ConsNull, ConsPair
3+
from unification import isvar, var
4+
5+
from kanren.condp import condp, condpseq
6+
from kanren.core import Zzz, conde, eq, run
7+
from kanren.goals import nullo
8+
9+
10+
def test_condp():
11+
"""Test `condp` using the example from [1]_.
12+
13+
.. [1] Boskin, Benjamin Strahan, Weixi Ma, David Thrane Christiansen, and Daniel
14+
P. Friedman, "A Surprisingly Competitive Conditional Operator."
15+
16+
"""
17+
18+
def _ls_keys(ls):
19+
if isvar(ls):
20+
return ("use-maybe",)
21+
elif isinstance(ls, ConsNull):
22+
return ("BASE",)
23+
elif isinstance(ls, ConsPair):
24+
return ("KEEP", "SWAP")
25+
else:
26+
return ()
27+
28+
def _o_keys(o):
29+
if isvar(o):
30+
return ("BASE", "KEEP", "SWAP")
31+
elif isinstance(o, ConsNull):
32+
return ("BASE",)
33+
elif isinstance(o, ConsPair):
34+
if isvar(car(o)) or "novel" == car(o):
35+
return ("KEEP", "SWAP")
36+
else:
37+
return ("KEEP",)
38+
else:
39+
return ()
40+
41+
def swap_somep(ls, o):
42+
a, d, res = var(), var(), var()
43+
res = condp(
44+
((_ls_keys, ls), (_o_keys, o)),
45+
{
46+
"BASE": (nullo(ls), nullo(o)),
47+
"KEEP": (
48+
eq(cons(a, d), ls),
49+
eq(cons(a, res), o),
50+
Zzz(swap_somep, d, res),
51+
),
52+
"SWAP": (
53+
eq(cons(a, d), ls),
54+
eq(cons("novel", res), o),
55+
Zzz(swap_somep, d, res),
56+
),
57+
},
58+
)
59+
return res
60+
61+
def swap_someo(ls, o):
62+
"""The original `conde` version."""
63+
a, d, res = var(), var(), var()
64+
return conde(
65+
[nullo(ls), nullo(o)],
66+
[eq(cons(a, d), ls), eq(cons(a, res), o), Zzz(swap_someo, d, res)],
67+
[eq(cons(a, d), ls), eq(cons("novel", res), o), Zzz(swap_someo, d, res)],
68+
)
69+
70+
q, r = var("q"), var("r")
71+
72+
condp_res = run(0, [q, r], swap_somep(q, ["novel", r]))
73+
74+
assert len(condp_res) == 4
75+
assert condp_res[0][0][0] == "novel"
76+
assert isvar(condp_res[0][0][1])
77+
assert isvar(condp_res[0][1])
78+
79+
assert isvar(condp_res[1][0][0])
80+
assert isvar(condp_res[1][0][1])
81+
assert isvar(condp_res[1][1])
82+
83+
assert condp_res[2][0][0] == "novel"
84+
assert isvar(condp_res[2][0][1])
85+
assert condp_res[2][1] == "novel"
86+
87+
assert isvar(condp_res[3][0][0])
88+
assert isvar(condp_res[3][0][1])
89+
assert condp_res[3][1] == "novel"
90+
91+
92+
def test_condpseq():
93+
def base_sug(a_branches):
94+
if a_branches["BRANCH1"] == 1:
95+
return ("BRANCH3",)
96+
else:
97+
return (
98+
"BRANCH2",
99+
"BRANCH3",
100+
)
101+
102+
def test_rel(a):
103+
return condpseq(
104+
{
105+
"BRANCH1": (None, eq(a, 1)),
106+
"BRANCH2": ((base_sug, a), eq(a, 2)),
107+
"BRANCH3": (None, eq(a, 3)),
108+
}
109+
)
110+
111+
q = var("q")
112+
113+
res = run(0, [q], test_rel(q))
114+
115+
assert res == ([1], [3])

0 commit comments

Comments
 (0)