Coverage for /home/ubuntu/yoneda/python/yoneda/monad.py: 100%
116 statements
« prev ^ index » next coverage.py v7.8.0, created at 2025-12-03 03:56 +0000
« prev ^ index » next coverage.py v7.8.0, created at 2025-12-03 03:56 +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)
339# TODO: Figure out how to make this type signature work
340def partial_dot(func):
341 # type: (Callable[[B], C]) -> Any
342 '''
343 Partial Dot: (b -> c) -> (a -> b)
345 Partial version of dot function.
347 Example:
348 ```
349 app = ym.app
350 u = Monad(lambda x: x + 1)
351 v = Monad(lambda x: x + 2)
352 w = Monad(3)
353 Monad(partial_dot) | iapp | u | iapp | v | iapp | w
354 ```
356 Args:
357 func (function): Outer composition function.
359 Returns:
360 partial: Function composition.
361 '''
362 return partial(dot, func)
365def catch(monad, func):
366 # type: (MA, Callable[[A], B]) -> Callable[[A], Union[B, Exception]]
367 '''
368 Catch: MA -> (A -> B) -> (MB | ME)
370 Catches exception and returns it rather then raising an error.
372 Args:
373 monad (Monad): Monad.
374 func (function): Function to attempt.
376 Raises:
377 EnforceError: If monad is not Monad subclass or instance.
379 Returns:
380 object: Partial function with catch logic.
381 '''
382 enforce_monad(monad)
384 def catch_(func, *args, **kwargs):
385 try:
386 return func(*args, **kwargs)
387 except Exception as error:
388 return fail(monad, error)
390 return partial(catch_, func)
393# ------------------------------------------------------------------------------
396class Monad(Generic[A]):
397 '''
398 Monad is a generic base class for monads. It implements all the monad
399 functions as methods which take itself as the first argument.
401 Haskell equivalence table:
403 ====== ===== ====== ===== ================ ========================
404 **Python** **Haskell** **Haskell Type Signature**
405 ------------ ------------ -----------------------------------------
406 prefix infix prefix infix implication signature
407 ====== ===== ====== ===== ================ ========================
408 app ^ <*> Applicative f => f (a -> b) -> fa -> fb
409 bind >> >>= Monad m => m a -> (a -> m b) -> m b
410 fail fail Monad m => String -> m a
411 fmap & fmap <$> Functor f => (a -> b) -> fa -> fb
412 right >> Monad m => m a -> m b -> m b
413 unwrap Monad m => m a -> a
414 wrap pure Applicative f => a -> f a
415 wrap return Monad m => a -> m a
416 ====== ===== ====== ===== ================ ========================
417 '''
419 def __init__(self, data):
420 # type: (A) -> None
421 '''
422 Constructs monad instance.
424 Args:
425 data (object): Data to be wrapped with Monad.
426 '''
427 self._data = data
429 def __repr__(self):
430 # type: () -> str
431 '''
432 String representation of Monad instance.
433 '''
434 return f'{self.__class__.__name__}({self._data.__repr__()})'
436 @classmethod
437 def wrap(cls, data):
438 # type: (A) -> MA
439 '''
440 Wrap: A -> MA
442 Create a new Monad with given data.
444 Args:
445 data (Any): Data to be wrapped as Monad.
447 Returns:
448 Monad[A]: Monad of data.
449 '''
450 return cls(data)
452 def unwrap(self):
453 # type: () -> A
454 '''
455 Unwrap: () -> A
457 Return self._data.
459 Returns:
460 A: Monad data.
461 '''
462 return unwrap(self)
464 def fmap(self, func):
465 # type: (Callable[[A], B]) -> MB
466 '''
467 Functor map: (A -> B) -> MB
469 Given a function A to B, return a Monad of B (MB).
470 Example: m.fmap(lambda x: x + 2)
472 Args:
473 func (function): Function (A -> B).
475 Returns:
476 Monad[B]: Monad of B.
477 '''
478 return fmap(func, self)
480 def app(self, monad_func):
481 # type: (Monad[Callable[[A], B]]) -> MB
482 '''
483 Applicative: M(A -> B) -> MB
485 Given a Monad of a function A to B, return a Monad of B (MB).
487 Args:
488 monad_func (Monad): Monad of function (A -> B).
490 Returns:
491 Monad[B]: Monad of B.
492 '''
493 return app(monad_func, self)
495 def bind(self, func):
496 # type: (Callable[[A], MB]) -> MB
497 '''
498 Bind: (A -> MB) -> MB
500 Given a function A to MB, return a Monad of B (MB).
502 Args:
503 func (function): Function (A -> MB).
505 Returns:
506 Monad[B]: Monad of B.
507 '''
508 return bind(func, self)
510 def right(self, monad):
511 # type: (MB) -> MB
512 '''
513 Right: MB -> MB
515 Return given monad (self is left, given monad is right).
517 Args:
518 monad (Monad): Right monad.
520 Returns:
521 Monad: Right Monad.
522 '''
523 return right(self, monad)
525 def fail(self, error):
526 # type (Exception) -> Monad[Exception]
527 '''
528 Fail: E -> ME
530 Return a Monad of given Exception.
532 Args:
533 error (Exception): Error.
535 Returns:
536 Monad: Error Monad.
537 '''
538 return fail(self, error)
540 def __and__(self, func):
541 # type: (Callable[[A], B]) -> MB
542 '''
543 Functor map: (A -> B) -> MB
545 Given a function A to B, return a Monad of B (MB).
546 Example: m & (lambda x: x + 2)
548 Args:
549 func (function): Function (A -> B).
551 Returns:
552 Monad[B]: Monad of B.
553 '''
554 return self.fmap(func)
556 def __xor__(self, monad_func):
557 # type: (MA, Monad[Callable[[A], B]]) -> MB
558 '''
559 Applicative: MA -> M(A -> B) -> MB
561 .. image:: images/app.png
563 Given a Monad of A (MA) and a Monad of a function A to B, return a Monad
564 of B (MB).
565 Example: m ^ Monad.wrap(lambda x: x + 2)
567 Args:
568 monad (Monad): Monad of A.
569 func (Monad): Monad of function (A -> B).
571 Raises:
572 EnforceError: If monad is not Monad subclass or instance.
574 Returns:
575 Monad[B]: Monad of B.
576 '''
577 return self.app(monad_func)
579 def __rshift__(self, func):
580 # type: (Callable[[A], MB]) -> MB
581 '''
582 Bind: (A -> MB) -> MB
584 Given a function A to MB, return a Monad of B (MB).
585 Example: m >> Monad
587 Args:
588 func (function): Function (A -> MB).
590 Returns:
591 Monad[B]: Monad of B.
592 '''
593 return self.bind(func)
596Monadlike = Union[Monad, Type[Monad]]
597MA = Monad[A]
598MB = Monad[B]
599MC = Monad[C]