Package: microkanren
microkanrenmicrokanrenis an implementation of a miniKanren style relational programming language, embedded in Python. The solver is implemented in the style of μKanren[^1]. It provides a framework for extending the language with constraints, as well as a basic implementation of disequality and finite domain constraints, in the style of cKanren[^2].Due to the differences between Python and the reference implementation languages (Scheme, Racket), some divergences from the typical miniKanren API are necessary. It is a goal to capture the spirit of the miniKanren language family, but not the exact API.InstallationUsageBasic usageConjunction and disjunctionThe result type and multiple top-level variablesDefining goal constructorsRecursive goal constructors andsnooze(Zzz)Developing microkanrenInstallationpipinstallmicrokanrenUsageBasic usageThe basic goal constructor iseq.eqtakes two terms as arguments, and returns a goal that will succeed if the terms can be unified, and fails otherwise.>>> from microkanren import eq
>>> eq("🍕", "🍕")
<microkanren.core.Goal object at 0x7f07d85cced0>To run a goal, use one of the provided interfaces:run,run_all, orirun.runtakes two arguments:an integer, the maximum number of results to return; anda callable with positional-only arguments, each of which will receive a fresh logic variable.run_allandiruntake a single argument, the fresh-var-receiver.>>> from microkanren import run
>>> run(1, lambda x: eq(x, "🍕"))
['🍕']The return type ofrunandrun_allis a (possibly-empty) list of results. If the list is empty, there are no solutions that satisfy the goal.irunreturns a generator that yields single results.>>> from microkanren import irun
>>> rs = irun(lambda x: eq(x, "😁"))
>>> next(rs)
'😁'
>>> next(rs)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
StopIterationConjunction and disjunctionConjunction and disjunction are provided by the varargconjanddisjfunctions.Goalobjects support combination using|and&operators, which map toconjanddisj.>>> from microkanren import run_all
>>> run_all(lambda x: disj(eq(x, "α"), eq(x, "β"), eq(x, "δ")))
['α', 'β', 'δ']
>>> run_all(lambda x: eq(x, "α") | eq(x, "β") | eq(x, "δ"))
['α', 'β', 'δ']
>>> run_all(lambda x: eq(x, "ω") & eq(x, "ω"))
['ω']
>>> run_all(lambda x: conj(eq(x, "ω"), eq(x, "ω")))
['ω']The result type and multiple top-level variablesIf the fresh-var-receiver provided to an interface has arity 1, results will be single elements. If it has arity > 1, the results will be a tuple of values, each mapping position-wise to the receiver's arguments.>>> run_all(lambda x, y: eq(x, "foo") & eq(y, "bar") | eq(x, "hello") & eq(y, "world"))
[('foo', 'bar'), ('hello', 'world')]Defining goal constructorsCalling goal constructors in your top-level program quickly becomes unwieldy. To mitigate this, you can define your own goal constructors.A goal constructor is a function that takes zero or more arguments, and returns aGoal(or some object that implements theGoalProto).AGoalis a callable that takes aStateand returns aStreamofStateobjects.AStreamis either:empty (mzero);a callable of no arguments that returns aStream(a thunk); ora tuple,(State, Stream).>>> def likes_pizza(person, out):
...     return eq(out, (person, "likes 🍕"))
...
>>> run_all(lambda q: likes_pizza("Jane", q) | likes_pizza("Bill", q))
[('Jane', 'likes 🍕'), ('Bill', 'likes 🍕')]As shown in the above example, it can be convenient to define goals in terms of the combination of other goals. However, if you require access to the current state, you can define the goal returned by your goal constructor explicitly.defmy_constructor(x):def_my_constructor(state):ifthere_is_something_about(x):returnunit(state)returnmzeroreturnGoal(_my_constructor)Wrapping your goal withGoalmeans it will be combinable with other goals using|and&.Recursive goal constructors andsnooze(Zzz)If your goal constructor is directly recursive, it will never terminate.>>> def always_pizza(x):
...     return eq(x, "🍕") | always_pizza(x)
...
>>> run(1, lambda x: always_pizza(x))
...
RecursionError: maximum recursion depth exceeded while calling a Python objectWe providesnoozeto delay the construction of a goal until it is needed. Usingsnoozewe can fixalways_pizzato return an infinite stream of pizza[^3].>>> def always_pizza(x):
...     return eq(x, "🍕") | snooze(always_pizza, x)
...
>>> rs = irun(lambda x: always_pizza(x))
>>> next(rs)
'🍕'
>>> next(rs)
'🍕'
>>> next(rs)
'🍕'
>>> next(rs)
'🍕'Developing microkanrenmicrokanrencurrently requires Python 3.11.git clone git@github.com:jams2/microkanren.gitpip install -e .[dev,testing]Run the tests withpytest.Format code withblackandruff:black.
ruffcheck--fixsrctests[^1]:μKanren: A Minimal Functional Core for Relational Programming (Hemann & Friedman, 2013)[^2]:cKanren: miniKanren with constraints (Alvis et al, 2011)[^3]: original examplefivesfrom the μKanren paper altered here to provide more pizza