├── .gitattributes ├── .github ├── FUNDING.yml └── workflows │ ├── pypi.yml │ └── tests.yml ├── .gitignore ├── .pre-commit-config.yaml ├── .pylintrc ├── LICENSE.txt ├── MANIFEST.in ├── Makefile ├── README.md ├── doc ├── basic.md └── graphs.md ├── examples ├── __init__.py ├── account.py ├── commutative.py ├── corleone.py ├── data │ ├── adjacent-states.txt │ └── coastal-states.txt ├── states.py ├── user_classes.py └── zebra-puzzle.py ├── kanren ├── __init__.py ├── _version.py ├── assoccomm.py ├── constraints.py ├── core.py ├── facts.py ├── goals.py ├── graph.py ├── py.typed ├── term.py └── util.py ├── pytest.ini ├── release-notes ├── requirements.txt ├── setup.cfg ├── setup.py ├── tests ├── __init__.py ├── test_assoccomm.py ├── test_constraints.py ├── test_core.py ├── test_facts.py ├── test_goals.py ├── test_graph.py ├── test_sudoku.py ├── test_term.py └── test_util.py ├── tox.ini └── versioneer.py /.gitattributes: -------------------------------------------------------------------------------- 1 | kanren/_version.py export-subst 2 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: [brandonwillard] 2 | -------------------------------------------------------------------------------- /.github/workflows/pypi.yml: -------------------------------------------------------------------------------- 1 | name: PyPI 2 | on: 3 | push: 4 | branches: 5 | - main 6 | - auto-release 7 | pull_request: 8 | branches: [main] 9 | release: 10 | types: [published] 11 | 12 | # Cancels all previous workflow runs for pull requests that have not completed. 13 | concurrency: 14 | # The concurrency group contains the workflow name and the branch name for pull requests 15 | # or the commit hash for any other events. 16 | group: ${{ github.workflow }}-${{ github.event_name == 'pull_request' && github.head_ref || github.sha }} 17 | cancel-in-progress: true 18 | 19 | jobs: 20 | build: 21 | name: Build source distribution 22 | runs-on: ubuntu-latest 23 | steps: 24 | - uses: actions/checkout@v3 25 | with: 26 | fetch-depth: 0 27 | - uses: actions/setup-python@v4 28 | with: 29 | python-version: "3.8" 30 | - name: Build the sdist 31 | run: | 32 | python setup.py sdist 33 | - name: Check the sdist installs and imports 34 | run: | 35 | mkdir -p test-sdist 36 | cd test-sdist 37 | python -m venv venv-sdist 38 | venv-sdist/bin/python -m pip install ../dist/miniKanren-*.tar.gz 39 | - uses: actions/upload-artifact@v2 40 | with: 41 | name: artifact 42 | path: dist/* 43 | 44 | upload_pypi: 45 | name: Upload to PyPI on release 46 | needs: [build] 47 | runs-on: ubuntu-latest 48 | if: github.event_name == 'release' && github.event.action == 'published' 49 | steps: 50 | - uses: actions/download-artifact@v2 51 | with: 52 | name: artifact 53 | path: dist 54 | - uses: pypa/gh-action-pypi-publish@master 55 | with: 56 | user: __token__ 57 | password: ${{ secrets.pypi_secret }} 58 | -------------------------------------------------------------------------------- /.github/workflows/tests.yml: -------------------------------------------------------------------------------- 1 | name: Tests 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | pull_request: 8 | branches: 9 | - main 10 | 11 | # Cancels all previous workflow runs for pull requests that have not completed. 12 | concurrency: 13 | # The concurrency group contains the workflow name and the branch name for pull requests 14 | # or the commit hash for any other events. 15 | group: ${{ github.workflow }}-${{ github.event_name == 'pull_request' && github.head_ref || github.sha }} 16 | cancel-in-progress: true 17 | 18 | jobs: 19 | changes: 20 | name: "Check for changes" 21 | runs-on: ubuntu-latest 22 | outputs: 23 | changes: ${{ steps.changes.outputs.src }} 24 | steps: 25 | - uses: actions/checkout@v3 26 | with: 27 | fetch-depth: 0 28 | - uses: dorny/paths-filter@v2 29 | id: changes 30 | with: 31 | filters: | 32 | python: &python 33 | - 'kanren/**/*.py' 34 | - 'tests/**/*.py' 35 | - '*.py' 36 | src: 37 | - *python 38 | - '.github/**/*.yml' 39 | - 'setup.cfg' 40 | - 'requirements.txt' 41 | - '.coveragerc' 42 | - '.pre-commit-config.yaml' 43 | 44 | style: 45 | name: Check code style 46 | needs: changes 47 | runs-on: ubuntu-latest 48 | if: ${{ needs.changes.outputs.changes == 'true' }} 49 | steps: 50 | - uses: actions/checkout@v3 51 | - uses: actions/setup-python@v4 52 | with: 53 | python-version: "3.8" 54 | - uses: pre-commit/action@v2.0.0 55 | 56 | test: 57 | needs: 58 | - changes 59 | - style 60 | runs-on: ubuntu-latest 61 | if: ${{ needs.changes.outputs.changes == 'true' && needs.style.result == 'success' }} 62 | strategy: 63 | matrix: 64 | python-version: 65 | - "3.7" 66 | - "3.8" 67 | - "3.9" 68 | - "3.10" 69 | - "pypy3.9" 70 | steps: 71 | - uses: actions/checkout@v3 72 | - uses: actions/setup-python@v4 73 | with: 74 | python-version: ${{ matrix.python-version }} 75 | - name: Install dependencies 76 | run: | 77 | python -m pip install --upgrade pip 78 | if [ -f requirements.txt ]; then pip install -r requirements.txt; fi 79 | - name: Test with pytest 80 | run: | 81 | pytest -v tests/ --cov=kanren --cov-report=xml:./coverage.xml 82 | - name: Coveralls 83 | uses: AndreMiras/coveralls-python-action@develop 84 | with: 85 | parallel: true 86 | flag-name: run-${{ matrix.python-version }} 87 | 88 | all-checks: 89 | if: ${{ always() }} 90 | runs-on: ubuntu-latest 91 | name: "All tests" 92 | needs: [changes, style, test] 93 | steps: 94 | - name: Check build matrix status 95 | if: ${{ needs.changes.outputs.changes == 'true' && (needs.style.result != 'success' || needs.test.result != 'success') }} 96 | run: exit 1 97 | 98 | upload-coverage: 99 | name: "Upload coverage" 100 | needs: [changes, all-checks] 101 | if: ${{ needs.changes.outputs.changes == 'true' && needs.all-checks.result == 'success' }} 102 | runs-on: ubuntu-latest 103 | steps: 104 | - name: Coveralls Finished 105 | uses: AndreMiras/coveralls-python-action@develop 106 | with: 107 | parallel-finished: true 108 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Created by https://www.gitignore.io/api/vim,emacs,python 2 | # Edit at https://www.gitignore.io/?templates=vim,emacs,python 3 | 4 | ### Emacs ### 5 | # -*- mode: gitignore; -*- 6 | *~ 7 | \#*\# 8 | /.emacs.desktop 9 | /.emacs.desktop.lock 10 | *.elc 11 | auto-save-list 12 | tramp 13 | .\#* 14 | 15 | # Org-mode 16 | .org-id-locations 17 | *_archive 18 | 19 | # flymake-mode 20 | *_flymake.* 21 | 22 | # eshell files 23 | /eshell/history 24 | /eshell/lastdir 25 | 26 | # elpa packages 27 | /elpa/ 28 | 29 | # reftex files 30 | *.rel 31 | 32 | # AUCTeX auto folder 33 | /auto/ 34 | 35 | # cask packages 36 | .cask/ 37 | dist/ 38 | 39 | # Flycheck 40 | flycheck_*.el 41 | 42 | # server auth directory 43 | /server/ 44 | 45 | # projectiles files 46 | .projectile 47 | 48 | # directory configuration 49 | .dir-locals.el 50 | 51 | # network security 52 | /network-security.data 53 | 54 | 55 | ### Python ### 56 | # Byte-compiled / optimized / DLL files 57 | __pycache__/ 58 | *.py[cod] 59 | *$py.class 60 | 61 | # C extensions 62 | *.so 63 | 64 | # Distribution / packaging 65 | .Python 66 | build/ 67 | develop-eggs/ 68 | downloads/ 69 | eggs/ 70 | .eggs/ 71 | lib/ 72 | lib64/ 73 | parts/ 74 | sdist/ 75 | var/ 76 | wheels/ 77 | pip-wheel-metadata/ 78 | share/python-wheels/ 79 | *.egg-info/ 80 | .installed.cfg 81 | *.egg 82 | MANIFEST 83 | 84 | # PyInstaller 85 | # Usually these files are written by a python script from a template 86 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 87 | *.manifest 88 | *.spec 89 | 90 | # Installer logs 91 | pip-log.txt 92 | pip-delete-this-directory.txt 93 | 94 | # Unit test / coverage reports 95 | htmlcov/ 96 | .tox/ 97 | .nox/ 98 | .coverage 99 | .coverage.* 100 | .cache 101 | nosetests.xml 102 | coverage.xml 103 | *.cover 104 | .hypothesis/ 105 | .pytest_cache/ 106 | testing-report.html 107 | 108 | # Translations 109 | *.mo 110 | *.pot 111 | 112 | # Django stuff: 113 | *.log 114 | local_settings.py 115 | db.sqlite3 116 | db.sqlite3-journal 117 | 118 | # Flask stuff: 119 | instance/ 120 | .webassets-cache 121 | 122 | # Scrapy stuff: 123 | .scrapy 124 | 125 | # Sphinx documentation 126 | docs/_build/ 127 | 128 | # PyBuilder 129 | target/ 130 | 131 | # Jupyter Notebook 132 | .ipynb_checkpoints 133 | 134 | # IPython 135 | profile_default/ 136 | ipython_config.py 137 | 138 | # pyenv 139 | .python-version 140 | 141 | # pipenv 142 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 143 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 144 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 145 | # install all needed dependencies. 146 | #Pipfile.lock 147 | 148 | # celery beat schedule file 149 | celerybeat-schedule 150 | 151 | # SageMath parsed files 152 | *.sage.py 153 | 154 | # Environments 155 | .env 156 | .venv 157 | env/ 158 | venv/ 159 | ENV/ 160 | env.bak/ 161 | venv.bak/ 162 | 163 | # Spyder project settings 164 | .spyderproject 165 | .spyproject 166 | 167 | # Rope project settings 168 | .ropeproject 169 | 170 | # mkdocs documentation 171 | /site 172 | 173 | # mypy 174 | .mypy_cache/ 175 | .dmypy.json 176 | dmypy.json 177 | 178 | # Pyre type checker 179 | .pyre/ 180 | 181 | ### Vim ### 182 | # Swap 183 | [._]*.s[a-v][a-z] 184 | [._]*.sw[a-p] 185 | [._]s[a-rt-v][a-z] 186 | [._]ss[a-gi-z] 187 | [._]sw[a-p] 188 | 189 | # Session 190 | Session.vim 191 | Sessionx.vim 192 | 193 | # Temporary 194 | .netrwhist 195 | # Auto-generated tag files 196 | tags 197 | # Persistent undo 198 | [._]*.un~ 199 | 200 | # End of https://www.gitignore.io/api/vim,emacs,python -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | exclude: | 2 | (?x)^( 3 | versioneer\.py| 4 | kanren/_version\.py| 5 | doc/.*| 6 | bin/.* 7 | )$ 8 | repos: 9 | - repo: https://github.com/pre-commit/pre-commit-hooks 10 | rev: v4.4.0 11 | hooks: 12 | - id: debug-statements 13 | exclude: | 14 | (?x)^( 15 | kanren/core\.py| 16 | )$ 17 | - id: check-merge-conflict 18 | - repo: https://github.com/psf/black 19 | rev: 22.12.0 20 | hooks: 21 | - id: black 22 | language_version: python3 23 | - repo: https://github.com/pycqa/flake8 24 | # NOTE: flake8 v6 requires python >=3.8.1 but 25 | # Aesara still supports python 3.7. 26 | rev: 3.8.4 27 | hooks: 28 | - id: flake8 29 | - repo: https://github.com/pycqa/isort 30 | rev: 5.11.4 31 | hooks: 32 | - id: isort 33 | - repo: https://github.com/humitos/mirrors-autoflake.git 34 | rev: v1.1 35 | hooks: 36 | - id: autoflake 37 | exclude: | 38 | (?x)^( 39 | .*/?__init__\.py| 40 | )$ 41 | args: ['--in-place', '--remove-all-unused-imports', '--remove-unused-variable'] 42 | - repo: https://github.com/pre-commit/mirrors-mypy 43 | rev: v0.991 44 | hooks: 45 | - id: mypy 46 | additional_dependencies: 47 | - numpy>=1.20 48 | - types-filelock 49 | - types-setuptools 50 | -------------------------------------------------------------------------------- /.pylintrc: -------------------------------------------------------------------------------- 1 | [MASTER] 2 | # Use multiple processes to speed up Pylint. 3 | jobs=0 4 | 5 | # Allow loading of arbitrary C extensions. Extensions are imported into the 6 | # active Python interpreter and may run arbitrary code. 7 | unsafe-load-any-extension=no 8 | 9 | # Allow optimization of some AST trees. This will activate a peephole AST 10 | # optimizer, which will apply various small optimizations. For instance, it can 11 | # be used to obtain the result of joining multiple strings with the addition 12 | # operator. Joining a lot of strings can lead to a maximum recursion error in 13 | # Pylint and this flag can prevent that. It has one side effect, the resulting 14 | # AST will be different than the one from reality. 15 | optimize-ast=no 16 | 17 | [MESSAGES CONTROL] 18 | 19 | # Only show warnings with the listed confidence levels. Leave empty to show 20 | # all. Valid levels: HIGH, INFERENCE, INFERENCE_FAILURE, UNDEFINED 21 | confidence= 22 | 23 | # Disable the message, report, category or checker with the given id(s). You 24 | # can either give multiple identifiers separated by comma (,) or put this 25 | # option multiple times (only on the command line, not in the configuration 26 | # file where it should appear only once).You can also use "--disable=all" to 27 | # disable everything first and then reenable specific checks. For example, if 28 | # you want to run only the similarities checker, you can use "--disable=all 29 | # --enable=similarities". If you want to run only the classes checker, but have 30 | # no Warning level messages displayed, use"--disable=all --enable=classes 31 | # --disable=W" 32 | disable=all 33 | 34 | # Enable the message, report, category or checker with the given id(s). You can 35 | # either give multiple identifier separated by comma (,) or put this option 36 | # multiple time. See also the "--disable" option for examples. 37 | enable=import-error, 38 | import-self, 39 | reimported, 40 | wildcard-import, 41 | misplaced-future, 42 | relative-import, 43 | deprecated-module, 44 | unpacking-non-sequence, 45 | invalid-all-object, 46 | undefined-all-variable, 47 | used-before-assignment, 48 | cell-var-from-loop, 49 | global-variable-undefined, 50 | dangerous-default-value, 51 | # redefined-builtin, 52 | redefine-in-handler, 53 | unused-import, 54 | unused-wildcard-import, 55 | global-variable-not-assigned, 56 | undefined-loop-variable, 57 | global-at-module-level, 58 | bad-open-mode, 59 | redundant-unittest-assert, 60 | boolean-datetime, 61 | # unused-variable 62 | 63 | 64 | [REPORTS] 65 | 66 | # Set the output format. Available formats are text, parseable, colorized, msvs 67 | # (visual studio) and html. You can also give a reporter class, eg 68 | # mypackage.mymodule.MyReporterClass. 69 | output-format=parseable 70 | 71 | # Put messages in a separate file for each module / package specified on the 72 | # command line instead of printing them on stdout. Reports (if any) will be 73 | # written in a file name "pylint_global.[txt|html]". 74 | files-output=no 75 | 76 | # Tells whether to display a full report or only the messages 77 | reports=no 78 | 79 | # Python expression which should return a note less than 10 (10 is the highest 80 | # note). You have access to the variables errors warning, statement which 81 | # respectively contain the number of errors / warnings messages and the total 82 | # number of statements analyzed. This is used by the global evaluation report 83 | # (RP0004). 84 | evaluation=10.0 - ((float(5 * error + warning + refactor + convention) / statement) * 10) 85 | 86 | [BASIC] 87 | 88 | # List of builtins function names that should not be used, separated by a comma 89 | bad-functions=map,filter,input 90 | 91 | # Good variable names which should always be accepted, separated by a comma 92 | good-names=i,j,k,ex,Run,_ 93 | 94 | # Bad variable names which should always be refused, separated by a comma 95 | bad-names=foo,bar,baz,toto,tutu,tata 96 | 97 | # Colon-delimited sets of names that determine each other's naming style when 98 | # the name regexes allow several styles. 99 | name-group= 100 | 101 | # Include a hint for the correct naming format with invalid-name 102 | include-naming-hint=yes 103 | 104 | # Regular expression matching correct method names 105 | method-rgx=[a-z_][a-z0-9_]{2,30}$ 106 | 107 | # Naming hint for method names 108 | method-name-hint=[a-z_][a-z0-9_]{2,30}$ 109 | 110 | # Regular expression matching correct function names 111 | function-rgx=[a-z_][a-z0-9_]{2,30}$ 112 | 113 | # Naming hint for function names 114 | function-name-hint=[a-z_][a-z0-9_]{2,30}$ 115 | 116 | # Regular expression matching correct module names 117 | module-rgx=(([a-z_][a-z0-9_]*)|([A-Z][a-zA-Z0-9]+))$ 118 | 119 | # Naming hint for module names 120 | module-name-hint=(([a-z_][a-z0-9_]*)|([A-Z][a-zA-Z0-9]+))$ 121 | 122 | # Regular expression matching correct attribute names 123 | attr-rgx=[a-z_][a-z0-9_]{2,30}$ 124 | 125 | # Naming hint for attribute names 126 | attr-name-hint=[a-z_][a-z0-9_]{2,30}$ 127 | 128 | # Regular expression matching correct class attribute names 129 | class-attribute-rgx=([A-Za-z_][A-Za-z0-9_]{2,30}|(__.*__))$ 130 | 131 | # Naming hint for class attribute names 132 | class-attribute-name-hint=([A-Za-z_][A-Za-z0-9_]{2,30}|(__.*__))$ 133 | 134 | # Regular expression matching correct constant names 135 | const-rgx=(([A-Z_][A-Z0-9_]*)|(__.*__))$ 136 | 137 | # Naming hint for constant names 138 | const-name-hint=(([A-Z_][A-Z0-9_]*)|(__.*__))$ 139 | 140 | # Regular expression matching correct class names 141 | class-rgx=[A-Z_][a-zA-Z0-9]+$ 142 | 143 | # Naming hint for class names 144 | class-name-hint=[A-Z_][a-zA-Z0-9]+$ 145 | 146 | # Regular expression matching correct argument names 147 | argument-rgx=[a-z_][a-z0-9_]{2,30}$ 148 | 149 | # Naming hint for argument names 150 | argument-name-hint=[a-z_][a-z0-9_]{2,30}$ 151 | 152 | # Regular expression matching correct inline iteration names 153 | inlinevar-rgx=[A-Za-z_][A-Za-z0-9_]*$ 154 | 155 | # Naming hint for inline iteration names 156 | inlinevar-name-hint=[A-Za-z_][A-Za-z0-9_]*$ 157 | 158 | # Regular expression matching correct variable names 159 | variable-rgx=[a-z_][a-z0-9_]{2,30}$ 160 | 161 | # Naming hint for variable names 162 | variable-name-hint=[a-z_][a-z0-9_]{2,30}$ 163 | 164 | # Regular expression which should only match function or class names that do 165 | # not require a docstring. 166 | no-docstring-rgx=^_ 167 | 168 | # Minimum line length for functions/classes that require docstrings, shorter 169 | # ones are exempt. 170 | docstring-min-length=-1 171 | 172 | 173 | [ELIF] 174 | 175 | # Maximum number of nested blocks for function / method body 176 | max-nested-blocks=5 177 | 178 | 179 | [FORMAT] 180 | 181 | # Maximum number of characters on a single line. 182 | max-line-length=100 183 | 184 | # Regexp for a line that is allowed to be longer than the limit. 185 | ignore-long-lines=^\s*(# )??$ 186 | 187 | # Allow the body of an if to be on the same line as the test if there is no 188 | # else. 189 | single-line-if-stmt=no 190 | 191 | # List of optional constructs for which whitespace checking is disabled. `dict- 192 | # separator` is used to allow tabulation in dicts, etc.: {1 : 1,\n222: 2}. 193 | # `trailing-comma` allows a space between comma and closing bracket: (a, ). 194 | # `empty-line` allows space-only lines. 195 | no-space-check=trailing-comma,dict-separator 196 | 197 | # Maximum number of lines in a module 198 | max-module-lines=1000 199 | 200 | # String used as indentation unit. This is usually " " (4 spaces) or "\t" (1 201 | # tab). 202 | indent-string=' ' 203 | 204 | # Number of spaces of indent required inside a hanging or continued line. 205 | indent-after-paren=4 206 | 207 | # Expected format of line ending, e.g. empty (any line ending), LF or CRLF. 208 | expected-line-ending-format= 209 | 210 | 211 | [LOGGING] 212 | 213 | # Logging modules to check that the string format arguments are in logging 214 | # function parameter format 215 | logging-modules=logging 216 | 217 | 218 | [MISCELLANEOUS] 219 | 220 | # List of note tags to take in consideration, separated by a comma. 221 | notes=FIXME,XXX,TODO 222 | 223 | 224 | [SIMILARITIES] 225 | 226 | # Minimum lines number of a similarity. 227 | min-similarity-lines=4 228 | 229 | # Ignore comments when computing similarities. 230 | ignore-comments=yes 231 | 232 | # Ignore docstrings when computing similarities. 233 | ignore-docstrings=yes 234 | 235 | # Ignore imports when computing similarities. 236 | ignore-imports=no 237 | 238 | 239 | [SPELLING] 240 | 241 | # Spelling dictionary name. Available dictionaries: none. To make it working 242 | # install python-enchant package. 243 | spelling-dict= 244 | 245 | # List of comma separated words that should not be checked. 246 | spelling-ignore-words= 247 | 248 | # A path to a file that contains private dictionary; one word per line. 249 | spelling-private-dict-file= 250 | 251 | # Tells whether to store unknown words to indicated private dictionary in 252 | # --spelling-private-dict-file option instead of raising a message. 253 | spelling-store-unknown-words=no 254 | 255 | 256 | [TYPECHECK] 257 | 258 | # Tells whether missing members accessed in mixin class should be ignored. A 259 | # mixin class is detected if its name ends with "mixin" (case insensitive). 260 | ignore-mixin-members=yes 261 | 262 | # List of module names for which member attributes should not be checked 263 | # (useful for modules/projects where namespaces are manipulated during runtime 264 | # and thus existing member attributes cannot be deduced by static analysis. It 265 | # supports qualified module names, as well as Unix pattern matching. 266 | ignored-modules=tensorflow.core.framework,tensorflow.python.framework,tensorflow.python.ops.gen_linalg_ops 267 | 268 | # List of classes names for which member attributes should not be checked 269 | # (useful for classes with attributes dynamically set). This supports can work 270 | # with qualified names. 271 | ignored-classes= 272 | 273 | # List of members which are set dynamically and missed by pylint inference 274 | # system, and so shouldn't trigger E1101 when accessed. Python regular 275 | # expressions are accepted. 276 | generated-members= 277 | 278 | 279 | [VARIABLES] 280 | 281 | # Tells whether we should check for unused import in __init__ files. 282 | init-import=no 283 | 284 | # A regular expression matching the name of dummy variables (i.e. expectedly 285 | # not used). 286 | dummy-variables-rgx=_$|dummy 287 | 288 | # List of additional names supposed to be defined in builtins. Remember that 289 | # you should avoid to define new builtins when possible. 290 | additional-builtins= 291 | 292 | # List of strings which can identify a callback function by name. A callback 293 | # name must start or end with one of those strings. 294 | callbacks=cb_,_cb 295 | 296 | 297 | [CLASSES] 298 | 299 | # List of method names used to declare (i.e. assign) instance attributes. 300 | defining-attr-methods=__init__,__new__,setUp 301 | 302 | # List of valid names for the first argument in a class method. 303 | valid-classmethod-first-arg=cls 304 | 305 | # List of valid names for the first argument in a metaclass class method. 306 | valid-metaclass-classmethod-first-arg=mcs 307 | 308 | # List of member names, which should be excluded from the protected access 309 | # warning. 310 | exclude-protected=_asdict,_fields,_replace,_source,_make 311 | 312 | 313 | [DESIGN] 314 | 315 | # Maximum number of arguments for function / method 316 | max-args=5 317 | 318 | # Argument names that match this expression will be ignored. Default to name 319 | # with leading underscore 320 | ignored-argument-names=_.* 321 | 322 | # Maximum number of locals for function / method body 323 | max-locals=15 324 | 325 | # Maximum number of return / yield for function / method body 326 | max-returns=6 327 | 328 | # Maximum number of branch for function / method body 329 | max-branches=12 330 | 331 | # Maximum number of statements in function / method body 332 | max-statements=50 333 | 334 | # Maximum number of parents for a class (see R0901). 335 | max-parents=7 336 | 337 | # Maximum number of attributes for a class (see R0902). 338 | max-attributes=7 339 | 340 | # Minimum number of public methods for a class (see R0903). 341 | min-public-methods=2 342 | 343 | # Maximum number of public methods for a class (see R0904). 344 | max-public-methods=20 345 | 346 | # Maximum number of boolean expressions in a if statement 347 | max-bool-expr=5 348 | 349 | 350 | [IMPORTS] 351 | 352 | # Deprecated modules which should not be used, separated by a comma 353 | deprecated-modules=optparse 354 | 355 | # Create a graph of every (i.e. internal and external) dependencies in the 356 | # given file (report RP0402 must not be disabled) 357 | import-graph= 358 | 359 | # Create a graph of external dependencies in the given file (report RP0402 must 360 | # not be disabled) 361 | ext-import-graph= 362 | 363 | # Create a graph of internal dependencies in the given file (report RP0402 must 364 | # not be disabled) 365 | int-import-graph= 366 | 367 | 368 | [EXCEPTIONS] 369 | 370 | # Exceptions that will emit a warning when being caught. Defaults to 371 | # "Exception" 372 | overgeneral-exceptions=Exception 373 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Copyright (c) 2019 Brandon T. Willard 2 | Copyright (c) 2012 Matthew Rocklin 3 | 4 | All rights reserved. 5 | 6 | Redistribution and use in source and binary forms, with or without 7 | modification, are permitted provided that the following conditions are met: 8 | 9 | a. Redistributions of source code must retain the above copyright notice, 10 | this list of conditions and the following disclaimer. 11 | b. Redistributions in binary form must reproduce the above copyright 12 | notice, this list of conditions and the following disclaimer in the 13 | documentation and/or other materials provided with the distribution. 14 | c. Neither the name of kanren nor the names of its contributors 15 | may be used to endorse or promote products derived from this software 16 | without specific prior written permission. 17 | 18 | 19 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 20 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 21 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE 22 | ARE DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR 23 | ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 24 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 25 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 26 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT 27 | LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY 28 | OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH 29 | DAMAGE. 30 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include versioneer.py 2 | include kanren/_version.py 3 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | .PHONY: help venv conda docker docstyle format style black test lint check coverage pypi 2 | .DEFAULT_GOAL = help 3 | 4 | PYTHON = python 5 | PIP = pip 6 | CONDA = conda 7 | SHELL = bash 8 | 9 | help: 10 | @printf "Usage:\n" 11 | @grep -E '^[a-zA-Z_-]+:.*?# .*$$' $(MAKEFILE_LIST) | awk 'BEGIN {FS = ":.*?# "}; {printf "\033[1;34mmake %-10s\033[0m%s\n", $$1, $$2}' 12 | 13 | conda: # Set up a conda environment for development. 14 | @printf "Creating conda environment...\n" 15 | ${CONDA} create --yes --name kanren-env python=3.6 16 | ( \ 17 | ${CONDA} activate kanren-env; \ 18 | ${PIP} install -U pip; \ 19 | ${PIP} install -r requirements.txt; \ 20 | ${PIP} install -r requirements-dev.txt; \ 21 | ${CONDA} deactivate; \ 22 | ) 23 | @printf "\n\nConda environment created! \033[1;34mRun \`conda activate kanren-env\` to activate it.\033[0m\n\n\n" 24 | 25 | venv: # Set up a Python virtual environment for development. 26 | @printf "Creating Python virtual environment...\n" 27 | rm -rf kanren-venv 28 | ${PYTHON} -m venv kanren-venv 29 | ( \ 30 | source kanren-venv/bin/activate; \ 31 | ${PIP} install -U pip; \ 32 | ${PIP} install -r requirements.txt; \ 33 | ${PIP} install -r requirements-dev.txt; \ 34 | deactivate; \ 35 | ) 36 | @printf "\n\nVirtual environment created! \033[1;34mRun \`source kanren-venv/bin/activate\` to activate it.\033[0m\n\n\n" 37 | 38 | docker: # Set up a Docker image for development. 39 | @printf "Creating Docker image...\n" 40 | ${SHELL} ./scripts/container.sh --build 41 | 42 | docstyle: 43 | @printf "Checking documentation with pydocstyle...\n" 44 | pydocstyle kanren/ 45 | @printf "\033[1;34mPydocstyle passes!\033[0m\n\n" 46 | 47 | format: 48 | @printf "Checking code style with black...\n" 49 | black --check kanren/ tests/ 50 | @printf "\033[1;34mBlack passes!\033[0m\n\n" 51 | 52 | style: 53 | @printf "Checking code style with pylint...\n" 54 | pylint kanren/ tests/ 55 | @printf "\033[1;34mPylint passes!\033[0m\n\n" 56 | 57 | black: # Format code in-place using black. 58 | black kanren/ tests/ 59 | 60 | test: # Test code using pytest. 61 | pytest -v tests/ kanren/ --cov=kanren/ --cov-report=xml --html=testing-report.html --self-contained-html 62 | 63 | coverage: test 64 | diff-cover coverage.xml --compare-branch=main --fail-under=100 65 | 66 | pypi: 67 | ${PYTHON} setup.py clean --all; \ 68 | ${PYTHON} setup.py rotate --match=.tar.gz,.whl,.egg,.zip --keep=0; \ 69 | ${PYTHON} setup.py sdist bdist_wheel; \ 70 | twine upload --skip-existing dist/*; 71 | 72 | lint: docstyle format style # Lint code using pydocstyle, black and pylint. 73 | 74 | check: lint test coverage # Both lint and test code. Runs `make lint` followed by `make test`. 75 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # `kanren` 2 | 3 | [![Build Status](https://travis-ci.org/pythological/kanren.svg?branch=main)](https://travis-ci.org/pythological/kanren) [![Coverage Status](https://coveralls.io/repos/github/pythological/kanren/badge.svg?branch=main)](https://coveralls.io/github/pythological/kanren?branch=main) [![PyPI](https://img.shields.io/pypi/v/miniKanren)](https://pypi.org/project/miniKanren/) [![Join the chat at https://gitter.im/pythological/kanren](https://badges.gitter.im/pythological/kanren.svg)](https://gitter.im/pythological/kanren?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) 4 | 5 | Logic/relational programming in Python with [miniKanren](http://minikanren.org/). 6 | 7 | ## Installation 8 | 9 | Using `pip`: 10 | ```bash 11 | pip install miniKanren 12 | ``` 13 | 14 | Using `conda`: 15 | ```bash 16 | conda install -c conda-forge miniKanren 17 | ``` 18 | 19 | ## Development 20 | 21 | First obtain the project source: 22 | ```bash 23 | git clone git@github.com:pythological/kanren.git 24 | cd kanren 25 | ``` 26 | 27 | Install the development dependencies: 28 | 29 | ```bash 30 | $ pip install -r requirements.txt 31 | ``` 32 | 33 | Set up `pre-commit` hooks: 34 | 35 | ```bash 36 | $ pre-commit install --install-hooks 37 | ``` 38 | 39 | Tests can be run with the provided `Makefile`: 40 | ```bash 41 | make check 42 | ``` 43 | 44 | ## Motivation 45 | 46 | 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. 47 | 48 | These points—along with `kanren` examples—are covered in the paper ["miniKanren as a Tool for Symbolic Computation in Python"](https://arxiv.org/abs/2005.11644). 49 | 50 | ## Examples 51 | 52 | `kanren` enables one to express sophisticated relations—in the form of *goals*—and generate values that satisfy the relations. The following code is the "Hello, world!" of logic programming; it asks for values of the *logic variable* `x` such that `x == 5`: 53 | 54 | ```python 55 | >>> from kanren import run, eq, membero, var, lall 56 | >>> x = var() 57 | >>> run(1, x, eq(x, 5)) 58 | (5,) 59 | ``` 60 | 61 | Multiple logic variables and goals can be used simultaneously. The following code asks for one list containing the values of `x` and `z` such that `x == z` **and** `z == 3`: 62 | 63 | ```python 64 | >>> z = var() 65 | >>> run(1, [x, z], eq(x, z), 66 | eq(z, 3)) 67 | ([3, 3],) 68 | ``` 69 | 70 | `kanren` uses [unification](http://en.wikipedia.org/wiki/Unification_%28computer_science%29) to match forms within expression trees. The following code asks for values of `x` such that `(1, 2) == (1, x)`: 71 | 72 | ```python 73 | >>> run(1, x, eq((1, 2), (1, x))) 74 | (2,) 75 | ``` 76 | 77 | The above examples use `eq`: a *goal constructor* that creates a goal for unification between two objects. Other goal constructors, such as `membero(item, coll)`, express more sophisticated relations and are often constructed from simpler ones like `eq`. More specifically, `membero` states that `item` is a member of the collection `coll`. 78 | 79 | The following example uses `membero` to ask for *all* values of `x`, such that `x` is a member of `(1, 2, 3)` **and** `x` is a member of `(2, 3, 4)`. 80 | 81 | ```python 82 | >>> run(0, x, membero(x, (1, 2, 3)), # x is a member of (1, 2, 3) 83 | membero(x, (2, 3, 4))) # x is a member of (2, 3, 4) 84 | (2, 3) 85 | ``` 86 | 87 | The examples above made implicit use of the goal constructors `lall` and `lany`, which represent goal *conjunction* and *disjunction*, respectively. Many useful relations can be expressed with `lall`, `lany`, and `eq` alone, but in `kanren` it's also easy to leverage the host language and explicitly create any relation expressible in Python. 88 | 89 | ### Representing Knowledge 90 | 91 | `kanren` stores data as facts that state relationships between terms. The following code creates a parent relationship and uses it to state facts about who is a parent of whom within the Simpsons family: 92 | 93 | ```python 94 | >>> from kanren import Relation, facts 95 | >>> parent = Relation() 96 | >>> facts(parent, ("Homer", "Bart"), 97 | ... ("Homer", "Lisa"), 98 | ... ("Abe", "Homer")) 99 | 100 | >>> run(1, x, parent(x, "Bart")) 101 | ('Homer',) 102 | 103 | >>> run(2, x, parent("Homer", x)) 104 | ('Lisa', 'Bart') 105 | ``` 106 | 107 | We can use intermediate variables for more complex queries. For instance, who is Bart's grandfather? 108 | 109 | ```python 110 | >>> grandparent_lv, parent_lv = var(), var() 111 | >>> run(1, grandparent_lv, parent(grandparent_lv, parent_lv), 112 | parent(parent_lv, 'Bart')) 113 | ('Abe',) 114 | ``` 115 | 116 | We can express the grandfather relationship as a distinct relation by creating a goal constructor: 117 | ```python 118 | >>> def grandparent(x, z): 119 | ... y = var() 120 | ... return lall(parent(x, y), parent(y, z)) 121 | 122 | >>> run(1, x, grandparent(x, 'Bart')) 123 | ('Abe,') 124 | ``` 125 | 126 | ## Constraints 127 | 128 | `kanren` provides a fully functional constraint system that allows one to restrict unification and object types: 129 | 130 | ```python 131 | >>> from kanren.constraints import neq, isinstanceo 132 | 133 | >>> run(0, x, 134 | ... neq(x, 1), # Not "equal" to 1 135 | ... neq(x, 3), # Not "equal" to 3 136 | ... membero(x, (1, 2, 3))) 137 | (2,) 138 | 139 | >>> from numbers import Integral 140 | >>> run(0, x, 141 | ... isinstanceo(x, Integral), # `x` must be of type `Integral` 142 | ... membero(x, (1.1, 2, 3.2, 4))) 143 | (2, 4) 144 | ``` 145 | 146 | ## Graph Relations 147 | 148 | `kanren` comes with support for relational graph operations suitable for basic symbolic algebra operations. See the examples in [`doc/graphs.md`](doc/graphs.md). 149 | 150 | ## Extending `kanren` 151 | 152 | `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. 153 | 154 | ## About 155 | 156 | This project is a fork of [`logpy`](https://github.com/logpy/logpy). 157 | 158 | ## References 159 | 160 | * [Logic Programming on wikipedia](http://en.wikipedia.org/wiki/Logic_programming) 161 | * [miniKanren](http://minikanren.org/), a Scheme library for relational programming on which this library is based. More information can be found in the 162 | [thesis of William 163 | Byrd](https://scholarworks.iu.edu/dspace/bitstream/handle/2022/8777/Byrd_indiana_0093A_10344.pdf). 164 | -------------------------------------------------------------------------------- /doc/basic.md: -------------------------------------------------------------------------------- 1 | # Basics of `miniKanren` 2 | 3 | The design of `miniKanren` is simple. It orchestrates only a few basic operations and yields a lot! 4 | 5 | ## Terms 6 | 7 | Terms can be 8 | 9 | - any Python object (e.g. `1`, `[1, 2]`, `object()`, etc.), 10 | - logical variables constructed with `var`—denoted here by a tilde prefix (e.g. `~x`), 11 | - or combinations of the two (e.g. `(1, ~x, 'cat')`) 12 | 13 | In short, they are trees in which leaves may be either constants or variables. Constants may be of any Python type. 14 | 15 | ## Unification 16 | 17 | 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. 18 | 19 | `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`. 20 | 21 | Examples that unify 22 | 23 | | u | v | s | 24 | |:-----------------:|:-----------------:|:-----------------:| 25 | | 123 | 123 | {} | 26 | | 'cat' | 'cat' | {} | 27 | | (1, 2) | (1, 2) | {} | 28 | | ~x | 1 | {~x: 1} | 29 | | 1 | ~x | {~x: 1} | 30 | | (1, ~x) | (1, 2) | {~x: 2} | 31 | | (1, 1) | (~x, ~x) | {~x: 1} | 32 | | (1, 2, ~x) | (~y, 2, 3) | {~x: 3, ~y: 1} | 33 | 34 | Examples that don't unify 35 | 36 | | u | v | 37 | |:-----------------:|:-----------------:| 38 | | 123 | 'cat' | 39 | | (1, 2) | 12 | 40 | | (1, ~x) | (2, 2) | 41 | | (1, 2) | (~x, ~x) | 42 | 43 | Actually we lied, `unify` also takes a substitution as input. This allows us to keep some history around. For example: 44 | 45 | ```python 46 | >>> unify((1, 2), (1, x), {}) # normal case 47 | {~x: 2} 48 | >>> unify((1, 2), (1, x), {x: 2}) # x is already two. This is consitent 49 | {~x: 2} 50 | >>> unify((1, 2), (1, x), {x: 3}) # x is already three. This conflicts 51 | False 52 | ``` 53 | 54 | ## Reification 55 | 56 | 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)`. 57 | ```python 58 | >>> reify((1, x), {x: 2}) 59 | (1, 2) 60 | ``` 61 | 62 | ## Goals and Goal Constructors 63 | 64 | A *goal* is a function from one substitution to a stream of substitutions. 65 | 66 | ``` 67 | goal :: substitution -> [substitutions] 68 | ``` 69 | 70 | 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). 71 | 72 | ``` 73 | goal = membero(x, (1, 2, 3) 74 | ``` 75 | 76 | 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` 77 | 78 | ```python 79 | >>> for s in goal({}): 80 | ... print s 81 | {~x: 1} 82 | {~x: 2} 83 | {~x: 3} 84 | ``` 85 | What if we already know that `x` is `2`? 86 | ```python 87 | >>> for s in goal({x: 2}): 88 | ... print s 89 | {~x: 2} 90 | ``` 91 | 92 | Remember *goals* are functions from one substitution to a stream of substitutions. Users usually make goals with *goal constructors* like `eq`, or `membero`. 93 | 94 | ### Stream Functions 95 | 96 | After this point `miniKanren` is just a library to manage streams of substitutions. 97 | 98 | 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: 99 | 100 | ```python 101 | >>> g1 = membero(x, (1, 2, 3)) 102 | >>> g2 = membero(x, (2, 3, 4)) 103 | >>> for s in g1({}): 104 | ... for ss in g2(s): 105 | ... print ss 106 | {~x: 2} 107 | {~x: 3} 108 | ``` 109 | 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. 110 | 111 | ``` 112 | combinator :: [goals] -> goal 113 | ``` 114 | 115 | Two important stream functions are logical all `lall` and logical any `lany`. 116 | ```python 117 | >>> g = lall(g1, g2) 118 | >>> for s in g({}): 119 | ... print s 120 | {~x: 2} 121 | {~x: 3} 122 | 123 | >>> g = lany(g1, g2) 124 | >>> for s in g({}): 125 | ... print s 126 | {~x: 1} 127 | {~x: 2} 128 | {~x: 3} 129 | {~x: 4} 130 | ``` 131 | 132 | ### Laziness 133 | 134 | 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. 135 | 136 | ```python 137 | >>> stream = g({}) 138 | >>> stream 139 | 140 | >>> next(stream) 141 | {~x: 1} 142 | ``` 143 | 144 | ## User Interface 145 | 146 | Traditionally programs are run with the `run` function 147 | 148 | ```python 149 | >>> x = var() 150 | >>> run(0, x, membero(x, (1, 2, 3)), membero(x, (2, 3, 4))) 151 | (2, 3) 152 | ``` 153 | `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. 154 | 155 | ## Conclusion 156 | 157 | These are all the fundamental concepts that exist in `miniKanren`. To summarize: 158 | 159 | - *Term*: a Python object, logic variable, or combination of the two 160 | - *Substitution Map*: a dictionary mapping logic variables to terms 161 | - *Unification*: A function that finds logic variable substitutions that make two terms equal 162 | - *Reification*: A function that substitutes logic variables in a term with values given by a substitution map 163 | - *Goal*: A generator function that takes a substitution and yields a stream of substitutions 164 | - *Goal Constructor*: A user-level function that constructs and returns a goal 165 | -------------------------------------------------------------------------------- /doc/graphs.md: -------------------------------------------------------------------------------- 1 | # Relational Graph Manipulation 2 | 3 | In this document, we show how `kanren` can be used to perform symbolic algebra operations *relationally*. 4 | 5 | ## Setup 6 | 7 | First, we import the necessary modules and create a helper function for pretty printing the algebraic expressions. 8 | 9 | ```python 10 | from math import log, exp 11 | from numbers import Real 12 | from functools import partial 13 | from operator import add, mul 14 | 15 | from unification import var 16 | 17 | from etuples.core import etuple, ExpressionTuple 18 | 19 | from kanren import run, eq, conde, lall 20 | from kanren.core import success 21 | from kanren.graph import walko, reduceo 22 | from kanren.constraints import isinstanceo 23 | 24 | # Just some nice formatting 25 | def etuple_str(self): 26 | if len(self) > 0: 27 | return f"{getattr(self[0], '__name__', self[0])}({', '.join(map(str, self[1:]))})" 28 | else: 29 | return 'noop' 30 | 31 | 32 | ExpressionTuple.__str__ = etuple_str 33 | del ExpressionTuple._repr_pretty_ 34 | 35 | ``` 36 | 37 | Next, we create a simple goal constructor that implements the algebraic relations `x + x == 2 * x` and `log(exp(x)) == x` and 38 | constrains the input types to real numbers and expression tuples from the [`etuples`](https://github.com/pythological/etuples) package. 39 | 40 | ```python 41 | def single_math_reduceo(expanded_term, reduced_term): 42 | """Construct a goal for some simple math reductions.""" 43 | # Create a logic variable to represent our variable term "x" 44 | x_lv = var() 45 | # `conde` is a relational version of Lisp's `cond`/if-else; here, each 46 | # "branch" pairs the right- and left-hand sides of a replacement rule with 47 | # the corresponding inputs. 48 | return lall( 49 | isinstanceo(x_lv, Real), 50 | isinstanceo(x_lv, ExpressionTuple), 51 | conde( 52 | # add(x, x) == mul(2, x) 53 | [eq(expanded_term, etuple(add, x_lv, x_lv)), 54 | eq(reduced_term, etuple(mul, 2, x_lv))], 55 | # log(exp(x)) == x 56 | [eq(expanded_term, etuple(log, etuple(exp, x_lv))), 57 | eq(reduced_term, x_lv)]), 58 | ) 59 | 60 | ``` 61 | 62 | In order to obtain "fully reduced" results, we need to turn `math_reduceo` into a fixed-point-producing relation (i.e. recursive). 63 | ```python 64 | math_reduceo = partial(reduceo, single_math_reduceo) 65 | ``` 66 | 67 | We also need a relation that walks term graphs specifically (i.e. graphs composed of operator and operand combinations) and necessarily produces its output in the form of expression tuples. 68 | ```python 69 | term_walko = partial(walko, rator_goal=eq, null_type=ExpressionTuple) 70 | ``` 71 | 72 | ## Reductions 73 | 74 | The following example is a straight-forward reduction—i.e. left-to-right applications of the relations in `math_reduceo`—of the term `add(etuple(add, 3, 3), exp(log(exp(5))))`. This is the direction in which results are normally computed in symbolic algebra libraries. 75 | 76 | ```python 77 | # This is the term we want to reduce 78 | expanded_term = etuple(add, etuple(add, 3, 3), etuple(exp, etuple(log, etuple(exp, 5)))) 79 | 80 | # Create a logic variable to represent the results we want to compute 81 | reduced_term = var() 82 | 83 | # Asking for 0 results means all results 84 | res = run(3, reduced_term, term_walko(math_reduceo, expanded_term, reduced_term)) 85 | ``` 86 | 87 | ```python 88 | >>> print('\n'.join((f'{expanded_term} == {r}' for r in res))) 89 | add(add(3, 3), exp(log(exp(5)))) == add(mul(2, 3), exp(5)) 90 | add(add(3, 3), exp(log(exp(5)))) == add(add(3, 3), exp(5)) 91 | add(add(3, 3), exp(log(exp(5)))) == add(mul(2, 3), exp(log(exp(5)))) 92 | ``` 93 | 94 | ## Expansions 95 | 96 | In this example, we're specifying a grounded reduced term (i.e. `mul(2, 5)`) and an unground expanded term (i.e. the logic variable `q_lv`). We're essentially asking for *graphs that would reduce to `mul(2, 5)`*. Naturally, there are infinitely many graphs that reduce to `mul(2, 5)`, so we're only going to ask for ten of them; nevertheless, miniKanren is inherently capable of handling infinitely many results through its use of lazily evaluated goal streams. 97 | 98 | ```python 99 | expanded_term = var() 100 | reduced_term = etuple(mul, 2, 5) 101 | 102 | # Ask for 10 results of `q_lv` 103 | res = run(10, expanded_term, term_walko(math_reduceo, expanded_term, reduced_term)) 104 | ``` 105 | ```python 106 | >>> rjust = max(map(lambda x: len(str(x)), res)) 107 | >>> print('\n'.join((f'{str(r):>{rjust}} == {reduced_term}' for r in res))) 108 | add(5, 5) == mul(2, 5) 109 | mul(log(exp(2)), log(exp(5))) == mul(2, 5) 110 | log(exp(add(5, 5))) == mul(2, 5) 111 | mul(2, log(exp(5))) == mul(2, 5) 112 | log(exp(log(exp(add(5, 5))))) == mul(2, 5) 113 | mul(log(exp(log(exp(2)))), log(exp(5))) == mul(2, 5) 114 | log(exp(log(exp(log(exp(add(5, 5))))))) == mul(2, 5) 115 | mul(2, log(exp(log(exp(5))))) == mul(2, 5) 116 | log(exp(log(exp(log(exp(log(exp(add(5, 5))))))))) == mul(2, 5) 117 | mul(log(exp(log(exp(log(exp(2)))))), log(exp(5))) == mul(2, 5) 118 | ``` 119 | 120 | ## Expansions _and_ Reductions 121 | Now, we set **both** term graphs to unground logic variables. 122 | 123 | ```python 124 | expanded_term = var() 125 | reduced_term = var() 126 | 127 | res = run(10, [expanded_term, reduced_term], 128 | term_walko(math_reduceo, expanded_term, reduced_term)) 129 | ``` 130 | 131 | ```python 132 | >>> rjust = max(map(lambda x: len(str(x[0])), res)) 133 | >>> print('\n'.join((f'{str(e):>{rjust}} == {str(r)}' for e, r in res))) 134 | add(~_2291, ~_2291) == mul(2, ~_2291) 135 | ~_2288() == ~_2288() 136 | log(exp(add(~_2297, ~_2297))) == mul(2, ~_2297) 137 | ~_2288(add(~_2303, ~_2303)) == ~_2288(mul(2, ~_2303)) 138 | log(exp(log(exp(add(~_2309, ~_2309))))) == mul(2, ~_2309) 139 | ~_2288(~_2294) == ~_2288(~_2294) 140 | log(exp(log(exp(log(exp(add(~_2315, ~_2315))))))) == mul(2, ~_2315) 141 | ~_2288(~_2300()) == ~_2288(~_2300()) 142 | log(exp(log(exp(log(exp(log(exp(add(~_2325, ~_2325))))))))) == mul(2, ~_2325) 143 | ~_2288(~_2294, add(~_2331, ~_2331)) == ~_2288(~_2294, mul(2, ~_2331)) 144 | ``` 145 | 146 | The symbols prefixed by `~` are the string form of logic variables, so a result like `add(~_2291, ~_2291)` essentially means `add(x, x)` for some variable `x`. In this instance, miniKanren has used our algebraic relations in `math_reduceo` to produce more relations—even some with variable operators with multiple arities! 147 | 148 | With additional goals, we can narrow-in on very specific types of expressions. In the following, we state that `expanded_term` must be the [`cons`](https://github.com/pythological/python-cons) of a `log` and logic variable (i.e. anything else). In other words, we're stating that the operator of `expanded_term` must be a `log`, or that we want all expressions expanding to a `log`. 149 | 150 | ```python 151 | from kanren.goals import conso 152 | 153 | res = run(10, [expanded_term, reduced_term], 154 | conso(log, var(), expanded_term), 155 | term_walko(math_reduceo, expanded_term, reduced_term)) 156 | ``` 157 | ```python 158 | >>> rjust = max(map(lambda x: len(str(x[0])), res)) 159 | >>> print('\n'.join((f'{str(e):>{rjust}} == {str(r)}' for e, r in res))) 160 | log(exp(add(~_2344, ~_2344))) == mul(2, ~_2344) 161 | log() == log() 162 | log(exp(~reduced_2285)) == ~reduced_2285 163 | log(add(~_2354, ~_2354)) == log(mul(2, ~_2354)) 164 | log(exp(log(exp(add(~_2360, ~_2360))))) == mul(2, ~_2360) 165 | log(~_2347) == log(~_2347) 166 | log(exp(log(exp(log(exp(add(~_2366, ~_2366))))))) == mul(2, ~_2366) 167 | log(~_2351()) == log(~_2351()) 168 | log(exp(log(exp(log(exp(log(exp(add(~_2376, ~_2376))))))))) == mul(2, ~_2376) 169 | log(~_2347, add(~_2382, ~_2382)) == log(~_2347, mul(2, ~_2382)) 170 | ``` 171 | 172 | The output contains a nullary `log` function, which isn't a valid expression. We can restrict this type of output by further stating that the `log` expression's `cdr` term is itself the result of a `cons` and, thus, not an empty sequence. 173 | 174 | ```python 175 | exp_term_cdr = var() 176 | 177 | res = run(10, [expanded_term, reduced_term], 178 | conso(log, exp_term_cdr, expanded_term), 179 | conso(var(), var(), exp_term_cdr), 180 | term_walko(math_reduceo, expanded_term, reduced_term)) 181 | ``` 182 | ```python 183 | >>> rjust = max(map(lambda x: len(str(x[0])), res)) 184 | >>> print('\n'.join((f'{str(e):>{rjust}} == {str(r)}' for e, r in res))) 185 | log(exp(add(~_2457, ~_2457))) == mul(2, ~_2457) 186 | log(add(~_2467, ~_2467)) == log(mul(2, ~_2467)) 187 | log(exp(~_2446)) == ~_2446 188 | log(~_2460) == log(~_2460) 189 | log(exp(log(exp(add(~_2477, ~_2477))))) == mul(2, ~_2477) 190 | log(~_2464()) == log(~_2464()) 191 | log(exp(log(exp(log(exp(add(~_2487, ~_2487))))))) == mul(2, ~_2487) 192 | log(~_2460, add(~_2493, ~_2493)) == log(~_2460, mul(2, ~_2493)) 193 | log(exp(log(exp(log(exp(log(exp(add(~_2499, ~_2499))))))))) == mul(2, ~_2499) 194 | log(log(exp(add(~_2501, ~_2501)))) == log(mul(2, ~_2501)) 195 | ``` 196 | -------------------------------------------------------------------------------- /examples/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pythological/kanren/071c626e1e252e2d8dab44dfd85196f69c70de36/examples/__init__.py -------------------------------------------------------------------------------- /examples/account.py: -------------------------------------------------------------------------------- 1 | class Account(object): 2 | def __init__(self, first, last, id, balance): 3 | self.first = first 4 | self.last = last 5 | self.id = id 6 | self.balance = balance 7 | 8 | def info(self): 9 | return (self.first, self.last, self.id, self.balance) 10 | 11 | def __eq__(self, other): 12 | if isinstance(other, type(self)): 13 | return self.info() == other.info() 14 | return False 15 | 16 | def __hash__(self): 17 | return hash((type(self), self.info())) 18 | 19 | def __str__(self): 20 | return "Account: %s %s, id %d, balance %d" % self.info() 21 | 22 | __repr__ = __str__ 23 | -------------------------------------------------------------------------------- /examples/commutative.py: -------------------------------------------------------------------------------- 1 | from kanren import fact, run, var 2 | from kanren.assoccomm import associative, commutative 3 | from kanren.assoccomm import eq_assoccomm as eq 4 | 5 | 6 | # Define some dummy Operationss 7 | add = "add" 8 | mul = "mul" 9 | 10 | # Declare that these ops are commutative using the facts system 11 | fact(commutative, mul) 12 | fact(commutative, add) 13 | fact(associative, mul) 14 | fact(associative, add) 15 | 16 | # Define some logic variables 17 | x, y = var(), var() 18 | 19 | # Two expressions to match 20 | pattern = (mul, (add, 1, x), y) # (1 + x) * y 21 | expr = (mul, 2, (add, 3, 1)) # 2 * (3 + 1) 22 | 23 | res = run(0, (x, y), eq(pattern, expr)) 24 | print(res) 25 | # prints ((3, 2),) meaning 26 | # x matches to 3 27 | # y matches to 2 28 | -------------------------------------------------------------------------------- /examples/corleone.py: -------------------------------------------------------------------------------- 1 | """ 2 | Family relationships from The Godfather Translated from the core.logic example 3 | found in "The Magical Island of Kanren - core.logic Intro Part 1" 4 | http://objectcommando.com/blog/2011/11/04/the-magical-island-of-kanren-core-logic-intro-part-1/ 5 | """ 6 | import toolz 7 | 8 | from kanren import Relation, conde, facts, run, var 9 | 10 | 11 | father = Relation() 12 | mother = Relation() 13 | 14 | facts( 15 | father, 16 | ("Vito", "Michael"), 17 | ("Vito", "Sonny"), 18 | ("Vito", "Fredo"), 19 | ("Michael", "Anthony"), 20 | ("Michael", "Mary"), 21 | ("Sonny", "Vicent"), 22 | ("Sonny", "Francesca"), 23 | ("Sonny", "Kathryn"), 24 | ("Sonny", "Frank"), 25 | ("Sonny", "Santino"), 26 | ) 27 | 28 | facts( 29 | mother, 30 | ("Carmela", "Michael"), 31 | ("Carmela", "Sonny"), 32 | ("Carmela", "Fredo"), 33 | ("Kay", "Mary"), 34 | ("Kay", "Anthony"), 35 | ("Sandra", "Francesca"), 36 | ("Sandra", "Kathryn"), 37 | ("Sandra", "Frank"), 38 | ("Sandra", "Santino"), 39 | ) 40 | 41 | q = var() 42 | 43 | print((run(0, q, father("Vito", q)))) # Vito is the father of who? 44 | # ('Sonny', 'Michael', 'Fredo') 45 | 46 | 47 | print((run(0, q, father(q, "Michael")))) # Who is the father of Michael? 48 | # ('Vito',) 49 | 50 | 51 | def parent(p, child): 52 | return conde([father(p, child)], [mother(p, child)]) 53 | 54 | 55 | print((run(0, q, parent(q, "Michael")))) # Who is a parent of Michael? 56 | # ('Vito', 'Carmela') 57 | 58 | 59 | def grandparent(gparent, child): 60 | p = var() 61 | return conde((parent(gparent, p), parent(p, child))) 62 | 63 | 64 | print(run(0, q, grandparent(q, "Anthony"))) # Who is a grandparent of Anthony? 65 | # ('Vito', 'Carmela') 66 | 67 | 68 | print(run(0, q, grandparent("Vito", q))) # Vito is a grandparent of whom? 69 | # ('Vicent', 'Anthony', 'Kathryn', 'Mary', 'Frank', 'Santino', 'Francesca') 70 | 71 | 72 | def sibling(a, b): 73 | p = var() 74 | return conde((parent(p, a), parent(p, b))) 75 | 76 | 77 | # All spouses 78 | x, y, z = var(), var(), var() 79 | 80 | print(run(0, (x, y), father(x, z), mother(y, z), results_filter=toolz.unique)) 81 | # (('Sonny', 'Sandra'), ('Vito', 'Carmela'), ('Michael', 'Kay')) 82 | -------------------------------------------------------------------------------- /examples/data/adjacent-states.txt: -------------------------------------------------------------------------------- 1 | # Author Gregg Lind 2 | # License: Public Domain. I would love to hear about any projects you use 3 | # if it for though! 4 | # http://writeonly.wordpress.com/2009/03/20/adjacency-list-of-states-of-the-united-states-us/ 5 | 6 | AK 7 | AL,MS,TN,GA,FL 8 | AR,MO,TN,MS,LA,TX,OK 9 | AZ,CA,NV,UT,CO,NM 10 | CA,OR,NV,AZ 11 | CO,WY,NE,KS,OK,NM,AZ,UT 12 | CT,NY,MA,RI 13 | DC,MD,VA 14 | DE,MD,PA,NJ 15 | FL,AL,GA 16 | GA,FL,AL,TN,NC,SC 17 | HI 18 | IA,MN,WI,IL,MO,NE,SD 19 | ID,MT,WY,UT,NV,OR,WA 20 | IL,IN,KY,MO,IA,WI 21 | IN,MI,OH,KY,IL 22 | KS,NE,MO,OK,CO 23 | KY,IN,OH,WV,VA,TN,MO,IL 24 | LA,TX,AR,MS 25 | MA,RI,CT,NY,NH,VT 26 | MD,VA,WV,PA,DC,DE 27 | ME,NH 28 | MI,WI,IN,OH 29 | MN,WI,IA,SD,ND 30 | MO,IA,IL,KY,TN,AR,OK,KS,NE 31 | MS,LA,AR,TN,AL 32 | MT,ND,SD,WY,ID 33 | NC,VA,TN,GA,SC 34 | ND,MN,SD,MT 35 | NE,SD,IA,MO,KS,CO,WY 36 | NH,VT,ME,MA 37 | NJ,DE,PA,NY 38 | NM,AZ,UT,CO,OK,TX 39 | NV,ID,UT,AZ,CA,OR 40 | NY,NJ,PA,VT,MA,CT 41 | OH,PA,WV,KY,IN,MI 42 | OK,KS,MO,AR,TX,NM,CO 43 | OR,CA,NV,ID,WA 44 | PA,NY,NJ,DE,MD,WV,OH 45 | RI,CT,MA 46 | SC,GA,NC 47 | SD,ND,MN,IA,NE,WY,MT 48 | TN,KY,VA,NC,GA,AL,MS,AR,MO 49 | TX,NM,OK,AR,LA 50 | UT,ID,WY,CO,NM,AZ,NV 51 | VA,NC,TN,KY,WV,MD,DC 52 | VT,NY,NH,MA 53 | WA,ID,OR 54 | WI,MI,MN,IA,IL 55 | WV,OH,PA,MD,VA,KY 56 | WY,MT,SD,NE,CO,UT,ID 57 | -------------------------------------------------------------------------------- /examples/data/coastal-states.txt: -------------------------------------------------------------------------------- 1 | WA,OR,CA,TX,LA,MI,AL,GA,FL,SC,NC,VI,MD,DW,NJ,NY,CT,RI,MA,MN,NH 2 | -------------------------------------------------------------------------------- /examples/states.py: -------------------------------------------------------------------------------- 1 | """ 2 | An example showing how to use facts and relations to store data and query data 3 | 4 | This example builds a small database of the US states. 5 | 6 | The `adjacency` relation expresses which states border each other. 7 | The `coastal` relation expresses which states border the ocean. 8 | """ 9 | from kanren import Relation, fact, run, var 10 | 11 | 12 | adjacent = Relation() 13 | coastal = Relation() 14 | 15 | 16 | coastal_states = ( 17 | "WA,OR,CA,TX,LA,MS,AL,GA,FL,SC,NC,VA,MD,DE,NJ,NY,CT,RI,MA,ME,NH,AK,HI".split(",") 18 | ) 19 | 20 | # ['NY', 'NJ', 'CT', ...] 21 | for state in coastal_states: 22 | # E.g. 'NY' is coastal 23 | fact(coastal, state) 24 | 25 | # Lines like 'CA,OR,NV,AZ' 26 | with open("examples/data/adjacent-states.txt") as f: 27 | adjlist = [line.strip().split(",") for line in f if line and line[0].isalpha()] 28 | 29 | # ['CA', 'OR', 'NV', 'AZ'] 30 | for L in adjlist: 31 | # 'CA', ['OR', 'NV', 'AZ'] 32 | head, tail = L[0], L[1:] 33 | for state in tail: 34 | # E.g. 'CA' is adjacent to 'OR', 'CA' is adjacent to 'NV', etc. 35 | fact(adjacent, head, state) 36 | 37 | x = var() 38 | y = var() 39 | 40 | # Is California adjacent to New York? 41 | print(run(0, x, adjacent("CA", "NY"))) 42 | # () 43 | 44 | # All states next to California 45 | print(run(0, x, adjacent("CA", x))) 46 | # ('AZ', 'OR', 'NV') 47 | 48 | # All coastal states next to Texas 49 | print(run(0, x, adjacent("TX", x), coastal(x))) 50 | # ('LA',) 51 | 52 | # Five states that border a coastal state 53 | print(run(5, x, coastal(y), adjacent(x, y))) 54 | # ('LA', 'NM', 'OK', 'AR', 'RI') 55 | 56 | # All states adjacent to Tennessee and adjacent to Florida 57 | print(run(0, x, adjacent("TN", x), adjacent("FL", x))) 58 | # ('AL', 'GA') 59 | -------------------------------------------------------------------------------- /examples/user_classes.py: -------------------------------------------------------------------------------- 1 | from operator import add, gt, sub 2 | 3 | from examples.account import Account 4 | from kanren import eq, membero, run, unifiable, var 5 | from kanren.core import lall 6 | from kanren.term import applyo, term # noqa: F401 7 | 8 | 9 | unifiable(Account) # Register Account class 10 | 11 | accounts = ( 12 | Account("Adam", "Smith", 1, 20), 13 | Account("Carl", "Marx", 2, 3), 14 | Account("John", "Rockefeller", 3, 1000), 15 | ) 16 | 17 | # optional name strings are helpful for debugging 18 | first = var(prefix="first") 19 | last = var(prefix="last") 20 | ident = var(prefix="ident") 21 | balance = var(prefix="balance") 22 | newbalance = var(prefix="newbalance") 23 | 24 | # Describe a couple of transformations on accounts 25 | source = Account(first, last, ident, balance) 26 | target = Account(first, last, ident, newbalance) 27 | 28 | theorists = ("Adam", "Carl") 29 | # Give $10 to theorists 30 | theorist_bonus = lall( 31 | membero(source, accounts), 32 | membero(first, theorists), 33 | applyo(add, (10, balance), newbalance), 34 | ) 35 | 36 | # Take $10 from anyone with more than $100 37 | a = var(prefix="a") 38 | tax_the_rich = lall( 39 | membero(source, accounts), 40 | applyo(gt, (balance, 100), a), 41 | eq(a, True), 42 | applyo(sub, (balance, 10), newbalance), 43 | ) 44 | 45 | print("Take $10 from anyone with more than $100") 46 | print(run(0, target, tax_the_rich)) 47 | 48 | print("Give $10 to theorists") 49 | print(run(0, target, theorist_bonus)) 50 | -------------------------------------------------------------------------------- /examples/zebra-puzzle.py: -------------------------------------------------------------------------------- 1 | """ 2 | Zebra puzzle as published in Life International in 1962. 3 | https://en.wikipedia.org/wiki/Zebra_Puzzle 4 | """ 5 | from dataclasses import dataclass, field 6 | from typing import Union 7 | 8 | from unification import Var, unifiable, var, vars 9 | 10 | from kanren import conde, eq, lall, membero, run 11 | 12 | 13 | @unifiable 14 | @dataclass 15 | class House: 16 | nationality: Union[str, Var] = field(default_factory=var) 17 | drink: Union[str, Var] = field(default_factory=var) 18 | animal: Union[str, Var] = field(default_factory=var) 19 | cigarettes: Union[str, Var] = field(default_factory=var) 20 | color: Union[str, Var] = field(default_factory=var) 21 | 22 | 23 | def righto(right, left, houses): 24 | """Express that `right` is on the right of `left` among all the houses.""" 25 | neighbors = tuple(zip(houses[:-1], houses[1:])) 26 | return membero((left, right), neighbors) 27 | 28 | 29 | def nexto(a, b, houses): 30 | """Express that `a` and `b` are next to each other.""" 31 | return conde([righto(a, b, houses)], [righto(b, a, houses)]) 32 | 33 | 34 | # And now for the riddle 35 | houses = vars(5) 36 | goals = lall( 37 | membero(House("Englishman", color="red"), houses), 38 | membero(House("Spaniard", animal="dog"), houses), 39 | membero(House(drink="coffee", color="green"), houses), 40 | membero(House("Ukrainian", drink="tea"), houses), 41 | righto(House(color="green"), House(color="ivory"), houses), 42 | membero(House(animal="snails", cigarettes="Old Gold"), houses), 43 | membero(House(color="yellow", cigarettes="Kools"), houses), 44 | eq(House(drink="milk"), houses[2]), 45 | eq(House("Norwegian"), houses[0]), 46 | nexto(House(cigarettes="Chesterfields"), House(animal="fox"), houses), 47 | nexto(House(cigarettes="Kools"), House(animal="horse"), houses), 48 | membero(House(drink="orange juice", cigarettes="Lucky Strike"), houses), 49 | membero(House("Japanese", cigarettes="Parliaments"), houses), 50 | nexto(House("Norwegian"), House(color="blue"), houses), 51 | membero(House(drink="water"), houses), 52 | membero(House(animal="zebra"), houses), 53 | ) 54 | 55 | 56 | results = run(0, houses, goals) 57 | print(results) 58 | # ( 59 | # [ 60 | # House( 61 | # nationality="Norwegian", 62 | # drink="water", 63 | # animal="fox", 64 | # cigarettes="Kools", 65 | # color="yellow", 66 | # ), 67 | # House( 68 | # nationality="Ukrainian", 69 | # drink="tea", 70 | # animal="horse", 71 | # cigarettes="Chesterfields", 72 | # color="blue", 73 | # ), 74 | # House( 75 | # nationality="Englishman", 76 | # drink="milk", 77 | # animal="snails", 78 | # cigarettes="Old Gold", 79 | # color="red", 80 | # ), 81 | # House( 82 | # nationality="Spaniard", 83 | # drink="orange juice", 84 | # animal="dog", 85 | # cigarettes="Lucky Strike", 86 | # color="ivory", 87 | # ), 88 | # House( 89 | # nationality="Japanese", 90 | # drink="coffee", 91 | # animal="zebra", 92 | # cigarettes="Parliaments", 93 | # color="green", 94 | # ), 95 | # ], 96 | # ) 97 | -------------------------------------------------------------------------------- /kanren/__init__.py: -------------------------------------------------------------------------------- 1 | # flake8: noqa 2 | """kanren is a Python library for logic and relational programming.""" 3 | from unification import Var, isvar, reify, unifiable, unify, var, variables, vars 4 | 5 | from ._version import get_versions 6 | from .core import conde, eq, lall, lany, run 7 | from .facts import Relation, fact, facts 8 | from .goals import ( 9 | appendo, 10 | conso, 11 | heado, 12 | itero, 13 | membero, 14 | nullo, 15 | permuteo, 16 | permuteq, 17 | rembero, 18 | tailo, 19 | ) 20 | from .term import arguments, operator, term, unifiable_with_term 21 | 22 | 23 | __version__ = get_versions()["version"] 24 | del get_versions 25 | -------------------------------------------------------------------------------- /kanren/assoccomm.py: -------------------------------------------------------------------------------- 1 | """Functions for associative and commutative unification. 2 | 3 | This module provides goals for associative and commutative unification. It 4 | accomplishes this through naively trying all possibilities. This was built to 5 | be used in the computer algebra systems SymPy and Theano. 6 | 7 | >>> from kanren import run, var, fact 8 | >>> from kanren.assoccomm import eq_assoccomm as eq 9 | >>> from kanren.assoccomm import commutative, associative 10 | 11 | >>> # Define some dummy Ops 12 | >>> add = 'add' 13 | >>> mul = 'mul' 14 | 15 | >>> # Declare that these ops are commutative using the facts system 16 | >>> fact(commutative, mul) 17 | >>> fact(commutative, add) 18 | >>> fact(associative, mul) 19 | >>> fact(associative, add) 20 | 21 | >>> # Define some wild variables 22 | >>> x, y = var('x'), var('y') 23 | 24 | >>> # Two expressions to match 25 | >>> pattern = (mul, (add, 1, x), y) # (1 + x) * y 26 | >>> expr = (mul, 2, (add, 3, 1)) # 2 * (3 + 1) 27 | 28 | >>> print(run(0, (x,y), eq(pattern, expr))) 29 | ((3, 2),) 30 | """ 31 | from collections.abc import Sequence 32 | from functools import partial 33 | from operator import eq as equal 34 | from operator import length_hint 35 | 36 | from cons.core import ConsPair, car, cdr 37 | from etuples import etuple 38 | from toolz import sliding_window 39 | from unification import reify, unify, var 40 | 41 | from .core import conde, eq, ground_order, lall, succeed 42 | from .facts import Relation 43 | from .goals import itero, permuteo 44 | from .graph import term_walko 45 | from .term import term 46 | 47 | 48 | associative = Relation("associative") 49 | commutative = Relation("commutative") 50 | 51 | 52 | def flatten_assoc_args(op_predicate, items): 53 | for i in items: 54 | if isinstance(i, ConsPair) and op_predicate(car(i)): 55 | i_cdr = cdr(i) 56 | if length_hint(i_cdr) > 0: 57 | yield from flatten_assoc_args(op_predicate, i_cdr) 58 | else: 59 | yield i 60 | else: 61 | yield i 62 | 63 | 64 | def assoc_args(rator, rands, n, ctor=None): 65 | """Produce all associative argument combinations of rator + rands in n-sized rand groupings. 66 | 67 | >>> from kanren.assoccomm import assoc_args 68 | >>> list(assoc_args('op', [1, 2, 3], 2)) 69 | [[['op', 1, 2], 3], [1, ['op', 2, 3]]] 70 | """ # noqa: E501 71 | assert n > 0 72 | 73 | rands_l = list(rands) 74 | 75 | if ctor is None: 76 | ctor = type(rands) 77 | 78 | if n == len(rands_l): 79 | yield ctor(rands) 80 | return 81 | 82 | for i, new_rands in enumerate(sliding_window(n, rands_l)): 83 | prefix = rands_l[:i] 84 | new_term = term(rator, ctor(new_rands)) 85 | suffix = rands_l[n + i :] 86 | res = ctor(prefix + [new_term] + suffix) 87 | yield res 88 | 89 | 90 | def eq_assoc_args( 91 | op, a_args, b_args, n=None, inner_eq=eq, no_ident=False, null_type=etuple 92 | ): 93 | """Create a goal that applies associative unification to an operator and two sets of arguments. 94 | 95 | This is a non-relational utility goal. It does assumes that the op and at 96 | least one set of arguments are ground under the state in which it is 97 | evaluated. 98 | """ # noqa: E501 99 | u_args, v_args = var(), var() 100 | 101 | def eq_assoc_args_goal(S): 102 | nonlocal op, u_args, v_args, n 103 | 104 | (op_rf, u_args_rf, v_args_rf, n_rf) = reify((op, u_args, v_args, n), S) 105 | 106 | if isinstance(v_args_rf, Sequence): 107 | u_args_rf, v_args_rf = v_args_rf, u_args_rf 108 | 109 | if isinstance(u_args_rf, Sequence) and isinstance(v_args_rf, Sequence): 110 | # TODO: We just ignore `n` when both are sequences? 111 | 112 | if type(u_args_rf) != type(v_args_rf): 113 | return 114 | 115 | if no_ident and unify(u_args_rf, v_args_rf, S) is not False: 116 | return 117 | 118 | op_pred = partial(equal, op_rf) 119 | u_args_flat = type(u_args_rf)(flatten_assoc_args(op_pred, u_args_rf)) 120 | v_args_flat = type(v_args_rf)(flatten_assoc_args(op_pred, v_args_rf)) 121 | 122 | if len(u_args_flat) == len(v_args_flat): 123 | g = inner_eq(u_args_flat, v_args_flat) 124 | else: 125 | if len(u_args_flat) < len(v_args_flat): 126 | sm_args, lg_args = u_args_flat, v_args_flat 127 | else: 128 | sm_args, lg_args = v_args_flat, u_args_flat 129 | 130 | grp_sizes = len(lg_args) - len(sm_args) + 1 131 | assoc_terms = assoc_args( 132 | op_rf, lg_args, grp_sizes, ctor=type(u_args_rf) 133 | ) 134 | 135 | g = conde([inner_eq(sm_args, a_args)] for a_args in assoc_terms) 136 | 137 | yield from g(S) 138 | 139 | elif isinstance(u_args_rf, Sequence): 140 | # TODO: We really need to know the arity (ranges) for the operator 141 | # in order to make good choices here. 142 | # For instance, does `(op, 1, 2) == (op, (op, 1, 2))` make sense? 143 | # If so, the lower-bound on this range should actually be `1`. 144 | if len(u_args_rf) == 1: 145 | if not no_ident and (n_rf == 1 or n_rf is None): 146 | g = inner_eq(u_args_rf, v_args_rf) 147 | else: 148 | return 149 | else: 150 | 151 | u_args_flat = list(flatten_assoc_args(partial(equal, op_rf), u_args_rf)) 152 | 153 | if n_rf is not None: 154 | arg_sizes = [n_rf] 155 | else: 156 | arg_sizes = range(2, len(u_args_flat) + (not no_ident)) 157 | 158 | v_ac_args = ( 159 | v_ac_arg 160 | for n_i in arg_sizes 161 | for v_ac_arg in assoc_args( 162 | op_rf, u_args_flat, n_i, ctor=type(u_args_rf) 163 | ) 164 | if not no_ident or v_ac_arg != u_args_rf 165 | ) 166 | g = conde([inner_eq(v_args_rf, v_ac_arg)] for v_ac_arg in v_ac_args) 167 | 168 | yield from g(S) 169 | 170 | return lall( 171 | ground_order((a_args, b_args), (u_args, v_args)), 172 | itero(u_args, nullo_refs=(v_args,), default_ConsNull=null_type), 173 | eq_assoc_args_goal, 174 | ) 175 | 176 | 177 | def eq_assoc(u, v, n=None, op_predicate=associative, null_type=etuple): 178 | """Create a goal for associative unification of two terms. 179 | 180 | >>> from kanren import run, var, fact 181 | >>> from kanren.assoccomm import eq_assoc as eq 182 | 183 | >>> fact(commutative, 'add') # declare that 'add' is commutative 184 | >>> fact(associative, 'add') # declare that 'add' is associative 185 | 186 | >>> x = var() 187 | >>> run(0, x, eq(('add', 1, 2, 3), ('add', 1, x))) 188 | (('add', 2, 3),) 189 | """ 190 | 191 | def assoc_args_unique(a, b, op, **kwargs): 192 | return eq_assoc_args(op, a, b, no_ident=True, null_type=null_type) 193 | 194 | return term_walko(op_predicate, assoc_args_unique, u, v, n=n) 195 | 196 | 197 | def eq_comm(u, v, op_predicate=commutative, null_type=etuple): 198 | """Create a goal for commutative equality. 199 | 200 | >>> from kanren import run, var, fact 201 | >>> from kanren.assoccomm import eq_comm as eq 202 | >>> from kanren.assoccomm import commutative, associative 203 | 204 | >>> fact(commutative, 'add') # declare that 'add' is commutative 205 | >>> fact(associative, 'add') # declare that 'add' is associative 206 | 207 | >>> x = var() 208 | >>> run(0, x, eq(('add', 1, 2, 3), ('add', 2, x, 1))) 209 | (3,) 210 | """ 211 | 212 | def permuteo_unique(x, y, op, **kwargs): 213 | return permuteo(x, y, no_ident=True, default_ConsNull=null_type) 214 | 215 | return term_walko(op_predicate, permuteo_unique, u, v) 216 | 217 | 218 | def assoc_flatten(a, a_flat): 219 | def assoc_flatten_goal(S): 220 | nonlocal a, a_flat 221 | 222 | a_rf = reify(a, S) 223 | 224 | if isinstance(a_rf, Sequence) and (a_rf[0],) in associative.facts: 225 | 226 | def op_pred(sub_op): 227 | nonlocal S 228 | sub_op_rf = reify(sub_op, S) 229 | return sub_op_rf == a_rf[0] 230 | 231 | a_flat_rf = type(a_rf)(flatten_assoc_args(op_pred, a_rf)) 232 | else: 233 | a_flat_rf = a_rf 234 | 235 | yield from eq(a_flat, a_flat_rf)(S) 236 | 237 | return assoc_flatten_goal 238 | 239 | 240 | def eq_assoccomm(u, v, null_type=etuple): 241 | """Construct a goal for associative and commutative unification. 242 | 243 | >>> from kanren.assoccomm import eq_assoccomm as eq 244 | >>> from kanren.assoccomm import commutative, associative 245 | >>> from kanren import fact, run, var 246 | 247 | >>> fact(commutative, 'add') # declare that 'add' is commutative 248 | >>> fact(associative, 'add') # declare that 'add' is associative 249 | 250 | >>> x = var() 251 | >>> e1 = ('add', 1, 2, 3) 252 | >>> e2 = ('add', 1, x) 253 | >>> run(0, x, eq(e1, e2)) 254 | (('add', 3, 2), ('add', 2, 3)) 255 | """ 256 | 257 | def eq_assoccomm_step(a, b, op): 258 | z = var() 259 | return lall( 260 | # Permute 261 | conde( 262 | [ 263 | commutative(op), 264 | permuteo(a, z, no_ident=True, default_ConsNull=etuple), 265 | ], 266 | [eq(a, z)], 267 | ), 268 | # Generate associative combinations 269 | conde( 270 | [associative(op), eq_assoc_args(op, z, b, no_ident=True)], [eq(z, b)] 271 | ), 272 | ) 273 | 274 | return term_walko( 275 | lambda x: succeed, 276 | eq_assoccomm_step, 277 | u, 278 | v, 279 | format_step=assoc_flatten, 280 | no_ident=False, 281 | ) 282 | -------------------------------------------------------------------------------- /kanren/constraints.py: -------------------------------------------------------------------------------- 1 | import weakref 2 | from abc import ABC, abstractmethod 3 | from collections import UserDict 4 | from collections.abc import Mapping 5 | from typing import Optional 6 | 7 | from cons.core import ConsPair 8 | from toolz import groupby 9 | from unification import Var, reify, unify, var 10 | from unification.core import _reify, isground 11 | from unification.utils import transitive_get as walk 12 | 13 | from .util import FlexibleSet 14 | 15 | 16 | class ConstraintStore(ABC): 17 | """A class that enforces constraints between logic variables in a miniKanren state. 18 | 19 | Attributes 20 | ---------- 21 | lvar_constraints: MutableMapping 22 | A mapping of logic variables to sets of objects that define their 23 | constraints (e.g. a set of items with which the logic variable cannot 24 | be unified). The mapping's values are entirely determined by the 25 | ConstraintStore implementation. 26 | 27 | """ 28 | 29 | __slots__ = ("lvar_constraints",) 30 | op_str: Optional[str] = None 31 | 32 | def __init__(self, lvar_constraints=None): 33 | # self.lvar_constraints = weakref.WeakKeyDictionary(lvar_constraints) 34 | self.lvar_constraints = lvar_constraints or dict() 35 | 36 | @abstractmethod 37 | def pre_unify_check(self, lvar_map, lvar=None, value=None): 38 | """Check a key-value pair before they're added to a ConstrainedState.""" 39 | raise NotImplementedError() 40 | 41 | @abstractmethod 42 | def post_unify_check(self, lvar_map, lvar=None, value=None, old_state=None): 43 | """Check a key-value pair after they're added to a ConstrainedState. 44 | 45 | XXX: This method may alter the internal constraints, so make a copy! 46 | """ 47 | raise NotImplementedError() 48 | 49 | def add(self, lvar, lvar_constraint, **kwargs): 50 | """Add a new constraint.""" 51 | if lvar not in self.lvar_constraints: 52 | self.lvar_constraints[lvar] = FlexibleSet([lvar_constraint]) 53 | else: 54 | self.lvar_constraints[lvar].add(lvar_constraint) 55 | 56 | def constraints_str(self, lvar): 57 | """Print the constraints on a logic variable.""" 58 | if lvar in self.lvar_constraints: 59 | return f"{self.op_str} {self.lvar_constraints[lvar]}" 60 | else: 61 | return "" 62 | 63 | def copy(self): 64 | return type(self)( 65 | lvar_constraints={k: v.copy() for k, v in self.lvar_constraints.items()}, 66 | ) 67 | 68 | def __contains__(self, lvar): 69 | return lvar in self.lvar_constraints 70 | 71 | def __eq__(self, other): 72 | return ( 73 | type(self) == type(other) 74 | and self.op_str == other.op_str 75 | and self.lvar_constraints == other.lvar_constraints 76 | ) 77 | 78 | def __repr__(self): 79 | return f"ConstraintStore({self.op_str}: {self.lvar_constraints})" 80 | 81 | 82 | class ConstrainedState(UserDict): 83 | """A miniKanren state that holds unifications of logic variables and upholds constraints on logic variables.""" # noqa: E501 84 | 85 | __slots__ = ("constraints",) 86 | 87 | def __init__(self, *s, constraints=None): 88 | super().__init__(*s) 89 | self.constraints = dict(constraints or []) 90 | 91 | def pre_unify_checks(self, lvar, value): 92 | """Check the constraints before unification.""" 93 | return all( 94 | cstore.pre_unify_check(self.data, lvar, value) 95 | for cstore in self.constraints.values() 96 | ) 97 | 98 | def post_unify_checks(self, lvar_map, lvar, value): 99 | """Check constraints and return an updated state and constraints. 100 | 101 | Returns 102 | ------- 103 | A new `ConstrainedState` and `False`. 104 | 105 | """ 106 | S = self.copy(data=lvar_map) 107 | if any( 108 | not cstore.post_unify_check(lvar_map, lvar, value, old_state=S) 109 | for cstore in S.constraints.values() 110 | ): 111 | return False 112 | 113 | return S 114 | 115 | def copy(self, data=None): 116 | if data is None: 117 | data = self.data.copy() 118 | return type(self)( 119 | data, constraints={k: v.copy() for k, v in self.constraints.items()} 120 | ) 121 | 122 | def __eq__(self, other): 123 | if isinstance(other, ConstrainedState): 124 | return self.data == other.data and self.constraints == other.constraints 125 | 126 | if isinstance(other, Mapping) and not self.constraints: 127 | return self.data == other 128 | 129 | return False 130 | 131 | def __repr__(self): 132 | return f"ConstrainedState({repr(self.data)}, {self.constraints})" 133 | 134 | 135 | def unify_ConstrainedState(u, v, S): 136 | if S.pre_unify_checks(u, v): 137 | s = unify(u, v, S.data) 138 | if s is not False: 139 | S = S.post_unify_checks(s, u, v) 140 | if S is not False: 141 | return S 142 | 143 | return False 144 | 145 | 146 | unify.add((object, object, ConstrainedState), unify_ConstrainedState) 147 | 148 | 149 | class ConstrainedVar(Var): 150 | """A logic variable that tracks its own constraints. 151 | 152 | Currently, this is only for display/reification purposes. 153 | 154 | """ 155 | 156 | __slots__ = ("S", "var") 157 | 158 | def __init__(self, var, S): 159 | self.S = weakref.ref(S) 160 | self.token = var.token 161 | self.var = weakref.ref(var) 162 | 163 | def __repr__(self): 164 | S = self.S() 165 | var = self.var() 166 | res = super().__repr__() 167 | if S is not None and var is not None: 168 | u_constraints = ",".join( 169 | [c.constraints_str(var) for c in S.constraints.values()] 170 | ) 171 | return f"{res}: {{{u_constraints}}}" 172 | else: 173 | return res 174 | 175 | def __eq__(self, other): 176 | if type(other) == type(self): 177 | return self.S == other.S and self.token == other.token 178 | elif type(other) == Var: 179 | # NOTE: A more valid comparison is same token and no constraints. 180 | return self.token == other.token 181 | return NotImplemented 182 | 183 | def __hash__(self): 184 | return hash((Var, self.token)) 185 | 186 | 187 | def _reify_ConstrainedState(u, S): 188 | u_res = walk(u, S.data) 189 | 190 | if u_res is u: 191 | yield ConstrainedVar(u_res, S) 192 | else: 193 | yield _reify(u_res, S) 194 | 195 | 196 | _reify.add((Var, ConstrainedState), _reify_ConstrainedState) 197 | 198 | 199 | class DisequalityStore(ConstraintStore): 200 | """A disequality constraint (i.e. two things do not unify).""" 201 | 202 | op_str = "neq" 203 | 204 | def __init__(self, lvar_constraints=None): 205 | super().__init__(lvar_constraints) 206 | 207 | def post_unify_check(self, lvar_map, lvar=None, value=None, old_state=None): 208 | 209 | for lv_key, constraints in list(self.lvar_constraints.items()): 210 | lv = reify(lv_key, lvar_map) 211 | constraints_rf = reify(tuple(constraints), lvar_map) 212 | 213 | for cs in constraints_rf: 214 | s = unify(lv, cs, {}) 215 | 216 | if s is not False and not s: 217 | # They already unify, but with no unground logic variables, 218 | # so we have an immediate violation of the constraint. 219 | return False 220 | elif s is False: 221 | # They don't unify and have no unground logic variables, so 222 | # the constraint is immediately satisfied and there's no 223 | # reason to continue checking this constraint. 224 | constraints.discard(cs) 225 | else: 226 | # They unify when/if the unifications in `s` are made, so 227 | # let's add these as new constraints. 228 | for k, v in s.items(): 229 | self.add(k, v) 230 | 231 | if len(constraints) == 0: 232 | # This logic variable has no more unground constraints, so 233 | # remove it. 234 | del self.lvar_constraints[lv_key] 235 | 236 | return True 237 | 238 | def pre_unify_check(self, lvar_map, lvar=None, value=None): 239 | return True 240 | 241 | 242 | def neq(u, v): 243 | """Construct a disequality goal.""" 244 | 245 | def neq_goal(S): 246 | nonlocal u, v 247 | 248 | u_rf, v_rf = reify((u, v), S) 249 | 250 | # Get the unground logic variables that would unify the two objects; 251 | # these are all the logic variables that we can't let unify. 252 | s_uv = unify(u_rf, v_rf, {}) 253 | 254 | if s_uv is False: 255 | # They don't unify and have no unground logic variables, so the 256 | # constraint is immediately satisfied. 257 | yield S 258 | return 259 | elif not s_uv: 260 | # They already unify, but with no unground logic variables, so we 261 | # have an immediate violation of the constraint. 262 | return 263 | 264 | if not isinstance(S, ConstrainedState): 265 | S = ConstrainedState(S) 266 | 267 | cs = S.constraints.setdefault(DisequalityStore, DisequalityStore()) 268 | 269 | for lvar, obj in s_uv.items(): 270 | cs.add(lvar, obj) 271 | 272 | # We need to check the current state for validity. 273 | if cs.post_unify_check(S.data): 274 | yield S 275 | 276 | return neq_goal 277 | 278 | 279 | class PredicateStore(ConstraintStore, ABC): 280 | """An abstract store for testing simple predicates.""" 281 | 282 | # Require that all constraints be satisfied for a term; otherwise, succeed 283 | # if only one is satisfied. 284 | require_all_constraints = True 285 | 286 | # @abstractmethod 287 | # def cterm_type_check(self, lvt): 288 | # """Check the type of the constrained term when it's ground.""" 289 | # raise NotImplementedError() 290 | 291 | @abstractmethod 292 | def cparam_type_check(self, lvt): 293 | """Check the type of the constraint parameter when it's ground.""" 294 | raise NotImplementedError() 295 | 296 | @abstractmethod 297 | def constraint_check(self, lv, lvt): 298 | """Check the constrained term against the constraint parameters when they're ground. 299 | 300 | I.e. test the constraint. 301 | """ 302 | raise NotImplementedError() 303 | 304 | @abstractmethod 305 | def constraint_isground(self, lv, lvar_map): 306 | """Check whether or not the constrained term is "ground enough" to be checked.""" # noqa: E501 307 | raise NotImplementedError() 308 | 309 | def post_unify_check(self, lvar_map, lvar=None, value=None, old_state=None): 310 | 311 | for lv_key, constraints in list(self.lvar_constraints.items()): 312 | 313 | lv = reify(lv_key, lvar_map) 314 | 315 | is_lv_ground = self.constraint_isground(lv, lvar_map) or isground( 316 | lv, lvar_map 317 | ) 318 | 319 | if not is_lv_ground: 320 | # This constraint isn't ready to be checked 321 | continue 322 | 323 | # if is_lv_ground and not self.cterm_type_check(lv): 324 | # self.lvar_constraints[lv_key] 325 | # return False 326 | 327 | constraint_grps = groupby( 328 | lambda x: isground(x, lvar_map), reify(iter(constraints), lvar_map) 329 | ) 330 | 331 | constraints_unground = constraint_grps.get(False, ()) 332 | constraints_ground = constraint_grps.get(True, ()) 333 | 334 | if len(constraints_ground) > 0 and not all( 335 | self.cparam_type_check(c) for c in constraints_ground 336 | ): 337 | # Some constraint parameters aren't the correct type, so fail. 338 | # del self.lvar_constraints[lv_key] 339 | return False 340 | 341 | assert constraints_unground or constraints_ground 342 | 343 | if is_lv_ground and len(constraints_unground) == 0: 344 | 345 | if self.require_all_constraints and any( 346 | not self.constraint_check(lv, t) for t in constraints_ground 347 | ): 348 | return False 349 | elif not self.require_all_constraints and not any( 350 | self.constraint_check(lv, t) for t in constraints_ground 351 | ): 352 | return False 353 | 354 | # The instance and constraint parameters are all ground and the 355 | # constraint is satisfied, so, since nothing should change from 356 | # here on, we can remove the constraint. 357 | 358 | del self.lvar_constraints[lv_key] 359 | 360 | # Some types are unground, so we continue checking until they are 361 | return True 362 | 363 | def pre_unify_check(self, lvar_map, lvar=None, value=None): 364 | return True 365 | 366 | 367 | class TypeStore(PredicateStore): 368 | """A constraint store for asserting object types.""" 369 | 370 | require_all_constraints = True 371 | 372 | op_str = "typeo" 373 | 374 | def __init__(self, lvar_constraints=None): 375 | super().__init__(lvar_constraints) 376 | 377 | def add(self, lvt, cparams): 378 | if lvt in self.lvar_constraints: 379 | raise ValueError("Only one type constraint can be applied to a term") 380 | 381 | return super().add(lvt, cparams) 382 | 383 | # def cterm_type_check(self, lvt): 384 | # return True 385 | 386 | def cparam_type_check(self, x): 387 | return isinstance(x, type) 388 | 389 | def constraint_check(self, x, cx): 390 | return type(x) == cx 391 | 392 | def constraint_isground(self, lv, lvar_map): 393 | return not (isinstance(lv, Var) or issubclass(type(lv), ConsPair)) 394 | 395 | 396 | def typeo(u, u_type): 397 | """Construct a goal specifying the type of a term.""" 398 | 399 | def typeo_goal(S): 400 | nonlocal u, u_type 401 | 402 | u_rf, u_type_rf = reify((u, u_type), S) 403 | 404 | if not isground(u_rf, S) or not isground(u_type_rf, S): 405 | 406 | if not isinstance(S, ConstrainedState): 407 | S = ConstrainedState(S) 408 | 409 | cs = S.constraints.setdefault(TypeStore, TypeStore()) 410 | 411 | try: 412 | cs.add(u_rf, u_type_rf) 413 | except TypeError: 414 | # If the instance object can't be hashed, we can simply use a 415 | # logic variable to uniquely identify it. 416 | u_lv = var() 417 | S[u_lv] = u_rf 418 | cs.add(u_lv, u_type_rf) 419 | 420 | if cs.post_unify_check(S.data, u_rf, u_type_rf): 421 | yield S 422 | 423 | elif isinstance(u_type_rf, type) and type(u_rf) == u_type_rf: 424 | yield S 425 | 426 | return typeo_goal 427 | 428 | 429 | class IsinstanceStore(PredicateStore): 430 | """A constraint store for asserting object instance types.""" 431 | 432 | op_str = "isinstanceo" 433 | 434 | # Satisfying any one constraint is good enough 435 | require_all_constraints = False 436 | 437 | def __init__(self, lvar_constraints=None): 438 | super().__init__(lvar_constraints) 439 | 440 | # def cterm_type_check(self, lvt): 441 | # return True 442 | 443 | def cparam_type_check(self, lvt): 444 | return isinstance(lvt, type) 445 | 446 | def constraint_check(self, lv, lvt): 447 | return isinstance(lv, lvt) 448 | 449 | def constraint_isground(self, lv, lvar_map): 450 | return not (isinstance(lv, Var) or issubclass(type(lv), ConsPair)) 451 | 452 | 453 | def isinstanceo(u, u_type): 454 | """Construct a goal specifying that a term is an instance of a type. 455 | 456 | Only a single instance type can be assigned per goal, i.e. 457 | 458 | lany(isinstanceo(var(), list), 459 | isinstanceo(var(), tuple)) 460 | 461 | and not 462 | 463 | isinstanceo(var(), (list, tuple)) 464 | 465 | """ 466 | 467 | def isinstanceo_goal(S): 468 | nonlocal u, u_type 469 | 470 | u_rf, u_type_rf = reify((u, u_type), S) 471 | 472 | if not isground(u_rf, S) or not isground(u_type_rf, S): 473 | 474 | if not isinstance(S, ConstrainedState): 475 | S = ConstrainedState(S) 476 | 477 | cs = S.constraints.setdefault(IsinstanceStore, IsinstanceStore()) 478 | 479 | try: 480 | cs.add(u_rf, u_type_rf) 481 | except TypeError: 482 | # If the instance object can't be hashed, we can simply use a 483 | # logic variable to uniquely identify it. 484 | u_lv = var() 485 | S[u_lv] = u_rf 486 | cs.add(u_lv, u_type_rf) 487 | 488 | if cs.post_unify_check(S.data, u_rf, u_type_rf): 489 | yield S 490 | 491 | # elif isground(u_type, S): 492 | # yield from lany(eq(u_type, u_t) for u_t in type(u).mro())(S) 493 | elif ( 494 | isinstance(u_type_rf, type) 495 | # or ( 496 | # isinstance(u_type, Iterable) 497 | # and all(isinstance(t, type) for t in u_type) 498 | # ) 499 | ) and isinstance(u_rf, u_type_rf): 500 | yield S 501 | 502 | return isinstanceo_goal 503 | -------------------------------------------------------------------------------- /kanren/core.py: -------------------------------------------------------------------------------- 1 | from collections.abc import Sequence 2 | from functools import partial, reduce 3 | from itertools import tee 4 | from operator import length_hint 5 | from typing import ( 6 | Any, 7 | Callable, 8 | Iterable, 9 | Iterator, 10 | MutableMapping, 11 | Optional, 12 | Tuple, 13 | Union, 14 | cast, 15 | ) 16 | 17 | from cons.core import ConsPair 18 | from toolz import interleave, take 19 | from typing_extensions import Literal 20 | from unification import isvar, reify, unify 21 | from unification.core import isground 22 | 23 | 24 | StateType = Union[MutableMapping, Literal[False]] 25 | StateStreamType = Iterator[StateType] 26 | GoalType = Callable[[StateType], StateStreamType] 27 | 28 | 29 | def fail(s: StateType) -> Iterator[StateType]: 30 | return iter(()) 31 | 32 | 33 | def succeed(s: StateType) -> Iterator[StateType]: 34 | return iter((s,)) 35 | 36 | 37 | def eq(u: Any, v: Any) -> GoalType: 38 | """Construct a goal stating that its arguments must unify. 39 | 40 | See Also 41 | -------- 42 | unify 43 | """ 44 | 45 | def eq_goal(s: StateType) -> StateStreamType: 46 | s = unify(u, v, s) 47 | if s is not False: 48 | return iter((s,)) 49 | else: 50 | return iter(()) 51 | 52 | return eq_goal 53 | 54 | 55 | def ldisj_seq(goals: Iterable[GoalType]) -> GoalType: 56 | """Produce a goal that returns the appended state stream from all successful goal arguments. 57 | 58 | In other words, it behaves like logical disjunction/OR for goals. 59 | """ # noqa: E501 60 | 61 | if length_hint(goals, -1) == 0: 62 | return succeed 63 | 64 | assert isinstance(goals, Iterable) 65 | 66 | def ldisj_seq_goal(S: StateType) -> StateStreamType: 67 | nonlocal goals 68 | 69 | goals, _goals = tee(goals) 70 | 71 | yield from interleave(g(S) for g in _goals) 72 | 73 | return ldisj_seq_goal 74 | 75 | 76 | def bind(z: StateStreamType, g: GoalType) -> StateStreamType: 77 | """Apply a goal to a state stream and then combine the resulting state streams.""" 78 | # We could also use `chain`, but `interleave` preserves the old behavior. 79 | # return chain.from_iterable(map(g, z)) 80 | return cast(StateStreamType, interleave(map(g, z))) 81 | 82 | 83 | def lconj_seq(goals: Iterable[GoalType]) -> GoalType: 84 | """Produce a goal that returns the appended state stream in which all goals are necessarily successful. 85 | 86 | In other words, it behaves like logical conjunction/AND for goals. 87 | """ # noqa: E501 88 | 89 | if length_hint(goals, -1) == 0: 90 | return succeed 91 | 92 | assert isinstance(goals, Iterable) 93 | 94 | def lconj_seq_goal(S: StateType) -> StateStreamType: 95 | nonlocal goals 96 | 97 | goals, _goals = tee(goals) 98 | 99 | g0 = next(_goals, None) 100 | 101 | if g0 is None: 102 | return 103 | 104 | yield from reduce(bind, _goals, g0(S)) 105 | 106 | return lconj_seq_goal 107 | 108 | 109 | def ldisj(*goals: Union[GoalType, Iterable[GoalType]]) -> GoalType: 110 | """Form a disjunction of goals.""" 111 | if len(goals) == 1 and isinstance(goals[0], Iterable): 112 | return ldisj_seq(goals[0]) 113 | 114 | return ldisj_seq(cast(Tuple[GoalType, ...], goals)) 115 | 116 | 117 | def lconj(*goals: Union[GoalType, Iterable[GoalType]]) -> GoalType: 118 | """Form a conjunction of goals.""" 119 | if len(goals) == 1 and isinstance(goals[0], Iterable): 120 | return lconj_seq(goals[0]) 121 | 122 | return lconj_seq(cast(Tuple[GoalType, ...], goals)) 123 | 124 | 125 | def conde( 126 | *goals: Union[Iterable[GoalType], Iterator[Iterable[GoalType]]] 127 | ) -> Union[GoalType, StateStreamType]: 128 | """Form a disjunction of goal conjunctions.""" 129 | if len(goals) == 1 and isinstance(goals[0], Iterator): 130 | return ldisj_seq( 131 | lconj_seq(g) for g in cast(Iterator[Iterable[GoalType]], goals[0]) 132 | ) 133 | 134 | return ldisj_seq(lconj_seq(g) for g in cast(Tuple[Iterable[GoalType], ...], goals)) 135 | 136 | 137 | lall = lconj 138 | lany = ldisj 139 | 140 | 141 | def ground_order_key(S: StateType, x: Any) -> Literal[-1, 0, 1, 2]: 142 | if isvar(x): 143 | return 2 144 | elif isground(x, S): 145 | return -1 146 | elif issubclass(type(x), ConsPair): 147 | return 1 148 | else: 149 | return 0 150 | 151 | 152 | def ground_order(in_args: Any, out_args: Any) -> GoalType: 153 | """Construct a non-relational goal that orders a list of terms based on groundedness (grounded precede ungrounded).""" # noqa: E501 154 | 155 | def ground_order_goal(S: StateType) -> StateStreamType: 156 | nonlocal in_args, out_args 157 | 158 | in_args_rf, out_args_rf = reify((in_args, out_args), S) 159 | 160 | S_new = unify( 161 | list(out_args_rf) if isinstance(out_args_rf, Sequence) else out_args_rf, 162 | sorted(in_args_rf, key=partial(ground_order_key, S)), 163 | S, 164 | ) 165 | 166 | if S_new is not False: 167 | yield S_new 168 | 169 | return ground_order_goal 170 | 171 | 172 | def ifa(g1: GoalType, g2: GoalType) -> GoalType: 173 | """Create a goal operator that returns the first stream unless it fails.""" 174 | 175 | def ifa_goal(S: StateType) -> StateStreamType: 176 | g1_stream = g1(S) 177 | S_new = next(g1_stream, None) 178 | 179 | if S_new is None: 180 | yield from g2(S) 181 | else: 182 | yield S_new 183 | yield from g1_stream 184 | 185 | return ifa_goal 186 | 187 | 188 | def Zzz(gctor: Callable[[Any], GoalType], *args, **kwargs) -> GoalType: 189 | """Create an inverse-η-delay for a goal.""" 190 | 191 | def Zzz_goal(S: StateType) -> StateStreamType: 192 | yield from gctor(*args, **kwargs)(S) 193 | 194 | return Zzz_goal 195 | 196 | 197 | def run( 198 | n: Union[None, int], 199 | x: Any, 200 | *goals: GoalType, 201 | results_filter: Optional[Callable[[Iterator[Any]], Any]] = None 202 | ) -> Union[Tuple[Any, ...], Iterator[Any]]: 203 | """Run a logic program and obtain `n` solutions that satisfy the given goals. 204 | 205 | >>> from kanren import run, var, eq 206 | >>> x = var() 207 | >>> run(1, x, eq(x, 1)) 208 | (1,) 209 | 210 | Parameters 211 | ---------- 212 | n 213 | The number of desired solutions. ``n=0`` returns a tuple with all 214 | results and ``n=None`` returns a lazy sequence of all results. 215 | x 216 | The form to reify and return. Usually contains logic variables used in 217 | the given goals. 218 | goals 219 | A sequence of goals that must be true in logical conjunction 220 | (i.e. `lall`). 221 | results_filter 222 | A function to apply to the results stream (e.g. a `unique` filter). 223 | 224 | Returns 225 | ------- 226 | Either an iterable or tuple of reified `x` values that satisfy the goals. 227 | 228 | """ 229 | g = lall(*goals) 230 | results = map(partial(reify, x), g({})) 231 | 232 | if results_filter is not None: 233 | results = results_filter(results) 234 | 235 | if n is None: 236 | return results 237 | elif n == 0: 238 | return tuple(results) 239 | else: 240 | return tuple(take(n, results)) 241 | 242 | 243 | def dbgo(*args: Any, msg: Optional[Any] = None) -> GoalType: # pragma: no cover 244 | """Construct a goal that sets a debug trace and prints reified arguments.""" 245 | from pprint import pprint 246 | 247 | def dbgo_goal(S: StateType) -> StateStreamType: 248 | nonlocal args 249 | args = reify(args, S) 250 | 251 | if msg is not None: 252 | print(msg) 253 | 254 | pprint(args) 255 | 256 | import pdb 257 | 258 | pdb.set_trace() 259 | yield S 260 | 261 | return dbgo_goal 262 | -------------------------------------------------------------------------------- /kanren/facts.py: -------------------------------------------------------------------------------- 1 | from toolz import merge 2 | from unification import reify, unify 3 | 4 | from .util import intersection 5 | 6 | 7 | class Relation(object): 8 | _id = 0 9 | 10 | def __init__(self, name=None): 11 | self.facts = set() 12 | self.index = dict() 13 | if not name: 14 | name = "_%d" % Relation._id 15 | Relation._id += 1 16 | self.name = name 17 | 18 | def add_fact(self, *inputs): 19 | """Add a fact to the knowledge-base. 20 | 21 | See Also 22 | -------- 23 | fact 24 | facts 25 | """ 26 | fact = tuple(inputs) 27 | 28 | self.facts.add(fact) 29 | 30 | for key in enumerate(inputs): 31 | if key not in self.index: 32 | self.index[key] = set() 33 | self.index[key].add(fact) 34 | 35 | def __call__(self, *args): 36 | """Return a goal that produces a list of substitutions matching a fact in the knowledge-base. 37 | 38 | >>> from kanren.facts import Relation 39 | >>> from unification import var 40 | >>> 41 | >>> x, y = var('x'), var('y') 42 | >>> r = Relation() 43 | >>> r.add_fact(1, 2, 3) 44 | >>> r.add_fact(4, 5, 6) 45 | >>> list(r(x, y, 3)({})) == [{y: 2, x: 1}] 46 | True 47 | >>> list(r(x, 5, y)({})) == [{y: 6, x: 4}] 48 | True 49 | >>> list(r(x, 42, y)({})) 50 | [] 51 | 52 | Parameters 53 | ---------- 54 | *args: 55 | The goal to evaluate. This consists of vars and values to match 56 | facts against. 57 | 58 | """ # noqa: E501 59 | 60 | def goal(substitution): 61 | args2 = reify(args, substitution) 62 | subsets = [self.index[key] for key in enumerate(args) if key in self.index] 63 | if subsets: # we are able to reduce the pool early 64 | facts = intersection(*sorted(subsets, key=len)) 65 | else: 66 | facts = self.facts 67 | 68 | for fact in facts: 69 | unified = unify(fact, args2, substitution) 70 | if unified != False: 71 | yield merge(unified, substitution) 72 | 73 | return goal 74 | 75 | def __str__(self): 76 | return f"Rel: {self.name}" 77 | 78 | def __repr__(self): 79 | return f"{type(self).__name__}({self.name}, {self.index}, {self.facts})" 80 | 81 | 82 | def fact(rel, *args): 83 | """Declare a fact. 84 | 85 | >>> from kanren import fact, Relation, var, run 86 | >>> parent = Relation() 87 | >>> fact(parent, "Homer", "Bart") 88 | >>> fact(parent, "Homer", "Lisa") 89 | 90 | >>> x = var() 91 | >>> run(1, x, parent(x, "Bart")) 92 | ('Homer',) 93 | """ 94 | rel.add_fact(*args) 95 | 96 | 97 | def facts(rel, *lists): 98 | """Declare several facts. 99 | 100 | >>> from kanren import fact, Relation, var, run 101 | >>> parent = Relation() 102 | >>> facts(parent, ("Homer", "Bart"), 103 | ... ("Homer", "Lisa")) 104 | 105 | >>> x = var() 106 | >>> run(1, x, parent(x, "Bart")) 107 | ('Homer',) 108 | """ 109 | for lst in lists: 110 | fact(rel, *lst) 111 | -------------------------------------------------------------------------------- /kanren/goals.py: -------------------------------------------------------------------------------- 1 | from collections import Counter 2 | from collections.abc import Sequence 3 | from functools import partial 4 | from itertools import permutations 5 | from operator import length_hint 6 | 7 | from cons import cons 8 | from cons.core import ConsNull, ConsPair 9 | from unification import reify, var 10 | from unification.core import isground 11 | 12 | from .core import conde, eq, lall, lany 13 | 14 | 15 | def heado(head, coll): 16 | """Construct a goal stating that head is the head of coll. 17 | 18 | See Also 19 | -------- 20 | tailo 21 | conso 22 | """ 23 | return eq(cons(head, var()), coll) 24 | 25 | 26 | def tailo(tail, coll): 27 | """Construct a goal stating that tail is the tail of coll. 28 | 29 | See Also 30 | -------- 31 | heado 32 | conso 33 | """ 34 | return eq(cons(var(), tail), coll) 35 | 36 | 37 | def conso(h, t, r): 38 | """Construct a goal stating that cons h + t == r.""" 39 | return eq(cons(h, t), r) 40 | 41 | 42 | def nullo(*args, refs=None, default_ConsNull=list): 43 | """Create a goal asserting that one or more terms are a/the same `ConsNull` type. 44 | 45 | `ConsNull` types return proper Python collections when used as a CDR value 46 | in a CONS (e.g. `cons(1, []) == [1]`). 47 | 48 | This goal doesn't require that all args be unifiable; only that they have 49 | the same `ConsNull` type. Unlike the classic `lall(eq(x, []), eq(y, x))` 50 | `conde`-branch idiom used when recursively walking a single sequence via 51 | `conso`, this allows us to perform the same essential function while 52 | walking distinct lists that do not necessarily terminate on the same 53 | iteration. 54 | 55 | Parameters 56 | ---------- 57 | args: tuple of objects 58 | The terms to consider as an instance of the `ConsNull` type 59 | refs: tuple of objects 60 | The terms to use as reference types. These are not unified with the 61 | `ConsNull` type, instead they are used to constrain the `ConsNull` 62 | types considered valid. 63 | default_ConsNull: type 64 | The sequence type to use when all logic variables are unground. 65 | 66 | """ 67 | 68 | def nullo_goal(s): 69 | 70 | nonlocal args, default_ConsNull 71 | 72 | if refs is not None: 73 | refs_rf = reify(refs, s) 74 | else: 75 | refs_rf = () 76 | 77 | args_rf = reify(args, s) 78 | 79 | arg_null_types = set( 80 | # Get an empty instance of the type 81 | type(a) 82 | for a in args_rf + refs_rf 83 | # `ConsPair` and `ConsNull` types that are not literally `ConsPair`s 84 | if isinstance(a, (ConsPair, ConsNull)) and not issubclass(type(a), ConsPair) 85 | ) 86 | 87 | try: 88 | null_type = arg_null_types.pop() 89 | except KeyError: 90 | null_type = default_ConsNull 91 | 92 | if len(arg_null_types) > 0 and any(a != null_type for a in arg_null_types): 93 | # Mismatching null types: fail. 94 | return 95 | 96 | g = lall(*[eq(a, null_type()) for a in args_rf]) 97 | 98 | yield from g(s) 99 | 100 | return nullo_goal 101 | 102 | 103 | def itero(lst, nullo_refs=None, default_ConsNull=list): 104 | """Construct a goal asserting that a term is an iterable type. 105 | 106 | This is a generic version of the standard `listo` that accounts for 107 | different iterable types supported by `cons` in Python. 108 | 109 | See `nullo` 110 | """ 111 | 112 | def itero_goal(S): 113 | nonlocal lst, nullo_refs, default_ConsNull 114 | l_rf = reify(lst, S) 115 | c, d = var(), var() 116 | g = conde( 117 | [nullo(l_rf, refs=nullo_refs, default_ConsNull=default_ConsNull)], 118 | [conso(c, d, l_rf), itero(d, default_ConsNull=default_ConsNull)], 119 | ) 120 | yield from g(S) 121 | 122 | return itero_goal 123 | 124 | 125 | def membero(x, ls): 126 | """Construct a goal stating that x is an item of coll.""" 127 | 128 | def membero_goal(S): 129 | nonlocal x, ls 130 | 131 | x_rf, ls_rf = reify((x, ls), S) 132 | a, d = var(), var() 133 | 134 | g = lall(conso(a, d, ls), conde([eq(a, x)], [membero(x, d)])) 135 | 136 | yield from g(S) 137 | 138 | return membero_goal 139 | 140 | 141 | def appendo(lst, s, out, default_ConsNull=list): 142 | """Construct a goal for the relation lst + s = ls. 143 | 144 | See Byrd thesis pg. 247 145 | https://scholarworks.iu.edu/dspace/bitstream/handle/2022/8777/Byrd_indiana_0093A_10344.pdf 146 | """ 147 | 148 | def appendo_goal(S): 149 | nonlocal lst, s, out 150 | 151 | l_rf, s_rf, out_rf = reify((lst, s, out), S) 152 | 153 | a, d, res = var(prefix="a"), var(prefix="d"), var(prefix="res") 154 | 155 | _nullo = partial(nullo, default_ConsNull=default_ConsNull) 156 | 157 | g = conde( 158 | [ 159 | # All empty 160 | _nullo(s_rf, l_rf, out_rf), 161 | ], 162 | [ 163 | # `lst` is empty 164 | conso(a, d, out_rf), 165 | eq(s_rf, out_rf), 166 | _nullo(l_rf, refs=(s_rf, out_rf)), 167 | ], 168 | [ 169 | conso(a, d, l_rf), 170 | conso(a, res, out_rf), 171 | appendo(d, s_rf, res, default_ConsNull=default_ConsNull), 172 | ], 173 | ) 174 | 175 | yield from g(S) 176 | 177 | return appendo_goal 178 | 179 | 180 | def rembero(x, lst, o, default_ConsNull=list): 181 | """Remove the first occurrence of `x` in `lst` resulting in `o`.""" 182 | 183 | from .constraints import neq 184 | 185 | def rembero_goal(s): 186 | nonlocal x, lst, o 187 | 188 | x_rf, l_rf, o_rf = reify((x, lst, o), s) 189 | 190 | l_car, l_cdr, r = var(), var(), var() 191 | 192 | g = conde( 193 | [ 194 | nullo(l_rf, o_rf, default_ConsNull=default_ConsNull), 195 | ], 196 | [ 197 | conso(l_car, l_cdr, l_rf), 198 | eq(x_rf, l_car), 199 | eq(l_cdr, o_rf), 200 | ], 201 | [ 202 | conso(l_car, l_cdr, l_rf), 203 | neq(l_car, x), 204 | conso(l_car, r, o_rf), 205 | rembero(x_rf, l_cdr, r, default_ConsNull=default_ConsNull), 206 | ], 207 | ) 208 | 209 | yield from g(s) 210 | 211 | return rembero_goal 212 | 213 | 214 | def permuteo(a, b, inner_eq=eq, default_ConsNull=list, no_ident=False): 215 | """Construct a goal asserting equality of sequences under permutation. 216 | 217 | For example, (1, 2, 2) equates to (2, 1, 2) under permutation 218 | >>> from kanren import var, run, permuteo 219 | >>> x = var() 220 | >>> run(0, x, permuteo(x, (1, 2))) 221 | ((1, 2), (2, 1)) 222 | 223 | >>> run(0, x, permuteo((2, 1, x), (2, 1, 2))) 224 | (2,) 225 | """ 226 | 227 | def permuteo_goal(S): 228 | nonlocal a, b, default_ConsNull, inner_eq 229 | 230 | a_rf, b_rf = reify((a, b), S) 231 | 232 | # If the lengths differ, then fail 233 | a_len, b_len = length_hint(a_rf, -1), length_hint(b_rf, -1) 234 | if a_len > 0 and b_len > 0 and a_len != b_len: 235 | return 236 | 237 | if isinstance(a_rf, Sequence): 238 | 239 | a_type = type(a_rf) 240 | 241 | a_perms = permutations(a_rf) 242 | 243 | if no_ident: 244 | next(a_perms) 245 | 246 | if isinstance(b_rf, Sequence): 247 | 248 | b_type = type(b_rf) 249 | 250 | # Fail on mismatched types or straight equality (when 251 | # `no_ident` is enabled) 252 | if a_type != b_type or (no_ident and a_rf == b_rf): 253 | return 254 | 255 | try: 256 | # `a` and `b` are sequences, so let's see if we can pull out 257 | # all the (hash-)equivalent elements. 258 | # XXX: Use of this requires that the equivalence relation 259 | # implied by `inner_eq` be a *superset* of `eq`. 260 | 261 | cntr_a, cntr_b = Counter(a_rf), Counter(b_rf) 262 | rdcd_a, rdcd_b = cntr_a - cntr_b, cntr_b - cntr_a 263 | 264 | if len(rdcd_a) == len(rdcd_b) == 0: 265 | yield S 266 | return 267 | elif len(rdcd_a) < len(cntr_a): 268 | a_rf, b_rf = tuple(rdcd_a.elements()), b_type(rdcd_b.elements()) 269 | a_perms = permutations(a_rf) 270 | 271 | except TypeError: 272 | # TODO: We could probably get more coverage for this case 273 | # by using `HashableForm`. 274 | pass 275 | 276 | # If they're both ground and we're using basic unification, 277 | # then simply check that one is a permutation of the other and 278 | # be done. No need to create and evaluate a bunch of goals in 279 | # order to do something that can be done right here. 280 | # Naturally, this assumes that the `isground` checks aren't 281 | # nearly as costly as all that other stuff. If the gains 282 | # depend on the sizes of `a` and `b`, then we could do 283 | # `length_hint` checks first. 284 | if inner_eq == eq and isground(a_rf, S) and isground(b_rf, S): 285 | if tuple(b_rf) in a_perms: 286 | yield S 287 | return 288 | else: 289 | # This has to be a definitive check, since we can only 290 | # use the `a_perms` generator once; plus, we don't want 291 | # to iterate over it more than once! 292 | return 293 | 294 | yield from lany(inner_eq(b_rf, a_type(i)) for i in a_perms)(S) 295 | 296 | elif isinstance(b_rf, Sequence): 297 | 298 | b_type = type(b_rf) 299 | b_perms = permutations(b_rf) 300 | 301 | if no_ident: 302 | next(b_perms) 303 | 304 | yield from lany(inner_eq(a_rf, b_type(i)) for i in b_perms)(S) 305 | 306 | else: 307 | 308 | # None of the arguments are proper sequences, so state that one 309 | # should be and apply `permuteo` to that. 310 | 311 | a_itero_g = itero( 312 | a_rf, nullo_refs=(b_rf,), default_ConsNull=default_ConsNull 313 | ) 314 | 315 | for S_new in a_itero_g(S): 316 | a_new = reify(a_rf, S_new) 317 | a_type = type(a_new) 318 | a_perms = permutations(a_new) 319 | 320 | if no_ident: 321 | next(a_perms) 322 | 323 | yield from lany(inner_eq(b_rf, a_type(i)) for i in a_perms)(S_new) 324 | 325 | return permuteo_goal 326 | 327 | 328 | # For backward compatibility 329 | permuteq = permuteo 330 | -------------------------------------------------------------------------------- /kanren/graph.py: -------------------------------------------------------------------------------- 1 | from functools import partial 2 | 3 | from etuples import etuple 4 | from unification import isvar, reify, var 5 | 6 | from .core import Zzz, conde, eq, fail, ground_order, lall, succeed 7 | from .goals import conso, nullo 8 | from .term import applyo 9 | 10 | 11 | def mapo(relation, a, b, null_type=list, null_res=True, first=True): 12 | """Apply a relation to corresponding elements in two sequences and succeed if the relation succeeds for all pairs.""" # noqa: E501 13 | 14 | b_car, b_cdr = var(), var() 15 | a_car, a_cdr = var(), var() 16 | 17 | return conde( 18 | [nullo(a, b, default_ConsNull=null_type) if (not first or null_res) else fail], 19 | [ 20 | conso(a_car, a_cdr, a), 21 | conso(b_car, b_cdr, b), 22 | Zzz(relation, a_car, b_car), 23 | Zzz(mapo, relation, a_cdr, b_cdr, null_type=null_type, first=False), 24 | ], 25 | ) 26 | 27 | 28 | def map_anyo( 29 | relation, a, b, null_type=list, null_res=False, first=True, any_succeed=False 30 | ): 31 | """Apply a relation to corresponding elements in two sequences and succeed if at least one pair succeeds. 32 | 33 | Parameters 34 | ---------- 35 | null_type: optional 36 | An object that's a valid cdr for the collection type desired. If 37 | `False` (i.e. the default value), the cdr will be inferred from the 38 | inputs, or defaults to an empty list. 39 | """ # noqa: E501 40 | 41 | b_car, b_cdr = var(), var() 42 | a_car, a_cdr = var(), var() 43 | 44 | return conde( 45 | [ 46 | nullo(a, b, default_ConsNull=null_type) 47 | if (any_succeed or (first and null_res)) 48 | else fail 49 | ], 50 | [ 51 | conso(a_car, a_cdr, a), 52 | conso(b_car, b_cdr, b), 53 | conde( 54 | [ 55 | Zzz(relation, a_car, b_car), 56 | Zzz( 57 | map_anyo, 58 | relation, 59 | a_cdr, 60 | b_cdr, 61 | null_type=null_type, 62 | any_succeed=True, 63 | first=False, 64 | ), 65 | ], 66 | [ 67 | eq(a_car, b_car), 68 | Zzz( 69 | map_anyo, 70 | relation, 71 | a_cdr, 72 | b_cdr, 73 | null_type=null_type, 74 | any_succeed=any_succeed, 75 | first=False, 76 | ), 77 | ], 78 | ), 79 | ], 80 | ) 81 | 82 | 83 | def vararg_success(*args): 84 | return succeed 85 | 86 | 87 | def eq_length(u, v, default_ConsNull=list): 88 | """Construct a goal stating that two sequences are the same length and type.""" 89 | 90 | return mapo(vararg_success, u, v, null_type=default_ConsNull) 91 | 92 | 93 | def reduceo(relation, in_term, out_term, *args, **kwargs): 94 | """Relate a term and the fixed-point of that term under a given relation. 95 | 96 | This includes the "identity" relation. 97 | """ 98 | 99 | def reduceo_goal(s): 100 | 101 | nonlocal in_term, out_term, relation, args, kwargs 102 | 103 | in_term_rf, out_term_rf = reify((in_term, out_term), s) 104 | 105 | # The result of reducing the input graph once 106 | term_rdcd = var() 107 | 108 | # Are we working "backward" and (potentially) "expanding" a graph 109 | # (e.g. when the relation is a reduction rule)? 110 | is_expanding = isvar(in_term_rf) 111 | 112 | # One application of the relation assigned to `term_rdcd` 113 | single_apply_g = relation(in_term_rf, term_rdcd, *args, **kwargs) 114 | 115 | # Assign/equate (unify, really) the result of a single application to 116 | # the "output" term. 117 | single_res_g = eq(term_rdcd, out_term_rf) 118 | 119 | # Recurse into applications of the relation (well, produce a goal that 120 | # will do that) 121 | another_apply_g = reduceo(relation, term_rdcd, out_term_rf, *args, **kwargs) 122 | 123 | # We want the fixed-point value to show up in the stream output 124 | # *first*, but that requires some checks. 125 | if is_expanding: 126 | # When an un-reduced term is a logic variable (e.g. we're 127 | # "expanding"), we can't go depth first. 128 | # We need to draw the association between (i.e. unify) the reduced 129 | # and expanded terms ASAP, in order to produce finite 130 | # expanded graphs first and yield results. 131 | # 132 | # In other words, there's no fixed-point to produce in this 133 | # situation. Instead, for example, we have to produce an infinite 134 | # stream of terms that have `out_term_rf` as a fixed point. 135 | # g = conde([single_res_g, single_apply_g], 136 | # [another_apply_g, single_apply_g]) 137 | g = lall(conde([single_res_g], [another_apply_g]), single_apply_g) 138 | else: 139 | # Run the recursion step first, so that we get the fixed-point as 140 | # the first result 141 | g = lall(single_apply_g, conde([another_apply_g], [single_res_g])) 142 | 143 | yield from g(s) 144 | 145 | return reduceo_goal 146 | 147 | 148 | def walko( 149 | goal, 150 | graph_in, 151 | graph_out, 152 | rator_goal=None, 153 | null_type=etuple, 154 | map_rel=partial(map_anyo, null_res=True), 155 | ): 156 | """Apply a binary relation between all nodes in two graphs. 157 | 158 | When `rator_goal` is used, the graphs are treated as term graphs, and the 159 | multi-functions `rator`, `rands`, and `apply` are used to walk the graphs. 160 | Otherwise, the graphs must be iterable according to `map_anyo`. 161 | 162 | Parameters 163 | ---------- 164 | goal: callable 165 | A goal that is applied to all terms in the graph. 166 | graph_in: object 167 | The graph for which the left-hand side of a binary relation holds. 168 | graph_out: object 169 | The graph for which the right-hand side of a binary relation holds. 170 | rator_goal: callable (default None) 171 | A goal that is applied to the rators of a graph. When specified, 172 | `goal` is only applied to rands and it must succeed along with the 173 | rator goal in order to descend into sub-terms. 174 | null_type: type 175 | The collection type used when it is not fully determined by the graph 176 | arguments. 177 | map_rel: callable 178 | The map relation used to apply `goal` to a sub-graph. 179 | """ 180 | 181 | def walko_goal(s): 182 | 183 | nonlocal goal, rator_goal, graph_in, graph_out, null_type, map_rel 184 | 185 | graph_in_rf, graph_out_rf = reify((graph_in, graph_out), s) 186 | 187 | rator_in, rands_in, rator_out, rands_out = var(), var(), var(), var() 188 | 189 | _walko = partial( 190 | walko, goal, rator_goal=rator_goal, null_type=null_type, map_rel=map_rel 191 | ) 192 | 193 | g = conde( 194 | # TODO: Use `Zzz`, if needed. 195 | [ 196 | goal(graph_in_rf, graph_out_rf), 197 | ], 198 | [ 199 | lall( 200 | applyo(rator_in, rands_in, graph_in_rf), 201 | applyo(rator_out, rands_out, graph_out_rf), 202 | rator_goal(rator_in, rator_out), 203 | map_rel(_walko, rands_in, rands_out, null_type=null_type), 204 | ) 205 | if rator_goal is not None 206 | else map_rel(_walko, graph_in_rf, graph_out_rf, null_type=null_type), 207 | ], 208 | ) 209 | 210 | yield from g(s) 211 | 212 | return walko_goal 213 | 214 | 215 | def term_walko( 216 | rator_goal, 217 | rands_goal, 218 | a, 219 | b, 220 | null_type=etuple, 221 | no_ident=False, 222 | format_step=None, 223 | **kwargs 224 | ): 225 | """Construct a goal for walking a term graph. 226 | 227 | This implementation is somewhat specific to the needs of `eq_comm` and 228 | `eq_assoc`, but it could be transferred to `kanren.graph`. 229 | 230 | XXX: Make sure `rator_goal` will succeed for unground logic variables; 231 | otherwise, this will diverge. 232 | XXX: `rands_goal` should not be contain `eq`, i.e. `rands_goal(x, x)` 233 | should always fail! 234 | """ 235 | 236 | def single_step(s, t): 237 | u, v = var(), var() 238 | u_rator, u_rands = var(), var() 239 | v_rands = var() 240 | 241 | return lall( 242 | ground_order((s, t), (u, v)), 243 | applyo(u_rator, u_rands, u), 244 | applyo(u_rator, v_rands, v), 245 | rator_goal(u_rator), 246 | # These make sure that there are at least two rands, which 247 | # makes sense for commutativity and associativity, at least. 248 | conso(var(), var(), u_rands), 249 | conso(var(), var(), v_rands), 250 | Zzz(rands_goal, u_rands, v_rands, u_rator, **kwargs), 251 | ) 252 | 253 | def term_walko_step(s, t): 254 | nonlocal rator_goal, rands_goal, null_type 255 | u, v = var(), var() 256 | z, w = var(), var() 257 | 258 | return lall( 259 | ground_order((s, t), (u, v)), 260 | format_step(u, w) if format_step is not None else eq(u, w), 261 | conde( 262 | [ 263 | # Apply, then walk or return 264 | single_step(w, v), 265 | ], 266 | [ 267 | # Walk, then apply or return 268 | map_anyo(term_walko_step, w, z, null_type=null_type), 269 | conde([eq(z, v)], [single_step(z, v)]), 270 | ], 271 | ), 272 | ) 273 | 274 | return lall( 275 | term_walko_step(a, b) 276 | if no_ident 277 | else conde([term_walko_step(a, b)], [eq(a, b)]), 278 | ) 279 | -------------------------------------------------------------------------------- /kanren/py.typed: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pythological/kanren/071c626e1e252e2d8dab44dfd85196f69c70de36/kanren/py.typed -------------------------------------------------------------------------------- /kanren/term.py: -------------------------------------------------------------------------------- 1 | from collections.abc import Mapping, Sequence 2 | 3 | from cons.core import ConsError, cons 4 | from etuples import apply as term 5 | from etuples import rands as arguments 6 | from etuples import rator as operator 7 | from unification.core import _reify, _unify, construction_sentinel, reify 8 | from unification.variable import isvar 9 | 10 | from .core import eq, lall 11 | from .goals import conso 12 | 13 | 14 | def applyo(o_rator, o_rands, obj): 15 | """Construct a goal that relates an object to the application of its (ope)rator to its (ope)rands. 16 | 17 | In other words, this is the relation `op(*args) == obj`. It uses the 18 | `rator`, `rands`, and `apply` dispatch functions from `etuples`, so 19 | implement/override those to get the desired behavior. 20 | 21 | """ # noqa: E501 22 | 23 | def applyo_goal(S): 24 | nonlocal o_rator, o_rands, obj 25 | 26 | o_rator_rf, o_rands_rf, obj_rf = reify((o_rator, o_rands, obj), S) 27 | 28 | if not isvar(obj_rf): 29 | 30 | # We should be able to use this goal with *any* arguments, so 31 | # fail when the ground operations fail/err. 32 | try: 33 | obj_rator, obj_rands = operator(obj_rf), arguments(obj_rf) 34 | except (ConsError, NotImplementedError): 35 | return 36 | 37 | # The object's rator + rands should be the same as the goal's 38 | yield from lall(eq(o_rator_rf, obj_rator), eq(o_rands_rf, obj_rands))(S) 39 | 40 | elif isvar(o_rands_rf) or isvar(o_rator_rf): 41 | # The object and at least one of the rand, rators is a logic 42 | # variable, so let's just assert a `cons` relationship between 43 | # them 44 | yield from conso(o_rator_rf, o_rands_rf, obj_rf)(S) 45 | else: 46 | # The object is a logic variable, but the rator and rands aren't. 47 | # We assert that the object is the application of the rand and 48 | # rators. 49 | try: 50 | obj_applied = term(o_rator_rf, o_rands_rf) 51 | except (ConsError, NotImplementedError): 52 | return 53 | yield from eq(obj_rf, obj_applied)(S) 54 | 55 | return applyo_goal 56 | 57 | 58 | @term.register(object, Sequence) 59 | def term_Sequence(rator, rands): 60 | # Overwrite the default `apply` dispatch function and make it preserve 61 | # types 62 | res = cons(rator, rands) 63 | return res 64 | 65 | 66 | def unifiable_with_term(cls): 67 | _reify.add((cls, Mapping), reify_term) 68 | _unify.add((cls, cls, Mapping), unify_term) 69 | return cls 70 | 71 | 72 | def reify_term(obj, s): 73 | op, args = operator(obj), arguments(obj) 74 | op = yield _reify(op, s) 75 | args = yield _reify(args, s) 76 | yield construction_sentinel 77 | yield term(op, args) 78 | 79 | 80 | def unify_term(u, v, s): 81 | u_op, u_args = operator(u), arguments(u) 82 | v_op, v_args = operator(v), arguments(v) 83 | s = yield _unify(u_op, v_op, s) 84 | if s is not False: 85 | s = yield _unify(u_args, v_args, s) 86 | yield s 87 | -------------------------------------------------------------------------------- /kanren/util.py: -------------------------------------------------------------------------------- 1 | from collections import namedtuple 2 | from collections.abc import Hashable, Iterable, Mapping, MutableSet, Set 3 | from itertools import chain 4 | 5 | 6 | HashableForm = namedtuple("HashableForm", ["type", "data"]) 7 | 8 | 9 | class FlexibleSet(MutableSet): 10 | """A set that uses a list (and costly identity check) for unhashable items.""" 11 | 12 | __slots__ = ("set", "list") 13 | 14 | def __init__(self, iterable=None): 15 | 16 | self.set = set() 17 | self.list = [] 18 | 19 | if iterable is not None: 20 | for i in iterable: 21 | self.add(i) 22 | 23 | def add(self, item): 24 | try: 25 | self.set.add(item) 26 | except TypeError: 27 | # TODO: Could try `make_hashable`. 28 | # TODO: Use `bisect` for unhashable but orderable elements 29 | if item not in self.list: 30 | self.list.append(item) 31 | 32 | def discard(self, item): 33 | try: 34 | self.remove(item) 35 | except KeyError: 36 | pass 37 | 38 | def clear(self): 39 | self.set.clear() 40 | self.list.clear() 41 | 42 | def pop(self): 43 | try: 44 | return self.set.pop() 45 | except (TypeError, KeyError): 46 | try: 47 | return self.list.pop(-1) 48 | except IndexError: 49 | raise KeyError() 50 | 51 | def remove(self, item): 52 | try: 53 | self.set.remove(item) 54 | except (TypeError, KeyError): 55 | try: 56 | self.list.remove(item) 57 | except ValueError: 58 | raise KeyError() 59 | 60 | def copy(self): 61 | res = type(self)() 62 | res.set = self.set.copy() 63 | res.list = self.list.copy() 64 | return res 65 | 66 | def __le__(self, other): 67 | raise NotImplementedError() 68 | 69 | def __ge__(self, other): 70 | raise NotImplementedError() 71 | 72 | def __iter__(self): 73 | return chain(self.set, self.list) 74 | 75 | def __contains__(self, value): 76 | try: 77 | return value in self.set or value in self.list 78 | except TypeError: 79 | return value in self.list 80 | 81 | def __len__(self): 82 | return len(self.set) + len(self.list) 83 | 84 | def __eq__(self, other): 85 | if type(self) == type(other): 86 | return self.set == other.set and self.list == other.list 87 | elif isinstance(other, Set): 88 | return len(self.list) == 0 and other.issuperset(self.set) 89 | 90 | return NotImplemented 91 | 92 | def __repr__(self): 93 | return f"FlexibleSet([{', '.join(str(s) for s in self)}])" 94 | 95 | 96 | def hashable(x): 97 | try: 98 | hash(x) 99 | return True 100 | except TypeError: 101 | return False 102 | 103 | 104 | def dicthash(d): 105 | return hash(frozenset(d.items())) 106 | 107 | 108 | def make_hashable(x): 109 | # TODO: Better as a dispatch function? 110 | if hashable(x): 111 | return x 112 | if isinstance(x, slice): 113 | return HashableForm(type(x), (x.start, x.stop, x.step)) 114 | if isinstance(x, Mapping): 115 | return HashableForm(type(x), frozenset(tuple(multihash(i) for i in x.items()))) 116 | if isinstance(x, Iterable): 117 | return HashableForm(type(x), tuple(multihash(i) for i in x)) 118 | raise TypeError(f"Hashing not covered for {x}") 119 | 120 | 121 | def multihash(x): 122 | return hash(make_hashable(x)) 123 | 124 | 125 | def unique(seq, key=lambda x: x): 126 | seen = set() 127 | for item in seq: 128 | try: 129 | k = key(item) 130 | except TypeError: 131 | # Just yield it and hope for the best, since we can't efficiently 132 | # check if we've seen it before. 133 | yield item 134 | continue 135 | if not isinstance(k, Hashable): 136 | # Just yield it and hope for the best, since we can't efficiently 137 | # check if we've seen it before. 138 | yield item 139 | elif k not in seen: 140 | seen.add(key(item)) 141 | yield item 142 | 143 | 144 | def intersection(*seqs): 145 | return (item for item in seqs[0] if all(item in seq for seq in seqs[1:])) 146 | 147 | 148 | def groupsizes(total, len): 149 | """Construct groups of length len that add up to total. 150 | 151 | >>> from kanren.util import groupsizes 152 | >>> tuple(groupsizes(4, 2)) 153 | ((1, 3), (2, 2), (3, 1)) 154 | """ 155 | if len == 1: 156 | yield (total,) 157 | else: 158 | for i in range(1, total - len + 1 + 1): 159 | for perm in groupsizes(total - i, len - 1): 160 | yield (i,) + perm 161 | 162 | 163 | def pprint(g): # pragma: no cover 164 | """Pretty print a tree of goals.""" 165 | if callable(g) and hasattr(g, "__name__"): 166 | return g.__name__ 167 | if isinstance(g, type): 168 | return g.__name__ 169 | if isinstance(g, tuple): 170 | return "(" + ", ".join(map(pprint, g)) + ")" 171 | return str(g) 172 | 173 | 174 | def index(tup, ind): 175 | """Fancy indexing with tuples.""" 176 | return tuple(tup[i] for i in ind) 177 | -------------------------------------------------------------------------------- /pytest.ini: -------------------------------------------------------------------------------- 1 | # content of pytest.ini 2 | [pytest] 3 | addopts = --doctest-modules 4 | norecursedirs = examples 5 | testpaths = tests 6 | doctest_optionflags= NORMALIZE_WHITESPACE IGNORE_EXCEPTION_DETAIL -------------------------------------------------------------------------------- /release-notes: -------------------------------------------------------------------------------- 1 | New in version 0.2 2 | 3 | * Python 3 support 4 | * Dictionary unification 5 | * Use multiple dispatch to extend unify, reify, isvar 6 | * Add convenience class decorator `unifiable` to facilitate trivial 7 | unification of user classes 8 | * Add term operations term, arguments, operator, also multiply dispatched 9 | * Depend on the toolz library 10 | * Performance degredation as a result of multiple dispatch 11 | * Arithmetic goals 12 | * Improved set matching performance 13 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | -e ./ 2 | coveralls 3 | pydocstyle>=3.0.0 4 | pytest>=5.0.0 5 | pytest-cov>=2.6.1 6 | pytest-html>=1.20.0 7 | pylint>=2.3.1 8 | black>=19.3b0; platform.python_implementation!='PyPy' 9 | diff-cover 10 | sympy 11 | versioneer 12 | coverage>=5.1 13 | pre-commit 14 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [versioneer] 2 | VCS = git 3 | style = pep440 4 | versionfile_source = kanren/_version.py 5 | versionfile_build = kanren/_version.py 6 | tag_prefix = v 7 | parentdir_prefix = kanren- 8 | 9 | [pydocstyle] 10 | # Ignore errors for missing docstrings. 11 | # Ignore D202 (No blank lines allowed after function docstring) 12 | # due to bug in black: https://github.com/ambv/black/issues/355 13 | add-ignore = D100,D101,D102,D103,D104,D105,D106,D107,D202 14 | convention = numpy 15 | 16 | [tool:pytest] 17 | python_files=test*.py 18 | testpaths=tests 19 | 20 | [coverage:run] 21 | relative_files = True 22 | omit = 23 | kanren/_version.py 24 | tests/* 25 | branch = True 26 | 27 | [coverage:report] 28 | exclude_lines = 29 | pragma: no cover 30 | def __repr__ 31 | raise AssertionError 32 | raise TypeError 33 | return NotImplemented 34 | raise NotImplementedError 35 | if __name__ == .__main__.: 36 | assert False 37 | show_missing = 1 38 | 39 | [isort] 40 | profile = black 41 | lines_after_imports = 2 42 | lines_between_sections = 1 43 | honor_noqa = True 44 | skip_gitignore = True 45 | 46 | [flake8] 47 | max-line-length = 88 48 | extend-ignore = E203, W503 49 | per-file-ignores = 50 | **/__init__.py:F401,E402,F403 51 | 52 | [pylint] 53 | max-line-length = 88 54 | 55 | [pylint.messages_control] 56 | disable = C0330, C0326 57 | 58 | [mypy] 59 | ignore_missing_imports = True 60 | no_implicit_optional = True 61 | check_untyped_defs = False 62 | strict_equality = True 63 | warn_redundant_casts = True 64 | warn_unused_configs = True 65 | warn_unused_ignores = True 66 | warn_return_any = True 67 | warn_no_return = False 68 | warn_unreachable = True 69 | show_error_codes = True 70 | allow_redefinition = False 71 | files = kanren,tests 72 | 73 | [mypy-versioneer] 74 | check_untyped_defs = False -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | from os.path import exists 3 | 4 | from setuptools import setup 5 | 6 | import versioneer 7 | 8 | 9 | setup( 10 | name="miniKanren", 11 | version=versioneer.get_version(), 12 | cmdclass=versioneer.get_cmdclass(), 13 | description="Relational programming in Python", 14 | url="http://github.com/pythological/kanren", 15 | maintainer="Brandon T. Willard", 16 | maintainer_email="brandonwillard+kanren@gmail.com", 17 | license="BSD", 18 | packages=["kanren"], 19 | install_requires=[ 20 | "toolz", 21 | "cons >= 0.4.0", 22 | "multipledispatch", 23 | "etuples >= 0.3.1", 24 | "logical-unification >= 0.4.1", 25 | "typing_extensions", 26 | ], 27 | package_data={ 28 | "kanren": ["py.typed"], 29 | }, 30 | tests_require=["pytest", "sympy"], 31 | long_description=open("README.md").read() if exists("README.md") else "", 32 | long_description_content_type="text/markdown", 33 | zip_safe=False, 34 | python_requires=">=3.6", 35 | classifiers=[ 36 | "Development Status :: 5 - Production/Stable", 37 | "Intended Audience :: Science/Research", 38 | "Intended Audience :: Developers", 39 | "License :: OSI Approved :: BSD License", 40 | "Operating System :: OS Independent", 41 | "Programming Language :: Python", 42 | "Programming Language :: Python :: 3", 43 | "Programming Language :: Python :: 3.7", 44 | "Programming Language :: Python :: 3.8", 45 | "Programming Language :: Python :: 3.9", 46 | "Programming Language :: Python :: 3.10", 47 | "Programming Language :: Python :: Implementation :: CPython", 48 | "Programming Language :: Python :: Implementation :: PyPy", 49 | ], 50 | ) 51 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pythological/kanren/071c626e1e252e2d8dab44dfd85196f69c70de36/tests/__init__.py -------------------------------------------------------------------------------- /tests/test_assoccomm.py: -------------------------------------------------------------------------------- 1 | from collections.abc import Sequence 2 | from copy import copy 3 | 4 | import pytest 5 | from cons import cons 6 | from etuples.core import etuple 7 | from unification import isvar, reify, unify, var 8 | 9 | from kanren.assoccomm import ( 10 | assoc_args, 11 | assoc_flatten, 12 | associative, 13 | commutative, 14 | eq_assoc, 15 | eq_assoc_args, 16 | eq_assoccomm, 17 | eq_comm, 18 | flatten_assoc_args, 19 | ) 20 | from kanren.core import run 21 | from kanren.facts import fact 22 | from kanren.term import arguments, operator, term 23 | 24 | 25 | @pytest.fixture(autouse=True) 26 | def clear_assoccomm(): 27 | old_commutative_index = copy(commutative.index) 28 | old_commutative_facts = copy(commutative.facts) 29 | old_associative_index = copy(associative.index) 30 | old_associative_facts = copy(associative.facts) 31 | try: 32 | yield 33 | finally: 34 | commutative.index = old_commutative_index 35 | commutative.facts = old_commutative_facts 36 | associative.index = old_associative_index 37 | associative.facts = old_associative_facts 38 | 39 | 40 | class Node(object): 41 | def __init__(self, op, args): 42 | self.op = op 43 | self.args = args 44 | 45 | def __eq__(self, other): 46 | return ( 47 | type(self) == type(other) 48 | and self.op == other.op 49 | and self.args == other.args 50 | ) 51 | 52 | def __hash__(self): 53 | return hash((type(self), self.op, self.args)) 54 | 55 | def __str__(self): 56 | return "%s(%s)" % (self.op.name, ", ".join(map(str, self.args))) 57 | 58 | __repr__ = __str__ 59 | 60 | 61 | class Operator(object): 62 | def __init__(self, name): 63 | self.name = name 64 | 65 | 66 | Add = Operator("add") 67 | Mul = Operator("mul") 68 | 69 | 70 | def add(*args): 71 | return Node(Add, args) 72 | 73 | 74 | def mul(*args): 75 | return Node(Mul, args) 76 | 77 | 78 | @term.register(Operator, Sequence) 79 | def term_Operator(op, args): 80 | return Node(op, args) 81 | 82 | 83 | @arguments.register(Node) 84 | def arguments_Node(n): 85 | return n.args 86 | 87 | 88 | @operator.register(Node) 89 | def operator_Node(n): 90 | return n.op 91 | 92 | 93 | def results(g, s=None): 94 | if s is None: 95 | s = dict() 96 | return tuple(g(s)) 97 | 98 | 99 | def test_eq_comm(): 100 | x, y, z = var(), var(), var() 101 | 102 | comm_op = "comm_op" 103 | 104 | fact(commutative, comm_op) 105 | 106 | assert run(0, True, eq_comm(1, 1)) == (True,) 107 | assert run(0, True, eq_comm((comm_op, 1, 2, 3), (comm_op, 1, 2, 3))) == (True,) 108 | 109 | assert run(0, True, eq_comm((comm_op, 3, 2, 1), (comm_op, 1, 2, 3))) == (True,) 110 | assert run(0, y, eq_comm((comm_op, 3, y, 1), (comm_op, 1, 2, 3))) == (2,) 111 | assert run(0, (x, y), eq_comm((comm_op, x, y, 1), (comm_op, 1, 2, 3))) == ( 112 | (2, 3), 113 | (3, 2), 114 | ) 115 | assert run(0, (x, y), eq_comm((comm_op, 2, 3, 1), (comm_op, 1, x, y))) == ( 116 | (2, 3), 117 | (3, 2), 118 | ) 119 | 120 | assert not run( 121 | 0, True, eq_comm(("op", 3, 2, 1), ("op", 1, 2, 3)) 122 | ) # not commutative 123 | assert not run(0, True, eq_comm((3, comm_op, 2, 1), (comm_op, 1, 2, 3))) 124 | assert not run(0, True, eq_comm((comm_op, 1, 2, 1), (comm_op, 1, 2, 3))) 125 | assert not run(0, True, eq_comm(("op", 1, 2, 3), (comm_op, 1, 2, 3))) 126 | 127 | # Test for variable args 128 | res = run(4, (x, y), eq_comm(x, y)) 129 | exp_res_form = ( 130 | (etuple(comm_op, x, y), etuple(comm_op, y, x)), 131 | (x, y), 132 | (etuple(etuple(comm_op, x, y)), etuple(etuple(comm_op, y, x))), 133 | (etuple(comm_op, x, y, z), etuple(comm_op, x, z, y)), 134 | ) 135 | 136 | for a, b in zip(res, exp_res_form): 137 | s = unify(a, b) 138 | assert s is not False 139 | assert all(isvar(i) for i in reify((x, y, z), s)) 140 | 141 | # Make sure it can unify single elements 142 | assert (3,) == run(0, x, eq_comm((comm_op, 1, 2, 3), (comm_op, 2, x, 1))) 143 | 144 | # `eq_comm` should propagate through 145 | assert (3,) == run( 146 | 0, x, eq_comm(("div", 1, (comm_op, 1, 2, 3)), ("div", 1, (comm_op, 2, x, 1))) 147 | ) 148 | # Now it should not 149 | assert () == run( 150 | 0, x, eq_comm(("div", 1, ("div", 1, 2, 3)), ("div", 1, ("div", 2, x, 1))) 151 | ) 152 | 153 | expected_res = {(1, 2, 3), (2, 1, 3), (3, 1, 2), (1, 3, 2), (2, 3, 1), (3, 2, 1)} 154 | assert expected_res == set( 155 | run(0, (x, y, z), eq_comm((comm_op, 1, 2, 3), (comm_op, x, y, z))) 156 | ) 157 | assert expected_res == set( 158 | run(0, (x, y, z), eq_comm((comm_op, x, y, z), (comm_op, 1, 2, 3))) 159 | ) 160 | assert expected_res == set( 161 | run( 162 | 0, 163 | (x, y, z), 164 | eq_comm(("div", 1, (comm_op, 1, 2, 3)), ("div", 1, (comm_op, x, y, z))), 165 | ) 166 | ) 167 | 168 | e1 = (comm_op, (comm_op, 1, x), y) 169 | e2 = (comm_op, 2, (comm_op, 3, 1)) 170 | assert run(0, (x, y), eq_comm(e1, e2)) == ((3, 2),) 171 | 172 | e1 = ((comm_op, 3, 1),) 173 | e2 = ((comm_op, 1, x),) 174 | 175 | assert run(0, x, eq_comm(e1, e2)) == (3,) 176 | 177 | e1 = (2, (comm_op, 3, 1)) 178 | e2 = (y, (comm_op, 1, x)) 179 | 180 | assert run(0, (x, y), eq_comm(e1, e2)) == ((3, 2),) 181 | 182 | e1 = (comm_op, (comm_op, 1, x), y) 183 | e2 = (comm_op, 2, (comm_op, 3, 1)) 184 | 185 | assert run(0, (x, y), eq_comm(e1, e2)) == ((3, 2),) 186 | 187 | 188 | @pytest.mark.xfail(reason="`applyo`/`buildo` needs to be a constraint.", strict=True) 189 | def test_eq_comm_object(): 190 | x = var("x") 191 | 192 | fact(commutative, Add) 193 | fact(associative, Add) 194 | 195 | assert run(0, x, eq_comm(add(1, 2, 3), add(3, 1, x))) == (2,) 196 | assert set(run(0, x, eq_comm(add(1, 2), x))) == set((add(1, 2), add(2, 1))) 197 | assert set(run(0, x, eq_assoccomm(add(1, 2, 3), add(1, x)))) == set( 198 | (add(2, 3), add(3, 2)) 199 | ) 200 | 201 | 202 | def test_flatten_assoc_args(): 203 | op = "add" 204 | 205 | def op_pred(x): 206 | return x == op 207 | 208 | assert list(flatten_assoc_args(op_pred, [op, 1, 2, 3, 4])) == [op, 1, 2, 3, 4] 209 | assert list(flatten_assoc_args(op_pred, [op, 1, 2, [op]])) == [op, 1, 2, [op]] 210 | assert list(flatten_assoc_args(op_pred, [[op, 1, 2, [op]]])) == [1, 2, [op]] 211 | 212 | res = list( 213 | flatten_assoc_args( 214 | op_pred, [[1, 2, op], 3, [op, 4, [op, [op]]], [op, 5], 6, op, 7] 215 | ) 216 | ) 217 | exp_res = [[1, 2, op], 3, 4, [op], 5, 6, op, 7] 218 | assert res == exp_res 219 | 220 | 221 | def test_assoc_args(): 222 | op = "add" 223 | 224 | def op_pred(x): 225 | return x == op 226 | 227 | assert tuple(assoc_args(op, (1, 2, 3), 2)) == ( 228 | ((op, 1, 2), 3), 229 | (1, (op, 2, 3)), 230 | ) 231 | assert tuple(assoc_args(op, [1, 2, 3], 2)) == ( 232 | [[op, 1, 2], 3], 233 | [1, [op, 2, 3]], 234 | ) 235 | assert tuple(assoc_args(op, (1, 2, 3), 1)) == ( 236 | ((op, 1), 2, 3), 237 | (1, (op, 2), 3), 238 | (1, 2, (op, 3)), 239 | ) 240 | assert tuple(assoc_args(op, (1, 2, 3), 3)) == ((1, 2, 3),) 241 | 242 | f_rands = flatten_assoc_args(op_pred, (1, (op, 2, 3))) 243 | assert tuple(assoc_args(op, f_rands, 2, ctor=tuple)) == ( 244 | ((op, 1, 2), 3), 245 | (1, (op, 2, 3)), 246 | ) 247 | 248 | 249 | def test_eq_assoc_args(): 250 | 251 | assoc_op = "assoc_op" 252 | 253 | fact(associative, assoc_op) 254 | 255 | assert not run(0, True, eq_assoc_args(assoc_op, (1,), [1], n=None)) 256 | assert run(0, True, eq_assoc_args(assoc_op, (1,), (1,), n=None)) == (True,) 257 | assert run(0, True, eq_assoc_args(assoc_op, (1, 1), (1, 1))) == (True,) 258 | assert run(0, True, eq_assoc_args(assoc_op, (1, 2, 3), (1, (assoc_op, 2, 3)))) == ( 259 | True, 260 | ) 261 | assert run(0, True, eq_assoc_args(assoc_op, (1, (assoc_op, 2, 3)), (1, 2, 3))) == ( 262 | True, 263 | ) 264 | assert run( 265 | 0, True, eq_assoc_args(assoc_op, (1, (assoc_op, 2, 3), 4), (1, 2, 3, 4)) 266 | ) == (True,) 267 | assert not run( 268 | 0, True, eq_assoc_args(assoc_op, (1, 2, 3), (1, (assoc_op, 2, 3), 4)) 269 | ) 270 | 271 | x, y = var(), var() 272 | 273 | assert run(0, True, eq_assoc_args(assoc_op, (x,), (x,), n=None)) == (True,) 274 | assert run(0, x, eq_assoc_args(assoc_op, x, (y,), n=None)) == ((y,),) 275 | assert run(0, x, eq_assoc_args(assoc_op, (y,), x, n=None)) == ((y,),) 276 | 277 | assert run(0, x, eq_assoc_args(assoc_op, (1, x, 4), (1, 2, 3, 4))) == ( 278 | (assoc_op, 2, 3), 279 | ) 280 | assert run(0, x, eq_assoc_args(assoc_op, (1, 2, 3, 4), (1, x, 4))) == ( 281 | (assoc_op, 2, 3), 282 | ) 283 | assert run(0, x, eq_assoc_args(assoc_op, [1, x, 4], [1, 2, 3, 4])) == ( 284 | [assoc_op, 2, 3], 285 | ) 286 | assert run(0, True, eq_assoc_args(assoc_op, (1, 1), ("other_op", 1, 1))) == () 287 | 288 | assert run(0, x, eq_assoc_args(assoc_op, (1, 2, 3), x, n=2)) == ( 289 | ((assoc_op, 1, 2), 3), 290 | (1, (assoc_op, 2, 3)), 291 | ) 292 | assert run(0, x, eq_assoc_args(assoc_op, x, (1, 2, 3), n=2)) == ( 293 | ((assoc_op, 1, 2), 3), 294 | (1, (assoc_op, 2, 3)), 295 | ) 296 | 297 | assert run(0, x, eq_assoc_args(assoc_op, (1, 2, 3), x)) == ( 298 | ((assoc_op, 1, 2), 3), 299 | (1, (assoc_op, 2, 3)), 300 | (1, 2, 3), 301 | ) 302 | 303 | assert () not in run(0, x, eq_assoc_args(assoc_op, (), x, no_ident=True)) 304 | assert (1,) not in run(0, x, eq_assoc_args(assoc_op, (1,), x, no_ident=True)) 305 | assert (1, 2, 3) not in run( 306 | 0, x, eq_assoc_args(assoc_op, (1, 2, 3), x, no_ident=True) 307 | ) 308 | 309 | assert ( 310 | run( 311 | 0, 312 | True, 313 | eq_assoc_args( 314 | assoc_op, 315 | (1, (assoc_op, 2, 3)), 316 | (1, (assoc_op, 2, 3)), 317 | no_ident=True, 318 | ), 319 | ) 320 | == () 321 | ) 322 | 323 | assert run( 324 | 0, 325 | True, 326 | eq_assoc_args( 327 | assoc_op, 328 | (1, (assoc_op, 2, 3)), 329 | ((assoc_op, 1, 2), 3), 330 | no_ident=True, 331 | ), 332 | ) == (True,) 333 | 334 | 335 | def test_eq_assoc(): 336 | 337 | assoc_op = "assoc_op" 338 | 339 | fact(associative, assoc_op) 340 | 341 | assert run(0, True, eq_assoc(1, 1)) == (True,) 342 | assert run(0, True, eq_assoc((assoc_op, 1, 2, 3), (assoc_op, 1, 2, 3))) == (True,) 343 | assert not run(0, True, eq_assoc((assoc_op, 3, 2, 1), (assoc_op, 1, 2, 3))) 344 | assert run( 345 | 0, True, eq_assoc((assoc_op, (assoc_op, 1, 2), 3), (assoc_op, 1, 2, 3)) 346 | ) == (True,) 347 | assert run( 348 | 0, True, eq_assoc((assoc_op, 1, 2, 3), (assoc_op, (assoc_op, 1, 2), 3)) 349 | ) == (True,) 350 | o = "op" 351 | assert not run(0, True, eq_assoc((o, 1, 2, 3), (o, (o, 1, 2), 3))) 352 | 353 | x = var() 354 | res = run(0, x, eq_assoc((assoc_op, 1, 2, 3), x, n=2)) 355 | assert res == ( 356 | (assoc_op, (assoc_op, 1, 2), 3), 357 | (assoc_op, 1, 2, 3), 358 | (assoc_op, 1, (assoc_op, 2, 3)), 359 | ) 360 | 361 | res = run(0, x, eq_assoc(x, (assoc_op, 1, 2, 3), n=2)) 362 | assert res == ( 363 | (assoc_op, (assoc_op, 1, 2), 3), 364 | (assoc_op, 1, 2, 3), 365 | (assoc_op, 1, (assoc_op, 2, 3)), 366 | ) 367 | 368 | y, z = var(), var() 369 | 370 | # Check results when both arguments are variables 371 | res = run(3, (x, y), eq_assoc(x, y)) 372 | exp_res_form = ( 373 | (etuple(assoc_op, x, y, z), etuple(assoc_op, etuple(assoc_op, x, y), z)), 374 | (x, y), 375 | ( 376 | etuple(etuple(assoc_op, x, y, z)), 377 | etuple(etuple(assoc_op, etuple(assoc_op, x, y), z)), 378 | ), 379 | ) 380 | 381 | for a, b in zip(res, exp_res_form): 382 | s = unify(a, b) 383 | assert s is not False, (a, b) 384 | assert all(isvar(i) for i in reify((x, y, z), s)) 385 | 386 | # Make sure it works with `cons` 387 | res = run(0, (x, y), eq_assoc(cons(x, y), (assoc_op, 1, 2, 3))) 388 | assert res == ( 389 | (assoc_op, ((assoc_op, 1, 2), 3)), 390 | (assoc_op, (1, 2, 3)), 391 | (assoc_op, (1, (assoc_op, 2, 3))), 392 | ) 393 | 394 | res = run(1, (x, y), eq_assoc(cons(x, y), (x, z, 2, 3))) 395 | assert res == ((assoc_op, ((assoc_op, z, 2), 3)),) 396 | 397 | # Don't use a predicate that can never succeed, e.g. 398 | # associative_2 = Relation("associative_2") 399 | # run(1, (x, y), eq_assoc(cons(x, y), (x, z), op_predicate=associative_2)) 400 | 401 | # Nested expressions should work now 402 | expr1 = (assoc_op, 1, 2, (assoc_op, x, 5, 6)) 403 | expr2 = (assoc_op, (assoc_op, 1, 2), 3, 4, 5, 6) 404 | assert run(0, x, eq_assoc(expr1, expr2, n=2)) == ((assoc_op, 3, 4),) 405 | 406 | 407 | def test_assoc_flatten(): 408 | 409 | add = "add" 410 | mul = "mul" 411 | 412 | fact(commutative, add) 413 | fact(associative, add) 414 | fact(commutative, mul) 415 | fact(associative, mul) 416 | 417 | assert run( 418 | 0, 419 | True, 420 | assoc_flatten((mul, 1, (add, 2, 3), (mul, 4, 5)), (mul, 1, (add, 2, 3), 4, 5)), 421 | ) == (True,) 422 | 423 | x = var() 424 | assert run( 425 | 0, 426 | x, 427 | assoc_flatten((mul, 1, (add, 2, 3), (mul, 4, 5)), x), 428 | ) == ((mul, 1, (add, 2, 3), 4, 5),) 429 | 430 | assert run( 431 | 0, 432 | True, 433 | assoc_flatten( 434 | ("op", 1, (add, 2, 3), (mul, 4, 5)), ("op", 1, (add, 2, 3), (mul, 4, 5)) 435 | ), 436 | ) == (True,) 437 | 438 | assert run(0, x, assoc_flatten(("op", 1, (add, 2, 3), (mul, 4, 5)), x)) == ( 439 | ("op", 1, (add, 2, 3), (mul, 4, 5)), 440 | ) 441 | 442 | 443 | def test_eq_assoccomm(): 444 | x, y = var(), var() 445 | 446 | ac = "commassoc_op" 447 | 448 | fact(commutative, ac) 449 | fact(associative, ac) 450 | 451 | assert run(0, True, eq_assoccomm(1, 1)) == (True,) 452 | assert run(0, True, eq_assoccomm((1,), (1,))) == (True,) 453 | assert run(0, True, eq_assoccomm(x, (1,))) == (True,) 454 | assert run(0, True, eq_assoccomm((1,), x)) == (True,) 455 | 456 | # Assoc only 457 | assert run(0, True, eq_assoccomm((ac, 1, (ac, 2, 3)), (ac, (ac, 1, 2), 3))) == ( 458 | True, 459 | ) 460 | # Commute only 461 | assert run(0, True, eq_assoccomm((ac, 1, (ac, 2, 3)), (ac, (ac, 3, 2), 1))) == ( 462 | True, 463 | ) 464 | # Both 465 | assert run(0, True, eq_assoccomm((ac, 1, (ac, 3, 2)), (ac, (ac, 1, 2), 3))) == ( 466 | True, 467 | ) 468 | 469 | exp_res = set( 470 | ( 471 | (ac, 1, 3, 2), 472 | (ac, 1, 2, 3), 473 | (ac, 2, 1, 3), 474 | (ac, 2, 3, 1), 475 | (ac, 3, 1, 2), 476 | (ac, 3, 2, 1), 477 | (ac, 1, (ac, 2, 3)), 478 | (ac, 1, (ac, 3, 2)), 479 | (ac, 2, (ac, 1, 3)), 480 | (ac, 2, (ac, 3, 1)), 481 | (ac, 3, (ac, 1, 2)), 482 | (ac, 3, (ac, 2, 1)), 483 | (ac, (ac, 2, 3), 1), 484 | (ac, (ac, 3, 2), 1), 485 | (ac, (ac, 1, 3), 2), 486 | (ac, (ac, 3, 1), 2), 487 | (ac, (ac, 1, 2), 3), 488 | (ac, (ac, 2, 1), 3), 489 | ) 490 | ) 491 | assert set(run(0, x, eq_assoccomm((ac, 1, (ac, 2, 3)), x))) == exp_res 492 | assert set(run(0, x, eq_assoccomm((ac, 1, 3, 2), x))) == exp_res 493 | assert set(run(0, x, eq_assoccomm((ac, 2, (ac, 3, 1)), x))) == exp_res 494 | # LHS variations 495 | assert set(run(0, x, eq_assoccomm(x, (ac, 1, (ac, 2, 3))))) == exp_res 496 | 497 | assert run(0, (x, y), eq_assoccomm((ac, (ac, 1, x), y), (ac, 2, (ac, 3, 1)))) == ( 498 | (2, 3), 499 | (3, 2), 500 | ) 501 | 502 | assert run(0, True, eq_assoccomm((ac, (ac, 1, 2), 3), (ac, 1, 2, 3))) == (True,) 503 | assert run(0, True, eq_assoccomm((ac, 3, (ac, 1, 2)), (ac, 1, 2, 3))) == (True,) 504 | assert run(0, True, eq_assoccomm((ac, 1, 1), ("other_op", 1, 1))) == () 505 | 506 | assert run(0, x, eq_assoccomm((ac, 3, (ac, 1, 2)), (ac, 1, x, 3))) == (2,) 507 | 508 | # Both arguments unground 509 | op_lv = var() 510 | z = var() 511 | res = run(4, (x, y), eq_assoccomm(x, y)) 512 | exp_res_form = ( 513 | (etuple(op_lv, x, y), etuple(op_lv, y, x)), 514 | (y, y), 515 | ( 516 | etuple(etuple(op_lv, x, y)), 517 | etuple(etuple(op_lv, y, x)), 518 | ), 519 | ( 520 | etuple(op_lv, x, y, z), 521 | etuple(op_lv, etuple(op_lv, x, y), z), 522 | ), 523 | ) 524 | 525 | for a, b in zip(res, exp_res_form): 526 | s = unify(a, b) 527 | assert ( 528 | op_lv not in s 529 | or (s[op_lv],) in associative.facts 530 | or (s[op_lv],) in commutative.facts 531 | ) 532 | assert s is not False, (a, b) 533 | assert all(isvar(i) for i in reify((x, y, z), s)) 534 | 535 | 536 | def test_assoccomm_algebra(): 537 | 538 | add = "add" 539 | mul = "mul" 540 | 541 | fact(commutative, add) 542 | fact(associative, add) 543 | fact(commutative, mul) 544 | fact(associative, mul) 545 | 546 | x, y = var(), var() 547 | 548 | pattern = (mul, (add, 1, x), y) # (1 + x) * y 549 | expr = (mul, 2, (add, 3, 1)) # 2 * (3 + 1) 550 | 551 | assert run(0, (x, y), eq_assoccomm(pattern, expr)) == ((3, 2),) 552 | 553 | 554 | def test_assoccomm_objects(): 555 | 556 | fact(commutative, Add) 557 | fact(associative, Add) 558 | 559 | x = var() 560 | 561 | assert run(0, True, eq_assoccomm(add(1, 2, 3), add(3, 1, 2))) == (True,) 562 | assert run(0, x, eq_assoccomm(add(1, 2, 3), add(1, 2, x))) == (3,) 563 | assert run(0, x, eq_assoccomm(add(1, 2, 3), add(x, 2, 1))) == (3,) 564 | -------------------------------------------------------------------------------- /tests/test_constraints.py: -------------------------------------------------------------------------------- 1 | from itertools import permutations 2 | 3 | from cons import cons 4 | from pytest import raises 5 | from unification import reify, unify, var 6 | from unification.core import _reify, stream_eval 7 | 8 | from kanren import conde, eq, run 9 | from kanren.constraints import ( 10 | ConstrainedState, 11 | ConstrainedVar, 12 | DisequalityStore, 13 | isinstanceo, 14 | neq, 15 | typeo, 16 | ) 17 | from kanren.core import lconj 18 | from kanren.goals import membero 19 | 20 | 21 | def test_ConstrainedState(): 22 | 23 | a_lv, b_lv = var(), var() 24 | 25 | ks = ConstrainedState() 26 | 27 | assert repr(ks) == "ConstrainedState({}, {})" 28 | 29 | assert ks == {} 30 | assert {} == ks 31 | assert not ks == {a_lv: 1} 32 | assert not ks == ConstrainedState({a_lv: 1}) 33 | 34 | assert unify(1, 1, ks) is not None 35 | assert unify(1, 2, ks) is False 36 | 37 | assert unify(b_lv, a_lv, ks) 38 | assert unify(a_lv, b_lv, ks) 39 | assert unify(a_lv, b_lv, ks) 40 | 41 | # Now, try that with a constraint (that's never used). 42 | ks.constraints[DisequalityStore] = DisequalityStore({a_lv: {1}}) 43 | 44 | assert not ks == {a_lv: 1} 45 | assert not ks == ConstrainedState({a_lv: 1}) 46 | 47 | assert unify(1, 1, ks) is not None 48 | assert unify(1, 2, ks) is False 49 | 50 | assert unify(b_lv, a_lv, ks) 51 | assert unify(a_lv, b_lv, ks) 52 | assert unify(a_lv, b_lv, ks) 53 | 54 | ks = ConstrainedState( 55 | {a_lv: 1}, constraints={DisequalityStore: DisequalityStore({b_lv: {1}})} 56 | ) 57 | ks_2 = ks.copy() 58 | assert ks == ks_2 59 | assert ks is not ks_2 60 | assert ks.constraints is not ks_2.constraints 61 | assert ks.constraints[DisequalityStore] is not ks_2.constraints[DisequalityStore] 62 | assert ( 63 | ks.constraints[DisequalityStore].lvar_constraints[b_lv] 64 | == ks_2.constraints[DisequalityStore].lvar_constraints[b_lv] 65 | ) 66 | assert ( 67 | ks.constraints[DisequalityStore].lvar_constraints[b_lv] 68 | is not ks_2.constraints[DisequalityStore].lvar_constraints[b_lv] 69 | ) 70 | 71 | 72 | def test_reify(): 73 | var_a = var("a") 74 | 75 | ks = ConstrainedState() 76 | assert repr(ConstrainedVar(var_a, ks)) == "~a: {}" 77 | 78 | de = DisequalityStore({var_a: {1, 2}}) 79 | ks.constraints[DisequalityStore] = de 80 | 81 | assert repr(de) == "ConstraintStore(neq: {~a: {1, 2}})" 82 | assert de.constraints_str(var()) == "" 83 | 84 | assert repr(ConstrainedVar(var_a, ks)) == "~a: {neq {1, 2}}" 85 | 86 | # TODO: Make this work with `reify` when `var('a')` isn't in `ks`. 87 | assert isinstance(reify(var_a, ks), ConstrainedVar) 88 | assert repr(stream_eval(_reify(var_a, ks))) == "~a: {neq {1, 2}}" 89 | 90 | 91 | def test_ConstraintStore(): 92 | a_lv, b_lv = var(), var() 93 | assert DisequalityStore({a_lv: {1}}) == DisequalityStore({a_lv: {1}}) 94 | assert DisequalityStore({a_lv: {1}}) != DisequalityStore({a_lv: {1}, b_lv: {}}) 95 | 96 | assert a_lv in DisequalityStore({a_lv: {1}}) 97 | 98 | 99 | def test_ConstrainedVar(): 100 | 101 | a_lv = var() 102 | a_clv = ConstrainedVar(a_lv, ConstrainedState()) 103 | 104 | assert a_lv == a_clv 105 | assert a_clv == a_lv 106 | 107 | assert hash(a_lv) == hash(a_clv) 108 | 109 | assert a_lv in {a_clv} 110 | assert a_clv in {a_lv} 111 | 112 | 113 | def test_disequality_basic(): 114 | 115 | a_lv, b_lv = var(), var() 116 | 117 | ks = ConstrainedState() 118 | de = DisequalityStore({a_lv: {1}}) 119 | ks.constraints[DisequalityStore] = de 120 | 121 | assert unify(a_lv, 1, ks) is False 122 | 123 | ks = unify(a_lv, b_lv, ks) 124 | assert unify(b_lv, 1, ks) is False 125 | 126 | res = list(lconj(neq({}, 1))({})) 127 | assert len(res) == 1 128 | 129 | res = list(lconj(neq(1, {}))({})) 130 | assert len(res) == 1 131 | 132 | res = list(lconj(neq({}, {}))({})) 133 | assert len(res) == 0 134 | 135 | res = list(lconj(neq(a_lv, 1))({})) 136 | assert len(res) == 1 137 | assert isinstance(res[0], ConstrainedState) 138 | assert res[0].constraints[DisequalityStore].lvar_constraints[a_lv] == {1} 139 | 140 | res = list(lconj(neq(1, a_lv))({})) 141 | assert len(res) == 1 142 | assert isinstance(res[0], ConstrainedState) 143 | assert res[0].constraints[DisequalityStore].lvar_constraints[a_lv] == {1} 144 | 145 | res = list(lconj(neq(a_lv, 1), neq(a_lv, 2), neq(a_lv, 1))({})) 146 | assert len(res) == 1 147 | assert isinstance(res[0], ConstrainedState) 148 | assert res[0].constraints[DisequalityStore].lvar_constraints[a_lv] == {1, 2} 149 | 150 | res = list(lconj(neq(a_lv, 1), eq(a_lv, 2))({})) 151 | assert len(res) == 1 152 | assert isinstance(res[0], ConstrainedState) 153 | # The constrained variable is already ground and satisfies the constraint, 154 | # so it should've been removed from the store 155 | assert a_lv not in res[0].constraints[DisequalityStore].lvar_constraints 156 | assert res[0][a_lv] == 2 157 | 158 | res = list(lconj(eq(a_lv, 1), neq(a_lv, 1))({})) 159 | assert res == [] 160 | 161 | 162 | def test_disequality(): 163 | 164 | a_lv, b_lv = var(), var() 165 | q_lv, c_lv = var(), var() 166 | 167 | goal_sets = [ 168 | ([neq(a_lv, 1)], 1), 169 | ([neq(cons(1, a_lv), [1]), eq(a_lv, [])], 0), 170 | ([neq(cons(1, a_lv), [1]), eq(a_lv, b_lv), eq(b_lv, [])], 0), 171 | ([neq([1], cons(1, a_lv)), eq(a_lv, b_lv), eq(b_lv, [])], 0), 172 | # TODO FIXME: This one won't work due to an ambiguity in `cons`. 173 | # ( 174 | # [ 175 | # neq([1], cons(1, a_lv)), 176 | # eq(a_lv, b_lv), 177 | # # Both make `cons` produce a list 178 | # conde([eq(b_lv, None)], [eq(b_lv, [])]), 179 | # ], 180 | # 0, 181 | # ), 182 | ([neq(cons(1, a_lv), [1]), eq(a_lv, b_lv), eq(b_lv, tuple())], 1), 183 | ([neq([1], cons(1, a_lv)), eq(a_lv, b_lv), eq(b_lv, tuple())], 1), 184 | ( 185 | [ 186 | neq([1], cons(1, a_lv)), 187 | eq(a_lv, b_lv), 188 | # The first should fail, the second should succeed 189 | conde([eq(b_lv, [])], [eq(b_lv, tuple())]), 190 | ], 191 | 1, 192 | ), 193 | ([neq(a_lv, 1), eq(a_lv, 1)], 0), 194 | ([neq(a_lv, 1), eq(b_lv, 1), eq(a_lv, b_lv)], 0), 195 | ([neq(a_lv, 1), eq(b_lv, 1), eq(a_lv, b_lv)], 0), 196 | ([neq(a_lv, b_lv), eq(b_lv, c_lv), eq(c_lv, a_lv)], 0), 197 | ] 198 | 199 | for i, (goal, num_results) in enumerate(goal_sets): 200 | # The order of goals should not matter, so try them all 201 | for goal_ord in permutations(goal): 202 | 203 | res = list(lconj(*goal_ord)({})) 204 | assert len(res) == num_results, (i, goal_ord) 205 | 206 | res = list(lconj(*goal_ord)(ConstrainedState())) 207 | assert len(res) == num_results, (i, goal_ord) 208 | 209 | assert len(run(0, q_lv, *goal_ord)) == num_results, (i, goal_ord) 210 | 211 | 212 | def test_typeo_basic(): 213 | a_lv, q_lv = var(), var() 214 | 215 | assert run(0, q_lv, typeo(q_lv, int)) == (q_lv,) 216 | assert run(0, q_lv, typeo(1, int)) == (q_lv,) 217 | assert run(0, q_lv, typeo(1, str)) == () 218 | assert run(0, q_lv, typeo("hi", str)) == (q_lv,) 219 | assert run(0, q_lv, typeo([], q_lv)) == (q_lv,) 220 | # Invalid second arg type (i.e. not a type) 221 | assert run(0, q_lv, typeo(1, 1)) == () 222 | assert run(0, q_lv, membero(q_lv, (1, "cat", 2.2, "hat")), typeo(q_lv, str)) == ( 223 | "cat", 224 | "hat", 225 | ) 226 | 227 | with raises(ValueError): 228 | run(0, q_lv, typeo(a_lv, str), typeo(a_lv, int)) 229 | 230 | 231 | def test_typeo(): 232 | a_lv, b_lv, q_lv = var(), var(), var() 233 | 234 | goal_sets = [ 235 | # Logic variable instance type that's immediately ground in another 236 | # goal 237 | ([typeo(q_lv, int), eq(q_lv, 1)], (1,)), 238 | # Use an unhashable constrained term 239 | ([typeo(q_lv, list), eq(q_lv, [])], ([],)), 240 | # TODO: A constraint parameter that is never ground 241 | # ([typeo(a_lv, q_lv), eq(a_lv, 1)], (int,)), 242 | # A non-ground, non-logic variable instance argument that changes type 243 | # when ground 244 | ([typeo(cons(1, a_lv), list), eq(a_lv, [])], (q_lv,)), 245 | # Logic variable instance and type arguments 246 | ([typeo(q_lv, int), eq(b_lv, 1), eq(b_lv, q_lv)], (1,)), 247 | # The same, but with `conde` 248 | ( 249 | [ 250 | typeo(q_lv, int), 251 | # One succeeds, one fails 252 | conde([eq(b_lv, 1)], [eq(b_lv, "hi")]), 253 | eq(b_lv, q_lv), 254 | ], 255 | (1,), 256 | ), 257 | # Logic variable instance argument that's eventually grounded to a 258 | # mismatched instance type through another logic variable 259 | ([typeo(q_lv, int), eq(b_lv, 1.0), eq(b_lv, q_lv)], ()), 260 | # Logic variable type argument that's eventually grounded to a 261 | # mismatched instance type through another logic variable (i.e. both 262 | # arguments are ground to `int` types) 263 | ([typeo(q_lv, b_lv), eq(b_lv, int), eq(b_lv, q_lv)], ()), 264 | # Logic variable type argument that's eventually grounded to a 265 | # mismatched instance type through another logic variable (i.e. both 266 | # arguments are ground to the value `1`, which violates the second 267 | # argument type expectations) 268 | ([typeo(q_lv, b_lv), eq(b_lv, 1), eq(b_lv, q_lv)], ()), 269 | # Check a term that's unground by ground enough for this constraint 270 | ([typeo(a_lv, tuple), eq([(b_lv,)], a_lv)], ()), 271 | ] 272 | 273 | for i, (goal, expected) in enumerate(goal_sets): 274 | for goal_ord in permutations(goal): 275 | res = run(0, q_lv, *goal_ord) 276 | assert res == expected, (i, goal_ord) 277 | 278 | 279 | def test_instanceo_basic(): 280 | q_lv = var() 281 | 282 | assert run(0, q_lv, isinstanceo(q_lv, int)) == (q_lv,) 283 | assert run(0, q_lv, isinstanceo(1, int)) == (q_lv,) 284 | assert run(0, q_lv, isinstanceo(1, object)) == (q_lv,) 285 | # NOTE: Not currently supported. 286 | # assert run(0, q_lv, isinstanceo(1, (int, object))) == (q_lv,) 287 | assert run(0, q_lv, isinstanceo(1, str)) == () 288 | # NOTE: Not currently supported. 289 | # assert run(0, q_lv, isinstanceo(1, (str, list))) == () 290 | assert run(0, q_lv, isinstanceo("hi", str)) == (q_lv,) 291 | # Invalid second arg type (i.e. not a type) 292 | assert run(0, q_lv, isinstanceo(1, 1)) == () 293 | 294 | 295 | def test_instanceo(): 296 | b_lv, q_lv = var(), var() 297 | 298 | goal_sets = [ 299 | # Logic variable instance type that's immediately ground in another 300 | # goal 301 | ([isinstanceo(q_lv, list), eq(q_lv, [])], ([],)), 302 | # Logic variable in the type argument that's eventually unified with 303 | # a valid type for the given instance argument 304 | ([isinstanceo([], q_lv), eq(q_lv, list)], (list,)), 305 | # Logic variable type argument that's eventually reified to a tuple 306 | # containing a valid type for the instance argument 307 | # NOTE: Not currently supported. 308 | # ( 309 | # [isinstanceo([], q_lv), eq(q_lv, (int, b_lv)), eq(b_lv, list)], 310 | # ((int, list),), 311 | # ), 312 | # A non-ground, non-logic variable instance argument that changes type 313 | # when ground 314 | ([isinstanceo(cons(1, q_lv), list), eq(q_lv, [])], ([],)), 315 | # Logic variable instance argument that's eventually grounded through 316 | # another logic variable 317 | ([isinstanceo(q_lv, int), eq(b_lv, 1), eq(b_lv, q_lv)], (1,)), 318 | # The same, but with `conde` 319 | ( 320 | [ 321 | isinstanceo(q_lv, int), 322 | # One succeeds, one fails 323 | conde([eq(b_lv, 1)], [eq(b_lv, "hi")]), 324 | eq(b_lv, q_lv), 325 | ], 326 | (1,), 327 | ), 328 | # Logic variable instance argument that's eventually grounded to a 329 | # mismatched instance type through another logic variable 330 | ([isinstanceo(q_lv, int), eq(b_lv, 1.0), eq(b_lv, q_lv)], ()), 331 | # Logic variable type argument that's eventually grounded to a 332 | # mismatched instance type through another logic variable (i.e. both 333 | # arguments are ground to `int` types) 334 | ([isinstanceo(q_lv, b_lv), eq(b_lv, int), eq(b_lv, q_lv)], ()), 335 | # Logic variable type argument that's eventually grounded to a 336 | # mismatched instance type through another logic variable (i.e. both 337 | # arguments are ground to the value `1`, which violates the second 338 | # argument type expectations) 339 | ([isinstanceo(q_lv, b_lv), eq(b_lv, 1), eq(b_lv, q_lv)], ()), 340 | # Check a term that's unground by ground enough for this constraint 341 | ([isinstanceo(q_lv, tuple), eq([(b_lv,)], q_lv)], ()), 342 | ] 343 | 344 | for i, (goal, expected) in enumerate(goal_sets): 345 | for goal_ord in permutations(goal): 346 | res = run(0, q_lv, *goal_ord) 347 | assert res == expected, (i, goal_ord) 348 | -------------------------------------------------------------------------------- /tests/test_core.py: -------------------------------------------------------------------------------- 1 | from collections.abc import Iterator 2 | from itertools import count 3 | 4 | from cons import cons 5 | from pytest import raises 6 | from unification import var 7 | 8 | from kanren.core import ( 9 | conde, 10 | eq, 11 | fail, 12 | ground_order, 13 | ifa, 14 | lall, 15 | lany, 16 | lconj, 17 | lconj_seq, 18 | ldisj, 19 | ldisj_seq, 20 | run, 21 | succeed, 22 | ) 23 | 24 | 25 | def results(g, s=None): 26 | if s is None: 27 | s = dict() 28 | return tuple(g(s)) 29 | 30 | 31 | def test_eq(): 32 | x = var() 33 | assert tuple(eq(x, 2)({})) == ({x: 2},) 34 | assert tuple(eq(x, 2)({x: 3})) == () 35 | 36 | 37 | def test_lconj_basics(): 38 | 39 | a, b = var(), var() 40 | res = list(lconj(eq(1, a), eq(2, b))({})) 41 | assert res == [{a: 1, b: 2}] 42 | 43 | res = list(lconj(eq(1, a))({})) 44 | assert res == [{a: 1}] 45 | 46 | res = list(lconj_seq([])({})) 47 | assert res == [{}] 48 | 49 | res = list(lconj(eq(1, a), eq(2, a))({})) 50 | assert res == [] 51 | 52 | res = list(lconj(eq(1, 2))({})) 53 | assert res == [] 54 | 55 | res = list(lconj(eq(1, 1))({})) 56 | assert res == [{}] 57 | 58 | def gen(): 59 | for i in [succeed, succeed]: 60 | yield i 61 | 62 | res = list(lconj(gen())({})) 63 | assert res == [{}] 64 | 65 | def gen(): 66 | return 67 | 68 | res = list(lconj_seq([gen()])({})) 69 | assert res == [] 70 | 71 | 72 | def test_ldisj_basics(): 73 | 74 | a = var() 75 | res = list(ldisj(eq(1, a))({})) 76 | assert res == [{a: 1}] 77 | 78 | res = list(ldisj(eq(1, 2))({})) 79 | assert res == [] 80 | 81 | res = list(ldisj(eq(1, 1))({})) 82 | assert res == [{}] 83 | 84 | res = list(ldisj(eq(1, a), eq(1, a))({})) 85 | assert res == [{a: 1}, {a: 1}] 86 | 87 | res = list(ldisj(eq(1, a), eq(2, a))({})) 88 | assert res == [{a: 1}, {a: 2}] 89 | 90 | res = list(ldisj_seq([])({})) 91 | assert res == [{}] 92 | 93 | def gen(): 94 | for i in [succeed, succeed]: 95 | yield i 96 | 97 | res = list(ldisj(gen())({})) 98 | assert res == [{}, {}] 99 | 100 | 101 | def test_conde_basics(): 102 | 103 | a, b = var(), var() 104 | res = list(conde([eq(1, a), eq(2, b)], [eq(1, b), eq(2, a)])({})) 105 | assert res == [{a: 1, b: 2}, {b: 1, a: 2}] 106 | 107 | res = list(conde([eq(1, a), eq(2, 1)], [eq(1, b), eq(2, a)])({})) 108 | assert res == [{b: 1, a: 2}] 109 | 110 | aa, ab, ba, bb, bc = var(), var(), var(), var(), var() 111 | res = list( 112 | conde( 113 | [eq(1, a), conde([eq(11, aa)], [eq(12, ab)])], 114 | [ 115 | eq(1, b), 116 | conde([eq(111, ba), eq(112, bb)], [eq(121, bc)]), 117 | ], 118 | )({}) 119 | ) 120 | assert res == [ 121 | {a: 1, aa: 11}, 122 | {b: 1, ba: 111, bb: 112}, 123 | {a: 1, ab: 12}, 124 | {b: 1, bc: 121}, 125 | ] 126 | 127 | res = list(conde([eq(1, 2)], [eq(1, 1)])({})) 128 | assert res == [{}] 129 | 130 | assert list(lconj(eq(1, 1))({})) == [{}] 131 | 132 | res = list(lconj(conde([eq(1, 2)], [eq(1, 1)]))({})) 133 | assert res == [{}] 134 | 135 | res = list(lconj(conde([eq(1, 2)], [eq(1, 1)]), conde([eq(1, 2)], [eq(1, 1)]))({})) 136 | assert res == [{}] 137 | 138 | 139 | def test_lany(): 140 | x = var() 141 | assert len(tuple(lany(eq(x, 2), eq(x, 3))({}))) == 2 142 | assert len(tuple(lany(eq(x, 2), eq(x, 3))({}))) == 2 143 | 144 | 145 | def test_lall(): 146 | x = var() 147 | assert results(lall(eq(x, 2))) == ({x: 2},) 148 | assert results(lall(eq(x, 2), eq(x, 3))) == () 149 | assert results(lall()) == ({},) 150 | assert run(0, x, lall()) == (x,) 151 | 152 | 153 | def test_conde(): 154 | x = var() 155 | assert results(conde([eq(x, 2)], [eq(x, 3)])) == ({x: 2}, {x: 3}) 156 | assert results(conde([eq(x, 2), eq(x, 3)])) == () 157 | 158 | assert set(run(0, x, conde([eq(x, 2)], [eq(x, 3)]))) == {2, 3} 159 | assert set(run(0, x, conde([eq(x, 2), eq(x, 3)]))) == set() 160 | 161 | goals = ([eq(x, i)] for i in count()) # infinite number of goals 162 | assert run(1, x, conde(goals)) == (0,) 163 | assert run(1, x, conde(goals)) == (1,) 164 | 165 | 166 | def test_short_circuit(): 167 | def badgoal(s): 168 | raise NotImplementedError() 169 | 170 | x = var("x") 171 | tuple(run(5, x, fail, badgoal)) # Does not raise exception 172 | 173 | 174 | def test_run(): 175 | x, y, z = var(), var(), var() 176 | res = run(None, x, eq(x, 1)) 177 | assert isinstance(res, Iterator) 178 | assert tuple(res) == (1,) 179 | assert run(1, x, eq(x, 1)) == (1,) 180 | assert run(2, x, eq(x, 1)) == (1,) 181 | assert run(0, x, eq(x, 1)) == (1,) 182 | assert run(1, x, eq(x, (y, z)), eq(y, 3), eq(z, 4)) == ((3, 4),) 183 | assert set(run(2, x, conde([eq(x, 1)], [eq(x, 2)]))) == set((1, 2)) 184 | 185 | 186 | def test_run_output_reify(): 187 | x = var() 188 | assert run(0, (1, 2, x), eq(x, 3)) == ((1, 2, 3),) 189 | 190 | 191 | def test_lanyseq(): 192 | x = var() 193 | g = lany((eq(x, i) for i in range(3))) 194 | assert list(g({})) == [{x: 0}, {x: 1}, {x: 2}] 195 | assert list(g({})) == [{x: 0}, {x: 1}, {x: 2}] 196 | 197 | # Test lanyseq with an infinite number of goals. 198 | assert set(run(3, x, lany((eq(x, i) for i in count())))) == {0, 1, 2} 199 | assert set(run(3, x, lany((eq(x, i) for i in count())))) == {0, 1, 2} 200 | 201 | 202 | def test_lall_errors(): 203 | class SomeException(Exception): 204 | pass 205 | 206 | def bad_relation(): 207 | def _bad_relation(s): 208 | raise SomeException("some exception") 209 | 210 | return lall(_bad_relation) 211 | 212 | with raises(SomeException): 213 | run(0, var(), bad_relation()) 214 | 215 | 216 | def test_dict(): 217 | x = var() 218 | assert run(0, x, eq({1: x}, {1: 2})) == (2,) 219 | 220 | 221 | def test_ifa(): 222 | x, y = var(), var() 223 | 224 | assert run(0, (x, y), ifa(lall(eq(x, True), eq(y, 1)), eq(y, 2))) == ((True, 1),) 225 | assert run( 226 | 0, y, eq(x, False), ifa(lall(eq(x, True), eq(y, 1)), lall(eq(y, 2))) 227 | ) == (2,) 228 | assert ( 229 | run( 230 | 0, 231 | y, 232 | eq(x, False), 233 | ifa(lall(eq(x, True), eq(y, 1)), lall(eq(x, True), eq(y, 2))), 234 | ) 235 | == () 236 | ) 237 | 238 | assert run( 239 | 0, 240 | y, 241 | eq(x, True), 242 | ifa(lall(eq(x, True), eq(y, 1)), lall(eq(x, True), eq(y, 2))), 243 | ) == (1,) 244 | 245 | 246 | def test_ground_order(): 247 | x, y, z = var(), var(), var() 248 | assert run(0, x, ground_order((y, [1, z], 1), x)) == ([1, [1, z], y],) 249 | a, b, c = var(), var(), var() 250 | assert run(0, (a, b, c), ground_order((y, [1, z], 1), (a, b, c))) == ( 251 | (1, [1, z], y), 252 | ) 253 | res = run(0, z, ground_order([cons(x, y), (x, y)], z)) 254 | assert res == ([(x, y), cons(x, y)],) 255 | res = run(0, z, ground_order([(x, y), cons(x, y)], z)) 256 | assert res == ([(x, y), cons(x, y)],) 257 | -------------------------------------------------------------------------------- /tests/test_facts.py: -------------------------------------------------------------------------------- 1 | from unification import var 2 | 3 | from kanren.core import conde, run 4 | from kanren.facts import Relation, fact, facts 5 | 6 | 7 | def test_relation(): 8 | parent = Relation() 9 | fact(parent, "Homer", "Bart") 10 | fact(parent, "Homer", "Lisa") 11 | fact(parent, "Marge", "Bart") 12 | fact(parent, "Marge", "Lisa") 13 | fact(parent, "Abe", "Homer") 14 | fact(parent, "Jackie", "Marge") 15 | 16 | x = var("x") 17 | assert set(run(5, x, parent("Homer", x))) == set(("Bart", "Lisa")) 18 | assert set(run(5, x, parent(x, "Bart"))) == set(("Homer", "Marge")) 19 | 20 | def grandparent(x, z): 21 | y = var() 22 | return conde((parent(x, y), parent(y, z))) 23 | 24 | assert set(run(5, x, grandparent(x, "Bart"))) == set(("Abe", "Jackie")) 25 | 26 | foo = Relation("foo") 27 | assert "foo" in str(foo) 28 | 29 | 30 | def test_fact(): 31 | rel = Relation() 32 | fact(rel, 1, 2) 33 | assert (1, 2) in rel.facts 34 | assert (10, 10) not in rel.facts 35 | 36 | facts(rel, (2, 3), (3, 4)) 37 | assert (2, 3) in rel.facts 38 | assert (3, 4) in rel.facts 39 | 40 | 41 | def test_unify_variable_with_itself_should_not_unify(): 42 | # Regression test for https://github.com/logpy/logpy/issues/33 43 | valido = Relation() 44 | fact(valido, "a", "b") 45 | fact(valido, "b", "a") 46 | x = var() 47 | assert run(0, x, valido(x, x)) == () 48 | 49 | 50 | def test_unify_variable_with_itself_should_unify(): 51 | valido = Relation() 52 | fact(valido, 0, 1) 53 | fact(valido, 1, 0) 54 | fact(valido, 1, 1) 55 | x = var() 56 | assert run(0, x, valido(x, x)) == (1,) 57 | 58 | 59 | def test_unify_tuple(): 60 | # Tests that adding facts can be unified with unpacked versions of those 61 | # facts. 62 | valido = Relation() 63 | fact(valido, (0, 1)) 64 | fact(valido, (1, 0)) 65 | fact(valido, (1, 1)) 66 | x = var() 67 | y = var() 68 | assert set(run(0, x, valido((x, y)))) == set([0, 1]) 69 | assert set(run(0, (x, y), valido((x, y)))) == set([(0, 1), (1, 0), (1, 1)]) 70 | assert run(0, x, valido((x, x))) == (1,) 71 | -------------------------------------------------------------------------------- /tests/test_goals.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from cons import cons 3 | from cons.core import ConsPair 4 | from unification import isvar, unify, var 5 | 6 | from kanren.core import conde, eq, run 7 | from kanren.goals import ( 8 | appendo, 9 | conso, 10 | heado, 11 | itero, 12 | membero, 13 | nullo, 14 | permuteo, 15 | rembero, 16 | tailo, 17 | ) 18 | 19 | 20 | def results(g, s=None): 21 | if s is None: 22 | s = dict() 23 | return tuple(g(s)) 24 | 25 | 26 | def test_heado(): 27 | x, y, z = var(), var(), var() 28 | assert (x, 1) in results(heado(x, (1, 2, 3)))[0].items() 29 | assert (x, 1) in results(heado(1, (x, 2, 3)))[0].items() 30 | assert results(heado(x, ())) == () 31 | 32 | assert run(0, x, heado(x, z), conso(1, y, z)) == (1,) 33 | 34 | 35 | def test_tailo(): 36 | x, y, z = var(), var(), var() 37 | 38 | assert (x, (2, 3)) in results(tailo(x, (1, 2, 3)))[0].items() 39 | assert (x, ()) in results(tailo(x, (1,)))[0].items() 40 | assert results(tailo(x, ())) == () 41 | 42 | assert run(0, y, tailo(y, z), conso(x, (1, 2), z)) == ((1, 2),) 43 | 44 | 45 | def test_conso(): 46 | x, y, z = var(), var(), var() 47 | 48 | assert not results(conso(x, y, ())) 49 | assert results(conso(1, (2, 3), (1, 2, 3))) 50 | assert results(conso(x, (2, 3), (1, 2, 3))) == ({x: 1},) 51 | assert results(conso(1, (2, 3), x)) == ({x: (1, 2, 3)},) 52 | assert results(conso(x, y, (1, 2, 3))) == ({x: 1, y: (2, 3)},) 53 | assert results(conso(x, (2, 3), y)) == ({y: (x, 2, 3)},) 54 | assert run(0, x, conso(x, y, z), eq(z, (1, 2, 3))) == (1,) 55 | 56 | # Confirm that custom types are preserved. 57 | class mytuple(tuple): 58 | def __add__(self, other): 59 | return type(self)(super(mytuple, self).__add__(other)) 60 | 61 | assert type(results(conso(x, mytuple((2, 3)), y))[0][y]) == mytuple 62 | 63 | 64 | def test_nullo_itero(): 65 | 66 | x, y, z = var(), var(), var() 67 | q_lv, a_lv = var(), var() 68 | 69 | assert run(0, q_lv, conso(1, q_lv, [1]), nullo(q_lv)) 70 | assert run(0, q_lv, nullo(q_lv), conso(1, q_lv, [1])) 71 | 72 | assert not run(0, q_lv, nullo(q_lv, [], ())) 73 | assert run(0, [a_lv, q_lv], nullo(q_lv, a_lv, default_ConsNull=tuple)) == ( 74 | [(), ()], 75 | ) 76 | assert run(0, [a_lv, q_lv], nullo(a_lv, [], q_lv)) == ([[], []],) 77 | 78 | assert ([],) == run(0, q_lv, nullo(q_lv, [])) 79 | assert ([],) == run(0, q_lv, nullo([], q_lv)) 80 | assert (None,) == run(0, q_lv, nullo(None, q_lv)) 81 | assert (tuple(),) == run(0, q_lv, nullo(tuple(), q_lv)) 82 | assert (q_lv,) == run(0, q_lv, nullo(tuple(), tuple())) 83 | assert ([],) == run(0, q_lv, nullo(var(), q_lv)) 84 | assert ([],) == run(0, q_lv, nullo(q_lv, var())) 85 | assert ([],) == run(0, q_lv, nullo(q_lv, q_lv)) 86 | 87 | assert isvar(run(0, y, nullo([]))[0]) 88 | assert isvar(run(0, y, nullo(None))[0]) 89 | assert run(0, y, nullo(y))[0] == [] 90 | assert run(0, y, conso(var(), y, [1]), nullo(y))[0] == [] 91 | assert run(0, y, conso(var(), y, (1,)), nullo(y))[0] == () 92 | 93 | assert run(1, y, conso(1, x, y), itero(y))[0] == [1] 94 | assert run(1, y, conso(1, x, y), conso(2, z, x), itero(y))[0] == [1, 2] 95 | 96 | # Make sure that the remaining results end in logic variables 97 | res_2 = run(2, y, conso(1, x, y), conso(2, z, x), itero(y))[1] 98 | assert res_2[:2] == [1, 2] 99 | assert isvar(res_2[-1]) 100 | 101 | 102 | def test_membero(): 103 | x, y = var(), var() 104 | 105 | assert set(run(5, x, membero(x, (1, 2, 3)), membero(x, (2, 3, 4)))) == {2, 3} 106 | 107 | assert run(5, x, membero(2, (1, x, 3))) == (2,) 108 | assert run(0, x, membero(1, (1, 2, 3))) == (x,) 109 | assert run(0, x, membero(1, (2, 3))) == () 110 | 111 | g = membero(x, (0, 1, 2)) 112 | assert tuple(r[x] for r in g({})) == (0, 1, 2) 113 | 114 | def in_cons(x, y): 115 | if issubclass(type(y), ConsPair): 116 | return x == y.car or in_cons(x, y.cdr) 117 | else: 118 | return False 119 | 120 | res = run(4, x, membero(1, x)) 121 | assert all(in_cons(1, r) for r in res) 122 | 123 | res = run(4, (x, y), membero(x, y)) 124 | assert all(in_cons(i, r) for i, r in res) 125 | 126 | 127 | def test_uneval_membero(): 128 | x, y = var(), var() 129 | assert set(run(100, x, membero(y, ((1, 2, 3), (4, 5, 6))), membero(x, y))) == { 130 | 1, 131 | 2, 132 | 3, 133 | 4, 134 | 5, 135 | 6, 136 | } 137 | 138 | 139 | def test_appendo(): 140 | q_lv = var() 141 | assert run(0, q_lv, appendo((), (1, 2), (1, 2))) == (q_lv,) 142 | assert run(0, q_lv, appendo((), (1, 2), 1)) == () 143 | assert run(0, q_lv, appendo((), (1, 2), (1,))) == () 144 | assert run(0, q_lv, appendo((1, 2), (3, 4), (1, 2, 3, 4))) == (q_lv,) 145 | assert run(5, q_lv, appendo((1, 2, 3), q_lv, (1, 2, 3, 4, 5))) == ((4, 5),) 146 | assert run(5, q_lv, appendo(q_lv, (4, 5), (1, 2, 3, 4, 5))) == ((1, 2, 3),) 147 | assert run(5, q_lv, appendo((1, 2, 3), (4, 5), q_lv)) == ((1, 2, 3, 4, 5),) 148 | 149 | q_lv, r_lv = var(), var() 150 | 151 | assert ([1, 2, 3, 4],) == run(0, q_lv, appendo([1, 2], [3, 4], q_lv)) 152 | assert ([3, 4],) == run(0, q_lv, appendo([1, 2], q_lv, [1, 2, 3, 4])) 153 | assert ([1, 2],) == run(0, q_lv, appendo(q_lv, [3, 4], [1, 2, 3, 4])) 154 | 155 | expected_res = set( 156 | [ 157 | ((), (1, 2, 3, 4)), 158 | ((1,), (2, 3, 4)), 159 | ((1, 2), (3, 4)), 160 | ((1, 2, 3), (4,)), 161 | ((1, 2, 3, 4), ()), 162 | ] 163 | ) 164 | assert expected_res == set(run(0, (q_lv, r_lv), appendo(q_lv, r_lv, (1, 2, 3, 4)))) 165 | 166 | res = run(3, (q_lv, r_lv), appendo(q_lv, [3, 4], r_lv)) 167 | assert len(res) == 3 168 | assert any(len(a) > 0 and isvar(a[0]) for a, b in res) 169 | assert all(a + [3, 4] == b for a, b in res) 170 | 171 | res = run(0, (q_lv, r_lv), appendo([3, 4], q_lv, r_lv)) 172 | assert len(res) == 2 173 | assert ([], [3, 4]) == res[0] 174 | assert all( 175 | type(v) == cons for v in unify((var(), cons(3, 4, var())), res[1]).values() 176 | ) 177 | 178 | 179 | @pytest.mark.skip("Misspecified test") 180 | def test_appendo_reorder(): 181 | # XXX: This test generates goal conjunctions that are non-terminating given 182 | # the specified goal ordering. More specifically, it generates 183 | # `lall(appendo(x, y, w), appendo(w, z, ()))`, for which the first 184 | # `appendo` produces an infinite stream of results and the second 185 | # necessarily fails for all values of the first `appendo` yielding 186 | # non-empty `w` unifications. 187 | # 188 | # The only reason it worked before is the `EarlyGoalError` 189 | # and it's implicit goal reordering, which made this case an out-of-place 190 | # test for a goal reordering feature that has nothing to do with `appendo`. 191 | # Furthermore, the `EarlyGoalError` mechanics do *not* fix this general 192 | # problem, and it's trivial to generate an equivalent situation in which 193 | # an `EarlyGoalError` is never thrown. 194 | # 195 | # In other words, it seems like a nice side effect of `EarlyGoalError`, but 196 | # it's actually a very costly approach that masks a bigger issue; one that 197 | # all miniKanren programmers need to think about when developing. 198 | 199 | x, y, z, w = var(), var(), var(), var() 200 | for t in [tuple(range(i)) for i in range(5)]: 201 | print(t) 202 | for xi, yi in run(0, (x, y), appendo(x, y, t)): 203 | assert xi + yi == t 204 | 205 | results = run(2, (x, y, z, w), appendo(x, y, w), appendo(w, z, t)) 206 | for xi, yi, zi, wi in results: 207 | assert xi + yi + zi == t 208 | 209 | 210 | def test_rembero(): 211 | 212 | q_lv = var() 213 | assert ([],) == run(0, q_lv, rembero(1, [1], q_lv)) 214 | assert ([], [1]) == run(0, q_lv, rembero(1, q_lv, [])) 215 | 216 | expected_res = ( 217 | [5, 1, 2, 3, 4], 218 | [1, 5, 2, 3, 4], 219 | [1, 2, 5, 3, 4], 220 | [1, 2, 3, 5, 4], 221 | [1, 2, 3, 4], 222 | [1, 2, 3, 4, 5], 223 | ) 224 | assert expected_res == run(0, q_lv, rembero(5, q_lv, [1, 2, 3, 4])) 225 | 226 | 227 | def test_permuteo(): 228 | 229 | from itertools import permutations 230 | 231 | a_lv = var() 232 | q_lv = var() 233 | 234 | class Blah: 235 | def __hash__(self): 236 | raise TypeError() 237 | 238 | # An unhashable sequence with an unhashable object in it 239 | obj_1 = [Blah()] 240 | 241 | assert results(permuteo((1, 2), (2, 1))) == ({},) 242 | assert results(permuteo((1, obj_1), (obj_1, 1))) == ({},) 243 | assert results(permuteo([1, 2], [2, 1])) == ({},) 244 | assert results(permuteo((1, 2, 2), (2, 1, 2))) == ({},) 245 | 246 | # (1, obj_1, a_lv) == (1, obj_1, a_lv) ==> {a_lv: a_lv} 247 | # (1, obj_1, a_lv) == (1, a_lv, obj_1) ==> {a_lv: obj_1} 248 | # (1, obj_1, a_lv) == (a_lv, obj_1, 1) ==> {a_lv: 1} 249 | assert run(0, a_lv, permuteo((1, obj_1, a_lv), (obj_1, a_lv, 1))) == ( 250 | 1, 251 | a_lv, 252 | obj_1, 253 | ) 254 | 255 | assert not results(permuteo((1, 2), (2, 1, 2))) 256 | assert not results(permuteo((1, 2), (2, 1, 2))) 257 | assert not results(permuteo((1, 2, 3), (2, 1, 2))) 258 | assert not results(permuteo((1, 2, 1), (2, 1, 2))) 259 | assert not results(permuteo([1, 2, 1], (2, 1, 2))) 260 | 261 | x = var() 262 | assert set(run(0, x, permuteo(x, (1, 2, 2)))) == set( 263 | ((1, 2, 2), (2, 1, 2), (2, 2, 1)) 264 | ) 265 | q_lv = var() 266 | 267 | assert run(0, q_lv, permuteo((1, 2, 3), (q_lv, 2, 1))) == (3,) 268 | 269 | assert run(0, q_lv, permuteo([1, 2, 3], [3, 2, 1])) 270 | assert run(0, q_lv, permuteo((1, 2, 3), (3, 2, 1))) 271 | assert run(0, q_lv, permuteo([1, 2, 3], [2, 1])) == () 272 | assert run(0, q_lv, permuteo([1, 2, 3], (3, 2, 1))) == () 273 | 274 | col = [1, 2, 3] 275 | exp_res = set(tuple(i) for i in permutations(col)) 276 | 277 | # The first term is ground 278 | res = run(0, q_lv, permuteo(col, q_lv)) 279 | assert all(type(r) == type(col) for r in res) 280 | 281 | res = set(tuple(r) for r in res) 282 | assert res == exp_res 283 | 284 | # The second term is ground 285 | res = run(0, q_lv, permuteo(q_lv, col)) 286 | assert all(type(r) == type(col) for r in res) 287 | 288 | res = set(tuple(r) for r in res) 289 | assert res == exp_res 290 | 291 | a_lv = var() 292 | # Neither terms are ground 293 | bi_res = run(5, [q_lv, a_lv], permuteo(q_lv, a_lv)) 294 | 295 | assert bi_res[0] == [[], []] 296 | bi_var_1 = bi_res[1][0][0] 297 | assert isvar(bi_var_1) 298 | assert bi_res[1][0] == bi_res[1][1] == [bi_var_1] 299 | bi_var_2 = bi_res[2][0][1] 300 | assert isvar(bi_var_2) and bi_var_1 is not bi_var_2 301 | assert bi_res[2][0] == bi_res[2][1] == [bi_var_1, bi_var_2] 302 | assert bi_res[3][0] != bi_res[3][1] == [bi_var_2, bi_var_1] 303 | bi_var_3 = bi_res[4][0][2] 304 | assert bi_res[4][0] == bi_res[4][1] == [bi_var_1, bi_var_2, bi_var_3] 305 | 306 | assert run(0, x, permuteo((1, 2), (1, 2), no_ident=True)) == () 307 | assert run(0, True, permuteo((1, 2), (2, 1), no_ident=True)) == (True,) 308 | assert run(0, x, permuteo((), x, no_ident=True)) == () 309 | assert run(0, x, permuteo(x, (), no_ident=True)) == () 310 | assert run(0, x, permuteo((1,), x, no_ident=True)) == () 311 | assert run(0, x, permuteo(x, (1,), no_ident=True)) == () 312 | assert (1, 2, 3) not in run(0, x, permuteo((1, 2, 3), x, no_ident=True)) 313 | assert (1, 2, 3) not in run(0, x, permuteo(x, (1, 2, 3), no_ident=True)) 314 | y = var() 315 | assert all(a != b for a, b in run(6, [x, y], permuteo(x, y, no_ident=True))) 316 | 317 | def eq_permute(x, y): 318 | return conde([eq(x, y)], [permuteo(a, b) for a, b in zip(x, y)]) 319 | 320 | assert run( 321 | 0, True, permuteo((1, (2, 3)), ((3, 2), 1), inner_eq=eq_permute, no_ident=True) 322 | ) == (True,) 323 | -------------------------------------------------------------------------------- /tests/test_graph.py: -------------------------------------------------------------------------------- 1 | from functools import partial 2 | from math import exp, log 3 | from numbers import Real 4 | from operator import add, mul 5 | 6 | import pytest 7 | import toolz 8 | from cons import cons 9 | from etuples.core import ExpressionTuple, etuple 10 | from unification import isvar, reify, unify, var 11 | 12 | from kanren import conde, eq, lall, run 13 | from kanren.constraints import isinstanceo 14 | from kanren.graph import eq_length, map_anyo, mapo, reduceo, walko 15 | 16 | 17 | class OrderedFunction(object): 18 | def __init__(self, func): 19 | self.func = func 20 | 21 | def __call__(self, *args, **kwargs): 22 | return self.func(*args, **kwargs) 23 | 24 | @property 25 | def __name__(self): 26 | return self.func.__name__ 27 | 28 | def __lt__(self, other): 29 | return self.__name__ < getattr(other, "__name__", str(other)) 30 | 31 | def __gt__(self, other): 32 | return self.__name__ > getattr(other, "__name__", str(other)) 33 | 34 | def __repr__(self): 35 | return self.__name__ 36 | 37 | 38 | add = OrderedFunction(add) 39 | mul = OrderedFunction(mul) 40 | log = OrderedFunction(log) 41 | exp = OrderedFunction(exp) 42 | 43 | 44 | ExpressionTuple.__lt__ = ( 45 | lambda self, other: self < (other,) 46 | if isinstance(other, int) 47 | else tuple(self) < tuple(other) 48 | ) 49 | ExpressionTuple.__gt__ = ( 50 | lambda self, other: self > (other,) 51 | if isinstance(other, int) 52 | else tuple(self) > tuple(other) 53 | ) 54 | 55 | 56 | def single_math_reduceo(expanded_term, reduced_term): 57 | """Construct a goal for some simple math reductions.""" 58 | x_lv = var() 59 | return lall( 60 | isinstanceo(x_lv, Real), 61 | isinstanceo(x_lv, ExpressionTuple), 62 | conde( 63 | [ 64 | eq(expanded_term, etuple(add, x_lv, x_lv)), 65 | eq(reduced_term, etuple(mul, 2, x_lv)), 66 | ], 67 | [eq(expanded_term, etuple(log, etuple(exp, x_lv))), eq(reduced_term, x_lv)], 68 | ), 69 | ) 70 | 71 | 72 | math_reduceo = partial(reduceo, single_math_reduceo) 73 | 74 | term_walko = partial( 75 | walko, 76 | rator_goal=eq, 77 | null_type=ExpressionTuple, 78 | map_rel=partial(map_anyo, null_res=False), 79 | ) 80 | 81 | 82 | def test_basics(): 83 | x_lv = var() 84 | res = unify( 85 | etuple(log, etuple(exp, etuple(log, 1))), etuple(log, etuple(exp, x_lv)) 86 | ) 87 | assert res[x_lv] == etuple(log, 1) 88 | 89 | 90 | def test_reduceo(): 91 | q_lv = var() 92 | 93 | # Reduce/forward 94 | res = run(0, q_lv, math_reduceo(etuple(log, etuple(exp, etuple(log, 1))), q_lv)) 95 | assert len(res) == 1 96 | assert res[0] == etuple(log, 1) 97 | 98 | res = run( 99 | 0, 100 | q_lv, 101 | math_reduceo(etuple(log, etuple(exp, etuple(log, etuple(exp, 1)))), q_lv), 102 | ) 103 | assert res[0] == 1 104 | assert res[1] == etuple(log, etuple(exp, 1)) 105 | 106 | # Expand/backward 107 | res = run(3, q_lv, math_reduceo(q_lv, 1)) 108 | assert res[0] == etuple(log, etuple(exp, 1)) 109 | assert res[1] == etuple(log, etuple(exp, etuple(log, etuple(exp, 1)))) 110 | 111 | 112 | def test_mapo(): 113 | q_lv = var() 114 | 115 | def blah(x, y): 116 | return conde([eq(x, 1), eq(y, "a")], [eq(x, 3), eq(y, "b")]) 117 | 118 | assert run(0, q_lv, mapo(blah, [], q_lv)) == ([],) 119 | assert run(0, q_lv, mapo(blah, [1, 2, 3], q_lv)) == () 120 | assert run(0, q_lv, mapo(blah, [1, 1, 3], q_lv)) == (["a", "a", "b"],) 121 | assert run(0, q_lv, mapo(blah, q_lv, ["a", "a", "b"])) == ([1, 1, 3],) 122 | 123 | exp_res = ( 124 | [[], []], 125 | [[1], ["a"]], 126 | [[3], ["b"]], 127 | [[1, 1], ["a", "a"]], 128 | [[3, 1], ["b", "a"]], 129 | ) 130 | 131 | a_lv = var() 132 | res = run(5, [q_lv, a_lv], mapo(blah, q_lv, a_lv)) 133 | assert res == exp_res 134 | 135 | 136 | def test_eq_length(): 137 | q_lv = var() 138 | 139 | res = run(0, q_lv, eq_length([1, 2, 3], q_lv)) 140 | assert len(res) == 1 and len(res[0]) == 3 and all(isvar(q) for q in res[0]) 141 | 142 | res = run(0, q_lv, eq_length(q_lv, [1, 2, 3])) 143 | assert len(res) == 1 and len(res[0]) == 3 and all(isvar(q) for q in res[0]) 144 | 145 | res = run(0, q_lv, eq_length(cons(1, q_lv), [1, 2, 3])) 146 | assert len(res) == 1 and len(res[0]) == 2 and all(isvar(q) for q in res[0]) 147 | 148 | v_lv = var() 149 | res = run(3, (q_lv, v_lv), eq_length(q_lv, v_lv, default_ConsNull=tuple)) 150 | assert len(res) == 3 and all( 151 | isinstance(a, tuple) 152 | and len(a) == len(b) 153 | and (len(a) == 0 or a != b) 154 | and all(isvar(r) for r in a) 155 | for a, b in res 156 | ) 157 | 158 | 159 | def test_map_anyo_types(): 160 | """Make sure that `map_anyo` preserves the types between its arguments.""" 161 | q_lv = var() 162 | res = run(1, q_lv, map_anyo(lambda x, y: eq(x, y), [1], q_lv)) 163 | assert res[0] == [1] 164 | res = run(1, q_lv, map_anyo(lambda x, y: eq(x, y), (1,), q_lv)) 165 | assert res[0] == (1,) 166 | res = run(1, q_lv, map_anyo(lambda x, y: eq(x, y), q_lv, (1,))) 167 | assert res[0] == (1,) 168 | res = run(1, q_lv, map_anyo(lambda x, y: eq(x, y), q_lv, [1])) 169 | assert res[0] == [1] 170 | res = run(1, q_lv, map_anyo(lambda x, y: eq(x, y), [1, 2], [1, 2])) 171 | assert len(res) == 1 172 | res = run(1, q_lv, map_anyo(lambda x, y: eq(x, y), [1, 2], [1, 3])) 173 | assert len(res) == 0 174 | res = run(1, q_lv, map_anyo(lambda x, y: eq(x, y), [1, 2], (1, 2))) 175 | assert len(res) == 0 176 | 177 | 178 | def test_map_anyo_misc(): 179 | q_lv = var("q") 180 | 181 | res = run(0, q_lv, map_anyo(eq, [1, 2, 3], [1, 2, 3])) 182 | # TODO: Remove duplicate results 183 | assert len(res) == 7 184 | res = run(0, q_lv, map_anyo(eq, [1, 2, 3], [1, 3, 3])) 185 | assert len(res) == 0 186 | 187 | def one_to_threeo(x, y): 188 | return conde([eq(x, 1), eq(y, 3)]) 189 | 190 | res = run(0, q_lv, map_anyo(one_to_threeo, [1, 2, 4, 1, 4, 1, 1], q_lv)) 191 | 192 | assert res[0] == [3, 2, 4, 3, 4, 3, 3] 193 | 194 | assert ( 195 | len(run(4, q_lv, map_anyo(math_reduceo, [etuple(mul, 2, var("x"))], q_lv))) == 0 196 | ) 197 | 198 | test_res = run(4, q_lv, map_anyo(math_reduceo, [etuple(add, 2, 2), 1], q_lv)) 199 | assert test_res == ([etuple(mul, 2, 2), 1],) 200 | 201 | test_res = run(4, q_lv, map_anyo(math_reduceo, [1, etuple(add, 2, 2)], q_lv)) 202 | assert test_res == ([1, etuple(mul, 2, 2)],) 203 | 204 | test_res = run(4, q_lv, map_anyo(math_reduceo, q_lv, var("z"))) 205 | assert all(isinstance(r, list) for r in test_res) 206 | 207 | test_res = run(4, q_lv, map_anyo(math_reduceo, q_lv, var("z"), tuple)) 208 | assert all(isinstance(r, tuple) for r in test_res) 209 | 210 | x, y, z = var(), var(), var() 211 | 212 | def test_bin(a, b): 213 | return conde([eq(a, 1), eq(b, 2)]) 214 | 215 | res = run(10, (x, y), map_anyo(test_bin, x, y, null_type=tuple)) 216 | exp_res_form = ( 217 | ((1,), (2,)), 218 | ((x, 1), (x, 2)), 219 | ((1, 1), (2, 2)), 220 | ((x, y, 1), (x, y, 2)), 221 | ((1, x), (2, x)), 222 | ((x, 1, 1), (x, 2, 2)), 223 | ((1, 1, 1), (2, 2, 2)), 224 | ((x, y, z, 1), (x, y, z, 2)), 225 | ((1, x, 1), (2, x, 2)), 226 | ((x, 1, y), (x, 2, y)), 227 | ) 228 | 229 | for a, b in zip(res, exp_res_form): 230 | s = unify(a, b) 231 | assert s is not False 232 | assert all(isvar(i) for i in reify((x, y, z), s)) 233 | 234 | 235 | @pytest.mark.parametrize( 236 | "test_input, test_output", 237 | [ 238 | ([], ()), 239 | ([1], ()), 240 | ( 241 | [ 242 | etuple(add, 1, 1), 243 | ], 244 | ([etuple(mul, 2, 1)],), 245 | ), 246 | ([1, etuple(add, 1, 1)], ([1, etuple(mul, 2, 1)],)), 247 | ([etuple(add, 1, 1), 1], ([etuple(mul, 2, 1), 1],)), 248 | ( 249 | [etuple(mul, 2, 1), etuple(add, 1, 1), 1], 250 | ([etuple(mul, 2, 1), etuple(mul, 2, 1), 1],), 251 | ), 252 | ( 253 | [ 254 | etuple(add, 1, 1), 255 | etuple(log, etuple(exp, 5)), 256 | ], 257 | ( 258 | [etuple(mul, 2, 1), 5], 259 | [etuple(add, 1, 1), 5], 260 | [etuple(mul, 2, 1), etuple(log, etuple(exp, 5))], 261 | ), 262 | ), 263 | ], 264 | ) 265 | def test_map_anyo(test_input, test_output): 266 | """Test `map_anyo` with fully ground terms (i.e. no logic variables).""" 267 | q_lv = var() 268 | test_res = run( 269 | 0, 270 | q_lv, 271 | map_anyo(math_reduceo, test_input, q_lv), 272 | ) 273 | 274 | assert len(test_res) == len(test_output) 275 | 276 | test_res = sorted(test_res) 277 | test_output = sorted(test_output) 278 | # Make sure the first result matches. 279 | # TODO: This is fairly implementation-specific (i.e. dependent on the order 280 | # in which `condeseq` returns results). 281 | if len(test_output) > 0: 282 | assert test_res[0] == test_output[0] 283 | 284 | # Make sure all the results match. 285 | # TODO: If we want to avoid fixing the output order, convert the lists to 286 | # tuples and add everything to a set, then compare. 287 | assert test_res == test_output 288 | 289 | 290 | def test_map_anyo_reverse(): 291 | """Test `map_anyo` in "reverse" (i.e. specify the reduced form and generate the un-reduced form).""" # noqa: E501 292 | # Unbounded reverse 293 | q_lv = var() 294 | rev_input = [etuple(mul, 2, 1)] 295 | test_res = run(4, q_lv, map_anyo(math_reduceo, q_lv, rev_input)) 296 | assert test_res == ( 297 | [etuple(add, 1, 1)], 298 | [etuple(log, etuple(exp, etuple(add, 1, 1)))], 299 | # [etuple(log, etuple(exp, etuple(mul, 2, 1)))], 300 | [etuple(log, etuple(exp, etuple(log, etuple(exp, etuple(add, 1, 1)))))], 301 | # [etuple(log, etuple(exp, etuple(log, etuple(exp, etuple(mul, 2, 1)))))], 302 | [ 303 | etuple( 304 | log, 305 | etuple( 306 | exp, 307 | etuple( 308 | log, etuple(exp, etuple(log, etuple(exp, etuple(add, 1, 1)))) 309 | ), 310 | ), 311 | ) 312 | ], 313 | ) 314 | 315 | # Guided reverse 316 | test_res = run( 317 | 4, 318 | q_lv, 319 | map_anyo(math_reduceo, [etuple(add, q_lv, 1)], [etuple(mul, 2, 1)]), 320 | ) 321 | 322 | assert test_res == (1,) 323 | 324 | 325 | def test_walko_misc(): 326 | q_lv = var(prefix="q") 327 | 328 | expr = etuple(add, etuple(mul, 2, 1), etuple(add, 1, 1)) 329 | res = run(0, q_lv, walko(eq, expr, expr)) 330 | # TODO: Remove duplicates 331 | assert len(res) == 162 332 | 333 | expr2 = etuple(add, etuple(mul, 2, 1), etuple(add, 2, 1)) 334 | res = run(0, q_lv, walko(eq, expr, expr2)) 335 | assert len(res) == 0 336 | 337 | def one_to_threeo(x, y): 338 | return conde([eq(x, 1), eq(y, 3)]) 339 | 340 | res = run( 341 | 1, 342 | q_lv, 343 | walko( 344 | one_to_threeo, 345 | [1, [1, 2, 4], 2, [[4, 1, 1]], 1], 346 | q_lv, 347 | ), 348 | ) 349 | assert res == ([3, [3, 2, 4], 2, [[4, 3, 3]], 3],) 350 | 351 | assert run(2, q_lv, walko(eq, q_lv, q_lv, null_type=ExpressionTuple)) == ( 352 | q_lv, 353 | etuple(), 354 | ) 355 | 356 | res = run( 357 | 1, 358 | q_lv, 359 | walko( 360 | one_to_threeo, 361 | etuple( 362 | add, 363 | 1, 364 | etuple(mul, etuple(add, 1, 2), 1), 365 | etuple(add, etuple(add, 1, 2), 2), 366 | ), 367 | q_lv, 368 | # Only descend into `add` terms 369 | rator_goal=lambda x, y: lall(eq(x, add), eq(y, add)), 370 | ), 371 | ) 372 | 373 | assert res == ( 374 | etuple( 375 | add, 3, etuple(mul, etuple(add, 1, 2), 1), etuple(add, etuple(add, 3, 2), 2) 376 | ), 377 | ) 378 | 379 | 380 | @pytest.mark.parametrize( 381 | "test_input, test_output", 382 | [ 383 | (1, ()), 384 | (etuple(add, 1, 1), (etuple(mul, 2, 1),)), 385 | ( 386 | # (2 * 1) + (1 + 1) 387 | etuple(add, etuple(mul, 2, 1), etuple(add, 1, 1)), 388 | ( 389 | # 2 * (2 * 1) 390 | etuple(mul, 2, etuple(mul, 2, 1)), 391 | # (2 * 1) + (2 * 1) 392 | etuple(add, etuple(mul, 2, 1), etuple(mul, 2, 1)), 393 | ), 394 | ), 395 | ( 396 | # (log(exp(2)) * 1) + (1 + 1) 397 | etuple(add, etuple(mul, etuple(log, etuple(exp, 2)), 1), etuple(add, 1, 1)), 398 | ( 399 | # 2 * (2 * 1) 400 | etuple(mul, 2, etuple(mul, 2, 1)), 401 | # (2 * 1) + (2 * 1) 402 | etuple(add, etuple(mul, 2, 1), etuple(mul, 2, 1)), 403 | # (log(exp(2)) * 1) + (2 * 1) 404 | etuple( 405 | add, etuple(mul, etuple(log, etuple(exp, 2)), 1), etuple(mul, 2, 1) 406 | ), 407 | etuple(add, etuple(mul, 2, 1), etuple(add, 1, 1)), 408 | ), 409 | ), 410 | ], 411 | ) 412 | def test_walko(test_input, test_output): 413 | """Test `walko` with fully ground terms (i.e. no logic variables).""" 414 | 415 | q_lv = var() 416 | term_walko_fp = partial(reduceo, partial(term_walko, single_math_reduceo)) 417 | test_res = run( 418 | len(test_output), 419 | q_lv, 420 | term_walko_fp(test_input, q_lv), 421 | results_filter=toolz.unique, 422 | ) 423 | 424 | assert len(test_res) == len(test_output) 425 | 426 | test_res = sorted(test_res) 427 | test_output = sorted(test_output) 428 | 429 | # Make sure the first result matches. 430 | if len(test_output) > 0: 431 | assert test_res[0] == test_output[0] 432 | 433 | # Make sure all the results match. 434 | assert set(test_res) == set(test_output) 435 | 436 | 437 | def test_walko_reverse(): 438 | """Test `walko` in "reverse" (i.e. specify the reduced form and generate the un-reduced form).""" # noqa: E501 439 | q_lv = var("q") 440 | 441 | test_res = run(2, q_lv, term_walko(math_reduceo, q_lv, 5)) 442 | assert test_res == ( 443 | etuple(log, etuple(exp, 5)), 444 | etuple(log, etuple(exp, etuple(log, etuple(exp, 5)))), 445 | ) 446 | assert all(e.eval_obj == 5.0 for e in test_res) 447 | 448 | # Make sure we get some variety in the results 449 | test_res = run(2, q_lv, term_walko(math_reduceo, q_lv, etuple(mul, 2, 5))) 450 | assert test_res == ( 451 | # Expansion of the term's root 452 | etuple(add, 5, 5), 453 | # Expansion in the term's arguments 454 | etuple(mul, etuple(log, etuple(exp, 2)), etuple(log, etuple(exp, 5))), 455 | # Two step expansion at the root 456 | # etuple(log, etuple(exp, etuple(add, 5, 5))), 457 | # Expansion into a sub-term 458 | # etuple(mul, 2, etuple(log, etuple(exp, 5))) 459 | ) 460 | assert all(e.eval_obj == 10.0 for e in test_res) 461 | 462 | r_lv = var("r") 463 | test_res = run(4, [q_lv, r_lv], term_walko(math_reduceo, q_lv, r_lv)) 464 | expect_res = ( 465 | [etuple(add, 1, 1), etuple(mul, 2, 1)], 466 | [etuple(log, etuple(exp, etuple(add, 1, 1))), etuple(mul, 2, 1)], 467 | [etuple(), etuple()], 468 | [ 469 | etuple(add, etuple(mul, 2, 1), etuple(add, 1, 1)), 470 | etuple(mul, 2, etuple(mul, 2, 1)), 471 | ], 472 | ) 473 | assert list( 474 | unify(a1, a2) and unify(b1, b2) 475 | for [a1, b1], [a2, b2] in zip(test_res, expect_res) 476 | ) 477 | -------------------------------------------------------------------------------- /tests/test_sudoku.py: -------------------------------------------------------------------------------- 1 | """ 2 | Based off 3 | https://github.com/holtchesley/embedded-logic/blob/master/kanren/sudoku.ipynb 4 | """ 5 | import pytest 6 | from unification import var 7 | 8 | from kanren import run 9 | from kanren.core import lall 10 | from kanren.goals import permuteq 11 | 12 | 13 | DIGITS = tuple(range(1, 10)) 14 | 15 | 16 | def get_rows(board): 17 | return tuple(board[i : i + 9] for i in range(0, len(board), 9)) 18 | 19 | 20 | def get_columns(rows): 21 | return tuple(tuple(x[i] for x in rows) for i in range(0, 9)) 22 | 23 | 24 | def get_square(rows, x, y): 25 | return tuple(rows[xi][yi] for xi in range(x, x + 3) for yi in range(y, y + 3)) 26 | 27 | 28 | def get_squares(rows): 29 | return tuple(get_square(rows, x, y) for x in range(0, 9, 3) for y in range(0, 9, 3)) 30 | 31 | 32 | def vars(hints): 33 | def helper(h): 34 | if h in DIGITS: 35 | return h 36 | else: 37 | return var() 38 | 39 | return tuple(helper(x) for x in hints) 40 | 41 | 42 | def all_numbers(coll): 43 | return permuteq(coll, DIGITS) 44 | 45 | 46 | def sudoku_solver(hints): 47 | variables = vars(hints) 48 | rows = get_rows(variables) 49 | cols = get_columns(rows) 50 | sqs = get_squares(rows) 51 | return run( 52 | 1, 53 | variables, 54 | lall(*(all_numbers(r) for r in rows)), 55 | lall(*(all_numbers(c) for c in cols)), 56 | lall(*(all_numbers(s) for s in sqs)), 57 | ) 58 | 59 | 60 | # fmt: off 61 | def test_missing_one_entry(): 62 | example_board = ( 63 | 5, 3, 4, 6, 7, 8, 9, 1, 2, 64 | 6, 7, 2, 1, 9, 5, 3, 4, 8, 65 | 1, 9, 8, 3, 4, 2, 5, 6, 7, 66 | 8, 5, 9, 7, 6, 1, 4, 2, 3, 67 | 4, 2, 6, 8, 5, 3, 7, 9, 1, 68 | 7, 1, 3, 9, 2, 4, 8, 5, 6, 69 | 9, 6, 1, 5, 3, 7, 2, 8, 4, 70 | 2, 8, 7, 4, 1, 9, 6, 3, 5, 71 | 3, 4, 5, 2, 8, 6, 0, 7, 9, 72 | ) 73 | expected_solution = ( 74 | 5, 3, 4, 6, 7, 8, 9, 1, 2, 75 | 6, 7, 2, 1, 9, 5, 3, 4, 8, 76 | 1, 9, 8, 3, 4, 2, 5, 6, 7, 77 | 8, 5, 9, 7, 6, 1, 4, 2, 3, 78 | 4, 2, 6, 8, 5, 3, 7, 9, 1, 79 | 7, 1, 3, 9, 2, 4, 8, 5, 6, 80 | 9, 6, 1, 5, 3, 7, 2, 8, 4, 81 | 2, 8, 7, 4, 1, 9, 6, 3, 5, 82 | 3, 4, 5, 2, 8, 6, 1, 7, 9, 83 | ) 84 | assert sudoku_solver(example_board)[0] == expected_solution 85 | 86 | 87 | # fmt: off 88 | def test_missing_complex_board(): 89 | example_board = ( 90 | 5, 3, 4, 6, 7, 8, 9, 0, 2, 91 | 6, 7, 2, 0, 9, 5, 3, 4, 8, 92 | 0, 9, 8, 3, 4, 2, 5, 6, 7, 93 | 8, 5, 9, 7, 6, 0, 4, 2, 3, 94 | 4, 2, 6, 8, 5, 3, 7, 9, 0, 95 | 7, 0, 3, 9, 2, 4, 8, 5, 6, 96 | 9, 6, 0, 5, 3, 7, 2, 8, 4, 97 | 2, 8, 7, 4, 0, 9, 6, 3, 5, 98 | 3, 4, 5, 2, 8, 6, 0, 7, 9, 99 | ) 100 | expected_solution = ( 101 | 5, 3, 4, 6, 7, 8, 9, 1, 2, 102 | 6, 7, 2, 1, 9, 5, 3, 4, 8, 103 | 1, 9, 8, 3, 4, 2, 5, 6, 7, 104 | 8, 5, 9, 7, 6, 1, 4, 2, 3, 105 | 4, 2, 6, 8, 5, 3, 7, 9, 1, 106 | 7, 1, 3, 9, 2, 4, 8, 5, 6, 107 | 9, 6, 1, 5, 3, 7, 2, 8, 4, 108 | 2, 8, 7, 4, 1, 9, 6, 3, 5, 109 | 3, 4, 5, 2, 8, 6, 1, 7, 9, 110 | ) 111 | assert sudoku_solver(example_board)[0] == expected_solution 112 | 113 | 114 | # fmt: off 115 | def test_unsolvable(): 116 | example_board = ( 117 | 5, 3, 4, 6, 7, 8, 9, 1, 2, 118 | 6, 7, 2, 1, 9, 5, 9, 4, 8, # Note column 7 has two 9's. 119 | 1, 9, 8, 3, 4, 2, 5, 6, 7, 120 | 8, 5, 9, 7, 6, 1, 4, 2, 3, 121 | 4, 2, 6, 8, 5, 3, 7, 9, 1, 122 | 7, 1, 3, 9, 2, 4, 8, 5, 6, 123 | 9, 6, 1, 5, 3, 7, 2, 8, 4, 124 | 2, 8, 7, 4, 1, 9, 6, 3, 5, 125 | 3, 4, 5, 2, 8, 6, 0, 7, 9, 126 | ) 127 | assert sudoku_solver(example_board) == () 128 | 129 | 130 | # fmt: off 131 | @pytest.mark.skip(reason="Currently too slow!") 132 | def test_many_missing_elements(): 133 | example_board = ( 134 | 5, 3, 0, 0, 7, 0, 0, 0, 0, 135 | 6, 0, 0, 1, 9, 5, 0, 0, 0, 136 | 0, 9, 8, 0, 0, 0, 0, 6, 0, 137 | 8, 0, 0, 0, 6, 0, 0, 0, 3, 138 | 4, 0, 0, 8, 0, 3, 0, 0, 1, 139 | 7, 0, 0, 0, 2, 0, 0, 0, 6, 140 | 0, 6, 0, 0, 0, 0, 2, 8, 0, 141 | 0, 0, 0, 4, 1, 9, 0, 0, 5, 142 | 0, 0, 0, 0, 8, 0, 0, 7, 9 143 | ) 144 | assert sudoku_solver(example_board)[0] == ( 145 | 5, 3, 4, 6, 7, 8, 9, 1, 2, 146 | 6, 7, 2, 1, 9, 5, 3, 4, 8, 147 | 1, 9, 8, 3, 4, 2, 5, 6, 7, 148 | 8, 5, 9, 7, 6, 1, 4, 2, 3, 149 | 4, 2, 6, 8, 5, 3, 7, 9, 1, 150 | 7, 1, 3, 9, 2, 4, 8, 5, 6, 151 | 9, 6, 1, 5, 3, 7, 2, 8, 4, 152 | 2, 8, 7, 4, 1, 9, 6, 3, 5, 153 | 3, 4, 5, 2, 8, 6, 1, 7, 9 154 | ) 155 | 156 | 157 | # fmt: off 158 | @pytest.mark.skip(reason="Currently too slow!") 159 | def test_websudoku_easy(): 160 | # A sudoku from websudoku.com. 161 | example_board = ( 162 | 0, 0, 8, 0, 0, 6, 0, 0, 0, 163 | 0, 0, 4, 3, 7, 9, 8, 0, 0, 164 | 5, 7, 0, 0, 1, 0, 3, 2, 0, 165 | 0, 5, 2, 0, 0, 7, 0, 0, 0, 166 | 0, 6, 0, 5, 9, 8, 0, 4, 0, 167 | 0, 0, 0, 4, 0, 0, 5, 7, 0, 168 | 0, 2, 1, 0, 4, 0, 0, 9, 8, 169 | 0, 0, 9, 6, 2, 3, 1, 0, 0, 170 | 0, 0, 0, 9, 0, 0, 7, 0, 0, 171 | ) 172 | assert sudoku_solver(example_board) == ( 173 | 9, 3, 8, 2, 5, 6, 4, 1, 7, 174 | 2, 1, 4, 3, 7, 9, 8, 6, 5, 175 | 5, 7, 6, 8, 1, 4, 3, 2, 9, 176 | 4, 5, 2, 1, 3, 7, 9, 8, 6, 177 | 1, 6, 7, 5, 9, 8, 2, 4, 3, 178 | 8, 9, 3, 4, 6, 2, 5, 7, 1, 179 | 3, 2, 1, 7, 4, 5, 6, 9, 8, 180 | 7, 8, 9, 6, 2, 3, 1, 5, 4, 181 | 6, 4, 5, 9, 8, 1, 7, 3, 2 182 | ) 183 | -------------------------------------------------------------------------------- /tests/test_term.py: -------------------------------------------------------------------------------- 1 | from cons import cons 2 | from etuples import etuple 3 | from unification import reify, unify, var 4 | 5 | from kanren.core import run 6 | from kanren.term import applyo, arguments, operator, term, unifiable_with_term 7 | 8 | 9 | @unifiable_with_term 10 | class Node(object): 11 | def __init__(self, op, args): 12 | self.op = op 13 | self.args = args 14 | 15 | def __eq__(self, other): 16 | return ( 17 | type(self) == type(other) 18 | and self.op == other.op 19 | and self.args == other.args 20 | ) 21 | 22 | def __hash__(self): 23 | return hash((type(self), self.op, self.args)) 24 | 25 | def __str__(self): 26 | return "%s(%s)" % (self.op.name, ", ".join(map(str, self.args))) 27 | 28 | __repr__ = __str__ 29 | 30 | 31 | class Operator(object): 32 | def __init__(self, name): 33 | self.name = name 34 | 35 | 36 | Add = Operator("add") 37 | Mul = Operator("mul") 38 | 39 | 40 | def add(*args): 41 | return Node(Add, args) 42 | 43 | 44 | def mul(*args): 45 | return Node(Mul, args) 46 | 47 | 48 | class Op(object): 49 | def __init__(self, name): 50 | self.name = name 51 | 52 | 53 | @arguments.register(Node) 54 | def arguments_Node(t): 55 | return t.args 56 | 57 | 58 | @operator.register(Node) 59 | def operator_Node(t): 60 | return t.op 61 | 62 | 63 | @term.register(Operator, (list, tuple)) 64 | def term_Op(op, args): 65 | return Node(op, args) 66 | 67 | 68 | def test_applyo(): 69 | x = var() 70 | assert run(0, x, applyo("add", (1, 2, 3), x)) == (("add", 1, 2, 3),) 71 | assert run(0, x, applyo(x, (1, 2, 3), ("add", 1, 2, 3))) == ("add",) 72 | assert run(0, x, applyo("add", x, ("add", 1, 2, 3))) == ((1, 2, 3),) 73 | 74 | a_lv, b_lv, c_lv = var(), var(), var() 75 | 76 | from operator import add 77 | 78 | assert run(0, c_lv, applyo(add, (1, 2), c_lv)) == (3,) 79 | assert run(0, c_lv, applyo(add, etuple(1, 2), c_lv)) == (3,) 80 | assert run(0, c_lv, applyo(add, a_lv, c_lv)) == (cons(add, a_lv),) 81 | 82 | for obj in ( 83 | (1, 2, 3), 84 | (add, 1, 2), 85 | [1, 2, 3], 86 | [add, 1, 2], 87 | etuple(1, 2, 3), 88 | etuple(add, 1, 2), 89 | ): 90 | o_rator, o_rands = operator(obj), arguments(obj) 91 | assert run(0, a_lv, applyo(o_rator, o_rands, a_lv)) == (term(o_rator, o_rands),) 92 | # Just acts like `conso` here 93 | assert run(0, a_lv, applyo(o_rator, a_lv, obj)) == (arguments(obj),) 94 | assert run(0, a_lv, applyo(a_lv, o_rands, obj)) == (operator(obj),) 95 | 96 | # Just acts like `conso` here, too 97 | assert run(0, c_lv, applyo(a_lv, b_lv, c_lv)) == (cons(a_lv, b_lv),) 98 | 99 | # with pytest.raises(ConsError): 100 | assert run(0, a_lv, applyo(a_lv, b_lv, object())) == () 101 | assert run(0, a_lv, applyo(1, 2, a_lv)) == () 102 | 103 | 104 | def test_applyo_object(): 105 | x = var() 106 | assert run(0, x, applyo(Add, (1, 2, 3), x)) == (add(1, 2, 3),) 107 | assert run(0, x, applyo(x, (1, 2, 3), add(1, 2, 3))) == (Add,) 108 | assert run(0, x, applyo(Add, x, add(1, 2, 3))) == ((1, 2, 3),) 109 | 110 | 111 | def test_unifiable_with_term(): 112 | add = Operator("add") 113 | t = Node(add, (1, 2)) 114 | 115 | assert arguments(t) == (1, 2) 116 | assert operator(t) == add 117 | assert term(operator(t), arguments(t)) == t 118 | 119 | x = var() 120 | s = unify(Node(add, (1, x)), Node(add, (1, 2)), {}) 121 | 122 | assert s == {x: 2} 123 | assert reify(Node(add, (1, x)), s) == Node(add, (1, 2)) 124 | -------------------------------------------------------------------------------- /tests/test_util.py: -------------------------------------------------------------------------------- 1 | from pytest import raises 2 | 3 | from kanren.util import ( 4 | FlexibleSet, 5 | dicthash, 6 | groupsizes, 7 | hashable, 8 | intersection, 9 | multihash, 10 | unique, 11 | ) 12 | 13 | 14 | def test_hashable(): 15 | assert hashable(2) 16 | assert hashable((2, 3)) 17 | assert not hashable({1: 2}) 18 | assert not hashable((1, {2: 3})) 19 | 20 | 21 | def test_unique(): 22 | assert tuple(unique((1, 2, 3))) == (1, 2, 3) 23 | assert tuple(unique((1, 2, 1, 3))) == (1, 2, 3) 24 | 25 | 26 | def test_unique_dict(): 27 | assert tuple(unique(({1: 2}, {2: 3}), key=dicthash)) == ({1: 2}, {2: 3}) 28 | assert tuple(unique(({1: 2}, {1: 2}), key=dicthash)) == ({1: 2},) 29 | 30 | 31 | def test_unique_not_hashable(): 32 | assert tuple(unique(([1], [1]))) 33 | 34 | 35 | def test_multihash(): 36 | inputs = 2, (1, 2), [1, 2], {1: 2}, (1, [2]), slice(1, 2) 37 | assert all(isinstance(multihash(i), int) for i in inputs) 38 | 39 | 40 | def test_intersection(): 41 | a, b, c = (1, 2, 3, 4), (2, 3, 4, 5), (3, 4, 5, 6) 42 | 43 | assert tuple(intersection(a, b, c)) == (3, 4) 44 | 45 | 46 | def test_groupsizes(): 47 | assert set(groupsizes(4, 2)) == set(((1, 3), (2, 2), (3, 1))) 48 | assert set(groupsizes(5, 2)) == set(((1, 4), (2, 3), (3, 2), (4, 1))) 49 | assert set(groupsizes(4, 1)) == set([(4,)]) 50 | assert set(groupsizes(4, 4)) == set([(1, 1, 1, 1)]) 51 | 52 | 53 | def test_flexibleset(): 54 | 55 | test_set = set([1, 2, 4]) 56 | test_fs = FlexibleSet([1, 2, 4]) 57 | 58 | assert test_fs.set == test_set 59 | assert test_fs.list == [] 60 | 61 | test_fs.discard(3) 62 | test_set.discard(3) 63 | 64 | assert test_fs == test_set 65 | 66 | test_fs.discard(2) 67 | test_set.discard(2) 68 | 69 | with raises(KeyError): 70 | test_set.remove(3) 71 | with raises(KeyError): 72 | test_fs.remove(3) 73 | 74 | res_fs = test_fs.pop() 75 | res_set = test_set.pop() 76 | 77 | assert res_fs == res_set and test_fs == test_set 78 | 79 | test_fs_2 = FlexibleSet([1, 2, [3, 4], {"a"}]) 80 | assert len(test_fs_2) == 4 81 | assert test_fs_2.set == {1, 2} 82 | assert test_fs_2.list == [[3, 4], {"a"}] 83 | 84 | test_fs_2.add(2) 85 | test_fs_2.add([3, 4]) 86 | test_fs_2.add({"a"}) 87 | assert test_fs_2.set == {1, 2} 88 | assert test_fs_2.list == [[3, 4], {"a"}] 89 | 90 | assert 1 in test_fs_2 91 | assert {"a"} in test_fs_2 92 | assert [3, 4] in test_fs_2 93 | 94 | assert test_fs_2 != test_set 95 | 96 | test_fs_2.discard(3) 97 | test_fs_2.discard([3, 4]) 98 | 99 | assert test_fs_2.set == {1, 2} 100 | assert test_fs_2.list == [{"a"}] 101 | 102 | with raises(KeyError): 103 | test_fs_2.remove(3) 104 | with raises(KeyError): 105 | test_fs_2.remove([1, 4]) 106 | 107 | test_fs_2.remove({"a"}) 108 | 109 | assert test_fs_2.set == {1, 2} 110 | assert test_fs_2.list == [] 111 | 112 | test_fs_2.add([5]) 113 | pop_var = test_fs_2.pop() 114 | assert pop_var not in test_fs_2.set 115 | assert test_fs_2.list == [[5]] 116 | pop_var = test_fs_2.pop() 117 | assert test_fs_2.set == set() 118 | assert test_fs_2.list == [[5]] 119 | assert [5] == test_fs_2.pop() 120 | assert test_fs_2.set == set() 121 | assert test_fs_2.list == [] 122 | 123 | with raises(KeyError): 124 | test_fs_2.pop() 125 | 126 | assert FlexibleSet([1, 2, [3, 4], {"a"}]) == FlexibleSet([1, 2, [3, 4], {"a"}]) 127 | assert FlexibleSet([1, 2, [3, 4], {"a"}]) != FlexibleSet([1, [3, 4], {"a"}]) 128 | 129 | test_fs_3 = FlexibleSet([1, 2, [3, 4], {"a"}]) 130 | test_fs_3.clear() 131 | assert test_fs_3.set == set() 132 | assert test_fs_3.list == list() 133 | 134 | test_fs_3 = FlexibleSet([1, 2, [3, 4], {"a"}]) 135 | assert repr(test_fs_3) == "FlexibleSet([1, 2, [3, 4], {'a'}])" 136 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | [tox] 2 | install_command = pip install {opts} {packages} 3 | envlist = py35,pypy35,lint 4 | indexserver = 5 | default = https://pypi.python.org/simple 6 | 7 | [testenv] 8 | usedevelop = True 9 | commands = 10 | rm -f .coverage 11 | py.test --cov=kanren -vv {posargs:kanren} 12 | deps = 13 | -r{toxinidir}/requirements.txt 14 | coverage 15 | nose 16 | pytest 17 | pytest-cov 18 | whitelist_externals = 19 | rm 20 | 21 | [testenv:lint] 22 | deps = 23 | flake8 24 | commands = 25 | flake8 kanren 26 | basepython = python3.5 27 | 28 | [testenv:yapf] 29 | # Tox target for autoformatting the code for pep8. 30 | deps = 31 | yapf 32 | commands = 33 | yapf --recursive kanren --in-place 34 | basepython = python3.5 35 | 36 | [flake8] 37 | ignore = E731,F811,E712,E127,E126,C901,W503,W504 38 | --------------------------------------------------------------------------------