Coverage for /home/ubuntu/yoneda/python/yoneda/monad.py: 100%
116 statements
« prev ^ index » next coverage.py v7.8.0, created at 2025-04-08 16:26 +0000
« prev ^ index » next coverage.py v7.8.0, created at 2025-04-08 16:26 +0000
1from typing import Any, Callable, Generic, Type, TypeVar, Union # noqa: F401
3from functools import partial
5from lunchbox.enforce import Enforce, EnforceError
6import infix
8A = TypeVar('A')
9B = TypeVar('B')
10C = TypeVar('C')
11# ------------------------------------------------------------------------------
14'''
15Monad is a library containing the Monad class and a library of monadic
16functions it calls.
18Haskell equivalence table:
20 =========== ======== ====== ===== ================ ================================
21 **Python** **Haskell** **Haskell Type Signature**
22 -------------------- ------------ -------------------------------------------------
23 prefix infix prefix infix implication signature
24 =========== ======== ====== ===== ================ ================================
25 app │iapp│ <*> Applicative f => f (a -> b) -> fa -> fb
26 bind │ibind│ >>= Monad m => m a -> (a -> m b) -> m b
27 fail │ifail│ fail Monad m => String -> m a
28 fmap │ifmap│ fmap <$> Functor f => (a -> b) -> fa -> fb
29 right │iright│ >> Monad m => m a -> m b -> m b
30 unwrap Monad m => m a -> a
31 wrap │iwrap│ pure Applicative f => a -> f a
32 wrap │iwrap│ return Monad m => a -> m a
33 curry │icurry│
34 dot │idot│ . . (b -> c) -> (a -> b) -> (a -> c)
35 partial_dot . . (b -> c) -> (a -> b) -> (a -> c)
36 =========== ======== ====== ===== ================ ================================
37'''
40def enforce_monad(item):
41 # type: (Any) -> None
42 '''
43 Enforces item being a Monad subclass or instance.
45 Args:
46 item (object): Item to be tested.
48 Raises:
49 EnforceError: If item is not Monad subclass or instance.
50 '''
51 pred = isinstance # type: Any
52 if item.__class__ is type:
53 pred = issubclass
54 if not pred(item, Monad):
55 raise EnforceError(f'{item} is not a subclass or instance of Monad.')
58@infix.or_infix
59def iwrap(*args, **kwargs):
60 return wrap(*args, **kwargs)
63def wrap(monad, data):
64 # type: (Monadlike, A) -> Monad[A]
65 '''
66 Wrap: M -> A -> MA
68 .. image:: images/wrap.png
70 Given a Monad class or instance, create a new Monad with given data.
72 Args:
73 monad (Monad): Monad class or instance.
74 data (Any): Data to be wrapped as Monad.
76 Raises:
77 EnforceError: If monad is not Monad subclass or instance.
79 Returns:
80 Monad[A]: Monad of data.
81 '''
82 enforce_monad(monad)
83 return monad.wrap(data)
86def unwrap(monad):
87 # type: (Monad[A]) -> A
88 '''
89 Unwrap: MA -> A
91 .. image:: images/unwrap.png
93 Return the data of a given Monad instance.
95 Args:
96 monad (Monad): Monad instance.
98 Raises:
99 EnforceError: If monad is not Monad subclass or instance.
101 Returns:
102 A: Monad data.
103 '''
104 enforce_monad(monad)
105 return monad._data
108@infix.or_infix
109def ifmap(*args, **kwargs):
110 return fmap(*args, **kwargs)
113def fmap(func, monad):
114 # type: (Callable[[A], B], Monad[A]) -> Monad[B]
115 '''
116 Functor map: (A -> B) -> MA -> MB
118 .. image:: images/fmap.png
120 Given a Monad of A (MA) and a function A to B, return a Monad of B (MB).
122 Args:
123 func (function): Function (A -> B).
124 monad (Monad): Monad of A.
126 Raises:
127 EnforceError: If monad is not Monad subclass or instance.
129 Returns:
130 Monad[B]: Monad of B.
131 '''
132 enforce_monad(monad)
133 return wrap(monad, func(unwrap(monad)))
136@infix.or_infix
137def iapp(*args, **kwargs):
138 return app(*args, **kwargs)
141def app(monad_func, monad):
142 # type: (Monad[Callable[[A], B]], Monad[A]) -> Monad[B]
143 '''
144 Applicative: M(A -> B) -> MA -> MB
146 .. image:: images/app.png
148 Given a Monad of A (MA) and a Monad of a function A to B, return a Monad
149 of B (MB).
151 Args:
152 monad_func (Monad): Monad of function (A -> B).
153 monad (Monad): Monad of A.
155 Raises:
156 EnforceError: If monad_func is not instance of Monad.
157 EnforceError: If monad is not Monad subclass or instance.
159 Returns:
160 Monad[B]: Monad of B.
161 '''
162 enforce_monad(monad_func)
163 enforce_monad(monad)
164 func = unwrap(monad_func)
165 value = unwrap(monad)
166 return wrap(monad, func(value))
169@infix.or_infix
170def ibind(*args, **kwargs):
171 return bind(*args, **kwargs)
174def bind(func, monad):
175 # type: (Callable[[A], Monad[B]], Monad[A]) -> Monad[B]
176 '''
177 Bind: (A -> MB) -> MA -> MB
179 .. image:: images/bind.png
181 Given a Monad of A (MA) and a function A to MB, return a Monad of B (MB).
183 Args:
184 func (function): Function (A -> MB).
185 monad (Monad): Monad of A.
187 Raises:
188 EnforceError: If monad is not Monad subclass or instance.
190 Returns:
191 Monad[B]: Monad of B.
192 '''
193 enforce_monad(monad)
194 return func(unwrap(monad))
197@infix.or_infix
198def iright(*args, **kwargs):
199 return right(*args, **kwargs)
202def right(monad_a, monad_b):
203 # type: (Monad[A], Monad[B]) -> Monad[B]
204 '''
205 Right: MA -> MB -> MB
207 .. image:: images/right.png
209 Given two Monads, a and b, return the right Monad b.
211 Args:
212 monad_a (Monad): Left monad.
213 monad_b (Monad): Right monad.
215 Raises:
216 EnforceError: If monad is not Monad subclass or instance.
218 Returns:
219 Monad: Right Monad.
220 '''
221 enforce_monad(monad_a)
222 enforce_monad(monad_b)
223 return monad_b
226@infix.or_infix
227def ifail(*args, **kwargs):
228 return fail(*args, **kwargs)
231def fail(monad, error):
232 # type (Monad, Exception) -> Monad[Exception]
233 '''
234 Fail: M -> E -> ME
236 .. image:: images/fail.png
238 Given a Monad and Exception, return a Monad of that Exception.
240 Args:
241 monad (Monad): Monad to wrap error with.
242 error (Exception): Error.
244 Raises:
245 EnforceError: If monad is not Monad subclass or instance.
246 EnforceError: If error is not an instance of Exception.
248 Returns:
249 Monad: Error Monad.
250 '''
251 enforce_monad(monad)
252 msg = 'Error must be an instance of Exception. Given value: {a}.'
253 Enforce(error, 'instance of', Exception, message=msg)
254 return wrap(monad, error)
257def succeed(monad, value):
258 # type (Monad, A) -> Monad[A]
259 '''
260 Succed: M -> A -> MA
262 .. image:: images/wrap.png
264 Given a Monad and a value, return a Monad of that value.
266 Args:
267 monad (Monad): Monad to wrap value with.
268 value (object): Value.
270 Raises:
271 EnforceError: If monad is not Monad subclass or instance.
272 EnforceError: If value is an instance of Exception.
274 Returns:
275 Monad: Monad of value.
276 '''
277 enforce_monad(monad)
278 msg = 'Error must not be an instance of Exception. Given value: {a}.'
279 Enforce(value, 'not instance of', Exception, message=msg)
280 return wrap(monad, value)
283@infix.or_infix
284def icurry(*args, **kwargs):
285 return curry(*args, **kwargs)
288def curry(func, *args, **kwargs):
289 # type: (Callable, Any, Any) -> Callable
290 '''
291 Infix notation for functools.partial.
293 Args:
294 func (function): Function to be curried.
295 args (optional): Arguments.
296 kwargs (optional): Keyword arguments.
298 Returns:
299 function: Curried function.
300 '''
301 return partial(func, *args, **kwargs)
304@infix.or_infix
305def idot(*args, **kwargs):
306 return dot(*args, **kwargs)
309def dot(func_b, func_a):
310 # type: (Callable[[B], C], Callable[[A], B]) -> Callable[[A], C]
311 '''
312 Dot: (b -> c) -> (a -> b) -> (a -> c)
313 fb |idot| fa == fb(fa)
315 Composes two functions.
317 Example:
318 ```
319 fa = lambda x: x + 'a'
320 fb = lambda x: x + 'b'
321 dot(fb, fa)('x') == 'xab'
322 (fb | idot | fa)('x') == 'xab'
323 ```
325 Args:
326 func_b (function): Outer function.
327 func_a (function): Inner function.
329 Returns:
330 partial: Function composition.
331 '''
333 def of(b, a, *args, **kwargs):
334 return b(a(*args, **kwargs))
336 return partial(of, func_b, func_a)
339def partial_dot(func):
340 # type: (Callable[[B], C]) -> partial[Callable[[A], B]]
341 '''
342 Partial Dot: (b -> c) -> (a -> b)
344 Partial version of dot function.
346 Example:
347 ```
348 app = ym.app
349 u = Monad(lambda x: x + 1)
350 v = Monad(lambda x: x + 2)
351 w = Monad(3)
352 Monad(partial_dot) | iapp | u | iapp | v | iapp | w
353 ```
355 Args:
356 func (function): Outer composition function.
358 Returns:
359 partial: Function composition.
360 '''
361 return partial(dot, func)
364def catch(monad, func):
365 # type: (MA, Callable[[A], B]) -> Callable[[A], Union[B, Exception]]
366 '''
367 Catch: MA -> (A -> B) -> (MB | ME)
369 Catches exception and returns it rather then raising an error.
371 Args:
372 monad (Monad): Monad.
373 func (function): Function to attempt.
375 Raises:
376 EnforceError: If monad is not Monad subclass or instance.
378 Returns:
379 object: Partial function with catch logic.
380 '''
381 enforce_monad(monad)
383 def catch_(func, *args, **kwargs):
384 try:
385 return func(*args, **kwargs)
386 except Exception as error:
387 return fail(monad, error)
389 return partial(catch_, func)
392# ------------------------------------------------------------------------------
395class Monad(Generic[A]):
396 '''
397 Monad is a generic base class for monads. It implements all the monad
398 functions as methods which take itself as the first argument.
400 Haskell equivalence table:
402 ====== ===== ====== ===== ================ ========================
403 **Python** **Haskell** **Haskell Type Signature**
404 ------------ ------------ -----------------------------------------
405 prefix infix prefix infix implication signature
406 ====== ===== ====== ===== ================ ========================
407 app ^ <*> Applicative f => f (a -> b) -> fa -> fb
408 bind >> >>= Monad m => m a -> (a -> m b) -> m b
409 fail fail Monad m => String -> m a
410 fmap & fmap <$> Functor f => (a -> b) -> fa -> fb
411 right >> Monad m => m a -> m b -> m b
412 unwrap Monad m => m a -> a
413 wrap pure Applicative f => a -> f a
414 wrap return Monad m => a -> m a
415 ====== ===== ====== ===== ================ ========================
416 '''
418 def __init__(self, data):
419 # type: (A) -> None
420 '''
421 Constructs monad instance.
423 Args:
424 data (object): Data to be wrapped with Monad.
425 '''
426 self._data = data
428 def __repr__(self):
429 # type: () -> str
430 '''
431 String representation of Monad instance.
432 '''
433 return f'{self.__class__.__name__}({self._data.__repr__()})'
435 @classmethod
436 def wrap(cls, data):
437 # type: (A) -> MA
438 '''
439 Wrap: A -> MA
441 Create a new Monad with given data.
443 Args:
444 data (Any): Data to be wrapped as Monad.
446 Returns:
447 Monad[A]: Monad of data.
448 '''
449 return cls(data)
451 def unwrap(self):
452 # type: () -> A
453 '''
454 Unwrap: () -> A
456 Return self._data.
458 Returns:
459 A: Monad data.
460 '''
461 return unwrap(self)
463 def fmap(self, func):
464 # type: (Callable[[A], B]) -> MB
465 '''
466 Functor map: (A -> B) -> MB
468 Given a function A to B, return a Monad of B (MB).
469 Example: m.fmap(lambda x: x + 2)
471 Args:
472 func (function): Function (A -> B).
474 Returns:
475 Monad[B]: Monad of B.
476 '''
477 return fmap(func, self)
479 def app(self, monad_func):
480 # type: (Monad[Callable[[A], B]]) -> MB
481 '''
482 Applicative: M(A -> B) -> MB
484 Given a Monad of a function A to B, return a Monad of B (MB).
486 Args:
487 monad_func (Monad): Monad of function (A -> B).
489 Returns:
490 Monad[B]: Monad of B.
491 '''
492 return app(monad_func, self)
494 def bind(self, func):
495 # type: (Callable[[A], MB]) -> MB
496 '''
497 Bind: (A -> MB) -> MB
499 Given a function A to MB, return a Monad of B (MB).
501 Args:
502 func (function): Function (A -> MB).
504 Returns:
505 Monad[B]: Monad of B.
506 '''
507 return bind(func, self)
509 def right(self, monad):
510 # type: (MB) -> MB
511 '''
512 Right: MB -> MB
514 Return given monad (self is left, given monad is right).
516 Args:
517 monad (Monad): Right monad.
519 Returns:
520 Monad: Right Monad.
521 '''
522 return right(self, monad)
524 def fail(self, error):
525 # type (Exception) -> Monad[Exception]
526 '''
527 Fail: E -> ME
529 Return a Monad of given Exception.
531 Args:
532 error (Exception): Error.
534 Returns:
535 Monad: Error Monad.
536 '''
537 return fail(self, error)
539 def __and__(self, func):
540 # type: (Callable[[A], B]) -> MB
541 '''
542 Functor map: (A -> B) -> MB
544 Given a function A to B, return a Monad of B (MB).
545 Example: m & (lambda x: x + 2)
547 Args:
548 func (function): Function (A -> B).
550 Returns:
551 Monad[B]: Monad of B.
552 '''
553 return self.fmap(func)
555 def __xor__(self, monad_func):
556 # type: (MA, Monad[Callable[[A], B]]) -> MB
557 '''
558 Applicative: MA -> M(A -> B) -> MB
560 .. image:: images/app.png
562 Given a Monad of A (MA) and a Monad of a function A to B, return a Monad
563 of B (MB).
564 Example: m ^ Monad.wrap(lambda x: x + 2)
566 Args:
567 monad (Monad): Monad of A.
568 func (Monad): Monad of function (A -> B).
570 Raises:
571 EnforceError: If monad is not Monad subclass or instance.
573 Returns:
574 Monad[B]: Monad of B.
575 '''
576 return self.app(monad_func)
578 def __rshift__(self, func):
579 # type: (Callable[[A], MB]) -> MB
580 '''
581 Bind: (A -> MB) -> MB
583 Given a function A to MB, return a Monad of B (MB).
584 Example: m >> Monad
586 Args:
587 func (function): Function (A -> MB).
589 Returns:
590 Monad[B]: Monad of B.
591 '''
592 return self.bind(func)
595Monadlike = Union[Monad, Type[Monad]]
596MA = Monad[A]
597MB = Monad[B]
598MC = Monad[C]