punctional 0.1.0__py3-none-any.whl
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.
- punctional/__init__.py +61 -0
- punctional/arithmetic.py +46 -0
- punctional/comparison.py +36 -0
- punctional/core.py +92 -0
- punctional/list_filters.py +40 -0
- punctional/logical.py +38 -0
- punctional/monads.py +357 -0
- punctional/string.py +29 -0
- punctional/types.py +63 -0
- punctional-0.1.0.dist-info/METADATA +445 -0
- punctional-0.1.0.dist-info/RECORD +13 -0
- punctional-0.1.0.dist-info/WHEEL +4 -0
- punctional-0.1.0.dist-info/licenses/LICENSE +21 -0
punctional/__init__.py
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Punctional - A functional programming framework for Python.
|
|
3
|
+
|
|
4
|
+
Provides composable filters and method chaining for native types and custom dataclasses.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from .core import Filter, Functional, Compose
|
|
8
|
+
from .types import FunctionalInt, FunctionalFloat, FunctionalStr, fint, ffloat, fstr
|
|
9
|
+
from .arithmetic import Add, Mult, Sub, Div
|
|
10
|
+
from .logical import AndFilter, OrFilter, NotFilter
|
|
11
|
+
from .comparison import GreaterThan, LessThan, Equals
|
|
12
|
+
from .string import ToUpper, ToLower, Contains
|
|
13
|
+
from .list_filters import Map, FilterList, Reduce
|
|
14
|
+
from .monads import Option, Some, Nothing, Result, Ok, Error, some, try_result
|
|
15
|
+
|
|
16
|
+
__version__ = "0.1.0"
|
|
17
|
+
|
|
18
|
+
__all__ = [
|
|
19
|
+
# Core
|
|
20
|
+
"Filter",
|
|
21
|
+
"Functional",
|
|
22
|
+
# Types
|
|
23
|
+
"FunctionalInt",
|
|
24
|
+
"FunctionalFloat",
|
|
25
|
+
"FunctionalStr",
|
|
26
|
+
"fint",
|
|
27
|
+
"ffloat",
|
|
28
|
+
"fstr",
|
|
29
|
+
# Arithmetic
|
|
30
|
+
"Add",
|
|
31
|
+
"Mult",
|
|
32
|
+
"Sub",
|
|
33
|
+
"Div",
|
|
34
|
+
# Logical
|
|
35
|
+
"AndFilter",
|
|
36
|
+
"OrFilter",
|
|
37
|
+
"NotFilter",
|
|
38
|
+
# Comparison
|
|
39
|
+
"GreaterThan",
|
|
40
|
+
"LessThan",
|
|
41
|
+
"Equals",
|
|
42
|
+
# String
|
|
43
|
+
"ToUpper",
|
|
44
|
+
"ToLower",
|
|
45
|
+
"Contains",
|
|
46
|
+
# Composition
|
|
47
|
+
"Compose",
|
|
48
|
+
# List
|
|
49
|
+
"Map",
|
|
50
|
+
"FilterList",
|
|
51
|
+
"Reduce",
|
|
52
|
+
# Monads
|
|
53
|
+
"Option",
|
|
54
|
+
"Some",
|
|
55
|
+
"Nothing",
|
|
56
|
+
"Result",
|
|
57
|
+
"Ok",
|
|
58
|
+
"Error",
|
|
59
|
+
"some",
|
|
60
|
+
"try_result",
|
|
61
|
+
]
|
punctional/arithmetic.py
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Arithmetic operation filters (Add, Mult, Sub, Div).
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from typing import Any
|
|
6
|
+
from .core import Filter
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class Add(Filter[Any, Any]):
|
|
10
|
+
"""Addition filter - adds a value."""
|
|
11
|
+
|
|
12
|
+
def __init__(self, addend: Any):
|
|
13
|
+
self.addend = addend
|
|
14
|
+
|
|
15
|
+
def apply(self, value: Any) -> Any:
|
|
16
|
+
return value + self.addend
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class Mult(Filter[Any, Any]):
|
|
20
|
+
"""Multiplication filter - multiplies by a value."""
|
|
21
|
+
|
|
22
|
+
def __init__(self, multiplier: Any):
|
|
23
|
+
self.multiplier = multiplier
|
|
24
|
+
|
|
25
|
+
def apply(self, value: Any) -> Any:
|
|
26
|
+
return value * self.multiplier
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class Sub(Filter[Any, Any]):
|
|
30
|
+
"""Subtraction filter - subtracts a value."""
|
|
31
|
+
|
|
32
|
+
def __init__(self, subtrahend: Any):
|
|
33
|
+
self.subtrahend = subtrahend
|
|
34
|
+
|
|
35
|
+
def apply(self, value: Any) -> Any:
|
|
36
|
+
return value - self.subtrahend
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
class Div(Filter[Any, Any]):
|
|
40
|
+
"""Division filter - divides by a value."""
|
|
41
|
+
|
|
42
|
+
def __init__(self, divisor: Any):
|
|
43
|
+
self.divisor = divisor
|
|
44
|
+
|
|
45
|
+
def apply(self, value: Any) -> Any:
|
|
46
|
+
return value / self.divisor
|
punctional/comparison.py
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Comparison filters (GreaterThan, LessThan, Equals).
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from typing import Any
|
|
6
|
+
from .core import Filter
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class GreaterThan(Filter[Any, bool]):
|
|
10
|
+
"""Check if value is greater than threshold."""
|
|
11
|
+
|
|
12
|
+
def __init__(self, threshold: Any):
|
|
13
|
+
self.threshold = threshold
|
|
14
|
+
|
|
15
|
+
def apply(self, value: Any) -> bool:
|
|
16
|
+
return value > self.threshold
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class LessThan(Filter[Any, bool]):
|
|
20
|
+
"""Check if value is less than threshold."""
|
|
21
|
+
|
|
22
|
+
def __init__(self, threshold: Any):
|
|
23
|
+
self.threshold = threshold
|
|
24
|
+
|
|
25
|
+
def apply(self, value: Any) -> bool:
|
|
26
|
+
return value < self.threshold
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class Equals(Filter[Any, bool]):
|
|
30
|
+
"""Check if value equals another value."""
|
|
31
|
+
|
|
32
|
+
def __init__(self, target: Any):
|
|
33
|
+
self.target = target
|
|
34
|
+
|
|
35
|
+
def apply(self, value: Any) -> bool:
|
|
36
|
+
return value == self.target
|
punctional/core.py
ADDED
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Core abstractions for the Punctional functional programming framework.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from __future__ import annotations
|
|
6
|
+
from abc import ABC, abstractmethod
|
|
7
|
+
from typing import Any, TypeVar, Generic
|
|
8
|
+
|
|
9
|
+
T = TypeVar('T')
|
|
10
|
+
U = TypeVar('U')
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class Filter(ABC, Generic[T, U]):
|
|
14
|
+
"""
|
|
15
|
+
Abstract base class for all functional filters.
|
|
16
|
+
A filter transforms input of type T to output of type U.
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
@abstractmethod
|
|
20
|
+
def apply(self, value: T) -> U:
|
|
21
|
+
"""Apply the filter to a value."""
|
|
22
|
+
pass
|
|
23
|
+
|
|
24
|
+
def __call__(self, value: T) -> U:
|
|
25
|
+
"""Allow filters to be called as functions."""
|
|
26
|
+
return self.apply(value)
|
|
27
|
+
|
|
28
|
+
def __or__(self, other: Filter) -> Compose:
|
|
29
|
+
"""Enable pipe operator for filter composition: filter1 | filter2"""
|
|
30
|
+
# Import here to avoid circular dependency at module level
|
|
31
|
+
# Compose is defined below in this same module
|
|
32
|
+
return Compose(self, other)
|
|
33
|
+
|
|
34
|
+
def __and__(self, other: Filter) -> 'AndFilter':
|
|
35
|
+
"""Enable & operator for logical AND"""
|
|
36
|
+
return AndFilter(self, other)
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
class AndFilter(Filter[T, bool]):
|
|
40
|
+
"""Logical AND filter - applies multiple filters and returns True if all are True."""
|
|
41
|
+
|
|
42
|
+
def __init__(self, *filters: Filter):
|
|
43
|
+
self.filters = filters
|
|
44
|
+
|
|
45
|
+
def apply(self, value: T) -> bool:
|
|
46
|
+
return all(f.apply(value) for f in self.filters)
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
class Compose(Filter[T, Any]):
|
|
50
|
+
"""Compose multiple filters into a pipeline."""
|
|
51
|
+
|
|
52
|
+
def __init__(self, *filters: Filter):
|
|
53
|
+
self.filters = filters
|
|
54
|
+
|
|
55
|
+
def apply(self, value: T) -> Any:
|
|
56
|
+
result = value
|
|
57
|
+
for filter in self.filters:
|
|
58
|
+
result = filter.apply(result)
|
|
59
|
+
return result
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
class Functional:
|
|
63
|
+
"""
|
|
64
|
+
Mixin class that adds functional programming capabilities to any class.
|
|
65
|
+
Inherit from this to enable method chaining and filter application.
|
|
66
|
+
"""
|
|
67
|
+
|
|
68
|
+
def pipe(self, filter: Filter) -> Any:
|
|
69
|
+
"""Apply a filter to this object."""
|
|
70
|
+
result = filter.apply(self)
|
|
71
|
+
return self._wrap_result(result)
|
|
72
|
+
|
|
73
|
+
def __or__(self, filter: Filter) -> Any:
|
|
74
|
+
"""Enable pipe operator: obj | filter"""
|
|
75
|
+
result = filter.apply(self)
|
|
76
|
+
return self._wrap_result(result)
|
|
77
|
+
|
|
78
|
+
@staticmethod
|
|
79
|
+
def _wrap_result(result: Any) -> Any:
|
|
80
|
+
"""Wrap basic types in functional wrappers for continued chaining."""
|
|
81
|
+
if isinstance(result, Functional):
|
|
82
|
+
return result
|
|
83
|
+
elif isinstance(result, int) and not isinstance(result, bool):
|
|
84
|
+
from .types import FunctionalInt
|
|
85
|
+
return FunctionalInt(result)
|
|
86
|
+
elif isinstance(result, float):
|
|
87
|
+
from .types import FunctionalFloat
|
|
88
|
+
return FunctionalFloat(result)
|
|
89
|
+
elif isinstance(result, str):
|
|
90
|
+
from .types import FunctionalStr
|
|
91
|
+
return FunctionalStr(result)
|
|
92
|
+
return result
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Filters for List operations (Map, FilterList, Reduce).
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from typing import Any, List
|
|
6
|
+
from functools import reduce
|
|
7
|
+
from .core import Filter
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class Map(Filter[List, List]):
|
|
11
|
+
"""Map a filter over a List."""
|
|
12
|
+
|
|
13
|
+
def __init__(self, filter: Filter):
|
|
14
|
+
self.filter = filter
|
|
15
|
+
|
|
16
|
+
def apply(self, values: List) -> List:
|
|
17
|
+
return [self.filter.apply(v) for v in values]
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class FilterList(Filter[List, List]):
|
|
21
|
+
"""Filter a List based on a predicate."""
|
|
22
|
+
|
|
23
|
+
def __init__(self, predicate: Filter[Any, bool]):
|
|
24
|
+
self.predicate = predicate
|
|
25
|
+
|
|
26
|
+
def apply(self, values: List) -> List:
|
|
27
|
+
return [v for v in values if self.predicate.apply(v)]
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class Reduce(Filter[List, Any]):
|
|
31
|
+
"""Reduce a List to a single value using a filter."""
|
|
32
|
+
|
|
33
|
+
def __init__(self, filter: Filter, initial: Any = None):
|
|
34
|
+
self.filter = filter
|
|
35
|
+
self.initial = initial
|
|
36
|
+
|
|
37
|
+
def apply(self, values: List) -> Any:
|
|
38
|
+
if self.initial is not None:
|
|
39
|
+
return reduce(lambda acc, x: self.filter.apply((acc, x)), values, self.initial)
|
|
40
|
+
return reduce(lambda acc, x: self.filter.apply((acc, x)), values)
|
punctional/logical.py
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Logical operation filters (And, Or, Not).
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from typing import TypeVar
|
|
6
|
+
from .core import Filter
|
|
7
|
+
|
|
8
|
+
T = TypeVar('T')
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class AndFilter(Filter[T, bool]):
|
|
12
|
+
"""Logical AND filter - applies multiple filters and returns True if all are True."""
|
|
13
|
+
|
|
14
|
+
def __init__(self, *filters: Filter):
|
|
15
|
+
self.filters = filters
|
|
16
|
+
|
|
17
|
+
def apply(self, value: T) -> bool:
|
|
18
|
+
return all(f.apply(value) for f in self.filters)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class OrFilter(Filter[T, bool]):
|
|
22
|
+
"""Logical OR filter - applies multiple filters and returns True if any is True."""
|
|
23
|
+
|
|
24
|
+
def __init__(self, *filters: Filter):
|
|
25
|
+
self.filters = filters
|
|
26
|
+
|
|
27
|
+
def apply(self, value: T) -> bool:
|
|
28
|
+
return any(f.apply(value) for f in self.filters)
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class NotFilter(Filter[T, bool]):
|
|
32
|
+
"""Logical NOT filter - negates the result of another filter."""
|
|
33
|
+
|
|
34
|
+
def __init__(self, filter: Filter[T, bool]):
|
|
35
|
+
self.filter = filter
|
|
36
|
+
|
|
37
|
+
def apply(self, value: T) -> bool:
|
|
38
|
+
return not self.filter.apply(value)
|
punctional/monads.py
ADDED
|
@@ -0,0 +1,357 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Monad implementations for functional programming patterns.
|
|
3
|
+
|
|
4
|
+
Provides Option (Some/Nothing) and Result (Ok/Error) monads with standard
|
|
5
|
+
monadic operations like map, bind, and flat_map.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
from abc import ABC, abstractmethod
|
|
10
|
+
from typing import TypeVar, Generic, Callable, Any, Union, Optional
|
|
11
|
+
from dataclasses import dataclass
|
|
12
|
+
|
|
13
|
+
T = TypeVar('T')
|
|
14
|
+
U = TypeVar('U')
|
|
15
|
+
E = TypeVar('E')
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class Option(ABC, Generic[T]):
|
|
19
|
+
"""
|
|
20
|
+
Option monad representing the presence (Some) or absence (Nothing) of a value.
|
|
21
|
+
|
|
22
|
+
Useful for handling nullable values without explicit None checks.
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
@abstractmethod
|
|
26
|
+
def map(self, f: Callable[[T], U]) -> Option[U]:
|
|
27
|
+
"""Transform the contained value if present."""
|
|
28
|
+
pass
|
|
29
|
+
|
|
30
|
+
@abstractmethod
|
|
31
|
+
def flat_map(self, f: Callable[[T], Option[U]]) -> Option[U]:
|
|
32
|
+
"""Chain operations that return Options (also called bind or >>=)."""
|
|
33
|
+
pass
|
|
34
|
+
|
|
35
|
+
@abstractmethod
|
|
36
|
+
def bind(self, f: Callable[[T], Option[U]]) -> Option[U]:
|
|
37
|
+
"""Alias for flat_map (monadic bind operation)."""
|
|
38
|
+
pass
|
|
39
|
+
|
|
40
|
+
@abstractmethod
|
|
41
|
+
def get_or_else(self, default: T) -> T:
|
|
42
|
+
"""Get the value or return a default."""
|
|
43
|
+
pass
|
|
44
|
+
|
|
45
|
+
@abstractmethod
|
|
46
|
+
def get_or_none(self) -> Optional[T]:
|
|
47
|
+
"""Get the value or return None."""
|
|
48
|
+
pass
|
|
49
|
+
|
|
50
|
+
@abstractmethod
|
|
51
|
+
def is_some(self) -> bool:
|
|
52
|
+
"""Check if this is Some."""
|
|
53
|
+
pass
|
|
54
|
+
|
|
55
|
+
@abstractmethod
|
|
56
|
+
def is_nothing(self) -> bool:
|
|
57
|
+
"""Check if this is Nothing."""
|
|
58
|
+
pass
|
|
59
|
+
|
|
60
|
+
@abstractmethod
|
|
61
|
+
def filter(self, predicate: Callable[[T], bool]) -> Option[T]:
|
|
62
|
+
"""Return Nothing if predicate is False, otherwise return self."""
|
|
63
|
+
pass
|
|
64
|
+
|
|
65
|
+
@abstractmethod
|
|
66
|
+
def or_else(self, alternative: Option[T]) -> Option[T]:
|
|
67
|
+
"""Return self if Some, otherwise return alternative."""
|
|
68
|
+
pass
|
|
69
|
+
|
|
70
|
+
def __bool__(self) -> bool:
|
|
71
|
+
"""Enable truthiness checks."""
|
|
72
|
+
return self.is_some()
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
@dataclass(frozen=True)
|
|
76
|
+
class Some(Option[T]):
|
|
77
|
+
"""Represents the presence of a value."""
|
|
78
|
+
|
|
79
|
+
value: T
|
|
80
|
+
|
|
81
|
+
def map(self, f: Callable[[T], U]) -> Option[U]:
|
|
82
|
+
"""Apply function to the contained value."""
|
|
83
|
+
return Some(f(self.value))
|
|
84
|
+
|
|
85
|
+
def flat_map(self, f: Callable[[T], Option[U]]) -> Option[U]:
|
|
86
|
+
"""Apply function that returns an Option."""
|
|
87
|
+
return f(self.value)
|
|
88
|
+
|
|
89
|
+
def bind(self, f: Callable[[T], Option[U]]) -> Option[U]:
|
|
90
|
+
"""Monadic bind (alias for flat_map)."""
|
|
91
|
+
return self.flat_map(f)
|
|
92
|
+
|
|
93
|
+
def get_or_else(self, default: T) -> T:
|
|
94
|
+
"""Return the contained value."""
|
|
95
|
+
return self.value
|
|
96
|
+
|
|
97
|
+
def get_or_none(self) -> Optional[T]:
|
|
98
|
+
"""Return the contained value."""
|
|
99
|
+
return self.value
|
|
100
|
+
|
|
101
|
+
def is_some(self) -> bool:
|
|
102
|
+
"""Always True for Some."""
|
|
103
|
+
return True
|
|
104
|
+
|
|
105
|
+
def is_nothing(self) -> bool:
|
|
106
|
+
"""Always False for Some."""
|
|
107
|
+
return False
|
|
108
|
+
|
|
109
|
+
def filter(self, predicate: Callable[[T], bool]) -> Option[T]:
|
|
110
|
+
"""Return self if predicate passes, otherwise Nothing."""
|
|
111
|
+
return self if predicate(self.value) else Nothing()
|
|
112
|
+
|
|
113
|
+
def or_else(self, alternative: Option[T]) -> Option[T]:
|
|
114
|
+
"""Return self since we have a value."""
|
|
115
|
+
return self
|
|
116
|
+
|
|
117
|
+
def __repr__(self) -> str:
|
|
118
|
+
return f"Some({self.value!r})"
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
class NothingType(Option[T]):
|
|
122
|
+
"""Represents the absence of a value (singleton pattern)."""
|
|
123
|
+
|
|
124
|
+
_instance = None
|
|
125
|
+
|
|
126
|
+
def __new__(cls):
|
|
127
|
+
if cls._instance is None:
|
|
128
|
+
cls._instance = super().__new__(cls)
|
|
129
|
+
return cls._instance
|
|
130
|
+
|
|
131
|
+
def map(self, f: Callable[[T], U]) -> Option[U]:
|
|
132
|
+
"""Nothing remains Nothing."""
|
|
133
|
+
return self
|
|
134
|
+
|
|
135
|
+
def flat_map(self, f: Callable[[T], Option[U]]) -> Option[U]:
|
|
136
|
+
"""Nothing remains Nothing."""
|
|
137
|
+
return self
|
|
138
|
+
|
|
139
|
+
def bind(self, f: Callable[[T], Option[U]]) -> Option[U]:
|
|
140
|
+
"""Nothing remains Nothing."""
|
|
141
|
+
return self
|
|
142
|
+
|
|
143
|
+
def get_or_else(self, default: T) -> T:
|
|
144
|
+
"""Return the default value."""
|
|
145
|
+
return default
|
|
146
|
+
|
|
147
|
+
def get_or_none(self) -> Optional[T]:
|
|
148
|
+
"""Return None."""
|
|
149
|
+
return None
|
|
150
|
+
|
|
151
|
+
def is_some(self) -> bool:
|
|
152
|
+
"""Always False for Nothing."""
|
|
153
|
+
return False
|
|
154
|
+
|
|
155
|
+
def is_nothing(self) -> bool:
|
|
156
|
+
"""Always True for Nothing."""
|
|
157
|
+
return True
|
|
158
|
+
|
|
159
|
+
def filter(self, predicate: Callable[[T], bool]) -> Option[T]:
|
|
160
|
+
"""Nothing remains Nothing."""
|
|
161
|
+
return self
|
|
162
|
+
|
|
163
|
+
def or_else(self, alternative: Option[T]) -> Option[T]:
|
|
164
|
+
"""Return the alternative since we have no value."""
|
|
165
|
+
return alternative
|
|
166
|
+
|
|
167
|
+
def __repr__(self) -> str:
|
|
168
|
+
return "Nothing"
|
|
169
|
+
|
|
170
|
+
def __bool__(self) -> bool:
|
|
171
|
+
"""Nothing is falsy."""
|
|
172
|
+
return False
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
# Singleton instance
|
|
176
|
+
Nothing = NothingType
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
def some(value: Optional[T], allow_none: bool = False) -> Option[T]:
|
|
180
|
+
"""
|
|
181
|
+
Create an Option from a potentially None value.
|
|
182
|
+
|
|
183
|
+
Args:
|
|
184
|
+
value: The value to wrap
|
|
185
|
+
allow_none: If True, wrap None as Some(None). If False, return Nothing for None.
|
|
186
|
+
|
|
187
|
+
Returns:
|
|
188
|
+
Some(value) if value is not None or allow_none is True
|
|
189
|
+
Nothing if value is None and allow_none is False
|
|
190
|
+
|
|
191
|
+
Examples:
|
|
192
|
+
>>> some(5)
|
|
193
|
+
Some(5)
|
|
194
|
+
>>> some(None)
|
|
195
|
+
Nothing
|
|
196
|
+
>>> some(None, allow_none=True)
|
|
197
|
+
Some(None)
|
|
198
|
+
"""
|
|
199
|
+
if value is None and not allow_none:
|
|
200
|
+
return Nothing()
|
|
201
|
+
return Some(value)
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
class Result(ABC, Generic[T, E]):
|
|
205
|
+
"""
|
|
206
|
+
Result monad representing either a success (Ok) or failure (Error).
|
|
207
|
+
|
|
208
|
+
Useful for operations that can fail with an error value.
|
|
209
|
+
"""
|
|
210
|
+
|
|
211
|
+
@abstractmethod
|
|
212
|
+
def map(self, f: Callable[[T], U]) -> Result[U, E]:
|
|
213
|
+
"""Transform the success value if present."""
|
|
214
|
+
pass
|
|
215
|
+
|
|
216
|
+
@abstractmethod
|
|
217
|
+
def map_error(self, f: Callable[[E], U]) -> Result[T, U]:
|
|
218
|
+
"""Transform the error value if present."""
|
|
219
|
+
pass
|
|
220
|
+
|
|
221
|
+
@abstractmethod
|
|
222
|
+
def flat_map(self, f: Callable[[T], Result[U, E]]) -> Result[U, E]:
|
|
223
|
+
"""Chain operations that return Results."""
|
|
224
|
+
pass
|
|
225
|
+
|
|
226
|
+
@abstractmethod
|
|
227
|
+
def bind(self, f: Callable[[T], Result[U, E]]) -> Result[U, E]:
|
|
228
|
+
"""Alias for flat_map (monadic bind)."""
|
|
229
|
+
pass
|
|
230
|
+
|
|
231
|
+
@abstractmethod
|
|
232
|
+
def get_or_else(self, default: T) -> T:
|
|
233
|
+
"""Get the success value or return a default."""
|
|
234
|
+
pass
|
|
235
|
+
|
|
236
|
+
@abstractmethod
|
|
237
|
+
def is_ok(self) -> bool:
|
|
238
|
+
"""Check if this is Ok."""
|
|
239
|
+
pass
|
|
240
|
+
|
|
241
|
+
@abstractmethod
|
|
242
|
+
def is_error(self) -> bool:
|
|
243
|
+
"""Check if this is Error."""
|
|
244
|
+
pass
|
|
245
|
+
|
|
246
|
+
@abstractmethod
|
|
247
|
+
def to_option(self) -> Option[T]:
|
|
248
|
+
"""Convert to Option, discarding error information."""
|
|
249
|
+
pass
|
|
250
|
+
|
|
251
|
+
def __bool__(self) -> bool:
|
|
252
|
+
"""Enable truthiness checks (Ok is truthy, Error is falsy)."""
|
|
253
|
+
return self.is_ok()
|
|
254
|
+
|
|
255
|
+
|
|
256
|
+
@dataclass(frozen=True)
|
|
257
|
+
class Ok(Result[T, E]):
|
|
258
|
+
"""Represents a successful result."""
|
|
259
|
+
|
|
260
|
+
value: T
|
|
261
|
+
|
|
262
|
+
def map(self, f: Callable[[T], U]) -> Result[U, E]:
|
|
263
|
+
"""Apply function to the success value."""
|
|
264
|
+
return Ok(f(self.value))
|
|
265
|
+
|
|
266
|
+
def map_error(self, f: Callable[[E], U]) -> Result[T, U]:
|
|
267
|
+
"""Leave Ok unchanged."""
|
|
268
|
+
return self
|
|
269
|
+
|
|
270
|
+
def flat_map(self, f: Callable[[T], Result[U, E]]) -> Result[U, E]:
|
|
271
|
+
"""Apply function that returns a Result."""
|
|
272
|
+
return f(self.value)
|
|
273
|
+
|
|
274
|
+
def bind(self, f: Callable[[T], Result[U, E]]) -> Result[U, E]:
|
|
275
|
+
"""Monadic bind (alias for flat_map)."""
|
|
276
|
+
return self.flat_map(f)
|
|
277
|
+
|
|
278
|
+
def get_or_else(self, default: T) -> T:
|
|
279
|
+
"""Return the success value."""
|
|
280
|
+
return self.value
|
|
281
|
+
|
|
282
|
+
def is_ok(self) -> bool:
|
|
283
|
+
"""Always True for Ok."""
|
|
284
|
+
return True
|
|
285
|
+
|
|
286
|
+
def is_error(self) -> bool:
|
|
287
|
+
"""Always False for Ok."""
|
|
288
|
+
return False
|
|
289
|
+
|
|
290
|
+
def to_option(self) -> Option[T]:
|
|
291
|
+
"""Convert to Some with the value."""
|
|
292
|
+
return Some(self.value)
|
|
293
|
+
|
|
294
|
+
def __repr__(self) -> str:
|
|
295
|
+
return f"Ok({self.value!r})"
|
|
296
|
+
|
|
297
|
+
|
|
298
|
+
@dataclass(frozen=True)
|
|
299
|
+
class Error(Result[T, E]):
|
|
300
|
+
"""Represents a failed result with an error value."""
|
|
301
|
+
|
|
302
|
+
error: E
|
|
303
|
+
|
|
304
|
+
def map(self, f: Callable[[T], U]) -> Result[U, E]:
|
|
305
|
+
"""Error remains Error."""
|
|
306
|
+
return self
|
|
307
|
+
|
|
308
|
+
def map_error(self, f: Callable[[E], U]) -> Result[T, U]:
|
|
309
|
+
"""Apply function to the error value."""
|
|
310
|
+
return Error(f(self.error))
|
|
311
|
+
|
|
312
|
+
def flat_map(self, f: Callable[[T], Result[U, E]]) -> Result[U, E]:
|
|
313
|
+
"""Error remains Error."""
|
|
314
|
+
return self
|
|
315
|
+
|
|
316
|
+
def bind(self, f: Callable[[T], Result[U, E]]) -> Result[U, E]:
|
|
317
|
+
"""Error remains Error."""
|
|
318
|
+
return self
|
|
319
|
+
|
|
320
|
+
def get_or_else(self, default: T) -> T:
|
|
321
|
+
"""Return the default value."""
|
|
322
|
+
return default
|
|
323
|
+
|
|
324
|
+
def is_ok(self) -> bool:
|
|
325
|
+
"""Always False for Error."""
|
|
326
|
+
return False
|
|
327
|
+
|
|
328
|
+
def is_error(self) -> bool:
|
|
329
|
+
"""Always True for Error."""
|
|
330
|
+
return True
|
|
331
|
+
|
|
332
|
+
def to_option(self) -> Option[T]:
|
|
333
|
+
"""Convert to Nothing."""
|
|
334
|
+
return Nothing()
|
|
335
|
+
|
|
336
|
+
def __repr__(self) -> str:
|
|
337
|
+
return f"Error({self.error!r})"
|
|
338
|
+
|
|
339
|
+
|
|
340
|
+
# Utility functions for creating Results
|
|
341
|
+
def try_result(f: Callable[[], T], error_handler: Optional[Callable[[Exception], E]] = None) -> Result[T, Union[E, Exception]]:
|
|
342
|
+
"""
|
|
343
|
+
Execute a function and wrap the result in Ok or Error.
|
|
344
|
+
|
|
345
|
+
Args:
|
|
346
|
+
f: Function to execute
|
|
347
|
+
error_handler: Optional function to transform exceptions into error values
|
|
348
|
+
|
|
349
|
+
Returns:
|
|
350
|
+
Ok(result) if successful, Error(exception) or Error(error_handler(exception)) if failed
|
|
351
|
+
"""
|
|
352
|
+
try:
|
|
353
|
+
return Ok(f())
|
|
354
|
+
except Exception as e:
|
|
355
|
+
if error_handler:
|
|
356
|
+
return Error(error_handler(e))
|
|
357
|
+
return Error(e)
|
punctional/string.py
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
"""
|
|
2
|
+
String manipulation filters.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from .core import Filter
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class ToUpper(Filter[str, str]):
|
|
9
|
+
"""Convert string to uppercase."""
|
|
10
|
+
|
|
11
|
+
def apply(self, value: str) -> str:
|
|
12
|
+
return value.upper()
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class ToLower(Filter[str, str]):
|
|
16
|
+
"""Convert string to lowercase."""
|
|
17
|
+
|
|
18
|
+
def apply(self, value: str) -> str:
|
|
19
|
+
return value.lower()
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class Contains(Filter[str, bool]):
|
|
23
|
+
"""Check if string contains substring."""
|
|
24
|
+
|
|
25
|
+
def __init__(self, substring: str):
|
|
26
|
+
self.substring = substring
|
|
27
|
+
|
|
28
|
+
def apply(self, value: str) -> bool:
|
|
29
|
+
return self.substring in value
|
punctional/types.py
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Functional wrapper types for Python native types (int, float, str).
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from .core import Functional, Filter
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class FunctionalInt(int, Functional):
|
|
9
|
+
"""Int with functional programming capabilities."""
|
|
10
|
+
|
|
11
|
+
def __or__(self, other):
|
|
12
|
+
"""Override to support Filter piping, fall back to bitwise OR for ints."""
|
|
13
|
+
if isinstance(other, Filter):
|
|
14
|
+
result = other.apply(self)
|
|
15
|
+
return self._wrap_result(result)
|
|
16
|
+
return int.__or__(self, other)
|
|
17
|
+
|
|
18
|
+
def __new__(cls, value):
|
|
19
|
+
return int.__new__(cls, value)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class FunctionalFloat(float, Functional):
|
|
23
|
+
"""Float with functional programming capabilities."""
|
|
24
|
+
|
|
25
|
+
def __or__(self, other):
|
|
26
|
+
"""Support Filter piping."""
|
|
27
|
+
if isinstance(other, Filter):
|
|
28
|
+
result = other.apply(self)
|
|
29
|
+
return self._wrap_result(result)
|
|
30
|
+
return NotImplemented
|
|
31
|
+
|
|
32
|
+
def __new__(cls, value):
|
|
33
|
+
return float.__new__(cls, value)
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class FunctionalStr(str, Functional):
|
|
37
|
+
"""String with functional programming capabilities."""
|
|
38
|
+
|
|
39
|
+
def __or__(self, other):
|
|
40
|
+
"""Support Filter piping."""
|
|
41
|
+
if isinstance(other, Filter):
|
|
42
|
+
result = other.apply(self)
|
|
43
|
+
return self._wrap_result(result)
|
|
44
|
+
return NotImplemented
|
|
45
|
+
|
|
46
|
+
def __new__(cls, value):
|
|
47
|
+
return str.__new__(cls, value)
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
# Convenience constructors
|
|
51
|
+
def fint(value: int) -> FunctionalInt:
|
|
52
|
+
"""Create a functional integer."""
|
|
53
|
+
return FunctionalInt(value)
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def ffloat(value: float) -> FunctionalFloat:
|
|
57
|
+
"""Create a functional float."""
|
|
58
|
+
return FunctionalFloat(value)
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def fstr(value: str) -> FunctionalStr:
|
|
62
|
+
"""Create a functional string."""
|
|
63
|
+
return FunctionalStr(value)
|
|
@@ -0,0 +1,445 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: punctional
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: A functional programming framework for Python โ composable filters, method chaining, and expressive data pipelines with Option and Result monads.
|
|
5
|
+
Project-URL: Homepage, https://github.com/peghaz/punctional
|
|
6
|
+
Project-URL: Repository, https://github.com/peghaz/punctional
|
|
7
|
+
Project-URL: Documentation, https://github.com/peghaz/punctional#readme
|
|
8
|
+
Project-URL: Issues, https://github.com/peghaz/punctional/issues
|
|
9
|
+
Author-email: Mehdi Peghaz <peghaz@example.com>
|
|
10
|
+
License: MIT
|
|
11
|
+
License-File: LICENSE
|
|
12
|
+
Keywords: composition,data-pipeline,filters,functional-programming,method-chaining,monads,option,pipe-operator,result
|
|
13
|
+
Classifier: Development Status :: 4 - Beta
|
|
14
|
+
Classifier: Intended Audience :: Developers
|
|
15
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
16
|
+
Classifier: Operating System :: OS Independent
|
|
17
|
+
Classifier: Programming Language :: Python :: 3
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
20
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
21
|
+
Classifier: Typing :: Typed
|
|
22
|
+
Requires-Python: >=3.12
|
|
23
|
+
Description-Content-Type: text/markdown
|
|
24
|
+
|
|
25
|
+
# Punctional
|
|
26
|
+
|
|
27
|
+
> A functional programming framework for Python โ enabling composable filters, method chaining, and expressive data pipelines.
|
|
28
|
+
|
|
29
|
+
[](https://www.python.org/downloads/)
|
|
30
|
+
[](LICENSE)
|
|
31
|
+
|
|
32
|
+
## ๐ฏ What is Punctional?
|
|
33
|
+
|
|
34
|
+
**Punctional** is a lightweight functional programming library that brings powerful functional programming patterns to Python. It allows you to compose operations using an intuitive pipe (`|`) operator, create reusable transformation filters, and apply common functional design patterns like Option and Result monads.
|
|
35
|
+
|
|
36
|
+
Whether you're building data pipelines, validation logic, or just want cleaner, more declarative code โ Punctional provides the building blocks.
|
|
37
|
+
|
|
38
|
+
## โจ Key Features
|
|
39
|
+
|
|
40
|
+
- **๐ Pipe Operator Chaining** โ Chain transformations using the intuitive `|` operator
|
|
41
|
+
- **๐งฑ Composable Filters** โ Create reusable, testable transformation units
|
|
42
|
+
- **๐ฆ Functional Wrappers** โ Wrap native types (`int`, `float`, `str`) for functional operations
|
|
43
|
+
- **๐ญ Monads** โ Built-in `Option` (Some/Nothing) and `Result` (Ok/Error) monads
|
|
44
|
+
- **๐ง Extensible** โ Easily create custom filters for your domain
|
|
45
|
+
- **๐ Dataclass Support** โ Make any dataclass functional with the `Functional` mixin
|
|
46
|
+
|
|
47
|
+
## ๐ฆ Installation
|
|
48
|
+
|
|
49
|
+
```bash
|
|
50
|
+
pip install punctional
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
Or install from source:
|
|
54
|
+
|
|
55
|
+
```bash
|
|
56
|
+
git clone https://github.com/peghaz/punctional.git
|
|
57
|
+
cd punctional
|
|
58
|
+
pip install -e .
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
## ๐ Quick Start
|
|
62
|
+
|
|
63
|
+
```python
|
|
64
|
+
from punctional import fint, fstr, Add, Mult, ToUpper, GreaterThan, AndFilter, LessThan
|
|
65
|
+
|
|
66
|
+
# Arithmetic chaining
|
|
67
|
+
result = fint(10) | Add(5) | Mult(2) # (10 + 5) * 2 = 30
|
|
68
|
+
|
|
69
|
+
# String transformations
|
|
70
|
+
text = fstr("hello") | ToUpper() | Add("!") # "HELLO!"
|
|
71
|
+
|
|
72
|
+
# Logical validation
|
|
73
|
+
is_valid = fint(42) | AndFilter(GreaterThan(10), LessThan(100)) # True
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
## ๐ Core Concepts
|
|
77
|
+
|
|
78
|
+
### Filters
|
|
79
|
+
|
|
80
|
+
A **Filter** is the fundamental building block โ a transformation that takes an input and produces an output:
|
|
81
|
+
|
|
82
|
+
```python
|
|
83
|
+
from punctional import Filter, fint
|
|
84
|
+
|
|
85
|
+
class Square(Filter[int, int]):
|
|
86
|
+
def apply(self, value: int) -> int:
|
|
87
|
+
return value ** 2
|
|
88
|
+
|
|
89
|
+
# Use it
|
|
90
|
+
result = fint(5) | Square() # 25
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
### Functional Wrappers
|
|
94
|
+
|
|
95
|
+
Wrap native Python types to enable pipe operations:
|
|
96
|
+
|
|
97
|
+
| Function | Type | Description |
|
|
98
|
+
|----------|------|-------------|
|
|
99
|
+
| `fint(x)` | `FunctionalInt` | Functional integer wrapper |
|
|
100
|
+
| `ffloat(x)` | `FunctionalFloat` | Functional float wrapper |
|
|
101
|
+
| `fstr(x)` | `FunctionalStr` | Functional string wrapper |
|
|
102
|
+
|
|
103
|
+
### The Pipe Operator
|
|
104
|
+
|
|
105
|
+
Chain filters using the `|` operator for readable, left-to-right transformations:
|
|
106
|
+
|
|
107
|
+
```python
|
|
108
|
+
# Instead of nested calls:
|
|
109
|
+
result = Div(4).apply(Sub(3).apply(Mult(2).apply(Add(5).apply(10))))
|
|
110
|
+
|
|
111
|
+
# Write declaratively:
|
|
112
|
+
result = fint(10) | Add(5) | Mult(2) | Sub(3) | Div(4)
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
## ๐ง Built-in Filters
|
|
116
|
+
|
|
117
|
+
### Arithmetic Filters
|
|
118
|
+
|
|
119
|
+
```python
|
|
120
|
+
from punctional import Add, Sub, Mult, Div
|
|
121
|
+
|
|
122
|
+
fint(10) | Add(5) # 15
|
|
123
|
+
fint(10) | Sub(3) # 7
|
|
124
|
+
fint(10) | Mult(2) # 20
|
|
125
|
+
fint(10) | Div(4) # 2.5
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
### Comparison Filters
|
|
129
|
+
|
|
130
|
+
```python
|
|
131
|
+
from punctional import GreaterThan, LessThan, Equals
|
|
132
|
+
|
|
133
|
+
fint(42) | GreaterThan(10) # True
|
|
134
|
+
fint(5) | LessThan(10) # True
|
|
135
|
+
fint(42) | Equals(42) # True
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
### Logical Filters
|
|
139
|
+
|
|
140
|
+
```python
|
|
141
|
+
from punctional import AndFilter, OrFilter, NotFilter, GreaterThan, LessThan, Equals
|
|
142
|
+
|
|
143
|
+
# AND: all conditions must be true
|
|
144
|
+
fint(42) | AndFilter(GreaterThan(10), LessThan(100)) # True
|
|
145
|
+
|
|
146
|
+
# OR: at least one condition must be true
|
|
147
|
+
fint(5) | OrFilter(LessThan(10), GreaterThan(100)) # True
|
|
148
|
+
|
|
149
|
+
# NOT: negate a condition
|
|
150
|
+
fint(5) | NotFilter(Equals(0)) # True
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
### String Filters
|
|
154
|
+
|
|
155
|
+
```python
|
|
156
|
+
from punctional import ToUpper, ToLower, Contains, fstr
|
|
157
|
+
|
|
158
|
+
fstr("hello") | ToUpper() # "HELLO"
|
|
159
|
+
fstr("WORLD") | ToLower() # "world"
|
|
160
|
+
fstr("hello world") | Contains("world") # True
|
|
161
|
+
fstr("ha") | Mult(3) # "hahaha"
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
### List Filters
|
|
165
|
+
|
|
166
|
+
```python
|
|
167
|
+
from punctional import Map, FilterList, Reduce, Mult, GreaterThan
|
|
168
|
+
|
|
169
|
+
numbers = [1, 2, 3, 4, 5]
|
|
170
|
+
|
|
171
|
+
# Transform each element
|
|
172
|
+
Map(Mult(2)).apply(numbers) # [2, 4, 6, 8, 10]
|
|
173
|
+
|
|
174
|
+
# Filter elements
|
|
175
|
+
FilterList(GreaterThan(2)).apply(numbers) # [3, 4, 5]
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
### Composition
|
|
179
|
+
|
|
180
|
+
Create reusable pipelines with `Compose`:
|
|
181
|
+
|
|
182
|
+
```python
|
|
183
|
+
from punctional import Compose, Mult, Add, fint
|
|
184
|
+
|
|
185
|
+
# Create a reusable transformation
|
|
186
|
+
double_plus_ten = Compose(Mult(2), Add(10))
|
|
187
|
+
|
|
188
|
+
fint(5) | double_plus_ten # 20
|
|
189
|
+
fint(10) | double_plus_ten # 30
|
|
190
|
+
```
|
|
191
|
+
|
|
192
|
+
## ๐ญ Functional Design Patterns
|
|
193
|
+
|
|
194
|
+
### Option Monad (Some/Nothing)
|
|
195
|
+
|
|
196
|
+
Handle nullable values without explicit `None` checks:
|
|
197
|
+
|
|
198
|
+
```python
|
|
199
|
+
from punctional import Some, Nothing, some
|
|
200
|
+
|
|
201
|
+
# Wrap a value
|
|
202
|
+
value = Some(42)
|
|
203
|
+
print(value.map(lambda x: x * 2)) # Some(84)
|
|
204
|
+
print(value.get_or_else(0)) # 42
|
|
205
|
+
|
|
206
|
+
# Handle absence
|
|
207
|
+
empty = Nothing()
|
|
208
|
+
print(empty.map(lambda x: x * 2)) # Nothing
|
|
209
|
+
print(empty.get_or_else(0)) # 0
|
|
210
|
+
|
|
211
|
+
# Auto-convert from potentially None values
|
|
212
|
+
result = some(potentially_none_value) # Returns Nothing if None
|
|
213
|
+
```
|
|
214
|
+
|
|
215
|
+
#### Option Operations
|
|
216
|
+
|
|
217
|
+
| Method | Description |
|
|
218
|
+
|--------|-------------|
|
|
219
|
+
| `map(f)` | Transform value if present |
|
|
220
|
+
| `flat_map(f)` | Chain operations returning Option |
|
|
221
|
+
| `bind(f)` | Alias for flat_map |
|
|
222
|
+
| `get_or_else(default)` | Get value or default |
|
|
223
|
+
| `get_or_none()` | Get value or None |
|
|
224
|
+
| `filter(predicate)` | Return Nothing if predicate fails |
|
|
225
|
+
| `or_else(alternative)` | Return alternative if Nothing |
|
|
226
|
+
| `is_some()` | Check if value is present |
|
|
227
|
+
| `is_nothing()` | Check if value is absent |
|
|
228
|
+
|
|
229
|
+
### Result Monad (Ok/Error)
|
|
230
|
+
|
|
231
|
+
Handle operations that can fail with meaningful errors:
|
|
232
|
+
|
|
233
|
+
```python
|
|
234
|
+
from punctional import Ok, Error, try_result
|
|
235
|
+
|
|
236
|
+
# Successful operation
|
|
237
|
+
success = Ok(42)
|
|
238
|
+
print(success.map(lambda x: x * 2)) # Ok(84)
|
|
239
|
+
|
|
240
|
+
# Failed operation
|
|
241
|
+
failure = Error("Something went wrong")
|
|
242
|
+
print(failure.map(lambda x: x * 2)) # Error("Something went wrong")
|
|
243
|
+
|
|
244
|
+
# Wrap potentially throwing functions
|
|
245
|
+
def divide(a, b):
|
|
246
|
+
return a / b
|
|
247
|
+
|
|
248
|
+
result = try_result(lambda: divide(10, 0))
|
|
249
|
+
# Error(ZeroDivisionError(...))
|
|
250
|
+
```
|
|
251
|
+
|
|
252
|
+
#### Result Operations
|
|
253
|
+
|
|
254
|
+
| Method | Description |
|
|
255
|
+
|--------|-------------|
|
|
256
|
+
| `map(f)` | Transform success value |
|
|
257
|
+
| `map_error(f)` | Transform error value |
|
|
258
|
+
| `flat_map(f)` | Chain operations returning Result |
|
|
259
|
+
| `bind(f)` | Alias for flat_map |
|
|
260
|
+
| `get_or_else(default)` | Get value or default |
|
|
261
|
+
| `is_ok()` | Check if successful |
|
|
262
|
+
| `is_error()` | Check if failed |
|
|
263
|
+
| `to_option()` | Convert to Option (discards error info) |
|
|
264
|
+
|
|
265
|
+
## ๐๏ธ Extending the Framework
|
|
266
|
+
|
|
267
|
+
### Creating Custom Filters
|
|
268
|
+
|
|
269
|
+
#### Simple Filter
|
|
270
|
+
|
|
271
|
+
```python
|
|
272
|
+
from punctional import Filter
|
|
273
|
+
|
|
274
|
+
class Increment(Filter[int, int]):
|
|
275
|
+
def apply(self, value: int) -> int:
|
|
276
|
+
return value + 1
|
|
277
|
+
```
|
|
278
|
+
|
|
279
|
+
#### Parameterized Filter
|
|
280
|
+
|
|
281
|
+
```python
|
|
282
|
+
class Power(Filter[int, int]):
|
|
283
|
+
def __init__(self, exponent: int):
|
|
284
|
+
self.exponent = exponent
|
|
285
|
+
|
|
286
|
+
def apply(self, value: int) -> int:
|
|
287
|
+
return value ** self.exponent
|
|
288
|
+
|
|
289
|
+
fint(2) | Power(3) # 8
|
|
290
|
+
```
|
|
291
|
+
|
|
292
|
+
#### Stateful Filter
|
|
293
|
+
|
|
294
|
+
```python
|
|
295
|
+
class Accumulator(Filter[int, int]):
|
|
296
|
+
def __init__(self, initial: int = 0):
|
|
297
|
+
self.total = initial
|
|
298
|
+
|
|
299
|
+
def apply(self, value: int) -> int:
|
|
300
|
+
self.total += value
|
|
301
|
+
return self.total
|
|
302
|
+
|
|
303
|
+
acc = Accumulator()
|
|
304
|
+
acc(5) # 5
|
|
305
|
+
acc(10) # 15
|
|
306
|
+
acc(3) # 18
|
|
307
|
+
```
|
|
308
|
+
|
|
309
|
+
### Functional Dataclasses
|
|
310
|
+
|
|
311
|
+
Make any dataclass functional with the `Functional` mixin:
|
|
312
|
+
|
|
313
|
+
```python
|
|
314
|
+
from dataclasses import dataclass
|
|
315
|
+
from punctional import Functional, Filter
|
|
316
|
+
|
|
317
|
+
@dataclass
|
|
318
|
+
class Point(Functional):
|
|
319
|
+
x: float
|
|
320
|
+
y: float
|
|
321
|
+
|
|
322
|
+
class ScalePoint(Filter[Point, Point]):
|
|
323
|
+
def __init__(self, factor: float):
|
|
324
|
+
self.factor = factor
|
|
325
|
+
|
|
326
|
+
def apply(self, point: Point) -> Point:
|
|
327
|
+
return Point(point.x * self.factor, point.y * self.factor)
|
|
328
|
+
|
|
329
|
+
# Now Point supports piping!
|
|
330
|
+
point = Point(3, 4)
|
|
331
|
+
scaled = point | ScalePoint(2.5) # Point(7.5, 10.0)
|
|
332
|
+
```
|
|
333
|
+
|
|
334
|
+
## ๐ Examples
|
|
335
|
+
|
|
336
|
+
The [examples/](examples/) directory contains comprehensive examples:
|
|
337
|
+
|
|
338
|
+
| File | Description |
|
|
339
|
+
|------|-------------|
|
|
340
|
+
| [basics.py](examples/basics.py) | Basic usage and introduction to all features |
|
|
341
|
+
| [extending.py](examples/extending.py) | Guide to creating custom filters and domain-specific extensions |
|
|
342
|
+
| [data_transformation.py](examples/data_transformation.py) | Advanced patterns: validation pipelines, data transformations |
|
|
343
|
+
| [quick_reference.py](examples/quick_reference.py) | Cheat sheet for quick lookup |
|
|
344
|
+
|
|
345
|
+
### Run the examples:
|
|
346
|
+
|
|
347
|
+
```bash
|
|
348
|
+
python -m examples.basics
|
|
349
|
+
python -m examples.extending
|
|
350
|
+
python -m examples.data_transformation
|
|
351
|
+
```
|
|
352
|
+
|
|
353
|
+
## ๐งช Common Patterns
|
|
354
|
+
|
|
355
|
+
### Validation Pipeline
|
|
356
|
+
|
|
357
|
+
```python
|
|
358
|
+
from punctional import AndFilter, Functional, Filter
|
|
359
|
+
from dataclasses import dataclass
|
|
360
|
+
|
|
361
|
+
@dataclass
|
|
362
|
+
class Person(Functional):
|
|
363
|
+
name: str
|
|
364
|
+
age: int
|
|
365
|
+
email: str
|
|
366
|
+
|
|
367
|
+
class ValidateName(Filter[Person, bool]):
|
|
368
|
+
def apply(self, person: Person) -> bool:
|
|
369
|
+
return 1 <= len(person.name) <= 100
|
|
370
|
+
|
|
371
|
+
class ValidateAge(Filter[Person, bool]):
|
|
372
|
+
def apply(self, person: Person) -> bool:
|
|
373
|
+
return 0 <= person.age <= 150
|
|
374
|
+
|
|
375
|
+
class ValidateEmail(Filter[Person, bool]):
|
|
376
|
+
def apply(self, person: Person) -> bool:
|
|
377
|
+
return "@" in person.email and "." in person.email
|
|
378
|
+
|
|
379
|
+
# Use the validation pipeline
|
|
380
|
+
person = Person("Alice", 30, "alice@example.com")
|
|
381
|
+
is_valid = person | AndFilter(ValidateName(), ValidateAge(), ValidateEmail()) # True
|
|
382
|
+
```
|
|
383
|
+
|
|
384
|
+
### Data Transformation Pipeline
|
|
385
|
+
|
|
386
|
+
```python
|
|
387
|
+
class ApplyBonus(Filter[Person, Person]):
|
|
388
|
+
def __init__(self, percentage: float):
|
|
389
|
+
self.percentage = percentage
|
|
390
|
+
|
|
391
|
+
def apply(self, person: Person) -> Person:
|
|
392
|
+
return Person(person.name, person.age, person.email)
|
|
393
|
+
|
|
394
|
+
person | ApplyBonus(10) | PromoteAge() | SaveToDatabase()
|
|
395
|
+
```
|
|
396
|
+
|
|
397
|
+
### List Processing Pipeline
|
|
398
|
+
|
|
399
|
+
```python
|
|
400
|
+
from punctional import FilterList, Map, Mult
|
|
401
|
+
|
|
402
|
+
class IsEven(Filter[int, bool]):
|
|
403
|
+
def apply(self, value: int) -> bool:
|
|
404
|
+
return value % 2 == 0
|
|
405
|
+
|
|
406
|
+
numbers = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
|
|
407
|
+
result = FilterList(IsEven()).apply(numbers) # [2, 4, 6, 8, 10]
|
|
408
|
+
doubled = Map(Mult(2)).apply(result) # [4, 8, 12, 16, 20]
|
|
409
|
+
```
|
|
410
|
+
|
|
411
|
+
### Error Handling with Result
|
|
412
|
+
|
|
413
|
+
```python
|
|
414
|
+
from punctional import Result, Ok, Error
|
|
415
|
+
|
|
416
|
+
def fetch_user(id: int) -> Result[dict, str]:
|
|
417
|
+
if id < 0:
|
|
418
|
+
return Error("Invalid ID")
|
|
419
|
+
return Ok({"id": id, "name": "Alice"})
|
|
420
|
+
|
|
421
|
+
result = fetch_user(42).map(lambda u: u["name"]).get_or_else("Unknown") # "Alice"
|
|
422
|
+
result = fetch_user(-1).map(lambda u: u["name"]).get_or_else("Unknown") # "Unknown"
|
|
423
|
+
```
|
|
424
|
+
|
|
425
|
+
## ๐ Design Principles
|
|
426
|
+
|
|
427
|
+
1. **Immutability** โ Filters don't modify input; they return new values
|
|
428
|
+
2. **Composability** โ Filters can be combined to create complex transformations
|
|
429
|
+
3. **Type Safety** โ Generic types help catch errors at development time
|
|
430
|
+
4. **Readability** โ Pipe operator makes data flow explicit and easy to follow
|
|
431
|
+
5. **Extensibility** โ Easy to create domain-specific filters
|
|
432
|
+
|
|
433
|
+
## ๐ค Contributing
|
|
434
|
+
|
|
435
|
+
Contributions are welcome! Please feel free to submit a Pull Request.
|
|
436
|
+
|
|
437
|
+
## ๐ License
|
|
438
|
+
|
|
439
|
+
This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details.
|
|
440
|
+
|
|
441
|
+
---
|
|
442
|
+
|
|
443
|
+
<p align="center">
|
|
444
|
+
Made with โค๏ธ for functional programming enthusiasts
|
|
445
|
+
</p>
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
punctional/__init__.py,sha256=gylyr0Ij6x97I0vc62JzaoRszhcPUdGqwhHl03ooBKY,1271
|
|
2
|
+
punctional/arithmetic.py,sha256=TzU5wJqu0olmEyh-TuHIpMH_2mw-EFoDRxmK6kAFbUU,1071
|
|
3
|
+
punctional/comparison.py,sha256=X9SzAXbNXUR5_lmPaZPaTMqM_0zwlvNPb93pcpipogI,850
|
|
4
|
+
punctional/core.py,sha256=g3VuKNpfbIBoNf8OEAUDtc5bLdr-PAbbMwupnkz8wPE,2838
|
|
5
|
+
punctional/list_filters.py,sha256=Ta9Te7D_qwf32hyQxkThw7nazbSts0jn6gF3qKjgzPk,1127
|
|
6
|
+
punctional/logical.py,sha256=3TR0Bgv-SwUjo4o177hHe8YLTr3jG4Kb13GZBGmEb9w,980
|
|
7
|
+
punctional/monads.py,sha256=zDGr7GMhiGc5whNj81lQTDgxfJtBOuh8IuNGpT7gq5g,9739
|
|
8
|
+
punctional/string.py,sha256=E8Ajo7_Rok55_ZNbDFQgCub0VCM4-fW_bUNswyUqDds,607
|
|
9
|
+
punctional/types.py,sha256=gS8Z3XtHKT0dttuGUMFsZWMoiLKuE51cRZMOVVaNKJw,1724
|
|
10
|
+
punctional-0.1.0.dist-info/METADATA,sha256=bht_JYz963a87ZmZVfSFspxfZAwxd67UrhPqFSOFBls,12337
|
|
11
|
+
punctional-0.1.0.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
|
|
12
|
+
punctional-0.1.0.dist-info/licenses/LICENSE,sha256=-iDh_gcypGAb576TWRWkMhTMvuAaOML4rZwKV1s8Xoo,1070
|
|
13
|
+
punctional-0.1.0.dist-info/RECORD,,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 Mehdi Yaminli
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|