kanren: Relational Programming in Python

repository·main·Indexed 42 days ago

https://github.com/pythological/kanren

A Python implementation of miniKanren providing a framework for logic and relational programming. It serves as a low-level core for symbolic computation, computer algebra systems, and automated software generation. Key features include unification via `unify`, goal composition with `lall` and `lany`, and the `run` function for executing logical programs. It also includes tools for symbolic algebra reductions using `reduceo` and `walko` from `kanren.graph`, as well as constraints like `neq` and `isinstanceo`.

Tokens
3.3K
Snippets
16
Records
18
Agent score
53%

What's inside kanren

  1. Work with Goals and Goal Constructors

    main

    A Goal is a function that takes a substitution and returns a stream (generator) of substitutions.

    Users typically do not write goals directly; instead, they use Goal Constructors (like membero or eq) to build them. When you call a goal constructor, it returns a goal that, when executed with a substitution, yields all possible ways to satisfy that logical constraint.

    # Creating a goal using a constructor
    g = membero(x, (1, 2, 3))
    
    # Executing the goal with an empty substitution
    for s in g({}):
        print(s)  # Yields {~x: 1}, {~x: 2}, {~x: 3}
  2. Understand Terms and Variables in miniKanren

    main

    In miniKanren, a Term is the fundamental data unit. Terms can be:

    • Any standard Python object (e.g., 1, [1, 2], 'cat').
    • A logic variable created using var().
    • A combination of the two (e.g., a tuple containing constants and variables like (1, x, 'cat')).

    Think of terms as trees where the leaves are either constants or logic variables.

  3. Extending kanren with custom types

    main
    To support pattern matching on user-defined types, kanren leverages the logical-unification library. If a type implements the unification interface required by logical-unification, it can be used seamlessly with most kanren goals.
  4. Perform symbolic algebra reductions with `reduceo` and `walko`

    main

    You can use kanren to perform relational symbolic algebra by combining reduceo and walko from kanren.graph.

    • reduceo(reduction_goal): Creates a fixed-point-producing relation that repeatedly applies a single reduction rule until no more can be applied.
    • walko(rator_goal, null_type): A relation that walks term graphs (composed of operator and operand combinations) and ensures the output matches the specified null_type (e.g., ExpressionTuple).

    By combining them using partial, you can create a powerful engine for both reducing complex terms and expanding grounded terms into their possible symbolic ancestors.

    from functools import partial
    from kanren.graph import walko, reduceo
    
    # Create a recursive reduction relation
    math_reduceo = partial(reduceo, single_math_reduceo)
    
    # Create a term-walking relation
    term_walko = partial(walko, rator_goal=eq, null_type=ExpressionTuple)
  5. How logic conjunction (lall) and disjunction (lany) work

    main

    In kanren, you can combine multiple goals to create complex relations:

    • Conjunction (lall): All goals must be satisfied (logical AND). This is often used implicitly when passing multiple goals to run.
    • Disjunction (lany): At least one of the goals must be satisfied (logical OR).

    Example of finding values that satisfy multiple membership constraints:

    from kanren import run, membero, var
    
    x = var()
    # Find x such that x is in (1, 2, 3) AND x is in (2, 3, 4)
    results = run(0, x, membero(x, (1, 2, 3)), membero(x, (2, 3, 4)))
    print(results)  # (2, 3)
  6. Creating custom goal constructors

    main

    You can define your own relations by writing functions that return goal combinations (typically using lall). This allows you to build complex logic on top of existing relations.

    from kanren import Relation, facts, run, var, lall
    
    parent = Relation()
    facts(parent, ("Homer", "Bart"), ("Homer", "Lisa"), ("Abe", "Homer"))
    
    def grandparent(x, z):
        y = var()
        return lall(parent(x, y), parent(y, z))
    
    x = var()
    # Who is Bart's grandparent?
    print(run(1, x, grandparent(x, 'Bart')))  # ('Abe',)
  7. Filter expanded terms using `conso`

    main

    When performing expansions, you can use kanren.goals.conso to constrain the structure of the resulting terms. For example, you can force the expanded_term to have a specific operator (like log) or ensure it is not a nullary function by constraining its cdr (the operand part).

    from kanren.goals import conso
    from kanren import run, var
    
    expanded_term = var()
    reduced_term = var()
    exp_term_cdr = var()
    
    # Constraint: expanded_term must start with 'log' AND have a non-empty operand
    res = run(10, [expanded_term, reduced_term],
              conso(log, exp_term_cdr, expanded_term),
              conso(var(), var(), exp_term_cdr),
              term_walko(math_reduceo, expanded_term, reduced_term))
  8. Install miniKanren

    main

    You can install kanren (the Python implementation of miniKanren) using pip or conda.

    # Using pip
    pip install miniKanren
    
    # Using conda
    conda install -c conda-forge miniKanren
  9. Basic usage of kanren: run, var, and eq

    main

    To perform logic programming, you use var() to create logic variables and run() to execute queries. The eq(a, b) goal constructor is used for unification (checking if a and b can be made equal).

    from kanren import run, eq, var
    
    x = var()
    # Find values of x such that x == 5
    results = run(1, x, eq(x, 5))
    print(results)  # (5,)
  10. Reduce an algebraic expression

    main

    To reduce a specific term to its simplest form, define the expanded_term (the starting expression) and a reduced_term (a logic variable to hold the result). Use run with term_walko and your reduction relation.

    Example: Reducing add(add(3, 3), exp(log(exp(5)))) using a custom math_reduceo relation.

    from kanren import run, var
    
    # The term to reduce
    expanded_term = etuple(add, etuple(add, 3, 3), etuple(exp, etuple(log, etuple(exp, 5))))
    # Logic variable for the result
    reduced_term = var()
    
    # Execute reduction
    res = run(3, reduced_term, term_walko(math_reduceo, expanded_term, reduced_term))
  11. Expand a reduced term into symbolic ancestors

    main

    Because kanren is relational, you can perform 'expansion' by providing a grounded reduced_term and an ungrounded expanded_term (a logic variable). This asks the engine: "What expressions would reduce to this result?"

    Note: Since there can be infinitely many expansions, use a finite number in run to limit results.

    expanded_term = var()
    reduced_term = etuple(mul, 2, 5)
    
    # Ask for 10 possible expansions of mul(2, 5)
    res = run(10, expanded_term, term_walko(math_reduceo, expanded_term, reduced_term))
  12. Unify two terms using the `unify` function

    main

    Unification is the process of finding a substitution (a mapping of variables to values) that makes two terms equal.

    The unify function takes two terms u and v, and an optional initial substitution s. It returns a new substitution if the terms can be unified, or False if they conflict.

    Key behaviors:

    • Unifying identical constants returns an empty substitution {}.
    • Unifying a variable with a constant returns a substitution mapping that variable to the constant.
    • If an initial substitution is provided, unify ensures the new unification is consistent with existing mappings. If a conflict occurs (e.g., a variable is already mapped to 2 but you try to unify it with 3), it returns False.
    # Normal case
    unify((1, 2), (1, x), {})
    # Returns: {~x: 2}
    
    # Consistent with existing substitution
    unify((1, 2), (1, x), {x: 2})
    # Returns: {~x: 2}
    
    # Conflict with existing substitution
    unify((1, 2), (1, x), {x: 3})
    # Returns: False