Skip to content

Commit

Permalink
Merge pull request #16 from brandonwillard/tail-call-optimization
Browse files Browse the repository at this point in the history
Introduce stream/trampoline-based reification and unification
  • Loading branch information
brandonwillard committed Feb 20, 2020
2 parents 95d2c29 + bf0ee7c commit 4d49b21
Show file tree
Hide file tree
Showing 15 changed files with 532 additions and 169 deletions.
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -198,3 +198,5 @@ tags
[._]*.un~

# End of https://www.gitignore.io/api/vim,emacs,python

.benchmarks/
3 changes: 2 additions & 1 deletion .travis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,8 @@ script:
- if [[ `command -v black` ]]; then
black --check unification tests;
fi
- pytest -v tests/ --cov=unification/
- pytest -v tests/ --benchmark-skip --cov=unification/
- pytest -v tests/ --benchmark-only --benchmark-autosave --benchmark-group-by=group,param:size --benchmark-max-time=3

after_success:
- coveralls
7 changes: 5 additions & 2 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -58,11 +58,14 @@ black: # Format code in-place using black.
black unification/ tests/

test: # Test code using pytest.
pytest -v tests/ --cov=unification/ --cov-report=xml --html=testing-report.html --self-contained-html
pytest -v tests/ --benchmark-skip --cov=unification/ --cov-report=xml --html=testing-report.html --self-contained-html

benchmark:
pytest -v tests/ --benchmark-only --benchmark-autosave --benchmark-group-by=group,param:size --benchmark-max-time=3

coverage: test
diff-cover coverage.xml --compare-branch=master --fail-under=100

lint: docstyle format style # Lint code using pydocstyle, black and pylint.

check: lint test coverage # Both lint and test code. Runs `make lint` followed by `make test`.
check: lint test coverage benchmark # Both lint and test code. Runs `make lint` followed by `make test`.
4 changes: 1 addition & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -120,9 +120,7 @@ See the full example in the [examples directory](https://github.com/pythological

## Performance and Reliability

Unification stresses extensibility over performance, preliminary benchmarks show that this is 2-5x slower than straight tuple-based unification.

`unification`'s approach is reliable; although one caveat is set unification, which is challenging to do in general. It should work well in moderately complex cases, but it may break down under very complex ones.
`unification`'s current design allows for unification and reification of nested structures that break the Python stack recursion limit. This scalability incurs an overhead cost compared to simple stack-based recursive unification/reificiation.

## About

Expand Down
25 changes: 14 additions & 11 deletions examples/account.py
Original file line number Diff line number Diff line change
@@ -1,31 +1,34 @@
from functools import partial
from collections import defaultdict
from unification import *
from unification.match import *

from unification import var
from unification.match import match, VarDispatcher


match = partial(match, Dispatcher=VarDispatcher)

balance = defaultdict(lambda: 0)

name, amount = var('name'), var('amount')
name, amount = var("name"), var("amount")


@match({'status': 200, 'data': {'name': name, 'credit': amount}})
@match({"status": 200, "data": {"name": name, "credit": amount}})
def respond(name, amount):
balance[name] +=amount
balance[name] += amount


@match({'status': 200, 'data': {'name': name, 'debit': amount}})
@match({"status": 200, "data": {"name": name, "debit": amount}})
def respond(name, amount):
balance[name] -= amount


@match({'status': 404})
@match({"status": 404})
def respond():
print("Bad Request")


if __name__ == '__main__':
respond({'status': 200, 'data': {'name': 'Alice', 'credit': 100}})
respond({'status': 200, 'data': {'name': 'Bob', 'debit': 100}})
respond({'status': 404})
if __name__ == "__main__":
respond({"status": 200, "data": {"name": "Alice", "credit": 100}})
respond({"status": 200, "data": {"name": "Bob", "debit": 100}})
respond({"status": 404})
print(dict(balance))
1 change: 1 addition & 0 deletions requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ pydocstyle>=3.0.0
pytest>=5.0.0
pytest-cov>=2.6.1
pytest-html>=1.20.0
pytest-benchmark
pylint>=2.3.1
black>=19.3b0; platform.python_implementation!='PyPy'
diff-cover
Expand Down
91 changes: 91 additions & 0 deletions tests/test_benchmarks.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
import pytest

from toolz import assoc

from unification import unify, reify, var, isvar
from unification.utils import transitive_get as walk

from tests.utils import gen_long_chain


nesting_sizes = [10, 35, 300]


def unify_stack(u, v, s):

u = walk(u, s)
v = walk(v, s)

if u == v:
return s
if isvar(u):
return assoc(s, u, v)
if isvar(v):
return assoc(s, v, u)

if isinstance(u, (tuple, list)) and type(u) == type(v):
for i_u, i_v in zip(u, v):
s = unify_stack(i_u, i_v, s)
if s is False:
return s

return s

return False


def reify_stack(u, s):

u_ = walk(u, s)

if u_ is not u:
return reify_stack(u_, s)

if isinstance(u_, (tuple, list)):
return type(u_)(reify_stack(i_u, s) for i_u in u_)

return u_


@pytest.mark.benchmark(group="unify_chain")
@pytest.mark.parametrize("size", nesting_sizes)
def test_unify_chain_stream(size, benchmark):
a_lv = var()
form = gen_long_chain(a_lv, size)
term = gen_long_chain("a", size)

res = benchmark(unify, form, term, {})
assert res[a_lv] == "a"


@pytest.mark.benchmark(group="unify_chain")
@pytest.mark.parametrize("size", nesting_sizes)
def test_unify_chain_stack(size, benchmark):
a_lv = var()
form = gen_long_chain(a_lv, size)
term = gen_long_chain("a", size)

res = benchmark(unify_stack, form, term, {})
assert res[a_lv] == "a"


@pytest.mark.benchmark(group="reify_chain")
@pytest.mark.parametrize("size", nesting_sizes)
def test_reify_chain_stream(size, benchmark):
a_lv = var()
form = gen_long_chain(a_lv, size)
term = gen_long_chain("a", size)

res = benchmark(reify, form, {a_lv: "a"})
assert res == term


@pytest.mark.benchmark(group="reify_chain")
@pytest.mark.parametrize("size", nesting_sizes)
def test_reify_chain_stack(size, benchmark):
a_lv = var()
form = gen_long_chain(a_lv, size)
term = gen_long_chain("a", size)

res = benchmark(reify_stack, form, {a_lv: "a"})
assert res == term
Loading

0 comments on commit 4d49b21

Please sign in to comment.