curryparty 0.1.1__tar.gz → 0.2.2__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: curryparty
3
- Version: 0.1.1
3
+ Version: 0.2.2
4
4
  Summary: Python playground to learn lambda calculus
5
5
  Author: Antonin P
6
6
  Author-email: Antonin P <antonin.peronnet@telecom-paris.fr>
@@ -62,6 +62,12 @@ If you use a notebook such as jupyternotebook or marimo, you will see something
62
62
 
63
63
  You can also use `term.show_reduction` to get an animated version.
64
64
 
65
+ # Tutorial
66
+
67
+ `lambda.py` shows a tutorial in marimo format. Click on the button to try it out:
68
+
69
+ [![Open with marimo](https://marimo.io/shield.svg)](https://marimo.app/github.com/rambip/curryparty/blob/main/lambda.py)
70
+
65
71
  # How it works
66
72
 
67
73
  Under the wood, all the terms are converted into a list of nodes, that can either be a `lambda`, an `application` (with 2 arguments) or a `variable` (with 0 arguments).
@@ -52,6 +52,12 @@ If you use a notebook such as jupyternotebook or marimo, you will see something
52
52
 
53
53
  You can also use `term.show_reduction` to get an animated version.
54
54
 
55
+ # Tutorial
56
+
57
+ `lambda.py` shows a tutorial in marimo format. Click on the button to try it out:
58
+
59
+ [![Open with marimo](https://marimo.io/shield.svg)](https://marimo.app/github.com/rambip/curryparty/blob/main/lambda.py)
60
+
55
61
  # How it works
56
62
 
57
63
  Under the wood, all the terms are converted into a list of nodes, that can either be a `lambda`, an `application` (with 2 arguments) or a `variable` (with 0 arguments).
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "curryparty"
3
- version = "0.1.1"
3
+ version = "0.2.2"
4
4
  description = "Python playground to learn lambda calculus"
5
5
  readme = "README.md"
6
6
  authors = [
@@ -6,10 +6,12 @@ except ImportError:
6
6
  raise ImportError(
7
7
  "curryparty needs the `polars` library. \n Please install it, typically with `pip install polars`"
8
8
  )
9
- from svg import SVG
9
+ from svg import SVG, Rect
10
+ import uuid
10
11
 
11
12
  from .core import SCHEMA, beta_reduce, compose, find_redexes, find_variables, subtree
12
13
  from .display import (
14
+ compute_height,
13
15
  compute_svg_frame_final,
14
16
  compute_svg_frame_init,
15
17
  compute_svg_frame_phase_a,
@@ -41,6 +43,12 @@ class Term:
41
43
  reduced = beta_reduce(self.nodes, lamb, b)
42
44
  return Term(reduced)
43
45
 
46
+ def reduce(self):
47
+ last_non_reduced = self
48
+ for term in self.reduction_chain():
49
+ last_non_reduced = term
50
+ return last_non_reduced
51
+
44
52
  def reduction_chain(self) -> Iterable["Term"]:
45
53
  term = self
46
54
  while True:
@@ -49,23 +57,29 @@ class Term:
49
57
  if term is None:
50
58
  break
51
59
 
52
- def show_reduction(self):
60
+ def show_beta(self, x0=-10, width=30):
61
+ """
62
+ Generates an HTML representation that toggles visibility between
63
+ a static state and a SMIL animation on hover using pure CSS.
64
+ """
53
65
  candidates = find_redexes(self.nodes)
54
66
  if len(candidates) == 0:
55
- return None
67
+ return self._repr_html_()
68
+
56
69
  _redex, lamb, b = candidates.row(0)
57
70
  new_nodes = beta_reduce(self.nodes, lamb, b)
58
71
  vars = find_variables(self.nodes, lamb)["id"]
59
72
  b_subtree = subtree(self.nodes, b)
73
+ height = compute_height(self.nodes) + 3
60
74
  shapes: dict[int, ShapeAnim] = {}
61
- N_STEPS = 8
75
+ N_STEPS = 6
62
76
 
63
77
  for t in range(N_STEPS):
64
- if t < 2:
78
+ if t == 0:
65
79
  items = compute_svg_frame_init(self.nodes)
66
- elif t == 2 or t == 3:
80
+ elif t == 1 or t == 2:
67
81
  items = compute_svg_frame_phase_a(self.nodes, lamb, b_subtree, vars)
68
- elif t == 4 or t == 5:
82
+ elif t == 3 or t == 4:
69
83
  items = compute_svg_frame_phase_b(
70
84
  self.nodes, lamb, b_subtree, new_nodes
71
85
  )
@@ -76,28 +90,47 @@ class Term:
76
90
  shapes[k] = ShapeAnim(e)
77
91
  shapes[k].append_frame(t, attributes.items())
78
92
 
79
- elements = [x.to_element(N_STEPS) for x in shapes.values()]
80
- height = 2 + int(0.6 * len(self.nodes))
93
+ figure_id = uuid.uuid4()
94
+ box_id = f"lambda_box_{figure_id}".replace("-", "")
95
+ anim_elements = [
96
+ x.to_element(N_STEPS, begin=f"{box_id}.click", reset=f"{box_id}.mouseover")
97
+ for x in shapes.values()
98
+ ]
99
+
100
+ anim_elements.append(
101
+ Rect(
102
+ id=box_id,
103
+ x=str(x0),
104
+ y="0",
105
+ width="100%",
106
+ height="100%",
107
+ fill="transparent",
108
+ )
109
+ )
110
+
111
+ anim_svg = SVG(
112
+ xmlns="http://www.w3.org/2000/svg",
113
+ viewBox=f"{x0} 0 {width} {height}",
114
+ height=f"{35 * height}px",
115
+ elements=anim_elements,
116
+ ).as_str()
117
+
81
118
  return Html(
82
- SVG(
83
- xmlns="http://www.w3.org/2000/svg",
84
- viewBox=f"-10 0 30 {height}", # type: ignore
85
- height=f"{35 * height}px", # type: ignore
86
- elements=elements,
87
- ).as_str()
119
+ f"<div><div style=\"margin:5px\">click to animate, move away and back to reset</div>{anim_svg}</div>"
88
120
  )
89
121
 
90
- def _repr_html_(self):
122
+ def _repr_html_(self, x0=-10, width=30):
91
123
  frame = compute_svg_frame_init(self.nodes)
92
124
  elements = []
93
- height = 2 + int(0.6 * len(self.nodes))
125
+
126
+ height = compute_height(self.nodes) + 1
94
127
  for _, e, attributes in frame:
95
128
  for name, v in attributes.items():
96
129
  e.__setattr__(name, v)
97
130
  elements.append(e)
98
131
  return SVG(
99
132
  xmlns="http://www.w3.org/2000/svg",
100
- viewBox=f"-10 0 30 {height}", # type: ignore
133
+ viewBox=f"{x0} 0 {width} {height}", # type: ignore
101
134
  height=f"{35 * height}px", # type: ignore
102
135
  elements=elements,
103
136
  ).as_str()
@@ -38,6 +38,10 @@ def compute_layout(
38
38
  y[node] = y[child] | y[node]
39
39
  return x, y
40
40
 
41
+ def compute_height(nodes: pl.DataFrame):
42
+ _, y = compute_layout(nodes)
43
+ return max(interval[1] for interval in y.values() if interval)
44
+
41
45
 
42
46
  def draw(
43
47
  x: dict[Union[int, tuple[int, int]], Interval],
@@ -56,7 +56,7 @@ class ShapeAnim:
56
56
  self.attributes.add(name)
57
57
  self.values[i, name] = v
58
58
 
59
- def to_element(self, n: int):
59
+ def to_element(self, n: int, begin: str, reset: str):
60
60
  elements = []
61
61
 
62
62
  visible = [
@@ -74,12 +74,24 @@ class ShapeAnim:
74
74
  for i in range(n):
75
75
  if (i, name) not in self.values:
76
76
  self.values[i, name] = non_nulls[0]
77
+ self.shape.__setattr__(name, non_nulls[0])
78
+
77
79
  elements.append(
78
80
  Animate(
79
81
  attributeName=name,
80
82
  values=";".join(str(self.values[i, name]) for i in range(n)),
81
83
  dur=timedelta(seconds=self.duration),
82
- repeatCount="indefinite",
84
+ begin=begin,
85
+ fill="freeze",
86
+ repeatCount="1",
87
+ )
88
+ )
89
+ elements.append(
90
+ Animate(
91
+ attributeName=name,
92
+ values=f"{non_nulls[0]}",
93
+ dur=0,
94
+ begin=reset,
83
95
  )
84
96
  )
85
97
  elements.append(
@@ -87,10 +99,21 @@ class ShapeAnim:
87
99
  attributeName="opacity",
88
100
  values=";".join("1" if v else "0" for v in visible),
89
101
  dur=timedelta(seconds=self.duration),
90
- repeatCount="indefinite",
102
+ begin=begin,
103
+ fill="freeze",
104
+ repeatCount="1",
105
+ )
106
+ )
107
+ elements.append(
108
+ Animate(
109
+ attributeName="opacity",
110
+ values="1" if visible[0] else "0",
111
+ dur=0,
112
+ begin=reset,
91
113
  )
92
114
  )
93
115
 
94
116
  assert not self.shape.elements
117
+ self.shape
95
118
  self.shape.elements = elements
96
119
  return self.shape