Skip to content

Commit 1cf04cc

Browse files
authored
Parser combinators (#80)
* [WIP] Parser combinators * test: add parser tests * chore: update pyright * feat: make parser an object * refactor: simplify parser * fix: more parsing and union fixes * Fix tag names for single case unions * fix: test * refactor: call Returns result not ParseResult - Add run that returns ParseResult * cleanup * feat: optimize pchar - add parser str and repr * chore: refactor code a bit
1 parent 69d4a5a commit 1cf04cc

File tree

15 files changed

+1049
-79
lines changed

15 files changed

+1049
-79
lines changed

.pre-commit-config.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ repos:
2828
language: node
2929
pass_filenames: false
3030
types: [python]
31-
additional_dependencies: ["[email protected].243"]
31+
additional_dependencies: ["[email protected].246"]
3232
repo: local
3333
- hooks:
3434
- id: jb-to-sphinx

README.md

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -114,6 +114,8 @@ on-demand as we go along.
114114
by [aioreactive](https://github.com/dbrattli/aioreactive).
115115
- **Data Modelling** - sum and product types
116116
- **TaggedUnion** - A tagged (discriminated) union type.
117+
- **Parser Combinators** - A recursive decent string parser combinator
118+
library.
117119
- **Effects**: - lightweight computational expressions for Python. This
118120
is amazing stuff.
119121
- **option** - an optional world for working with optional values.
@@ -518,7 +520,7 @@ tagged union cases.
518520

519521
```python
520522
from dataclasses import dataclass
521-
from expression import TaggedUnion, Tag
523+
from expression import TaggedUnion, tag
522524

523525
@dataclass
524526
class Rectangle:
@@ -530,8 +532,8 @@ class Circle:
530532
radius: float
531533

532534
class Shape(TaggedUnion):
533-
RECTANGLE = Tag[Rectangle]()
534-
CIRCLE = Tag[Circle]()
535+
RECTANGLE = tag(Rectangle)
536+
CIRCLE = tag(Circle)
535537

536538
@staticmethod
537539
def rectangle(width: float, length: float) -> Shape:

expression/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,7 @@
5656
pipe3,
5757
result,
5858
snd,
59+
tag,
5960
tailrec,
6061
tailrec_async,
6162
try_downcast,
@@ -113,6 +114,7 @@
113114
"tailrec",
114115
"tailrec_async",
115116
"Try",
117+
"tag",
116118
"try_downcast",
117119
"upcast",
118120
"__version__",

expression/collections/block.py

Lines changed: 52 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -412,6 +412,26 @@ def range(start: int, stop: int, step: int) -> Block[int]:
412412
def range(*args: int, **kw: int) -> Block[int]:
413413
return range(*args, **kw)
414414

415+
def reduce(
416+
self,
417+
reduction: Callable[[_TSource, _TSource], _TSource],
418+
) -> _TSource:
419+
"""Apply a function to each element of the collection, threading an
420+
accumulator argument through the computation. Apply the function to
421+
the first two elements of the list. Then feed this result into the
422+
function along with the third element and so on. Return the final
423+
result. If the input function is f and the elements are i0...iN then
424+
computes f (... (f i0 i1) i2 ...) iN.
425+
426+
Args:
427+
reduction: The function to reduce two list elements to a
428+
single element.
429+
430+
Returns:
431+
Returns the final state value.
432+
"""
433+
return reduce(reduction)(self)
434+
415435
@staticmethod
416436
def singleton(item: _TSource) -> Block[_TSource]:
417437
return singleton(item)
@@ -646,9 +666,10 @@ def cons(head: _TSource, tail: Block[_TSource]) -> Block[_TSource]:
646666
"""The empty list."""
647667

648668

669+
@curry_flipped(1)
649670
def filter(
650-
predicate: Callable[[_TSource], bool]
651-
) -> Callable[[Block[_TSource]], Block[_TSource]]:
671+
source: Block[_TSource], predicate: Callable[[_TSource], bool]
672+
) -> Block[_TSource]:
652673
"""Returns a new collection containing only the elements of the
653674
collection for which the given predicate returns `True`
654675
@@ -659,20 +680,7 @@ def filter(
659680
Partially applied filter function.
660681
"""
661682

662-
def _filter(source: Block[_TSource]) -> Block[_TSource]:
663-
"""Returns a new collection containing only the elements of the
664-
collection for which the given predicate returns `True`
665-
666-
Args:
667-
source: The input list.
668-
669-
Returns:
670-
A list containing only the elements that satisfy the
671-
predicate.
672-
"""
673-
return source.filter(predicate)
674-
675-
return _filter
683+
return source.filter(predicate)
676684

677685

678686
def fold(
@@ -788,6 +796,34 @@ def _map(source: Block[_TSource]) -> Block[_TResult]:
788796
return _map
789797

790798

799+
@curry_flipped(1)
800+
def reduce(
801+
source: Block[_TSource],
802+
reduction: Callable[[_TSource, _TSource], _TSource],
803+
) -> _TSource:
804+
"""Apply a function to each element of the collection, threading an
805+
accumulator argument through the computation. Apply the function to
806+
the first two elements of the list. Then feed this result into the
807+
function along with the third element and so on. Return the final
808+
result. If the input function is f and the elements are i0...iN then
809+
computes f (... (f i0 i1) i2 ...) iN.
810+
811+
Args:
812+
source: The input block (curried flipped)
813+
reduction: The function to reduce two list elements to a single
814+
element.
815+
816+
Returns:
817+
Partially applied reduce function that takes the source block
818+
and returns the final state value.
819+
"""
820+
821+
if source.is_empty():
822+
raise ValueError("Collection was empty")
823+
824+
return source.tail().fold(reduction, source.head())
825+
826+
791827
@overload
792828
def starmap(
793829
mapper: Callable[[_T1, _T2], _TResult]

expression/core/__init__.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@
3434
try_downcast,
3535
upcast,
3636
)
37-
from .union import SingleCaseUnion, Tag, TaggedUnion
37+
from .union import SingleCaseUnion, Tag, TaggedUnion, tag
3838

3939
__all__ = [
4040
"aiotools",
@@ -90,6 +90,7 @@
9090
"tailrec",
9191
"tailrec_async",
9292
"Try",
93+
"tag",
9394
"try_downcast",
9495
"upcast",
9596
]

expression/core/match.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -142,17 +142,17 @@ def default(self) -> Iterable[_TSource]:
142142
...
143143

144144
@overload
145-
def default(self, ret: Optional[_TResult]) -> _TResult:
145+
def default(self, default_value: _TResult) -> _TResult:
146146
...
147147

148-
def default(self, ret: Optional[Any] = None) -> Any:
148+
def default(self, default_value: Any = None) -> Any:
149149
"""Handle default case. Always matches."""
150150

151151
if self.is_matched:
152152
return []
153153

154154
self.is_matched = True
155-
return [ret or self.value]
155+
return default_value or [self.value]
156156

157157
def __enter__(self) -> Case[_TSource]:
158158
"""Enter context management."""

expression/core/option.py

Lines changed: 10 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,8 @@
2424
get_origin,
2525
)
2626

27+
from typing_extensions import TypeGuard
28+
2729
from .error import EffectError
2830
from .match import MatchMixin, SupportsMatch
2931
from .pipe import PipeMixin
@@ -128,12 +130,12 @@ def to_seq(self) -> Seq[_TSource]:
128130
raise NotImplementedError
129131

130132
@abstractmethod
131-
def is_some(self) -> bool:
133+
def is_some(self) -> TypeGuard[Some[_TSource]]:
132134
"""Returns true if the option is not Nothing."""
133135
raise NotImplementedError
134136

135137
@abstractmethod
136-
def is_none(self) -> bool:
138+
def is_none(self) -> TypeGuard[Nothing_[_TSource]]:
137139
"""Returns true if the option is Nothing."""
138140
raise NotImplementedError
139141

@@ -189,11 +191,11 @@ def default_value(self, value: _TSource) -> _TSource:
189191
"""
190192
return self._value
191193

192-
def is_some(self) -> bool:
194+
def is_some(self) -> TypeGuard[Some[_TSource]]:
193195
"""Returns `True`."""
194196
return True
195197

196-
def is_none(self) -> bool:
198+
def is_none(self) -> TypeGuard[Nothing_[_TSource]]:
197199
"""Returns `False`."""
198200
return False
199201

@@ -307,11 +309,11 @@ def default_value(self, value: _TSource) -> _TSource:
307309
"""
308310
return value
309311

310-
def is_some(self) -> bool:
312+
def is_some(self) -> TypeGuard[Some[_TSource]]:
311313
"""Returns `False`."""
312314
return False
313315

314-
def is_none(self) -> bool:
316+
def is_none(self) -> TypeGuard[Nothing_[_TSource]]:
315317
"""Returns `True`."""
316318
return True
317319

@@ -447,11 +449,11 @@ def _default_value(option: Option[_TSource]) -> _TSource:
447449
return _default_value
448450

449451

450-
def is_none(option: Option[Any]) -> bool:
452+
def is_none(option: Option[_TSource]) -> TypeGuard[Nothing_[_TSource]]:
451453
return option.is_none()
452454

453455

454-
def is_some(option: Option[Any]) -> bool:
456+
def is_some(option: Option[_TSource]) -> TypeGuard[Some[_TSource]]:
455457
return option.is_some()
456458

457459

expression/core/result.py

Lines changed: 15 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
Callable,
1717
Dict,
1818
Generator,
19+
Generic,
1920
Iterable,
2021
Iterator,
2122
List,
@@ -27,6 +28,8 @@
2728
overload,
2829
)
2930

31+
from typing_extensions import TypeGuard
32+
3033
from .error import EffectError
3134
from .match import Case, SupportsMatch
3235
from .pipe import PipeMixin
@@ -71,8 +74,8 @@ def _validate(result: Any, field: ModelField) -> Result[Any, Any]:
7174
class Result(
7275
Iterable[_TSource],
7376
PipeMixin,
74-
SupportsMatch[Union[_TSource, _TError]],
7577
SupportsValidation["Result[_TSource, _TError]"],
78+
Generic[_TSource, _TError],
7679
ABC,
7780
):
7881
"""The result abstract base class."""
@@ -124,13 +127,13 @@ def match(self, pattern: Any) -> Any:
124127
return case(pattern) if pattern else case
125128

126129
@abstractmethod
127-
def is_error(self) -> bool:
130+
def is_error(self) -> TypeGuard[Error[_TSource, _TError]]:
128131
"""Returns `True` if the result is an `Error` value."""
129132

130133
raise NotImplementedError
131134

132135
@abstractmethod
133-
def is_ok(self) -> bool:
136+
def is_ok(self) -> TypeGuard[Ok[_TSource, _TError]]:
134137
"""Returns `True` if the result is an `Ok` value."""
135138

136139
raise NotImplementedError
@@ -182,12 +185,12 @@ def map_error(
182185
function, or Ok if the input is Ok."""
183186
return Ok(self._value)
184187

185-
def is_error(self) -> bool:
188+
def is_error(self) -> TypeGuard[Error[_TSource, _TError]]:
186189
"""Returns `True` if the result is an `Ok` value."""
187190

188191
return False
189192

190-
def is_ok(self) -> bool:
193+
def is_ok(self) -> TypeGuard[Ok[_TSource, _TError]]:
191194
"""Returns `True` if the result is an `Ok` value."""
192195

193196
return True
@@ -236,7 +239,11 @@ def __init__(self, message: str):
236239
self.message = message
237240

238241

239-
class Error(ResultException, Result[_TSource, _TError]):
242+
class Error(
243+
ResultException,
244+
Result[_TSource, _TError],
245+
SupportsMatch[_TError],
246+
):
240247
"""The Error result case class."""
241248

242249
def __init__(self, error: _TError) -> None:
@@ -262,11 +269,11 @@ def map_error(
262269
function, or Ok if the input is Ok."""
263270
return Error(mapper(self._error))
264271

265-
def is_error(self) -> bool:
272+
def is_error(self) -> TypeGuard[Error[_TSource, _TError]]:
266273
"""Returns `True` if the result is an `Ok` value."""
267274
return True
268275

269-
def is_ok(self) -> bool:
276+
def is_ok(self) -> TypeGuard[Ok[_TSource, _TError]]:
270277
"""Returns `True` if the result is an `Ok` value."""
271278
return False
272279

0 commit comments

Comments
 (0)