Skip to content

Commit 2714e32

Browse files
committed
improve BetaNormalisingVisitor and Substitutions
This commit modifies BetaNormalisingVisitor to yield alpha conversions, which required refactoring the Visitors used for substitution
1 parent 6e46997 commit 2714e32

File tree

16 files changed

+770
-351
lines changed

16 files changed

+770
-351
lines changed

README.md

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,18 @@ and nest them to create more complex lambda terms.
1111
You can also use the `visitors` subpackage to define your own operations on terms or
1212
use predefined ones from the `terms` subpackage.
1313

14+
## Notice
15+
16+
This package is intended to be used for educational purposes and is not optimized for speed.
17+
18+
Furthermore, it expects all terms to be finite, which means the absense of cycles.
19+
20+
This results in the Visitor for term normalisation included in this package (`BetaNormalisingVisitor`)
21+
having problems when handling terms which are passed a reference to themselves during evaluation,
22+
which is the case for all recursive functions.
23+
24+
`RecursionError` may be raised if the visitors get passed an infinite term.
25+
1426
## Requirements
1527

1628
Python >= 3.10 is required to use this package.

lambda_calculus/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44

55
from .terms import Variable, Abstraction, Application
66

7-
__version__ = "1.11.0"
7+
__version__ = "2.0.0"
88
__author__ = "Eric Niklas Wolf"
99
__email__ = "[email protected]"
1010
__all__ = (

lambda_calculus/terms/__init__.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,8 @@
99
from typing import TypeVar
1010
from .. import visitors
1111
from ..errors import CollisionError
12-
from ..visitors import substitution, walking
12+
from ..visitors import walking
13+
from ..visitors.substitution import checked
1314

1415
__all__ = (
1516
"Term",
@@ -63,7 +64,7 @@ def apply_to(self, *arguments: Term[V]) -> Application[V]:
6364

6465
def substitute(self, variable: V, value: Term[V]) -> Term[V]:
6566
"""substitute a free variable with a Term, possibly raising a CollisionError"""
66-
return self.accept(substitution.SubstitutingVisitor(variable, value))
67+
return self.accept(checked.CheckedSubstitution.from_substitution(variable, value))
6768

6869
def is_combinator(self) -> bool:
6970
"""return if this Term has no free variables"""

lambda_calculus/visitors/__init__.py

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,13 @@
44

55
from __future__ import annotations
66
from abc import ABC, abstractmethod
7-
from typing import TypeVar, Generic
7+
from typing import TypeVar, Generic, final
88
from .. import terms
99

1010
__all__ = (
1111
"Visitor",
1212
"BottomUpVisitor",
13+
"DeferrableVisitor",
1314
"substitution",
1415
"normalisation",
1516
"walking"
@@ -27,14 +28,15 @@ class Visitor(ABC, Generic[T, V]):
2728

2829
__slots__ = ()
2930

31+
@final
3032
def visit(self, term: terms.Term[V]) -> T:
3133
"""visit a term"""
3234
return term.accept(self)
3335

3436
@abstractmethod
3537
def visit_variable(self, variable: terms.Variable[V]) -> T:
3638
"""visit a Variable term"""
37-
raise NotADirectoryError()
39+
raise NotImplementedError()
3840

3941
@abstractmethod
4042
def visit_abstraction(self, abstraction: terms.Abstraction[V]) -> T:
@@ -52,13 +54,15 @@ class BottomUpVisitor(Visitor[T, V]):
5254

5355
__slots__ = ()
5456

57+
@final
5558
def visit_abstraction(self, abstraction: terms.Abstraction[V]) -> T:
5659
"""visit an Abstraction term"""
5760
return self.ascend_abstraction(
5861
abstraction,
5962
abstraction.body.accept(self)
6063
)
6164

65+
@final
6266
def visit_application(self, application: terms.Application[V]) -> T:
6367
"""visit an Application term"""
6468
return self.ascend_application(
@@ -76,3 +80,19 @@ def ascend_abstraction(self, abstraction: terms.Abstraction[V], body: T) -> T:
7680
def ascend_application(self, application: terms.Application[V], abstraction: T, argument: T) -> T:
7781
"""visit an Application term after visiting its abstraction and argument"""
7882
raise NotImplementedError()
83+
84+
85+
class DeferrableVisitor(Visitor[T, V]):
86+
"""ABC for visitors which can visit terms top down lazyly"""
87+
88+
__slots__ = ()
89+
90+
@abstractmethod
91+
def defer_abstraction(self, abstraction: terms.Abstraction[V]) -> tuple[T, DeferrableVisitor[T, V] | None]:
92+
"""visit an Abstraction term and return the visitor used to visit its body"""
93+
raise NotImplementedError()
94+
95+
@abstractmethod
96+
def defer_application(self, application: terms.Application[V]) -> tuple[T, DeferrableVisitor[T, V] | None, DeferrableVisitor[T, V] | None]:
97+
"""visit an Application term and return the visitors used to visit its abstraction and argument"""
98+
raise NotImplementedError()

lambda_calculus/visitors/normalisation.py

Lines changed: 52 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -4,65 +4,84 @@
44

55
from __future__ import annotations
66
from collections.abc import Iterator
7-
from typing import TypeVar
7+
from enum import Enum, unique
8+
from typing import TypeVar, final, Generator, TypeAlias
89
from .. import terms
910
from . import Visitor
10-
from .substitution import CountingSubstitutingVisitor
11+
from .substitution.renaming import CountingSubstitution
1112

1213
__all__ = (
14+
"Conversion",
1315
"BetaNormalisingVisitor",
1416
)
1517

1618
V = TypeVar("V")
1719

20+
Step: TypeAlias = tuple["Conversion", terms.Term[str]]
1821

19-
class BetaNormalisingVisitor(Visitor[Iterator[terms.Term[str]], str]):
22+
23+
@unique
24+
class Conversion(Enum):
25+
"""Conversion performed by normalisation"""
26+
ALPHA = 0
27+
BETA = 1
28+
29+
30+
@final
31+
class BetaNormalisingVisitor(Visitor[Iterator[Step], str]):
2032
"""
2133
Visitor which transforms a term into its beta normal form,
22-
yielding intermediate results until it is reached
34+
yielding intermediate steps until it is reached
2335
"""
2436

2537
__slots__ = ()
2638

2739
def skip_intermediate(self, term: terms.Term[str]) -> terms.Term[str]:
2840
"""return the beta normal form directly"""
2941
result = term
30-
for intermediate in term.accept(self):
42+
for _, intermediate in term.accept(self):
3143
result = intermediate
3244
return result
3345

34-
def visit_variable(self, variable: terms.Variable[str]) -> Iterator[terms.Variable[str]]:
46+
def visit_variable(self, variable: terms.Variable[str]) -> Iterator[Step]:
3547
"""visit a Variable term"""
3648
return iter(())
3749

38-
def visit_abstraction(self, abstraction: terms.Abstraction[str]) -> Iterator[terms.Abstraction[str]]:
50+
def visit_abstraction(self, abstraction: terms.Abstraction[str]) -> Iterator[Step]:
3951
"""visit an Abstraction term"""
4052
results = abstraction.body.accept(self)
41-
return map(lambda b: terms.Abstraction(abstraction.bound, b), results)
53+
return map(lambda s: (s[0], terms.Abstraction(abstraction.bound, s[1])), results)
54+
55+
def beta_reducation(self, abstraction: terms.Abstraction[str], argument: terms.Term[str]) -> Generator[Step, None, terms.Term[str]]:
56+
"""perform beta reduction of an application"""
57+
conversions = CountingSubstitution.from_substitution(abstraction.bound, argument).trace()
58+
reduced = yield from map(
59+
lambda body: (
60+
Conversion.ALPHA,
61+
terms.Application(terms.Abstraction(abstraction.bound, body), argument)
62+
),
63+
abstraction.body.accept(conversions) # type: ignore
64+
)
65+
yield (Conversion.BETA, reduced)
66+
return reduced # type: ignore
4267

43-
def visit_application(self, application: terms.Application[str]) -> Iterator[terms.Term[str]]:
68+
def visit_application(self, application: terms.Application[str]) -> Iterator[Step]:
4469
"""visit an Application term"""
45-
match application.abstraction:
46-
# normal order dictates we reduce leftmost outermost redex first
47-
case terms.Abstraction(bound, body):
48-
reduced = body.accept(CountingSubstitutingVisitor(bound, application.argument))
49-
yield reduced
50-
yield from reduced.accept(self)
51-
case _:
52-
# try to reduce the abstraction until this is a redex
53-
abstraction = application.abstraction
54-
for transformation in application.abstraction.accept(self):
55-
yield terms.Application(transformation, application.argument)
56-
match transformation:
57-
case terms.Abstraction(bound, body):
58-
reduced = body.accept(
59-
CountingSubstitutingVisitor(bound, application.argument)
60-
)
61-
yield reduced
62-
yield from reduced.accept(self)
63-
return
64-
case _:
65-
abstraction = transformation
66-
# no redex, continue with argument
67-
transformations = application.argument.accept(self)
68-
yield from map(lambda a: terms.Application(abstraction, a), transformations)
70+
if isinstance(application.abstraction, terms.Abstraction):
71+
# normal order dictates we reduce the leftmost outermost redex first
72+
reduced = yield from self.beta_reducation(application.abstraction, application.argument)
73+
yield from reduced.accept(self)
74+
else:
75+
# try to reduce the abstraction until this is a redex
76+
abstraction = application.abstraction
77+
for conversion, transformation in application.abstraction.accept(self):
78+
yield (conversion, terms.Application(transformation, application.argument))
79+
if isinstance(transformation, terms.Abstraction):
80+
reduced = yield from self.beta_reducation(transformation, application.argument)
81+
yield from reduced.accept(self)
82+
return
83+
else:
84+
abstraction = transformation
85+
# no redex, continue with argument
86+
transformations = application.argument.accept(self)
87+
yield from map(lambda s: (s[0], terms.Application(abstraction, s[1])), transformations)

lambda_calculus/visitors/substitution.py

Lines changed: 0 additions & 166 deletions
This file was deleted.

0 commit comments

Comments
 (0)