From d8c1d75dea169cfe6476dfd607866552ed9e2229 Mon Sep 17 00:00:00 2001 From: "Brandon T. Willard" Date: Sat, 22 Feb 2020 04:06:49 -0600 Subject: [PATCH 1/2] Remove EarlyGoalError and tuple evaluation Closes #10. --- .travis.yml | 1 + Makefile | 2 +- README.md | 38 +-- doc/README.md | 8 - doc/basic.md | 180 ++++++------ doc/differences.md | 73 ----- examples/commutative.py | 23 +- examples/corleone.py | 78 +++--- examples/prime.py | 35 --- examples/states.py | 58 ++-- examples/user_classes.py | 42 +-- kanren/__init__.py | 1 - kanren/arith.py | 82 ------ kanren/assoccomm.py | 15 +- kanren/core.py | 266 +++++------------- kanren/goals.py | 56 +--- kanren/graph.py | 60 +--- kanren/term.py | 54 +++- kanren/util.py | 40 +-- setup.cfg | 1 - tests/test_arith.py | 87 ------ tests/test_assoccomm.py | 19 +- tests/test_constraints.py | 6 +- tests/test_core.py | 276 +++++++++---------- tests/test_goals.py | 18 +- tests/test_graph.py | 145 +++++----- tests/test_sudoku.py | 566 ++++++++------------------------------ tests/test_term.py | 116 ++++++-- tests/test_util.py | 14 - 29 files changed, 798 insertions(+), 1562 deletions(-) delete mode 100644 doc/README.md delete mode 100644 doc/differences.md delete mode 100644 examples/prime.py delete mode 100644 kanren/arith.py delete mode 100644 tests/test_arith.py diff --git a/.travis.yml b/.travis.yml index 6a2ab76..0fa2179 100644 --- a/.travis.yml +++ b/.travis.yml @@ -15,6 +15,7 @@ install: - pip install -r requirements.txt script: + - pydocstyle kanren/ - pylint kanren/ tests/ - if [[ `command -v black` ]]; then black --check kanren tests; diff --git a/Makefile b/Makefile index 94add80..3e61119 100644 --- a/Makefile +++ b/Makefile @@ -46,7 +46,7 @@ docstyle: format: @printf "Checking code style with black...\n" - black --check kanren/ + black --check kanren/ tests/ @printf "\033[1;34mBlack passes!\033[0m\n\n" style: diff --git a/README.md b/README.md index 51edca8..893b830 100644 --- a/README.md +++ b/README.md @@ -4,6 +4,25 @@ Logic/relational programming in Python with [miniKanren](http://minikanren.org/). +## Installation + +Using `pip`: +```bash +pip install miniKanren +``` + +To install from source: +```bash +git clone git@github.com:pythological/kanren.git +cd kanren +pip install -r requirements.txt +``` + +Tests can be run with the provided `Makefile`: +```bash +make check +``` + ## Motivation Logic programming is a general programming paradigm. This implementation however came about specifically to serve as an algorithmic core for Computer Algebra Systems in Python and for the automated generation and optimization of numeric software. Domain specific languages, code generation, and compilers have recently been a hot topic in the Scientific Python community. `kanren` aims to be a low-level core for these projects. @@ -112,25 +131,6 @@ We can express the grandfather relationship as a distinct relation by creating a `kanren` uses [`multipledispatch`](http://github.com/mrocklin/multipledispatch/) and the [`logical-unification` library](https://github.com/pythological/unification) to support pattern matching on user defined types. Essentially, types that can be unified can be used with most `kanren` goals. See the [`logical-unification` project's examples](https://github.com/pythological/unification#examples) for demonstrations of how arbitrary types can be made unifiable. -## Installation - -Using `pip`: -```bash -pip install miniKanren -``` - -To install from source: -```bash -git clone git@github.com:pythological/kanren.git -cd kanren -pip install -r requirements.txt -``` - -Tests can be run with the provided `Makefile`: -```bash -make check -``` - ## About This project is a fork of [`logpy`](https://github.com/logpy/logpy). diff --git a/doc/README.md b/doc/README.md deleted file mode 100644 index 99145ee..0000000 --- a/doc/README.md +++ /dev/null @@ -1,8 +0,0 @@ -Documentation -============= - -kanren is very new. Documentation is still very sparse. Forunately the basic interface is shared with some more mature projects and so you may find looking at the following sources of use. - -[core.logic](https://github.com/clojure/core.logic/wiki/A-Core.logic-Primer) is a popular Clojure project with very similar syntax. - -[The Reasoned Schemer](http://www.amazon.com/The-Reasoned-Schemer-Daniel-Friedman/dp/0262562146/) is a quick book on miniKanren, the original implementation in Scheme diff --git a/doc/basic.md b/doc/basic.md index fdc1f60..e3d6e80 100644 --- a/doc/basic.md +++ b/doc/basic.md @@ -1,23 +1,22 @@ -kanren's Types and Common Functions ----------------------------------- +# Basics of `miniKanren` -The design of kanren/miniKanren is simple. It manipulates only a few types with only a few important functions. +The design of `miniKanren` is simple. It orchestrates only a few basic operations and yields a lot! -### Terms +## Terms Terms can be -* constants like `123` or `'cat'` -* logical variables which we denote with a tilde like `~x` -* tuples of terms like `(123, 'cat')` or `(~x, 1, (2, 3))` +- any Python object (e.g. `1`, `[1, 2]`, `object()`, etc.), +- logical variables constructed with `var`—denoted here by a tilde prefix (e.g. `~x`), +- or combinations of the two (e.g. `(1, ~x, 'cat')`) In short, they are trees in which leaves may be either constants or variables. Constants may be of any Python type. -### Unify +## Unification We *unify* two similar terms like `(1, 2)` and `(1, ~x)` to form a *substitution* `{~x: 2}`. We say that `(1, 2)` and `(1, ~x)` unify under the substitution `{~x: 2}`. Variables may assume the value of any term. -Unify is a function that takes two terms, `u` and `v`, and returns a substitution `s`. +`unify` is a function, provided by the [`logical-unification`](https://github.com/pythological/unification) library, that takes two terms, `u` and `v`, and returns a substitution `s`. Examples that unify @@ -41,110 +40,125 @@ Examples that don't unify | (1, ~x) | (2, 2) | | (1, 2) | (~x, ~x) | -Actually we lied, `unify` also takes a substitution as input. This allows us to keep some history around. For example +Actually we lied, `unify` also takes a substitution as input. This allows us to keep some history around. For example: - >>> unify((1, 2), (1, x), {}) # normal case - {~x: 2} - >>> unify((1, 2), (1, x), {x: 2}) # x is already two. This is consitent - {~x: 2} - >>> unify((1, 2), (1, x), {x: 3}) # x is already three. This conflicts - False +```python +>>> unify((1, 2), (1, x), {}) # normal case +{~x: 2} +>>> unify((1, 2), (1, x), {x: 2}) # x is already two. This is consitent +{~x: 2} +>>> unify((1, 2), (1, x), {x: 3}) # x is already three. This conflicts +False +``` -### Reify +## Reification -Reify is the opposite of unify. `reify` transforms a term with logic variables like `(1, ~x)` and a substitution like `{~x: 2}` into a term without logic variables like `(1, 2)`. +Reification is the opposite of unification. `reify` transforms a term with logic variables like `(1, ~x)` and a substitution like `{~x: 2}` into a term without logic variables like `(1, 2)`. +```python +>>> reify((1, x), {x: 2}) +(1, 2) +``` - >>> reify((1, x), {x: 2}) - (1, 2) - -### Goals and Goal Constructors +## Goals and Goal Constructors A *goal* is a function from one substitution to a stream of substitutions. - goal :: substitution -> [substitutions] +``` +goal :: substitution -> [substitutions] +``` We make goals with a *goal constructors*. Goal constructors are the normal building block of a logical program. Lets look at the goal constructor `membero` which states that the first input must be a member of the second input (a collection). - goal = membero(x, (1, 2, 3) +``` +goal = membero(x, (1, 2, 3) +``` We can feed this goal a substitution and it will give us a stream of substitutions. Here we'll feed it the substitution with no information and it will tell us that either `x` can be `1` or `x` can be `2` or `x` can be `3` - >>> for s in goal({}): - ... print s - {~x: 1} - {~x: 2} - {~x: 3} - +```python +>>> for s in goal({}): +... print s +{~x: 1} +{~x: 2} +{~x: 3} +``` What if we already know that `x` is `2`? - - >>> for s in goal({x: 2}): - ... print s - {~x: 2} +```python +>>> for s in goal({x: 2}): +... print s +{~x: 2} +``` Remember *goals* are functions from one substitution to a stream of substitutions. Users usually make goals with *goal constructors* like `eq`, or `membero`. -### Goal Combinators +### Stream Functions -After this point kanren is just a library to manage streams of substitutions. +After this point `miniKanren` is just a library to manage streams of substitutions. For example if we know both that `membero(x, (1, 2, 3))` and `membero(x, (2, 3, 4))` then we could do something like the following: - >>> g1 = membero(x, (1, 2, 3)) - >>> g2 = membero(x, (2, 3, 4)) - >>> for s in g1({}): - ... for ss in g2(s): - ... print ss - {~x: 2} - {~x: 3} - -Logic programs can have many goals in complex hierarchies. Writing explicit for loops would quickly become tedious. Instead we provide functions that conglomerate goals logically. - - combinator :: [goals] -> goal - -Two important logical goal combinators are logical all `lall` and logical any `lany`. - - >>> g = lall(g1, g2) - >>> for s in g({}): - ... print s - {~x: 2} - {~x: 3} - - >>> g = lany(g1, g2) - >>> for s in g({}): - ... print s - {~x: 1} - {~x: 2} - {~x: 3} - {~x: 4} - +```python +>>> g1 = membero(x, (1, 2, 3)) +>>> g2 = membero(x, (2, 3, 4)) +>>> for s in g1({}): +... for ss in g2(s): +... print ss +{~x: 2} +{~x: 3} +``` +Logic programs can have many goals in complex hierarchies. Writing explicit for loops would quickly become tedious. Instead `miniKanren` provide functions that perform logic-like operations on goal streams. + +``` +combinator :: [goals] -> goal +``` + +Two important stream functions are logical all `lall` and logical any `lany`. +```python +>>> g = lall(g1, g2) +>>> for s in g({}): +... print s +{~x: 2} +{~x: 3} + +>>> g = lany(g1, g2) +>>> for s in g({}): +... print s +{~x: 1} +{~x: 2} +{~x: 3} +{~x: 4} +``` ### Laziness -Goals produce a stream of substitutions. This stream is computed lazily, returning values only as they are needed. kanren depends on standard Python generators to maintain the necessary state and control flow. +Goals produce a stream of substitutions. This stream is computed lazily, returning values only as they are needed. `miniKanren` depends on standard Python generators to maintain the necessary state and control flow. - >>> stream = g({}) - >>> stream - - >>> next(stream) - {~x: 1} +```python +>>> stream = g({}) +>>> stream + +>>> next(stream) +{~x: 1} +``` - -### User interface +## User Interface Traditionally programs are run with the `run` function - >>> x = var('x') - >>> run(0, x, membero(x, (1, 2, 3)), membero(x, (2, 3, 4))) - (2, 3) - +```python +>>> x = var() +>>> run(0, x, membero(x, (1, 2, 3)), membero(x, (2, 3, 4))) +(2, 3) +``` `run` has an implicit `lall` for the goals at the end of the call. It `reifies` results when it returns so that the user never has to touch logic variables or substitutions. -### Conclusion +## Conclusion -These are all the fundamental concepts that exist in kanren. To summarize +These are all the fundamental concepts that exist in `miniKanren`. To summarize: -* Term: a constant, variable, or tree of terms -* Substitution: a dictionary mapping variables to terms -* Unify: A function to turn two terms into a substitution that makes them match -* Goal: A function from a substitution to a stream of substitutions -* Goal Constructor: A user-level function that defines a goal +- *Term*: a Python object, logic variable, or combination of the two +- *Substitution Map*: a dictionary mapping logic variables to terms +- *Unification*: A function that finds logic variable substitutions that make two terms equal +- *Reification*: A function that substitutes logic variables in a term with values given by a substitution map +- *Goal*: A generator function that takes a substitution and yields a stream of substitutions +- *Goal Constructor*: A user-level function that constructs and returns a goal diff --git a/doc/differences.md b/doc/differences.md deleted file mode 100644 index ed8f086..0000000 --- a/doc/differences.md +++ /dev/null @@ -1,73 +0,0 @@ -Differences with miniKanren -=========================== - -kanren is a Python library. The Python language introduces some necessary deviations from the original design. Other deviations have been followed by choice. - -Syntax ------- - -Basic kanren syntax is as follows - - >>> x = var() - >>> run(1, x, (eq, x, 2)) - -The first argument is the maximum number of desired results. Select `0` for all values and `None` to receive a lazy iterator. - -The second argument is the result variable. Because Python does not support macros this variable must be created beforehand on the previous line. Similarly there is no `fresh`; additional variables must be created ahead of time. - - >>> x, y = var(), var() - >>> run(1, x, (eq, x, y), (eq, y, 2)) - -Evaluation of goals -- `eq(x, 2)` vs `(eq, x, 2)` -------------------------------------------------- - -Traditional Python code is written `f(x)`. Traditional Scheme code is written `(f, x)`. kanren uses both syntaxes but prefers `(f, x)` so that goals may be constructed at the last moment. This allows the goals to be reified with as much information as possible. Consider the following - - >>> x, y = var(), var() - >>> run(0, x, eq(y, (1, 2, 3)), membero(x, y))) - -In this example `membero(x, y)` is unable to provide sensible results because, at the time it is run y is a variable. However, if membero is called *after* `eq(y, (1, 2, 3))` then we know that `y == (1, 2, 3)`. With this additional information `membero is more useful. If we write this as follows - - >>> x, y = var(), var() - >>> run(0, x, eq(y, (1, 2, 3)), (membero, x, y))) - -then kanren is able to evaluate the `membero` goal after it learns that `y == (1, 2, 3)`. - -In short, `goal(arg, arg)` is conceptually equivalent to `(goal, arg, arg)` but the latter gives more control to kanren. - -Strategies and goal ordering ----------------------------- - -Python does not naturally support the car/cdr or head/tail list concept. As a result functions like `conso` or `appendo` are difficult to write generally because there is no way to match a head and tail variable to a list variable. This is a substantial weakness. - -To overcome this weakness kanren detects failed goals and reorders them at goal construction time. This is evident in the following example - - >>> x, y = var(), var() - >>> run(0, x, (membero, x, y), (eq, y, (1, 2, 3))) - -`(membero, x, y)` does not produce sensible results when both `x` and `y` are variables. When this goal is evaluated it raises an `EarlyGoalError`. The kanren logical all function (equivalent to `bind*`) is sensitive to this and reorders goals so that erring goals are run after non-erring goals. The code is converted - - >>> run(0, x, (membero, x, y), (eq, y, (1, 2, 3))) # original - >>> run(0, x, (eq, y, (1, 2, 3)), (membero, x, y)) # optimized - -`conde` -------- - -`conde` is available and common in kanren; it is not however related to -any idiomatic Python concept. We separate `conde` into two functions - -* `lall` - Logical All -* `lany` - Logical Any - -As a result the following are equivalent and the first expands to the second - - (conde, (a, b, c), - (d, e)) - - (lany, (lall, a, b, c), - (lall, d, e)) - -`lany` and `lall` were roughly `mplus` and `bind*` in miniKanren. `lany` -interleaves results rather than chain them. `lall` reorders goals as mentioned -above. There is some development to make these behaviors programmable through -strategies. diff --git a/examples/commutative.py b/examples/commutative.py index 9a30658..55bd766 100644 --- a/examples/commutative.py +++ b/examples/commutative.py @@ -2,21 +2,26 @@ from kanren.assoccomm import eq_assoccomm as eq from kanren.assoccomm import commutative, associative + # Define some dummy Operationss -add = 'add' -mul = 'mul' +add = "add" +mul = "mul" + # Declare that these ops are commutative using the facts system fact(commutative, mul) fact(commutative, add) fact(associative, mul) fact(associative, add) -# Define some wild variables -x, y = var('x'), var('y') +# Define some logic variables +x, y = var(), var() # Two expressions to match -pattern = (mul, (add, 1, x), y) # (1 + x) * y -expr = (mul, 2, (add, 3, 1)) # 2 * (3 + 1) -print(run(0, (x,y), eq(pattern, expr))) # prints ((3, 2),) meaning - # x matches to 3 - # y matches to 2 +pattern = (mul, (add, 1, x), y) # (1 + x) * y +expr = (mul, 2, (add, 3, 1)) # 2 * (3 + 1) + +res = run(0, (x, y), eq(pattern, expr)) +print(res) +# prints ((3, 2),) meaning +# x matches to 3 +# y matches to 2 diff --git a/examples/corleone.py b/examples/corleone.py index f5b3762..8438427 100644 --- a/examples/corleone.py +++ b/examples/corleone.py @@ -1,67 +1,81 @@ -# Family relationships from The Godfather -# Translated from the core.logic example found in -# "The Magical Island of Kanren - core.logic Intro Part 1" -# http://objectcommando.com/blog/2011/11/04/the-magical-island-of-kanren-core-logic-intro-part-1/ +""" +Family relationships from The Godfather Translated from the core.logic example +found in "The Magical Island of Kanren - core.logic Intro Part 1" +http://objectcommando.com/blog/2011/11/04/the-magical-island-of-kanren-core-logic-intro-part-1/ +""" +import toolz + +from kanren import Relation, conde, facts, run, var -from kanren import Relation, facts, run, conde, var, eq father = Relation() mother = Relation() -facts(father, ('Vito', 'Michael'), - ('Vito', 'Sonny'), - ('Vito', 'Fredo'), - ('Michael', 'Anthony'), - ('Michael', 'Mary'), - ('Sonny', 'Vicent'), - ('Sonny', 'Francesca'), - ('Sonny', 'Kathryn'), - ('Sonny', 'Frank'), - ('Sonny', 'Santino')) - -facts(mother, ('Carmela', 'Michael'), - ('Carmela', 'Sonny'), - ('Carmela', 'Fredo'), - ('Kay', 'Mary'), - ('Kay', 'Anthony'), - ('Sandra', 'Francesca'), - ('Sandra', 'Kathryn'), - ('Sandra', 'Frank'), - ('Sandra', 'Santino')) +facts( + father, + ("Vito", "Michael"), + ("Vito", "Sonny"), + ("Vito", "Fredo"), + ("Michael", "Anthony"), + ("Michael", "Mary"), + ("Sonny", "Vicent"), + ("Sonny", "Francesca"), + ("Sonny", "Kathryn"), + ("Sonny", "Frank"), + ("Sonny", "Santino"), +) + +facts( + mother, + ("Carmela", "Michael"), + ("Carmela", "Sonny"), + ("Carmela", "Fredo"), + ("Kay", "Mary"), + ("Kay", "Anthony"), + ("Sandra", "Francesca"), + ("Sandra", "Kathryn"), + ("Sandra", "Frank"), + ("Sandra", "Santino"), +) q = var() -print((run(0, q, father('Vito', q)))) # Vito is the father of who? +print((run(0, q, father("Vito", q)))) # Vito is the father of who? # ('Sonny', 'Michael', 'Fredo') -print((run(0, q, father(q, 'Michael')))) # Who is the father of Michael? +print((run(0, q, father(q, "Michael")))) # Who is the father of Michael? # ('Vito',) + def parent(p, child): return conde([father(p, child)], [mother(p, child)]) -print((run(0, q, parent(q, 'Michael')))) # Who is a parent of Michael? +print((run(0, q, parent(q, "Michael")))) # Who is a parent of Michael? # ('Vito', 'Carmela') + def grandparent(gparent, child): p = var() return conde((parent(gparent, p), parent(p, child))) -print((run(0, q, grandparent(q, 'Anthony')))) # Who is a grandparent of Anthony? +print(run(0, q, grandparent(q, "Anthony"))) # Who is a grandparent of Anthony? # ('Vito', 'Carmela') -print((run(0, q, grandparent('Vito', q)))) # Vito is a grandparent of whom? +print(run(0, q, grandparent("Vito", q))) # Vito is a grandparent of whom? # ('Vicent', 'Anthony', 'Kathryn', 'Mary', 'Frank', 'Santino', 'Francesca') + def sibling(a, b): p = var() return conde((parent(p, a), parent(p, b))) + # All spouses x, y, z = var(), var(), var() -print((run(0, (x, y), (father, x, z), (mother, y, z)))) -# (('Vito', 'Carmela'), ('Sonny', 'Sandra'), ('Michael', 'Kay')) + +print(run(0, (x, y), father(x, z), mother(y, z), results_filter=toolz.unique)) +# (('Sonny', 'Sandra'), ('Vito', 'Carmela'), ('Michael', 'Kay')) diff --git a/examples/prime.py b/examples/prime.py deleted file mode 100644 index 76be58d..0000000 --- a/examples/prime.py +++ /dev/null @@ -1,35 +0,0 @@ -""" Example using SymPy to construct a prime number goal """ -import itertools as it - -import pytest - -from unification import isvar - -from kanren import membero -from kanren.core import succeed, fail, var, run, condeseq, eq - -try: - import sympy.ntheory.generate as sg -except ImportError: - sg = None - - -def primo(x): - """ x is a prime number """ - if isvar(x): - return condeseq([(eq, x, p)] for p in map(sg.prime, it.count(1))) - else: - return succeed if sg.isprime(x) else fail - - -def test_primo(): - if not sg: - pytest.skip("Test missing required library: sympy.ntheory.generate") - - x = var() - res = (set(run(0, x, (membero, x, (20, 21, 22, 23, 24, 25, 26, 27, 28, 29, - 30)), (primo, x)))) - - assert {23, 29} == res - - assert ((run(5, x, primo(x)))) == (2, 3, 5, 7, 11) diff --git a/examples/states.py b/examples/states.py index e9ba8da..de549e3 100644 --- a/examples/states.py +++ b/examples/states.py @@ -3,47 +3,55 @@ This example builds a small database of the US states. -The `adjacency` relation expresses which states border each other -The `coastal` relation expresses which states border the ocean +The `adjacency` relation expresses which states border each other. +The `coastal` relation expresses which states border the ocean. """ -from kanren import run, fact, eq, Relation, var +from kanren import Relation, fact, run, var adjacent = Relation() -coastal = Relation() +coastal = Relation() -coastal_states = 'WA,OR,CA,TX,LA,MS,AL,GA,FL,SC,NC,VA,MD,DE,NJ,NY,CT,RI,MA,ME,NH,AK,HI'.split(',') +coastal_states = "WA,OR,CA,TX,LA,MS,AL,GA,FL,SC,NC,VA,MD,DE,NJ,NY,CT,RI,MA,ME,NH,AK,HI".split( + "," +) -for state in coastal_states: # ['NY', 'NJ', 'CT', ...] - fact(coastal, state) # e.g. 'NY' is coastal +# ['NY', 'NJ', 'CT', ...] +for state in coastal_states: + # E.g. 'NY' is coastal + fact(coastal, state) -with open('examples/data/adjacent-states.txt') as f: # lines like 'CA,OR,NV,AZ' - adjlist = [line.strip().split(',') for line in f - if line and line[0].isalpha()] +# Lines like 'CA,OR,NV,AZ' +with open("examples/data/adjacent-states.txt") as f: + adjlist = [line.strip().split(",") for line in f if line and line[0].isalpha()] -for L in adjlist: # ['CA', 'OR', 'NV', 'AZ'] - head, tail = L[0], L[1:] # 'CA', ['OR', 'NV', 'AZ'] +# ['CA', 'OR', 'NV', 'AZ'] +for L in adjlist: + # 'CA', ['OR', 'NV', 'AZ'] + head, tail = L[0], L[1:] for state in tail: - fact(adjacent, head, state) # e.g. 'CA' is adjacent to 'OR', - # 'CA' is adjacent to 'NV', etc... + # E.g. 'CA' is adjacent to 'OR', 'CA' is adjacent to 'NV', etc. + fact(adjacent, head, state) x = var() y = var() -print((run(0, x, adjacent('CA', 'NY')))) # is California adjacent to New York? +# Is California adjacent to New York? +print(run(0, x, adjacent("CA", "NY"))) # () -print((run(0, x, adjacent('CA', x)))) # all states next to California -# ('OR', 'NV', 'AZ') +# All states next to California +print(run(0, x, adjacent("CA", x))) +# ('AZ', 'OR', 'NV') -print((run(0, x, adjacent('TX', x), # all coastal states next to Texas - coastal(x)))) +# All coastal states next to Texas +print(run(0, x, adjacent("TX", x), coastal(x))) # ('LA',) -print((run(5, x, coastal(y), # five states that border a coastal state - adjacent(x, y)))) -# ('VT', 'AL', 'WV', 'DE', 'MA') +# Five states that border a coastal state +print(run(5, x, coastal(y), adjacent(x, y))) +# ('LA', 'NM', 'OK', 'AR', 'RI') -print((run(0, x, adjacent('TN', x), # all states adjacent to Tennessee - adjacent('FL', x)))) # and adjacent to Florida -# ('GA', 'AL') +# All states adjacent to Tennessee and adjacent to Florida +print(run(0, x, adjacent("TN", x), adjacent("FL", x))) +# ('AL', 'GA') diff --git a/examples/user_classes.py b/examples/user_classes.py index 2178b1f..f07a677 100644 --- a/examples/user_classes.py +++ b/examples/user_classes.py @@ -1,38 +1,42 @@ from account import Account -from kanren import unifiable, run, var, eq, membero, variables + +from kanren import membero, run, unifiable, var from kanren.core import lall -from kanren.arith import add, gt, sub +from kanren.term import term + unifiable(Account) # Register Account class -accounts = (Account('Adam', 'Smith', 1, 20), - Account('Carl', 'Marx', 2, 3), - Account('John', 'Rockefeller', 3, 1000)) +accounts = ( + Account("Adam", "Smith", 1, 20), + Account("Carl", "Marx", 2, 3), + Account("John", "Rockefeller", 3, 1000), +) # optional name strings are helpful for debugging -first = var('first') -last = var('last') -ident = var('ident') -balance = var('balance') -newbalance = var('newbalance') +first = var("first") +last = var("last") +ident = var("ident") +balance = var("balance") +newbalance = var("newbalance") # Describe a couple of transformations on accounts source = Account(first, last, ident, balance) target = Account(first, last, ident, newbalance) -theorists = ('Adam', 'Carl') +theorists = ("Adam", "Carl") # Give $10 to theorists -theorist_bonus = lall((membero, source, accounts), - (membero, first, theorists), - (add, 10, balance, newbalance)) +theorist_bonus = lall( + membero(source, accounts), membero(first, theorists), add(10, balance, newbalance), +) # Take $10 from anyone with more than $100 -tax_the_rich = lall((membero, source, accounts), - (gt, balance, 100), - (sub, balance, 10, newbalance)) +tax_the_rich = lall( + membero(source, accounts), gt(balance, 100), sub(balance, 10, newbalance) +) print("Take $10 from anyone with more than $100") -print((run(0, target, tax_the_rich))) +print(run(0, target, tax_the_rich)) print("Give $10 to theorists") -print((run(0, target, theorist_bonus))) +print(run(0, target, theorist_bonus)) diff --git a/kanren/__init__.py b/kanren/__init__.py index 37a0504..c5868d3 100644 --- a/kanren/__init__.py +++ b/kanren/__init__.py @@ -14,7 +14,6 @@ permuteo, permuteq, membero, - goalify, ) from .facts import Relation, fact, facts from .term import arguments, operator, term, unifiable_with_term diff --git a/kanren/arith.py b/kanren/arith.py deleted file mode 100644 index b12ce78..0000000 --- a/kanren/arith.py +++ /dev/null @@ -1,82 +0,0 @@ -import operator - -from unification import isvar - -from .core import eq, EarlyGoalError, lany - - -def gt(x, y): - """Construct a goal stating x > y.""" - if not isvar(x) and not isvar(y): - return eq(x > y, True) - else: - raise EarlyGoalError() - - -def lt(x, y): - """Construct a goal stating x > y.""" - if not isvar(x) and not isvar(y): - return eq(x < y, True) - else: - raise EarlyGoalError() - - -def lor(*goalconsts): - """Construct a goal representing a logical OR for goal constructors. - - >>> from kanren.arith import lor, eq, gt - >>> gte = lor(eq, gt) # greater than or equal to is `eq or gt` - """ - - def goal(*args): - return lany(*[gc(*args) for gc in goalconsts]) - - return goal - - -gte = lor(gt, eq) -lte = lor(lt, eq) - - -def binop(op, revop=None): - """Transform binary operator into goal. - - >>> from kanren.arith import binop - >>> import operator - >>> add = binop(operator.add, operator.sub) - - >>> from kanren import var, run - >>> x = var('x') - >>> next(add(1, 2, x)({})) - {~x: 3} - """ - - def goal(x, y, z): - if not isvar(x) and not isvar(y): - return eq(op(x, y), z) - if not isvar(y) and not isvar(z) and revop: - return eq(x, revop(z, y)) - if not isvar(x) and not isvar(z) and revop: - return eq(y, revop(z, x)) - raise EarlyGoalError() - - goal.__name__ = op.__name__ - return goal - - -add = binop(operator.add, operator.sub) -add.__doc__ = """ x + y == z """ -mul = binop(operator.mul, operator.truediv) -mul.__doc__ = """ x * y == z """ -mod = binop(operator.mod) -mod.__doc__ = """ x % y == z """ - - -def sub(x, y, z): - """Construct a goal stating x - y == z.""" - return add(y, z, x) - - -def div(x, y, z): - """Construct a goal stating x / y == z.""" - return mul(z, y, x) diff --git a/kanren/assoccomm.py b/kanren/assoccomm.py index e9f1fe6..b45f025 100644 --- a/kanren/assoccomm.py +++ b/kanren/assoccomm.py @@ -40,18 +40,15 @@ from etuples import etuple -from .core import conde, condeseq, eq, goaleval, ground_order, lall, succeed +from .core import conde, eq, ground_order, lall, succeed from .goals import itero, permuteo from .facts import Relation -from .graph import applyo, term_walko +from .graph import term_walko from .term import term, operator, arguments associative = Relation("associative") commutative = Relation("commutative") -# For backward compatibility -buildo = applyo - def op_args(x): """Break apart x into an operation and tuple of args.""" @@ -146,9 +143,9 @@ def eq_assoc_args_goal(S): op_rf, lg_args, grp_sizes, ctor=type(u_args_rf) ) - g = condeseq([inner_eq(sm_args, a_args)] for a_args in assoc_terms) + g = conde([inner_eq(sm_args, a_args)] for a_args in assoc_terms) - yield from goaleval(g)(S) + yield from g(S) elif isinstance(u_args_rf, Sequence): # TODO: We really need to know the arity (ranges) for the operator @@ -177,9 +174,9 @@ def eq_assoc_args_goal(S): ) if not no_ident or v_ac_arg != u_args_rf ) - g = condeseq([inner_eq(v_args_rf, v_ac_arg)] for v_ac_arg in v_ac_args) + g = conde([inner_eq(v_args_rf, v_ac_arg)] for v_ac_arg in v_ac_args) - yield from goaleval(g)(S) + yield from g(S) return lall( ground_order((a_args, b_args), (u_args, v_args)), diff --git a/kanren/core.py b/kanren/core.py index d4e5c03..a155b09 100644 --- a/kanren/core.py +++ b/kanren/core.py @@ -1,13 +1,13 @@ from itertools import tee -from functools import partial -from collections.abc import Sequence +from operator import length_hint +from functools import partial, reduce +from collections.abc import Sequence, Generator -from toolz import groupby, map from cons.core import ConsPair from unification import reify, unify, isvar from unification.core import isground -from .util import dicthash, interleave, take, multihash, unique, evalt +from toolz import interleave, take def fail(s): @@ -26,167 +26,92 @@ def eq(u, v): unify """ - def goal_eq(s): - result = unify(u, v, s) - if result is not False: - yield result - - return goal_eq + def eq_goal(s): + s = unify(u, v, s) + if s is not False: + return iter((s,)) + else: + return iter(()) + return eq_goal -def lall(*goals): - """Construct a logical all with goal reordering to avoid EarlyGoalErrors. - See Also - -------- - EarlyGoalError - earlyorder +def ldisj_seq(goals): + """Produce a goal that returns the appended state stream from all successful goal arguments. - >>> from kanren import lall, membero, var - >>> x = var('x') - >>> run(0, x, lall(membero(x, (1,2,3)), membero(x, (2,3,4)))) - (2, 3) + In other words, it behaves like logical disjunction/OR for goals. """ - return (lallgreedy,) + tuple(earlyorder(*goals)) - - -def lallgreedy(*goals): - """Construct a logical all that greedily evaluates each goals in the order provided. - Note that this may raise EarlyGoalError when the ordering of the goals is - incorrect. It is faster than lall, but should be used with care. - - """ - if not goals: + if length_hint(goals, -1) == 0: return succeed - if len(goals) == 1: - return goals[0] - - def allgoal(s): - g = goaleval(reify(goals[0], s)) - return unique( - interleave( - goaleval(reify((lallgreedy,) + tuple(goals[1:]), ss))(ss) for ss in g(s) - ), - key=dicthash, - ) - return allgoal + def ldisj_seq_goal(S): + nonlocal goals + goals, _goals = tee(goals) -def lallfirst(*goals): - """Construct a logical all that runs goals one at a time.""" - if not goals: - return succeed - if len(goals) == 1: - return goals[0] - - def allgoal(s): - for i, g in enumerate(goals): - try: - goal = goaleval(reify(g, s)) - except EarlyGoalError: - continue - other_goals = tuple(goals[:i] + goals[i + 1 :]) - return unique( - interleave( - goaleval(reify((lallfirst,) + other_goals, ss))(ss) - for ss in goal(s) - ), - key=dicthash, - ) - else: - raise EarlyGoalError() - - return allgoal + yield from interleave(g(S) for g in _goals) + return ldisj_seq_goal -def lany(*goals): - """Construct a logical any goal.""" - if len(goals) == 1: - return goals[0] - return lanyseq(goals) +def bind(z, g): + """Apply a goal to a state stream and then combine the resulting state streams.""" + # We could also use `chain`, but `interleave` preserves the old behavior. + # return chain.from_iterable(map(g, z)) + return interleave(map(g, z)) -def earlysafe(goal): - """Check if a goal can be evaluated without raising an EarlyGoalError.""" - try: - goaleval(goal) - return True - except EarlyGoalError: - return False +def lconj_seq(goals): + """Produce a goal that returns the appended state stream in which all goals are necessarily successful. -def earlyorder(*goals): - """Reorder goals to avoid EarlyGoalErrors. + In other words, it behaves like logical conjunction/AND for goals. + """ - All goals are evaluated. Those that raise EarlyGoalErrors are placed at - the end in a lall + if length_hint(goals, -1) == 0: + return succeed - See Also - -------- - EarlyGoalError - """ - if not goals: - return () - groups = groupby(earlysafe, goals) - good = groups.get(True, []) - bad = groups.get(False, []) - - if not good: - raise EarlyGoalError() - elif not bad: - return tuple(good) - else: - return tuple(good) + ((lall,) + tuple(bad),) + def lconj_seq_goal(S): + nonlocal goals + goals, _goals = tee(goals) -def conde(*goalseqs): - """Construct a logical cond goal. + g0 = next(_goals, None) - Goal constructor to provides logical AND and OR + if g0 is None: + return - conde((A, B, C), (D, E)) means (A and B and C) or (D and E) - Equivalent to the (A, B, C); (D, E) syntax in Prolog. + yield from reduce(bind, _goals, g0(S)) - See Also - -------- - lall - logical all - lany - logical any - """ - return (lany,) + tuple((lall,) + tuple(gs) for gs in goalseqs) + return lconj_seq_goal -def lanyseq(goals): - """Construct a logical any with a possibly infinite number of goals.""" +def ldisj(*goals): + """Form a disjunction of goals.""" + if len(goals) == 1 and isinstance(goals[0], Generator): + goals = goals[0] - def anygoal(s): - anygoal.goals, local_goals = tee(anygoal.goals) + return ldisj_seq(goals) - def f(goals): - for goal in goals: - try: - yield goaleval(reify(goal, s))(s) - except EarlyGoalError: - pass - return unique( - interleave(f(local_goals), pass_exceptions=[EarlyGoalError]), key=dicthash - ) +def lconj(*goals): + """Form a conjunction of goals.""" + if len(goals) == 1 and isinstance(goals[0], Generator): + goals = goals[0] - anygoal.goals = goals + return lconj_seq(goals) - return anygoal +def conde(*goals): + """Form a disjunction of goal conjunctions.""" + if len(goals) == 1 and isinstance(goals[0], Generator): + goals = goals[0] -def condeseq(goalseqs): - """Construct a goal like conde, but support generic, possibly infinite iterators of goals.""" - return (lanyseq, ((lall,) + tuple(gs) for gs in goalseqs)) + return ldisj_seq(lconj_seq(g) for g in goals) -def everyg(predicate, coll): - """Assert that a predicate applies to all elements of a collection.""" - return (lall,) + tuple((predicate, x) for x in coll) +lall = lconj +lany = ldisj def ground_order_key(S, x): @@ -224,11 +149,11 @@ def ifa(g1, g2): """Create a goal operator that returns the first stream unless it fails.""" def ifa_goal(S): - g1_stream = goaleval(g1)(S) + g1_stream = g1(S) S_new = next(g1_stream, None) if S_new is None: - yield from goaleval(g2)(S) + yield from g2(S) else: yield S_new yield from g1_stream @@ -240,12 +165,12 @@ def Zzz(gctor, *args, **kwargs): """Create an inverse-η-delay for a goal.""" def Zzz_goal(S): - yield from goaleval(gctor(*args, **kwargs))(S) + yield from gctor(*args, **kwargs)(S) return Zzz_goal -def run_all(n, x, *goals, results_filter=None): +def run(n, x, *goals, results_filter=None): """Run a logic program and obtain n solutions that satisfy the given goals. >>> from kanren import run, var, eq @@ -256,7 +181,7 @@ def run_all(n, x, *goals, results_filter=None): Parameters ---------- n: int - The number of desired solutions (see `take`). `n=0` returns a tuple + The number of desired solutions. `n=0` returns a tuple with all results and `n=None` returns a lazy sequence of all results. x: object The form to reify and output. Usually contains logic variables used in @@ -264,69 +189,20 @@ def run_all(n, x, *goals, results_filter=None): goals: Callables A sequence of goals that must be true in logical conjunction (i.e. `lall`). + results_filter: Callable + A function to apply to the results stream (e.g. a `unique` filter). """ - results = map(partial(reify, x), goaleval(lall(*goals))({})) + results = map(partial(reify, x), lall(*goals)({})) + if results_filter is not None: results = results_filter(results) - return take(n, results) - -run = partial(run_all, results_filter=partial(unique, key=multihash)) - - -class EarlyGoalError(Exception): - """An exception indicating that a goal has been constructed prematurely. - - Consider the following case - - >>> from kanren import run, eq, membero, var - >>> x, coll = var(), var() - >>> run(0, x, (membero, x, coll), (eq, coll, (1, 2, 3))) # doctest: +SKIP - - The first goal, membero, iterates over an infinite sequence of all possible - collections. This is unproductive. Rather than proceed, membero raises an - EarlyGoalError, stating that this goal has been called early. - - The goal constructor lall Logical-All-Early will reorder such goals to - the end so that the call becomes - - >>> run(0, x, (eq, coll, (1, 2, 3)), (membero, x, coll)) # doctest: +SKIP - - In this case coll is first unified to ``(1, 2, 3)`` then x iterates over - all elements of coll, 1, then 2, then 3. - - See Also - -------- - lall - earlyorder - """ - - -def find_fixed_point(f, arg): - """Repeatedly calls f until a fixed point is reached. - - This may not terminate, but should if you apply some eventually-idempotent - simplification operation like evalt. - """ - last, cur = object(), arg - while last != cur: - last = cur - cur = f(cur) - return cur - - -def goaleval(goal): - """Expand and then evaluate a goal. - - See Also - -------- - goalexpand - """ - if callable(goal): # goal is already a function like eq(x, 1) - return goal - if isinstance(goal, tuple): # goal is not yet evaluated like (eq, x, 1) - return find_fixed_point(evalt, goal) - raise TypeError("Expected either function or tuple") + if n is None: + return results + elif n == 0: + return tuple(results) + else: + return tuple(take(n, results)) def dbgo(*args, msg=None): # pragma: no cover diff --git a/kanren/goals.py b/kanren/goals.py index 3cf922c..bf1776a 100644 --- a/kanren/goals.py +++ b/kanren/goals.py @@ -6,16 +6,14 @@ from cons import cons from cons.core import ConsNull, ConsPair -from unification import isvar, reify, var +from unification import reify, var from unification.core import isground from .core import ( eq, - EarlyGoalError, conde, lall, - lanyseq, - goaleval, + lany, ) @@ -102,7 +100,7 @@ def nullo_goal(s): g = lall(*[eq(a, null_type()) for a in args_rf]) - yield from goaleval(g)(s) + yield from g(s) return nullo_goal @@ -124,7 +122,7 @@ def itero_goal(S): [nullo(l_rf, refs=nullo_refs, default_ConsNull=default_ConsNull)], [conso(c, d, l_rf), itero(d, default_ConsNull=default_ConsNull)], ) - yield from goaleval(g)(S) + yield from g(S) return itero_goal @@ -140,7 +138,7 @@ def membero_goal(S): g = lall(conso(a, d, ls), conde([eq(a, x)], [membero(x, d)])) - yield from goaleval(g)(S) + yield from g(S) return membero_goal @@ -179,7 +177,7 @@ def appendo_goal(S): ], ) - yield from goaleval(g)(S) + yield from g(S) return appendo_goal @@ -207,7 +205,7 @@ def rembero_goal(s): ], ) - yield from goaleval(g)(s) + yield from g(s) return rembero_goal @@ -292,7 +290,7 @@ def permuteo_goal(S): # to iterate over it more than once! return - yield from lanyseq(inner_eq(b_rf, a_type(i)) for i in a_perms)(S) + yield from lany(inner_eq(b_rf, a_type(i)) for i in a_perms)(S) elif isinstance(b_rf, Sequence): @@ -302,7 +300,7 @@ def permuteo_goal(S): if no_ident: next(b_perms) - yield from lanyseq(inner_eq(a_rf, b_type(i)) for i in b_perms)(S) + yield from lany(inner_eq(a_rf, b_type(i)) for i in b_perms)(S) else: @@ -321,44 +319,10 @@ def permuteo_goal(S): if no_ident: next(a_perms) - yield from lanyseq(inner_eq(b_rf, a_type(i)) for i in a_perms)(S_new) + yield from lany(inner_eq(b_rf, a_type(i)) for i in a_perms)(S_new) return permuteo_goal # For backward compatibility permuteq = permuteo - - -def goalify(func, name=None): # pragma: noqa - """Convert a Python function into kanren goal. - - >>> from kanren import run, goalify, var, membero - >>> typo = goalify(type) - >>> x = var('x') - >>> run(0, x, membero(x, (1, 'cat', 'hat', 2)), (typo, x, str)) - ('cat', 'hat') - - Goals go both ways. Here are all of the types in the collection - - >>> typ = var('typ') - >>> results = run(0, typ, membero(x, (1, 'cat', 'hat', 2)), (typo, x, typ)) - >>> print([result.__name__ for result in results]) - ['int', 'str'] - """ - - def funco(inputs, out): # pragma: noqa - if isvar(inputs): - raise EarlyGoalError() - else: - if isinstance(inputs, (tuple, list)): - if any(map(isvar, inputs)): - raise EarlyGoalError() - return (eq, func(*inputs), out) - else: - return (eq, func(inputs), out) - - name = name or (func.__name__ + "o") - funco.__name__ = name - - return funco diff --git a/kanren/graph.py b/kanren/graph.py index 49adc2c..1e477e4 100644 --- a/kanren/graph.py +++ b/kanren/graph.py @@ -3,58 +3,11 @@ from unification import var, isvar from unification import reify -from cons.core import ConsError +from etuples import etuple -from etuples import etuple, apply, rands, rator - -from .core import eq, conde, lall, goaleval, succeed, Zzz, fail, ground_order +from .core import eq, conde, lall, succeed, Zzz, fail, ground_order from .goals import conso, nullo - - -def applyo(o_rator, o_rands, obj): - """Construct a goal that relates an object to the application of its (ope)rator to its (ope)rands. - - In other words, this is the relation `op(*args) == obj`. It uses the - `rator`, `rands`, and `apply` dispatch functions from `etuples`, so - implement/override those to get the desired behavior. - - """ - - def applyo_goal(S): - nonlocal o_rator, o_rands, obj - - o_rator_rf, o_rands_rf, obj_rf = reify((o_rator, o_rands, obj), S) - - if not isvar(obj_rf): - - # We should be able to use this goal with *any* arguments, so - # fail when the ground operations fail/err. - try: - obj_rator, obj_rands = rator(obj_rf), rands(obj_rf) - except (ConsError, NotImplementedError): - return - - # The object's rator + rands should be the same as the goal's - yield from goaleval( - lall(eq(o_rator_rf, obj_rator), eq(o_rands_rf, obj_rands)) - )(S) - - elif isvar(o_rands_rf) or isvar(o_rator_rf): - # The object and at least one of the rand, rators is a logic - # variable, so let's just assert a `cons` relationship between - # them - yield from goaleval(conso(o_rator_rf, o_rands_rf, obj_rf))(S) - else: - # The object is a logic variable, but the rator and rands aren't. - # We assert that the object is the application of the rand and - # rators. - try: - obj_applied = apply(o_rator_rf, o_rands_rf) - except (ConsError, NotImplementedError): - return - yield from eq(obj_rf, obj_applied)(S) - - return applyo_goal +from .term import applyo def mapo(relation, a, b, null_type=list, null_res=True, first=True): @@ -189,7 +142,6 @@ def reduceo_goal(s): # the first result g = lall(single_apply_g, conde([another_apply_g], [single_res_g])) - g = goaleval(g) yield from g(s) return reduceo_goal @@ -251,13 +203,11 @@ def walko_goal(s): map_rel(_walko, rands_in, rands_out, null_type=null_type), ) if rator_goal is not None - else lall( - map_rel(_walko, graph_in_rf, graph_out_rf, null_type=null_type) - ), + else map_rel(_walko, graph_in_rf, graph_out_rf, null_type=null_type), ], ) - yield from goaleval(g)(s) + yield from g(s) return walko_goal diff --git a/kanren/term.py b/kanren/term.py index c45ab66..d2843f6 100644 --- a/kanren/term.py +++ b/kanren/term.py @@ -1,10 +1,58 @@ from collections.abc import Sequence, Mapping -from unification.core import _unify, _reify, construction_sentinel +from unification.core import reify, _unify, _reify, construction_sentinel +from unification.variable import isvar -from cons.core import cons +from cons.core import cons, ConsError -from etuples import rator as operator, rands as arguments, apply as term +from etuples import apply as term, rands as arguments, rator as operator + +from .core import eq, lall +from .goals import conso + + +def applyo(o_rator, o_rands, obj): + """Construct a goal that relates an object to the application of its (ope)rator to its (ope)rands. + + In other words, this is the relation `op(*args) == obj`. It uses the + `rator`, `rands`, and `apply` dispatch functions from `etuples`, so + implement/override those to get the desired behavior. + + """ + + def applyo_goal(S): + nonlocal o_rator, o_rands, obj + + o_rator_rf, o_rands_rf, obj_rf = reify((o_rator, o_rands, obj), S) + + if not isvar(obj_rf): + + # We should be able to use this goal with *any* arguments, so + # fail when the ground operations fail/err. + try: + obj_rator, obj_rands = operator(obj_rf), arguments(obj_rf) + except (ConsError, NotImplementedError): + return + + # The object's rator + rands should be the same as the goal's + yield from lall(eq(o_rator_rf, obj_rator), eq(o_rands_rf, obj_rands))(S) + + elif isvar(o_rands_rf) or isvar(o_rator_rf): + # The object and at least one of the rand, rators is a logic + # variable, so let's just assert a `cons` relationship between + # them + yield from conso(o_rator_rf, o_rands_rf, obj_rf)(S) + else: + # The object is a logic variable, but the rator and rands aren't. + # We assert that the object is the application of the rand and + # rators. + try: + obj_applied = term(o_rator_rf, o_rands_rf) + except (ConsError, NotImplementedError): + return + yield from eq(obj_rf, obj_applied)(S) + + return applyo_goal @term.register(object, Sequence) diff --git a/kanren/util.py b/kanren/util.py index dd24468..901e8ce 100644 --- a/kanren/util.py +++ b/kanren/util.py @@ -1,4 +1,4 @@ -from itertools import chain, islice +from itertools import chain from collections import namedtuple from collections.abc import Hashable, MutableSet, Set, Mapping, Iterable @@ -153,44 +153,6 @@ def unique(seq, key=lambda x: x): yield item -def interleave(seqs, pass_exceptions=()): - iters = map(iter, seqs) - while iters: - newiters = [] - for itr in iters: - try: - yield next(itr) - newiters.append(itr) - except (StopIteration,) + tuple(pass_exceptions): - pass - iters = newiters - - -def take(n, seq): - if n is None: - return seq - if n == 0: - return tuple(seq) - return tuple(islice(seq, 0, n)) - - -def evalt(t): - """Evaluate a tuple if unevaluated. - - >>> from kanren.util import evalt - >>> add = lambda x, y: x + y - >>> evalt((add, 2, 3)) - 5 - >>> evalt(add(2, 3)) - 5 - """ - - if isinstance(t, tuple) and len(t) >= 1 and callable(t[0]): - return t[0](*t[1:]) - else: - return t - - def intersection(*seqs): return (item for item in seqs[0] if all(item in seq for seq in seqs[1:])) diff --git a/setup.cfg b/setup.cfg index 1621fe9..e3ca370 100644 --- a/setup.cfg +++ b/setup.cfg @@ -28,7 +28,6 @@ exclude_lines = pragma: no cover def __repr__ raise AssertionError - raise EarlyGoalError raise TypeError return NotImplemented raise NotImplementedError diff --git a/tests/test_arith.py b/tests/test_arith.py deleted file mode 100644 index 1091d73..0000000 --- a/tests/test_arith.py +++ /dev/null @@ -1,87 +0,0 @@ -from unification import var - -from kanren import run, membero -from kanren.arith import lt, gt, lte, gte, add, sub, mul, mod, div - -x = var("x") -y = var("y") - - -def results(g): - return list(g({})) - - -def test_lt(): - assert results(lt(1, 2)) - assert not results(lt(2, 1)) - assert not results(lt(2, 2)) - - -def test_gt(): - assert results(gt(2, 1)) - assert not results(gt(1, 2)) - assert not results(gt(2, 2)) - - -def test_lte(): - assert results(lte(2, 2)) - - -def test_gte(): - assert results(gte(2, 2)) - - -def test_add(): - assert results(add(1, 2, 3)) - assert not results(add(1, 2, 4)) - assert results(add(1, 2, 3)) - - assert results(add(1, 2, x)) == [{x: 3}] - assert results(add(1, x, 3)) == [{x: 2}] - assert results(add(x, 2, 3)) == [{x: 1}] - - -def test_sub(): - assert results(sub(3, 2, 1)) - assert not results(sub(4, 2, 1)) - - assert results(sub(3, 2, x)) == [{x: 1}] - assert results(sub(3, x, 1)) == [{x: 2}] - assert results(sub(x, 2, 1)) == [{x: 3}] - - -def test_mul(): - assert results(mul(2, 3, 6)) - assert not results(mul(2, 3, 7)) - - assert results(mul(2, 3, x)) == [{x: 6}] - assert results(mul(2, x, 6)) == [{x: 3}] - assert results(mul(x, 3, 6)) == [{x: 2}] - - assert mul.__name__ == "mul" - - -def test_mod(): - assert results(mod(5, 3, 2)) - - -def test_div(): - assert results(div(6, 2, 3)) - assert not results(div(6, 2, 2)) - assert results(div(6, 2, x)) == [{x: 3}] - - -def test_complex(): - numbers = tuple(range(10)) - results = set( - run( - 0, - x, - (sub, y, x, 1), - (membero, y, numbers), - (mod, y, 2, 0), - (membero, x, numbers), - ) - ) - expected = set((1, 3, 5, 7)) - assert results == expected diff --git a/tests/test_assoccomm.py b/tests/test_assoccomm.py index 14db1ba..5aae649 100644 --- a/tests/test_assoccomm.py +++ b/tests/test_assoccomm.py @@ -8,7 +8,7 @@ from unification import reify, var, isvar, unify -from kanren.core import goaleval, run_all as run +from kanren.core import run from kanren.facts import fact from kanren.assoccomm import ( associative, @@ -18,7 +18,6 @@ eq_assoc, eq_assoccomm, assoc_args, - buildo, op_args, flatten_assoc_args, assoc_flatten, @@ -82,7 +81,7 @@ def operator_Node(n): def results(g, s=None): if s is None: s = dict() - return tuple(goaleval(g)(s)) + return tuple(g(s)) def test_op_args(): @@ -91,20 +90,6 @@ def test_op_args(): assert op_args("foo") == (None, None) -def test_buildo(): - x = var() - assert run(0, x, buildo("add", (1, 2, 3), x)) == (("add", 1, 2, 3),) - assert run(0, x, buildo(x, (1, 2, 3), ("add", 1, 2, 3))) == ("add",) - assert run(0, x, buildo("add", x, ("add", 1, 2, 3))) == ((1, 2, 3),) - - -def test_buildo_object(): - x = var() - assert run(0, x, buildo(Add, (1, 2, 3), x)) == (add(1, 2, 3),) - assert run(0, x, buildo(x, (1, 2, 3), add(1, 2, 3))) == (Add,) - assert run(0, x, buildo(Add, x, add(1, 2, 3))) == ((1, 2, 3),) - - def test_eq_comm(): x, y, z = var(), var(), var() diff --git a/tests/test_constraints.py b/tests/test_constraints.py index 38610a0..6088b44 100644 --- a/tests/test_constraints.py +++ b/tests/test_constraints.py @@ -9,7 +9,7 @@ from kanren import run, eq, conde from kanren.goals import membero -from kanren.core import lall, goaleval +from kanren.core import lconj from kanren.constraints import ( ConstrainedState, DisequalityStore, @@ -20,10 +20,6 @@ ) -def lconj(*goals): - return goaleval(lall(*goals)) - - def test_ConstrainedState(): a_lv, b_lv = var(), var() diff --git a/tests/test_core.py b/tests/test_core.py index a5b0212..f5b9328 100644 --- a/tests/test_core.py +++ b/tests/test_core.py @@ -1,115 +1,165 @@ from itertools import count -from pytest import raises, mark +from pytest import raises + +from collections.abc import Iterator from cons import cons -from unification import var, isvar +from unification import var from kanren.core import ( run, fail, eq, conde, - goaleval, + lconj, + lconj_seq, + ldisj, + ldisj_seq, lany, - lallgreedy, - lanyseq, - earlyorder, - EarlyGoalError, lall, - earlysafe, - lallfirst, - condeseq, ifa, + succeed, ground_order, ) -from kanren.util import evalt -def ege_membero(x, coll): - if not isvar(coll): - return (lany,) + tuple((eq, x, item) for item in coll) - raise EarlyGoalError() +def results(g, s=None): + if s is None: + s = dict() + return tuple(g(s)) def test_eq(): - x = var("x") + x = var() assert tuple(eq(x, 2)({})) == ({x: 2},) assert tuple(eq(x, 2)({x: 3})) == () -def test_lany(): - x = var("x") - assert len(tuple(lany(eq(x, 2), eq(x, 3))({}))) == 2 - assert len(tuple(lany((eq, x, 2), (eq, x, 3))({}))) == 2 +def test_lconj_basics(): - g = lany(ege_membero(x, (1, 2, 3)), ege_membero(x, (2, 3, 4))) - assert tuple(g({})) == ({x: 1}, {x: 2}, {x: 3}, {x: 4}) + a, b = var(), var() + res = list(lconj(eq(1, a), eq(2, b))({})) + assert res == [{a: 1, b: 2}] + res = list(lconj(eq(1, a))({})) + assert res == [{a: 1}] -def test_lallfirst(): - x = var("x") - g = lallfirst(ege_membero(x, (1, 2, 3)), ege_membero(x, (2, 3, 4))) - assert tuple(g({})) == ({x: 2}, {x: 3}) - assert tuple(lallfirst()({})) == ({},) + res = list(lconj_seq([])({})) + assert res == [{}] + res = list(lconj(eq(1, a), eq(2, a))({})) + assert res == [] -def test_lallgreedy(): - x, y = var("x"), var("y") - assert run(0, x, lallgreedy((eq, y, set([1]))), (ege_membero, x, y)) == (1,) - with raises(EarlyGoalError): - run(0, x, lallgreedy((ege_membero, x, y), (eq, y, {1}))) + res = list(lconj(eq(1, 2))({})) + assert res == [] + res = list(lconj(eq(1, 1))({})) + assert res == [{}] -@mark.parametrize("lall_impl", [lallgreedy, lall, lallfirst]) -def test_lall(lall_impl): - """Test that all three implementations of lallgreedy behave identically for correctly ordered goals.""" - x, y = var("x"), var("y") - assert results(lall_impl((eq, x, 2))) == ({x: 2},) - assert results(lall_impl((eq, x, 2), (eq, x, 3))) == () - assert results(lall_impl()) == ({},) + def gen(): + for i in [succeed, succeed]: + yield i - assert run(0, x, lall_impl((eq, y, (1, 2)), (ege_membero, x, y))) - assert run(0, x, lall_impl()) == (x,) - with raises(EarlyGoalError): - run(0, x, lall_impl(ege_membero(x, y))) + res = list(lconj(gen())({})) + assert res == [{}] + def gen(): + return -@mark.parametrize("lall_impl", [lall, lallfirst]) -def test_safe_reordering_lall(lall_impl): - x, y = var("x"), var("y") - assert run(0, x, lall_impl((ege_membero, x, y), (eq, y, (1, 2)))) == (1, 2) + res = list(lconj_seq([gen()])({})) + assert res == [] -def test_earlysafe(): - x, y = var("x"), var("y") - assert earlysafe((eq, 2, 2)) - assert earlysafe((eq, 2, 3)) - assert earlysafe((ege_membero, x, (1, 2, 3))) - assert not earlysafe((ege_membero, x, y)) +def test_ldisj_basics(): + a = var() + res = list(ldisj(eq(1, a))({})) + assert res == [{a: 1}] -def test_earlyorder(): - x, y = var(), var() - assert earlyorder((eq, 2, x)) == ((eq, 2, x),) - assert earlyorder((eq, 2, x), (eq, 3, x)) == ((eq, 2, x), (eq, 3, x)) - assert earlyorder((ege_membero, x, y), (eq, y, (1, 2, 3)))[0] == (eq, y, (1, 2, 3)) + res = list(ldisj(eq(1, 2))({})) + assert res == [] + + res = list(ldisj(eq(1, 1))({})) + assert res == [{}] + + res = list(ldisj(eq(1, a), eq(1, a))({})) + assert res == [{a: 1}, {a: 1}] + + res = list(ldisj(eq(1, a), eq(2, a))({})) + assert res == [{a: 1}, {a: 2}] + + res = list(ldisj_seq([])({})) + assert res == [{}] + + def gen(): + for i in [succeed, succeed]: + yield i + + res = list(ldisj(gen())({})) + assert res == [{}, {}] + + +def test_conde_basics(): + + a, b = var(), var() + res = list(conde([eq(1, a), eq(2, b)], [eq(1, b), eq(2, a)])({})) + assert res == [{a: 1, b: 2}, {b: 1, a: 2}] + + res = list(conde([eq(1, a), eq(2, 1)], [eq(1, b), eq(2, a)])({})) + assert res == [{b: 1, a: 2}] + + aa, ab, ba, bb, bc = var(), var(), var(), var(), var() + res = list( + conde( + [eq(1, a), conde([eq(11, aa)], [eq(12, ab)])], + [eq(1, b), conde([eq(111, ba), eq(112, bb)], [eq(121, bc)]),], + )({}) + ) + assert res == [ + {a: 1, aa: 11}, + {b: 1, ba: 111, bb: 112}, + {a: 1, ab: 12}, + {b: 1, bc: 121}, + ] + + res = list(conde([eq(1, 2)], [eq(1, 1)])({})) + assert res == [{}] + + assert list(lconj(eq(1, 1))({})) == [{}] + + res = list(lconj(conde([eq(1, 2)], [eq(1, 1)]))({})) + assert res == [{}] + + res = list(lconj(conde([eq(1, 2)], [eq(1, 1)]), conde([eq(1, 2)], [eq(1, 1)]))({})) + assert res == [{}] + + +def test_lany(): + x = var() + assert len(tuple(lany(eq(x, 2), eq(x, 3))({}))) == 2 + assert len(tuple(lany(eq(x, 2), eq(x, 3))({}))) == 2 + + +def test_lall(): + x = var() + assert results(lall(eq(x, 2))) == ({x: 2},) + assert results(lall(eq(x, 2), eq(x, 3))) == () + assert results(lall()) == ({},) + assert run(0, x, lall()) == (x,) def test_conde(): - x = var("x") + x = var() assert results(conde([eq(x, 2)], [eq(x, 3)])) == ({x: 2}, {x: 3}) assert results(conde([eq(x, 2), eq(x, 3)])) == () - -def test_condeseq(): - x = var("x") - assert set(run(0, x, condeseq(([eq(x, 2)], [eq(x, 3)])))) == {2, 3} - assert set(run(0, x, condeseq([[eq(x, 2), eq(x, 3)]]))) == set() + assert set(run(0, x, conde([eq(x, 2)], [eq(x, 3)]))) == {2, 3} + assert set(run(0, x, conde([eq(x, 2), eq(x, 3)]))) == set() goals = ([eq(x, i)] for i in count()) # infinite number of goals - assert run(1, x, condeseq(goals)) == (0,) - assert run(1, x, condeseq(goals)) == (1,) + assert run(1, x, conde(goals)) == (0,) + assert run(1, x, conde(goals)) == (1,) def test_short_circuit(): @@ -121,7 +171,10 @@ def badgoal(s): def test_run(): - x, y, z = map(var, "xyz") + x, y, z = var(), var(), var() + res = run(None, x, eq(x, 1)) + assert isinstance(res, Iterator) + assert tuple(res) == (1,) assert run(1, x, eq(x, 1)) == (1,) assert run(2, x, eq(x, 1)) == (1,) assert run(0, x, eq(x, 1)) == (1,) @@ -135,60 +188,28 @@ def test_run_output_reify(): def test_lanyseq(): - x = var("x") - g = lanyseq(((eq, x, i) for i in range(3))) - assert list(goaleval(g)({})) == [{x: 0}, {x: 1}, {x: 2}] - assert list(goaleval(g)({})) == [{x: 0}, {x: 1}, {x: 2}] + x = var() + g = lany((eq(x, i) for i in range(3))) + assert list(g({})) == [{x: 0}, {x: 1}, {x: 2}] + assert list(g({})) == [{x: 0}, {x: 1}, {x: 2}] # Test lanyseq with an infinite number of goals. - assert set(run(3, x, lanyseq(((eq, x, i) for i in count())))) == {0, 1, 2} - assert set(run(3, x, (lanyseq, ((eq, x, i) for i in count())))) == {0, 1, 2} - - -def test_evalt(): - add = lambda x, y: x + y - assert evalt((add, 2, 3)) == 5 - assert evalt(add(2, 3)) == 5 - assert evalt((1, 2)) == (1, 2) - - -def test_goaleval(): - x, y = var("x"), var("y") - g = eq(x, 2) - assert goaleval(g) == g - assert callable(goaleval((eq, x, 2))) - with raises(EarlyGoalError): - goaleval((ege_membero, x, y)) - assert callable(goaleval((lallgreedy, (eq, x, 2)))) + assert set(run(3, x, lany((eq(x, i) for i in count())))) == {0, 1, 2} + assert set(run(3, x, lany((eq(x, i) for i in count())))) == {0, 1, 2} def test_lall_errors(): - """Make sure we report the originating exception when it isn't just an - `EarlyGoalError`. - """ - class SomeException(Exception): pass def bad_relation(): - def _bad_relation(): + def _bad_relation(s): raise SomeException("some exception") - return (lall, (_bad_relation,)) + return lall(_bad_relation) with raises(SomeException): - run(0, var(), (bad_relation,)) - - -def test_lany_is_early_safe(): - x, y = var(), var() - assert run(0, x, lany((ege_membero, x, y), (eq, x, 2))) == (2,) - - -def results(g, s=None): - if s is None: - s = dict() - return tuple(goaleval(g)(s)) + run(0, var(), bad_relation()) def test_dict(): @@ -196,39 +217,6 @@ def test_dict(): assert run(0, x, eq({1: x}, {1: 2})) == (2,) -def test_goal_ordering(): - # Regression test for https://github.com/logpy/logpy/issues/58 - - def lefto(q, p, lst): - if isvar(lst): - raise EarlyGoalError() - return ege_membero((q, p), zip(lst, lst[1:])) - - vals = var() - - # Verify the solution can be computed when we specify the execution - # ordering. - rules_greedy = ( - lallgreedy, - (eq, (var(), var()), vals), - (lefto, "green", "white", vals), - ) - - (solution,) = run(1, vals, rules_greedy) - assert solution == ("green", "white") - - # Verify that attempting to compute the "safe" order does not itself cause - # the evaluation to fail. - rules_greedy = ( - lall, - (eq, (var(), var()), vals), - (lefto, "green", "white", vals), - ) - - (solution,) = run(1, vals, rules_greedy) - assert solution == ("green", "white") - - def test_ifa(): x, y = var(), var() @@ -261,5 +249,7 @@ def test_ground_order(): assert run(0, (a, b, c), ground_order((y, [1, z], 1), (a, b, c))) == ( (1, [1, z], y), ) - assert run(0, z, ground_order([cons(x, y), (x, y)], z)) == ([(x, y), cons(x, y)],) - assert run(0, z, ground_order([(x, y), cons(x, y)], z)) == ([(x, y), cons(x, y)],) + res = run(0, z, ground_order([cons(x, y), (x, y)], z)) + assert res == ([(x, y), cons(x, y)],) + res = run(0, z, ground_order([(x, y), cons(x, y)], z)) + assert res == ([(x, y), cons(x, y)],) diff --git a/tests/test_goals.py b/tests/test_goals.py index 75d1ce3..0125b85 100644 --- a/tests/test_goals.py +++ b/tests/test_goals.py @@ -16,13 +16,13 @@ rembero, permuteo, ) -from kanren.core import eq, goaleval, run, conde +from kanren.core import eq, run, conde def results(g, s=None): if s is None: s = dict() - return tuple(goaleval(g)(s)) + return tuple(g(s)) def test_heado(): @@ -31,17 +31,17 @@ def test_heado(): assert (x, 1) in results(heado(1, (x, 2, 3)))[0].items() assert results(heado(x, ())) == () - assert run(0, x, (heado, x, z), (conso, 1, y, z)) == (1,) + assert run(0, x, heado(x, z), conso(1, y, z)) == (1,) def test_tailo(): x, y, z = var(), var(), var() - assert (x, (2, 3)) in results((tailo, x, (1, 2, 3)))[0].items() - assert (x, ()) in results((tailo, x, (1,)))[0].items() - assert results((tailo, x, ())) == () + assert (x, (2, 3)) in results(tailo(x, (1, 2, 3)))[0].items() + assert (x, ()) in results(tailo(x, (1,)))[0].items() + assert results(tailo(x, ())) == () - assert run(0, y, (tailo, y, z), (conso, x, (1, 2), z)) == ((1, 2),) + assert run(0, y, tailo(y, z), conso(x, (1, 2), z)) == ((1, 2),) def test_conso(): @@ -111,7 +111,7 @@ def test_membero(): assert run(0, x, membero(1, (2, 3))) == () g = membero(x, (0, 1, 2)) - assert tuple(r[x] for r in goaleval(g)({})) == (0, 1, 2) + assert tuple(r[x] for r in g({})) == (0, 1, 2) def in_cons(x, y): if issubclass(type(y), ConsPair): @@ -128,7 +128,7 @@ def in_cons(x, y): def test_uneval_membero(): x, y = var(), var() - assert set(run(100, x, (membero, y, ((1, 2, 3), (4, 5, 6))), (membero, x, y))) == { + assert set(run(100, x, membero(y, ((1, 2, 3), (4, 5, 6))), membero(x, y))) == { 1, 2, 3, diff --git a/tests/test_graph.py b/tests/test_graph.py index fc5f154..c774656 100644 --- a/tests/test_graph.py +++ b/tests/test_graph.py @@ -1,19 +1,21 @@ import pytest +import toolz + from operator import add, mul from functools import partial +from numbers import Real from math import log, exp from unification import var, unify, isvar, reify -from etuples.dispatch import rator, rands, apply from etuples.core import etuple, ExpressionTuple from cons import cons from kanren import run, eq, conde, lall from kanren.constraints import isinstanceo -from kanren.graph import applyo, reduceo, map_anyo, walko, mapo, eq_length +from kanren.graph import reduceo, map_anyo, walko, mapo, eq_length class OrderedFunction(object): @@ -55,64 +57,30 @@ def __repr__(self): ) -def math_reduceo(in_expr, out_expr): - """Create a relation for a couple math-based identities.""" - x_lv = var(prefix="x") - +def single_math_reduceo(expanded_term, reduced_term): + """Construct a goal for some simple math reductions.""" + x_lv = var() return lall( + isinstanceo(x_lv, Real), + isinstanceo(x_lv, ExpressionTuple), conde( - [eq(in_expr, etuple(add, x_lv, x_lv)), eq(out_expr, etuple(mul, 2, x_lv))], - [eq(in_expr, etuple(log, etuple(exp, x_lv))), eq(out_expr, x_lv)], - ), - conde( - [isinstanceo(in_expr, float)], - [isinstanceo(in_expr, int)], - [isinstanceo(in_expr, ExpressionTuple)], - [isinstanceo(out_expr, float)], - [isinstanceo(out_expr, int)], - [isinstanceo(out_expr, ExpressionTuple)], + [ + eq(expanded_term, etuple(add, x_lv, x_lv)), + eq(reduced_term, etuple(mul, 2, x_lv)), + ], + [eq(expanded_term, etuple(log, etuple(exp, x_lv))), eq(reduced_term, x_lv)], ), ) -def full_math_reduceo(a, b): - """Produce all results for repeated applications of the math-based relation.""" - return reduceo(math_reduceo, a, b) - - -def fixedp_walko(r, x, y): - return reduceo(partial(walko, r), x, y) - - -def test_applyo(): - a_lv, b_lv, c_lv = var(), var(), var() - - assert run(0, c_lv, applyo(add, (1, 2), c_lv)) == (3,) - assert run(0, c_lv, applyo(add, etuple(1, 2), c_lv)) == (3,) - assert run(0, c_lv, applyo(add, a_lv, c_lv)) == (cons(add, a_lv),) - - for obj in ( - (1, 2, 3), - (add, 1, 2), - [1, 2, 3], - [add, 1, 2], - etuple(1, 2, 3), - etuple(add, 1, 2), - ): - o_rator, o_rands = rator(obj), rands(obj) - assert run(0, a_lv, applyo(o_rator, o_rands, a_lv)) == ( - apply(o_rator, o_rands), - ) - # Just acts like `conso` here - assert run(0, a_lv, applyo(o_rator, a_lv, obj)) == (rands(obj),) - assert run(0, a_lv, applyo(a_lv, o_rands, obj)) == (rator(obj),) - - # Just acts like `conso` here, too - assert run(0, c_lv, applyo(a_lv, b_lv, c_lv)) == (cons(a_lv, b_lv),) +math_reduceo = partial(reduceo, single_math_reduceo) - # with pytest.raises(ConsError): - assert run(0, a_lv, applyo(a_lv, b_lv, object())) == () - assert run(0, a_lv, applyo(1, 2, a_lv)) == () +term_walko = partial( + walko, + rator_goal=eq, + null_type=ExpressionTuple, + map_rel=partial(map_anyo, null_res=False), +) def test_basics(): @@ -127,22 +95,20 @@ def test_reduceo(): q_lv = var() # Reduce/forward - res = run( - 0, q_lv, full_math_reduceo(etuple(log, etuple(exp, etuple(log, 1))), q_lv) - ) + res = run(0, q_lv, math_reduceo(etuple(log, etuple(exp, etuple(log, 1))), q_lv)) assert len(res) == 1 assert res[0] == etuple(log, 1) res = run( 0, q_lv, - full_math_reduceo(etuple(log, etuple(exp, etuple(log, etuple(exp, 1)))), q_lv), + math_reduceo(etuple(log, etuple(exp, etuple(log, etuple(exp, 1)))), q_lv), ) assert res[0] == 1 assert res[1] == etuple(log, etuple(exp, 1)) # Expand/backward - res = run(2, q_lv, full_math_reduceo(q_lv, 1)) + res = run(3, q_lv, math_reduceo(q_lv, 1)) assert res[0] == etuple(log, etuple(exp, 1)) assert res[1] == etuple(log, etuple(exp, etuple(log, etuple(exp, 1)))) @@ -167,7 +133,8 @@ def blah(x, y): ) a_lv = var() - assert run(5, [q_lv, a_lv], mapo(blah, q_lv, a_lv)) == exp_res + res = run(5, [q_lv, a_lv], mapo(blah, q_lv, a_lv)) + assert res == exp_res def test_eq_length(): @@ -194,7 +161,7 @@ def test_eq_length(): def test_map_anyo_types(): - """Make sure that `applyo` preserves the types between its arguments.""" + """Make sure that `map_anyo` preserves the types between its arguments.""" q_lv = var() res = run(1, q_lv, map_anyo(lambda x, y: eq(x, y), [1], q_lv)) assert res[0] == [1] @@ -215,9 +182,11 @@ def test_map_anyo_types(): def test_map_anyo_misc(): q_lv = var("q") - assert len(run(0, q_lv, map_anyo(eq, [1, 2, 3], [1, 2, 3]))) == 1 - - assert len(run(0, q_lv, map_anyo(eq, [1, 2, 3], [1, 3, 3]))) == 0 + res = run(0, q_lv, map_anyo(eq, [1, 2, 3], [1, 2, 3])) + # TODO: Remove duplicate results + assert len(res) == 7 + res = run(0, q_lv, map_anyo(eq, [1, 2, 3], [1, 3, 3])) + assert len(res) == 0 def one_to_threeo(x, y): return conde([eq(x, 1), eq(y, 3)]) @@ -292,7 +261,7 @@ def test_bin(a, b): def test_map_anyo(test_input, test_output): """Test `map_anyo` with fully ground terms (i.e. no logic variables).""" q_lv = var() - test_res = run(0, q_lv, map_anyo(full_math_reduceo, test_input, q_lv),) + test_res = run(0, q_lv, map_anyo(math_reduceo, test_input, q_lv),) assert len(test_res) == len(test_output) @@ -315,10 +284,24 @@ def test_map_anyo_reverse(): # Unbounded reverse q_lv = var() rev_input = [etuple(mul, 2, 1)] - test_res = run(4, q_lv, (map_anyo, math_reduceo, q_lv, rev_input)) + test_res = run(4, q_lv, map_anyo(math_reduceo, q_lv, rev_input)) assert test_res == ( [etuple(add, 1, 1)], - [etuple(log, etuple(exp, etuple(mul, 2, 1)))], + [etuple(log, etuple(exp, etuple(add, 1, 1)))], + # [etuple(log, etuple(exp, etuple(mul, 2, 1)))], + [etuple(log, etuple(exp, etuple(log, etuple(exp, etuple(add, 1, 1)))))], + # [etuple(log, etuple(exp, etuple(log, etuple(exp, etuple(mul, 2, 1)))))], + [ + etuple( + log, + etuple( + exp, + etuple( + log, etuple(exp, etuple(log, etuple(exp, etuple(add, 1, 1)))) + ), + ), + ) + ], ) # Guided reverse @@ -333,10 +316,13 @@ def test_walko_misc(): q_lv = var(prefix="q") expr = etuple(add, etuple(mul, 2, 1), etuple(add, 1, 1)) - assert len(run(0, q_lv, walko(eq, expr, expr))) == 1 + res = run(0, q_lv, walko(eq, expr, expr)) + # TODO: Remove duplicates + assert len(res) == 162 expr2 = etuple(add, etuple(mul, 2, 1), etuple(add, 2, 1)) - assert len(run(0, q_lv, walko(eq, expr, expr2))) == 0 + res = run(0, q_lv, walko(eq, expr, expr2)) + assert len(res) == 0 def one_to_threeo(x, y): return conde([eq(x, 1), eq(y, 3)]) @@ -379,17 +365,24 @@ def one_to_threeo(x, y): (1, ()), (etuple(add, 1, 1), (etuple(mul, 2, 1),)), ( + # (2 * 1) + (1 + 1) etuple(add, etuple(mul, 2, 1), etuple(add, 1, 1)), ( + # 2 * (2 * 1) etuple(mul, 2, etuple(mul, 2, 1)), + # (2 * 1) + (2 * 1) etuple(add, etuple(mul, 2, 1), etuple(mul, 2, 1)), ), ), ( + # (log(exp(2)) * 1) + (1 + 1) etuple(add, etuple(mul, etuple(log, etuple(exp, 2)), 1), etuple(add, 1, 1)), ( + # 2 * (2 * 1) etuple(mul, 2, etuple(mul, 2, 1)), + # (2 * 1) + (2 * 1) etuple(add, etuple(mul, 2, 1), etuple(mul, 2, 1)), + # (log(exp(2)) * 1) + (2 * 1) etuple( add, etuple(mul, etuple(log, etuple(exp, 2)), 1), etuple(mul, 2, 1) ), @@ -402,8 +395,12 @@ def test_walko(test_input, test_output): """Test `walko` with fully ground terms (i.e. no logic variables).""" q_lv = var() + term_walko_fp = partial(reduceo, partial(term_walko, single_math_reduceo)) test_res = run( - len(test_output), q_lv, fixedp_walko(full_math_reduceo, test_input, q_lv) + len(test_output), + q_lv, + term_walko_fp(test_input, q_lv), + results_filter=toolz.unique, ) assert len(test_res) == len(test_output) @@ -423,7 +420,7 @@ def test_walko_reverse(): """Test `walko` in "reverse" (i.e. specify the reduced form and generate the un-reduced form).""" q_lv = var("q") - test_res = run(2, q_lv, fixedp_walko(math_reduceo, q_lv, 5)) + test_res = run(2, q_lv, term_walko(math_reduceo, q_lv, 5)) assert test_res == ( etuple(log, etuple(exp, 5)), etuple(log, etuple(exp, etuple(log, etuple(exp, 5)))), @@ -431,21 +428,21 @@ def test_walko_reverse(): assert all(e.eval_obj == 5.0 for e in test_res) # Make sure we get some variety in the results - test_res = run(2, q_lv, fixedp_walko(math_reduceo, q_lv, etuple(mul, 2, 5))) + test_res = run(2, q_lv, term_walko(math_reduceo, q_lv, etuple(mul, 2, 5))) assert test_res == ( # Expansion of the term's root etuple(add, 5, 5), # Expansion in the term's arguments - # etuple(mul, etuple(log, etuple(exp, 2)), etuple(log, etuple(exp, 5))), + etuple(mul, etuple(log, etuple(exp, 2)), etuple(log, etuple(exp, 5))), # Two step expansion at the root - etuple(log, etuple(exp, etuple(add, 5, 5))), + # etuple(log, etuple(exp, etuple(add, 5, 5))), # Expansion into a sub-term # etuple(mul, 2, etuple(log, etuple(exp, 5))) ) assert all(e.eval_obj == 10.0 for e in test_res) r_lv = var("r") - test_res = run(4, [q_lv, r_lv], fixedp_walko(math_reduceo, q_lv, r_lv)) + test_res = run(4, [q_lv, r_lv], term_walko(math_reduceo, q_lv, r_lv)) expect_res = ( [etuple(add, 1, 1), etuple(mul, 2, 1)], [etuple(log, etuple(exp, etuple(add, 1, 1))), etuple(mul, 2, 1)], diff --git a/tests/test_sudoku.py b/tests/test_sudoku.py index 714c0a9..ca8753d 100644 --- a/tests/test_sudoku.py +++ b/tests/test_sudoku.py @@ -2,10 +2,12 @@ Based off https://github.com/holtchesley/embedded-logic/blob/master/kanren/sudoku.ipynb """ +import pytest + from unification import var from kanren import run -from kanren.core import everyg +from kanren.core import lall from kanren.goals import permuteq @@ -50,484 +52,132 @@ def sudoku_solver(hints): return run( 1, variables, - everyg(all_numbers, rows), - everyg(all_numbers, cols), - everyg(all_numbers, sqs), + lall(*(all_numbers(r) for r in rows)), + lall(*(all_numbers(c) for c in cols)), + lall(*(all_numbers(s) for s in sqs)), ) +# fmt: off def test_missing_one_entry(): example_board = ( - 5, - 3, - 4, - 6, - 7, - 8, - 9, - 1, - 2, - 6, - 7, - 2, - 1, - 9, - 5, - 3, - 4, - 8, - 1, - 9, - 8, - 3, - 4, - 2, - 5, - 6, - 7, - 8, - 5, - 9, - 7, - 6, - 1, - 4, - 2, - 3, - 4, - 2, - 6, - 8, - 5, - 3, - 7, - 9, - 1, - 7, - 1, - 3, - 9, - 2, - 4, - 8, - 5, - 6, - 9, - 6, - 1, - 5, - 3, - 7, - 2, - 8, - 4, - 2, - 8, - 7, - 4, - 1, - 9, - 6, - 3, - 5, - 3, - 4, - 5, - 2, - 8, - 6, - 0, - 7, - 9, + 5, 3, 4, 6, 7, 8, 9, 1, 2, + 6, 7, 2, 1, 9, 5, 3, 4, 8, + 1, 9, 8, 3, 4, 2, 5, 6, 7, + 8, 5, 9, 7, 6, 1, 4, 2, 3, + 4, 2, 6, 8, 5, 3, 7, 9, 1, + 7, 1, 3, 9, 2, 4, 8, 5, 6, + 9, 6, 1, 5, 3, 7, 2, 8, 4, + 2, 8, 7, 4, 1, 9, 6, 3, 5, + 3, 4, 5, 2, 8, 6, 0, 7, 9, ) expected_solution = ( - 5, - 3, - 4, - 6, - 7, - 8, - 9, - 1, - 2, - 6, - 7, - 2, - 1, - 9, - 5, - 3, - 4, - 8, - 1, - 9, - 8, - 3, - 4, - 2, - 5, - 6, - 7, - 8, - 5, - 9, - 7, - 6, - 1, - 4, - 2, - 3, - 4, - 2, - 6, - 8, - 5, - 3, - 7, - 9, - 1, - 7, - 1, - 3, - 9, - 2, - 4, - 8, - 5, - 6, - 9, - 6, - 1, - 5, - 3, - 7, - 2, - 8, - 4, - 2, - 8, - 7, - 4, - 1, - 9, - 6, - 3, - 5, - 3, - 4, - 5, - 2, - 8, - 6, - 1, - 7, - 9, + 5, 3, 4, 6, 7, 8, 9, 1, 2, + 6, 7, 2, 1, 9, 5, 3, 4, 8, + 1, 9, 8, 3, 4, 2, 5, 6, 7, + 8, 5, 9, 7, 6, 1, 4, 2, 3, + 4, 2, 6, 8, 5, 3, 7, 9, 1, + 7, 1, 3, 9, 2, 4, 8, 5, 6, + 9, 6, 1, 5, 3, 7, 2, 8, 4, + 2, 8, 7, 4, 1, 9, 6, 3, 5, + 3, 4, 5, 2, 8, 6, 1, 7, 9, ) assert sudoku_solver(example_board)[0] == expected_solution +# fmt: off def test_missing_complex_board(): example_board = ( - 5, - 3, - 4, - 6, - 7, - 8, - 9, - 0, - 2, - 6, - 7, - 2, - 0, - 9, - 5, - 3, - 4, - 8, - 0, - 9, - 8, - 3, - 4, - 2, - 5, - 6, - 7, - 8, - 5, - 9, - 7, - 6, - 0, - 4, - 2, - 3, - 4, - 2, - 6, - 8, - 5, - 3, - 7, - 9, - 0, - 7, - 0, - 3, - 9, - 2, - 4, - 8, - 5, - 6, - 9, - 6, - 0, - 5, - 3, - 7, - 2, - 8, - 4, - 2, - 8, - 7, - 4, - 0, - 9, - 6, - 3, - 5, - 3, - 4, - 5, - 2, - 8, - 6, - 0, - 7, - 9, + 5, 3, 4, 6, 7, 8, 9, 0, 2, + 6, 7, 2, 0, 9, 5, 3, 4, 8, + 0, 9, 8, 3, 4, 2, 5, 6, 7, + 8, 5, 9, 7, 6, 0, 4, 2, 3, + 4, 2, 6, 8, 5, 3, 7, 9, 0, + 7, 0, 3, 9, 2, 4, 8, 5, 6, + 9, 6, 0, 5, 3, 7, 2, 8, 4, + 2, 8, 7, 4, 0, 9, 6, 3, 5, + 3, 4, 5, 2, 8, 6, 0, 7, 9, ) expected_solution = ( - 5, - 3, - 4, - 6, - 7, - 8, - 9, - 1, - 2, - 6, - 7, - 2, - 1, - 9, - 5, - 3, - 4, - 8, - 1, - 9, - 8, - 3, - 4, - 2, - 5, - 6, - 7, - 8, - 5, - 9, - 7, - 6, - 1, - 4, - 2, - 3, - 4, - 2, - 6, - 8, - 5, - 3, - 7, - 9, - 1, - 7, - 1, - 3, - 9, - 2, - 4, - 8, - 5, - 6, - 9, - 6, - 1, - 5, - 3, - 7, - 2, - 8, - 4, - 2, - 8, - 7, - 4, - 1, - 9, - 6, - 3, - 5, - 3, - 4, - 5, - 2, - 8, - 6, - 1, - 7, - 9, + 5, 3, 4, 6, 7, 8, 9, 1, 2, + 6, 7, 2, 1, 9, 5, 3, 4, 8, + 1, 9, 8, 3, 4, 2, 5, 6, 7, + 8, 5, 9, 7, 6, 1, 4, 2, 3, + 4, 2, 6, 8, 5, 3, 7, 9, 1, + 7, 1, 3, 9, 2, 4, 8, 5, 6, + 9, 6, 1, 5, 3, 7, 2, 8, 4, + 2, 8, 7, 4, 1, 9, 6, 3, 5, + 3, 4, 5, 2, 8, 6, 1, 7, 9, ) assert sudoku_solver(example_board)[0] == expected_solution -def test_insolvable(): +# fmt: off +def test_unsolvable(): example_board = ( - 5, - 3, - 4, - 6, - 7, - 8, - 9, - 1, - 2, - 6, - 7, - 2, - 1, - 9, - 5, - 9, - 4, - 8, # Note column 7 has two 9's. - 1, - 9, - 8, - 3, - 4, - 2, - 5, - 6, - 7, - 8, - 5, - 9, - 7, - 6, - 1, - 4, - 2, - 3, - 4, - 2, - 6, - 8, - 5, - 3, - 7, - 9, - 1, - 7, - 1, - 3, - 9, - 2, - 4, - 8, - 5, - 6, - 9, - 6, - 1, - 5, - 3, - 7, - 2, - 8, - 4, - 2, - 8, - 7, - 4, - 1, - 9, - 6, - 3, - 5, - 3, - 4, - 5, - 2, - 8, - 6, - 0, - 7, - 9, + 5, 3, 4, 6, 7, 8, 9, 1, 2, + 6, 7, 2, 1, 9, 5, 9, 4, 8, # Note column 7 has two 9's. + 1, 9, 8, 3, 4, 2, 5, 6, 7, + 8, 5, 9, 7, 6, 1, 4, 2, 3, + 4, 2, 6, 8, 5, 3, 7, 9, 1, + 7, 1, 3, 9, 2, 4, 8, 5, 6, + 9, 6, 1, 5, 3, 7, 2, 8, 4, + 2, 8, 7, 4, 1, 9, 6, 3, 5, + 3, 4, 5, 2, 8, 6, 0, 7, 9, ) assert sudoku_solver(example_board) == () -# @pytest.mark.skip(reason="Currently too slow!") -# def test_many_missing_elements(): -# example_board = ( -# 5, 3, 0, 0, 7, 0, 0, 0, 0, -# 6, 0, 0, 1, 9, 5, 0, 0, 0, -# 0, 9, 8, 0, 0, 0, 0, 6, 0, -# 8, 0, 0, 0, 6, 0, 0, 0, 3, -# 4, 0, 0, 8, 0, 3, 0, 0, 1, -# 7, 0, 0, 0, 2, 0, 0, 0, 6, -# 0, 6, 0, 0, 0, 0, 2, 8, 0, -# 0, 0, 0, 4, 1, 9, 0, 0, 5, -# 0, 0, 0, 0, 8, 0, 0, 7, 9) -# assert sudoku_solver(example_board)[0] == ( -# 5, 3, 4, 6, 7, 8, 9, 1, 2, -# 6, 7, 2, 1, 9, 5, 3, 4, 8, -# 1, 9, 8, 3, 4, 2, 5, 6, 7, -# 8, 5, 9, 7, 6, 1, 4, 2, 3, -# 4, 2, 6, 8, 5, 3, 7, 9, 1, -# 7, 1, 3, 9, 2, 4, 8, 5, 6, -# 9, 6, 1, 5, 3, 7, 2, 8, 4, -# 2, 8, 7, 4, 1, 9, 6, 3, 5, -# 3, 4, 5, 2, 8, 6, 1, 7, 9) -# -# -# @pytest.mark.skip(reason="Currently too slow!") -# def test_websudoku_easy(): -# # A sudoku from websudoku.com. -# example_board = ( -# 0, 0, 8, 0, 0, 6, 0, 0, 0, -# 0, 0, 4, 3, 7, 9, 8, 0, 0, -# 5, 7, 0, 0, 1, 0, 3, 2, 0, -# 0, 5, 2, 0, 0, 7, 0, 0, 0, -# 0, 6, 0, 5, 9, 8, 0, 4, 0, -# 0, 0, 0, 4, 0, 0, 5, 7, 0, -# 0, 2, 1, 0, 4, 0, 0, 9, 8, -# 0, 0, 9, 6, 2, 3, 1, 0, 0, -# 0, 0, 0, 9, 0, 0, 7, 0, 0, -# ) -# assert sudoku_solver(example_board) == ( -# 9, 3, 8, 2, 5, 6, 4, 1, 7, -# 2, 1, 4, 3, 7, 9, 8, 6, 5, -# 5, 7, 6, 8, 1, 4, 3, 2, 9, -# 4, 5, 2, 1, 3, 7, 9, 8, 6, -# 1, 6, 7, 5, 9, 8, 2, 4, 3, -# 8, 9, 3, 4, 6, 2, 5, 7, 1, -# 3, 2, 1, 7, 4, 5, 6, 9, 8, -# 7, 8, 9, 6, 2, 3, 1, 5, 4, -# 6, 4, 5, 9, 8, 1, 7, 3, 2) +# fmt: off +@pytest.mark.skip(reason="Currently too slow!") +def test_many_missing_elements(): + example_board = ( + 5, 3, 0, 0, 7, 0, 0, 0, 0, + 6, 0, 0, 1, 9, 5, 0, 0, 0, + 0, 9, 8, 0, 0, 0, 0, 6, 0, + 8, 0, 0, 0, 6, 0, 0, 0, 3, + 4, 0, 0, 8, 0, 3, 0, 0, 1, + 7, 0, 0, 0, 2, 0, 0, 0, 6, + 0, 6, 0, 0, 0, 0, 2, 8, 0, + 0, 0, 0, 4, 1, 9, 0, 0, 5, + 0, 0, 0, 0, 8, 0, 0, 7, 9 + ) + assert sudoku_solver(example_board)[0] == ( + 5, 3, 4, 6, 7, 8, 9, 1, 2, + 6, 7, 2, 1, 9, 5, 3, 4, 8, + 1, 9, 8, 3, 4, 2, 5, 6, 7, + 8, 5, 9, 7, 6, 1, 4, 2, 3, + 4, 2, 6, 8, 5, 3, 7, 9, 1, + 7, 1, 3, 9, 2, 4, 8, 5, 6, + 9, 6, 1, 5, 3, 7, 2, 8, 4, + 2, 8, 7, 4, 1, 9, 6, 3, 5, + 3, 4, 5, 2, 8, 6, 1, 7, 9 + ) + + +# fmt: off +@pytest.mark.skip(reason="Currently too slow!") +def test_websudoku_easy(): + # A sudoku from websudoku.com. + example_board = ( + 0, 0, 8, 0, 0, 6, 0, 0, 0, + 0, 0, 4, 3, 7, 9, 8, 0, 0, + 5, 7, 0, 0, 1, 0, 3, 2, 0, + 0, 5, 2, 0, 0, 7, 0, 0, 0, + 0, 6, 0, 5, 9, 8, 0, 4, 0, + 0, 0, 0, 4, 0, 0, 5, 7, 0, + 0, 2, 1, 0, 4, 0, 0, 9, 8, + 0, 0, 9, 6, 2, 3, 1, 0, 0, + 0, 0, 0, 9, 0, 0, 7, 0, 0, + ) + assert sudoku_solver(example_board) == ( + 9, 3, 8, 2, 5, 6, 4, 1, 7, + 2, 1, 4, 3, 7, 9, 8, 6, 5, + 5, 7, 6, 8, 1, 4, 3, 2, 9, + 4, 5, 2, 1, 3, 7, 9, 8, 6, + 1, 6, 7, 5, 9, 8, 2, 4, 3, + 8, 9, 3, 4, 6, 2, 5, 7, 1, + 3, 2, 1, 7, 4, 5, 6, 9, 8, + 7, 8, 9, 6, 2, 3, 1, 5, 4, + 6, 4, 5, 9, 8, 1, 7, 3, 2 + ) diff --git a/tests/test_term.py b/tests/test_term.py index 1f96518..e3b8c4f 100644 --- a/tests/test_term.py +++ b/tests/test_term.py @@ -1,48 +1,124 @@ from unification import var, unify, reify -from kanren.term import term, operator, arguments, unifiable_with_term +from cons import cons +from etuples import etuple - -class Op(object): - def __init__(self, name): - self.name = name +from kanren.core import run +from kanren.term import arguments, operator, term, unifiable_with_term, applyo @unifiable_with_term -class MyTerm(object): - def __init__(self, op, arguments): +class Node(object): + def __init__(self, op, args): self.op = op - self.arguments = arguments + self.args = args def __eq__(self, other): - return self.op == other.op and self.arguments == other.arguments + return ( + type(self) == type(other) + and self.op == other.op + and self.args == other.args + ) + + def __hash__(self): + return hash((type(self), self.op, self.args)) + + def __str__(self): + return "%s(%s)" % (self.op.name, ", ".join(map(str, self.args))) + + __repr__ = __str__ + + +class Operator(object): + def __init__(self, name): + self.name = name + + +Add = Operator("add") +Mul = Operator("mul") + + +def add(*args): + return Node(Add, args) + + +def mul(*args): + return Node(Mul, args) -@arguments.register(MyTerm) -def arguments_MyTerm(t): - return t.arguments +class Op(object): + def __init__(self, name): + self.name = name + + +@arguments.register(Node) +def arguments_Node(t): + return t.args -@operator.register(MyTerm) -def operator_MyTerm(t): +@operator.register(Node) +def operator_Node(t): return t.op -@term.register(Op, (list, tuple)) +@term.register(Operator, (list, tuple)) def term_Op(op, args): - return MyTerm(op, args) + return Node(op, args) + + +def test_applyo(): + x = var() + assert run(0, x, applyo("add", (1, 2, 3), x)) == (("add", 1, 2, 3),) + assert run(0, x, applyo(x, (1, 2, 3), ("add", 1, 2, 3))) == ("add",) + assert run(0, x, applyo("add", x, ("add", 1, 2, 3))) == ((1, 2, 3),) + + a_lv, b_lv, c_lv = var(), var(), var() + + from operator import add + + assert run(0, c_lv, applyo(add, (1, 2), c_lv)) == (3,) + assert run(0, c_lv, applyo(add, etuple(1, 2), c_lv)) == (3,) + assert run(0, c_lv, applyo(add, a_lv, c_lv)) == (cons(add, a_lv),) + + for obj in ( + (1, 2, 3), + (add, 1, 2), + [1, 2, 3], + [add, 1, 2], + etuple(1, 2, 3), + etuple(add, 1, 2), + ): + o_rator, o_rands = operator(obj), arguments(obj) + assert run(0, a_lv, applyo(o_rator, o_rands, a_lv)) == (term(o_rator, o_rands),) + # Just acts like `conso` here + assert run(0, a_lv, applyo(o_rator, a_lv, obj)) == (arguments(obj),) + assert run(0, a_lv, applyo(a_lv, o_rands, obj)) == (operator(obj),) + + # Just acts like `conso` here, too + assert run(0, c_lv, applyo(a_lv, b_lv, c_lv)) == (cons(a_lv, b_lv),) + + # with pytest.raises(ConsError): + assert run(0, a_lv, applyo(a_lv, b_lv, object())) == () + assert run(0, a_lv, applyo(1, 2, a_lv)) == () + + +def test_applyo_object(): + x = var() + assert run(0, x, applyo(Add, (1, 2, 3), x)) == (add(1, 2, 3),) + assert run(0, x, applyo(x, (1, 2, 3), add(1, 2, 3))) == (Add,) + assert run(0, x, applyo(Add, x, add(1, 2, 3))) == ((1, 2, 3),) def test_unifiable_with_term(): - add = Op("add") - t = MyTerm(add, (1, 2)) + add = Operator("add") + t = Node(add, (1, 2)) assert arguments(t) == (1, 2) assert operator(t) == add assert term(operator(t), arguments(t)) == t x = var() - s = unify(MyTerm(add, (1, x)), MyTerm(add, (1, 2)), {}) + s = unify(Node(add, (1, x)), Node(add, (1, 2)), {}) assert s == {x: 2} - assert reify(MyTerm(add, (1, x)), s) == MyTerm(add, (1, 2)) + assert reify(Node(add, (1, x)), s) == Node(add, (1, 2)) diff --git a/tests/test_util.py b/tests/test_util.py index cd19ad1..d07c9fb 100644 --- a/tests/test_util.py +++ b/tests/test_util.py @@ -1,9 +1,7 @@ from pytest import raises from kanren.util import ( - take, unique, - interleave, intersection, groupsizes, dicthash, @@ -45,18 +43,6 @@ def test_intersection(): assert tuple(intersection(a, b, c)) == (3, 4) -def test_take(): - assert take(2, range(5)) == (0, 1) - assert take(0, range(5)) == (0, 1, 2, 3, 4) - seq = range(5) - assert take(None, seq) == seq - - -def test_interleave(): - assert "".join(interleave(("ABC", "123"))) == "A1B2C3" - assert "".join(interleave(("ABC", "1"))) == "A1BC" - - def test_groupsizes(): assert set(groupsizes(4, 2)) == set(((1, 3), (2, 2), (3, 1))) assert set(groupsizes(5, 2)) == set(((1, 4), (2, 3), (3, 2), (4, 1))) From beea559e5a21aa589af989d9f88c61aaee740f70 Mon Sep 17 00:00:00 2001 From: "Brandon T. Willard" Date: Sun, 23 Feb 2020 19:56:20 -0600 Subject: [PATCH 2/2] Remove unused op_args --- kanren/assoccomm.py | 16 +++------------- tests/test_assoccomm.py | 7 ------- 2 files changed, 3 insertions(+), 20 deletions(-) diff --git a/kanren/assoccomm.py b/kanren/assoccomm.py index b45f025..6af19be 100644 --- a/kanren/assoccomm.py +++ b/kanren/assoccomm.py @@ -34,9 +34,9 @@ from toolz import sliding_window -from unification import isvar, var, reify, unify +from unification import reify, unify, var -from cons.core import ConsError, ConsPair, car, cdr +from cons.core import ConsPair, car, cdr from etuples import etuple @@ -44,22 +44,12 @@ from .goals import itero, permuteo from .facts import Relation from .graph import term_walko -from .term import term, operator, arguments +from .term import term associative = Relation("associative") commutative = Relation("commutative") -def op_args(x): - """Break apart x into an operation and tuple of args.""" - if isvar(x): - return None, None - try: - return operator(x), arguments(x) - except (ConsError, NotImplementedError): - return None, None - - def flatten_assoc_args(op_predicate, items): for i in items: if isinstance(i, ConsPair) and op_predicate(car(i)): diff --git a/tests/test_assoccomm.py b/tests/test_assoccomm.py index 5aae649..fbf2c29 100644 --- a/tests/test_assoccomm.py +++ b/tests/test_assoccomm.py @@ -18,7 +18,6 @@ eq_assoc, eq_assoccomm, assoc_args, - op_args, flatten_assoc_args, assoc_flatten, ) @@ -84,12 +83,6 @@ def results(g, s=None): return tuple(g(s)) -def test_op_args(): - assert op_args(var()) == (None, None) - assert op_args(add(1, 2, 3)) == (Add, (1, 2, 3)) - assert op_args("foo") == (None, None) - - def test_eq_comm(): x, y, z = var(), var(), var()