Skip to main content

microkanren

microkanren is an implementation of a miniKanren style relational programming language, embedded in Python. The solver is implemented in the style of μKanren1. 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 cKanren2.

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.

Installation

pip install microkanren

Usage

Basic usage

The basic goal constructor is eq. eq takes 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, or irun. run takes two arguments:

  1. an integer, the maximum number of results to return; and
  2. a callable with positional-only arguments, each of which will receive a fresh logic variable.

run_all and irun take a single argument, the fresh-var-receiver.

>>> from microkanren import run
>>> run(1, lambda x: eq(x, "🍕"))
['🍕']

The return type of run and run_all is a (possibly-empty) list of results. If the list is empty, there are no solutions that satisfy the goal. irun returns 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>
StopIteration

Conjunction and disjunction

Conjunction and disjunction are provided by the vararg conj and disj functions. Goal objects support combination using | and & operators, which map to conj and disj.

>>> 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 variables

If 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 constructors

Calling 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 a Goal (or some object that implements the GoalProto).

A Goal is a callable that takes a State and returns a Stream of State objects.

A Stream is either:

  • empty (mzero);
  • a callable of no arguments that returns a Stream (a thunk); or
  • a 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.

def my_constructor(x):
    def _my_constructor(state):
        if there_is_something_about(x):
            return unit(state)
        return mzero

    return Goal(_my_constructor)

Wrapping your goal with Goal means it will be combinable with other goals using | and &.

Recursive goal constructors and snooze (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 object

We provide snooze to delay the construction of a goal until it is needed. Using snooze we can fix always_pizza to return an infinite stream of pizza3.

>>> 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 microkanren

microkanren currently requires Python 3.11.

  1. git clone git@github.com:jams2/microkanren.git
  2. pip install -e .[dev,testing]

Run the tests with pytest.

Format code with black and ruff:

black .
ruff check --fix src tests
  1. μKanren: A Minimal Functional Core for Relational Programming (Hemann & Friedman, 2013) ↩

  2. cKanren: miniKanren with constraints (Alvis et al, 2011) ↩

  3. original example fives from the μKanren paper altered here to provide more pizza ↩

Metadata

Release files for microkanren 0.4.4

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for microkanren 0.4.4
File Size Uploaded Provenance Actions
microkanren-0.4.4.tar.gz 17.1 kB Without provenance Details for microkanren-0.4.4.tar.gz

Built distribution (wheel)

Table of built distributions (wheels) for microkanren 0.4.4
File Interpreter ABI Platform Actions
microkanren-0.4.4-py3-none-any.whl Python 3 none any Details for microkanren-0.4.4-py3-none-any.whl

Total release size: 30.9 kB

Release files / microkanren-0.4.4.tar.gz

Download URL microkanren-0.4.4.tar.gz
Size 17.1 kB
Tags Source
SHA-256 checksum
How to use checksums
bcd84b8471761b5fd2235c1ecb018f3666e1853cd91744ad41190dcd351bddc4
BLAKE2b-256 checksum
How to use checksums
89af6e7cd93df4222a3240c5c8adb9cd205d9618636697ec52c93669d58f1e04
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/4.0.2 CPython/3.11.6

Release files / microkanren-0.4.4-py3-none-any.whl

Download URL microkanren-0.4.4-py3-none-any.whl
Size 13.8 kB
Tags Python 3
SHA-256 checksum
How to use checksums
0071990867b4f0e05a7b0c083a665f07d7b1c97e8c7e59782a81c7730da65bef
BLAKE2b-256 checksum
How to use checksums
d07e9a154eec280072aa3a293e39285a5c6d49538a6bba2c110ef68c29c58d03
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/4.0.2 CPython/3.11.6

Security for microkanren 0.4.4

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

No provenance found

None of the files in this release were uploaded with provenance metadata.

A note on package security

PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.

To learn more about protecting yourself from malicious actors, visit our package security documentation.

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page