├── monad_do ├── functor.py ├── monad.py ├── __init__.py ├── list_.py ├── io.py ├── reader.py ├── combinators.py ├── cont.py ├── free.py ├── state.py ├── maybe.py ├── either.py ├── applicative.py ├── do_simple.py └── do_cached.py ├── setup.py ├── README.md ├── .gitignore └── examples.py /monad_do/functor.py: -------------------------------------------------------------------------------- 1 | class Functor: 2 | def map(self, f): 3 | raise NotImplementedError 4 | 5 | @staticmethod 6 | def from_native(a): 7 | return a 8 | -------------------------------------------------------------------------------- /monad_do/monad.py: -------------------------------------------------------------------------------- 1 | from .applicative import Applicative 2 | from .combinators import * 3 | 4 | 5 | class Monad(Applicative): 6 | def bind(self, k): 7 | raise NotImplementedError 8 | 9 | def join(self): 10 | return self.bind(identity) 11 | 12 | def ap1(self, arg): 13 | return self.bind(lambda a: arg.bind(compose(self.pure, a))) 14 | 15 | 16 | def mcompose(*k): 17 | def composed(a): 18 | return reduce(lambda a, k: a.bind(k), reversed(k), a) 19 | return composed 20 | -------------------------------------------------------------------------------- /monad_do/__init__.py: -------------------------------------------------------------------------------- 1 | from .applicative import Applicative, liftA 2 | from .combinators import * 3 | from .cont import Cont, callcc 4 | from .do_cached import do 5 | from .either import Either, Left, Right 6 | from .free import Free, Pure, Roll 7 | from .functor import Functor 8 | from .io import IO, as_io, io_input, io_print 9 | from .list_ import List 10 | from .maybe import Just, Maybe, Nothing 11 | from .monad import Monad, mcompose 12 | from .reader import Reader, ask, local 13 | from .state import State, get, gets, modify, put 14 | -------------------------------------------------------------------------------- /monad_do/list_.py: -------------------------------------------------------------------------------- 1 | import itertools 2 | 3 | from .monad import Monad 4 | 5 | 6 | class List(Monad, list): 7 | def __repr__(self): 8 | return f'List({super().__repr__()})' 9 | 10 | @staticmethod 11 | def pure(a): 12 | return List([a]) 13 | 14 | def bind(self, k): 15 | return List(list(itertools.chain(*map(k, self)))) 16 | 17 | @classmethod 18 | def from_native(cls, a): 19 | if isinstance(a, list) and not isinstance(a, List): 20 | return List(a) 21 | else: 22 | return a 23 | -------------------------------------------------------------------------------- /monad_do/io.py: -------------------------------------------------------------------------------- 1 | from .combinators import * 2 | from .do_cached import do 3 | from .free import Free 4 | from .functor import Functor 5 | 6 | 7 | class _IOFunctor(Functor): 8 | def __init__(self, proc): 9 | self.proc = proc 10 | 11 | def map(self, f): 12 | return _IOFunctor(compose(f, self.proc)) 13 | 14 | def get(self): 15 | return self.proc() 16 | 17 | 18 | IO = Free 19 | as_io = compose(IO.lift, _IOFunctor) 20 | 21 | 22 | def io_input(prompt=None): 23 | @wraps(input) 24 | def wrapped(): 25 | return input(prompt) 26 | return as_io(wrapped) 27 | 28 | 29 | def io_print(*args, **kwargs): 30 | @wraps(print) 31 | def wrapped(): 32 | print(*args, **kwargs) 33 | return as_io(wrapped) 34 | -------------------------------------------------------------------------------- /monad_do/reader.py: -------------------------------------------------------------------------------- 1 | from .monad import Monad 2 | from .combinators import identity, constant 3 | 4 | 5 | class Reader(Monad): 6 | def __init__(self, f): 7 | self.f = f 8 | 9 | @staticmethod 10 | def pure(a): 11 | return Reader(constant(a)) 12 | 13 | def bind(self, g): 14 | def f(e): 15 | a = self.f(e) 16 | return g(a)(e) 17 | return Reader(f) 18 | 19 | def run(self, e): 20 | return self.f(e) 21 | 22 | def __call__(self, e): 23 | return self.f(e) 24 | 25 | @staticmethod 26 | def from_native(f): 27 | if not isinstance(f, Reader) and callable(f): 28 | return Reader(f) 29 | else: 30 | return f 31 | 32 | 33 | ask = Reader(identity) 34 | 35 | 36 | def local(f, reader): 37 | return Reader(lambda e: reader.run(f(e))) 38 | -------------------------------------------------------------------------------- /monad_do/combinators.py: -------------------------------------------------------------------------------- 1 | from functools import reduce, wraps 2 | 3 | 4 | def identity(x): 5 | return x 6 | 7 | 8 | def constant(c): 9 | return lambda x: c 10 | 11 | 12 | def flip(f): 13 | @wraps(f) 14 | def flipped(b): 15 | def wrapped(a): 16 | return f(a)(b) 17 | return wrapped 18 | return flipped 19 | 20 | 21 | def compose(*funcs): 22 | if not funcs: 23 | return identity 24 | 25 | def composed(*args, **kwargs): 26 | first_result = funcs[-1](*args, **kwargs) 27 | return reduce(lambda x, f: f(x), reversed(funcs[:-1]), first_result) 28 | 29 | return composed 30 | 31 | 32 | def apply(func, *args, **kwargs): 33 | return func(*args, **kwargs) 34 | 35 | 36 | def uncurry(func): 37 | @wraps(func) 38 | def wrapped(args, kwargs={}): 39 | return func(*args, **kwargs) 40 | return wrapped 41 | -------------------------------------------------------------------------------- /monad_do/cont.py: -------------------------------------------------------------------------------- 1 | from .combinators import * 2 | from .monad import Monad 3 | 4 | 5 | class Cont(Monad): 6 | def __init__(self, c): 7 | self.c = c 8 | 9 | def __repr__(self): 10 | return f'Cont({repr(self.c)})' 11 | 12 | @staticmethod 13 | def pure(a): 14 | return Cont(lambda f: f(a)) 15 | 16 | def bind(self, k): 17 | return Cont(lambda b2r: self.c(flip(lambda a: k(a).c)(b2r))) 18 | 19 | def run(self, f): 20 | return self.c(f) 21 | 22 | def __call__(self, f): 23 | return self.c(f) 24 | 25 | @staticmethod 26 | def from_native(f): 27 | if not isinstance(f, Cont) and callable(f): 28 | return Cont(f) 29 | else: 30 | return f 31 | 32 | 33 | def callcc(f): 34 | def cont(k): 35 | def ret(a): 36 | return Cont(lambda _: k(a)) 37 | return f(ret).run(k) 38 | return Cont(cont) 39 | -------------------------------------------------------------------------------- /monad_do/free.py: -------------------------------------------------------------------------------- 1 | from .monad import Monad 2 | 3 | 4 | class Free(Monad): 5 | @staticmethod 6 | def pure(a): 7 | return Pure(a) 8 | 9 | @staticmethod 10 | def lift(a): 11 | return Roll(a.map(Pure)) 12 | 13 | def fold(self, phi): 14 | raise NotImplementedError 15 | 16 | def run(self, *args, **kwargs): 17 | return self.fold(lambda f: f.get(*args, **kwargs)) 18 | 19 | 20 | class Pure(Free): 21 | def __init__(self, value): 22 | self.value = value 23 | 24 | def bind(self, k): 25 | return k(self.value) 26 | 27 | def fold(self, phi): 28 | return self.value 29 | 30 | 31 | class Roll(Free): 32 | def __init__(self, value): 33 | self.value = value 34 | 35 | def bind(self, k): 36 | return Roll(self.value.map(lambda x: x.bind(k))) 37 | 38 | def fold(self, phi): 39 | return phi(self.value.map(lambda x: x.fold(phi))) 40 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | import setuptools 2 | 3 | with open('README.md', 'r') as fh: 4 | long_description = fh.read() 5 | 6 | setuptools.setup( 7 | name='monad_do', 8 | version='0.1.1', 9 | author='TRCYX', 10 | license='MIT', 11 | description='Do notation in Python', 12 | long_description=long_description, 13 | long_description_content_type='text/markdown', 14 | url='https://github.com/TRCYX/py_monad_do', 15 | packages=['monad_do'], 16 | install_requires=['fastcache'], 17 | classifiers=[ 18 | 'Programming Language :: Python :: 3', 19 | 'License :: OSI Approved :: MIT License', 20 | 'Operating System :: OS Independent', 21 | 'Intended Audience :: Developers', 22 | 'Intended Audience :: Education', 23 | 'Topic :: Education', 24 | 'Topic :: Software Development :: Libraries', 25 | # 'Typing :: TypedTyping :: Typed' 26 | ], 27 | keywords='monad' 28 | ) 29 | -------------------------------------------------------------------------------- /monad_do/state.py: -------------------------------------------------------------------------------- 1 | from .monad import Monad 2 | 3 | 4 | class State(Monad): 5 | def __init__(self, f): 6 | self.f = f 7 | 8 | @staticmethod 9 | def pure(a): 10 | return State(lambda s: (a, s)) 11 | 12 | def bind(self, k): 13 | def f(s): 14 | a, s_ = self.f(s) 15 | return k(a)(s_) 16 | return State(f) 17 | 18 | def run(self, s): 19 | return self.f(s) 20 | 21 | def __call__(self, s): 22 | return self.f(s) 23 | 24 | def eval(self, s): 25 | return self.f(s)[0] 26 | 27 | def exec(self, s): 28 | return self.f(s)[1] 29 | 30 | @staticmethod 31 | def from_native(f): 32 | if not isinstance(f, State) and callable(f): 33 | return State(f) 34 | else: 35 | return f 36 | 37 | 38 | get = State(lambda s: (s, s)) 39 | 40 | 41 | def gets(f): 42 | return State(lambda s: (f(s), s)) 43 | 44 | 45 | def put(s): 46 | return State(lambda _: ((), s)) 47 | 48 | 49 | def modify(f): 50 | return State(lambda s: ((), f(s))) 51 | -------------------------------------------------------------------------------- /monad_do/maybe.py: -------------------------------------------------------------------------------- 1 | from .monad import Monad 2 | 3 | 4 | class Maybe(Monad): 5 | @staticmethod 6 | def pure(b): 7 | return Just(b) 8 | 9 | def bind(self, k): 10 | raise NotImplementedError 11 | 12 | def is_nothing(self): 13 | return not self.is_just() 14 | 15 | def is_just(self): 16 | raise NotImplementedError 17 | 18 | def match(self, n, j): 19 | raise NotImplementedError 20 | 21 | @staticmethod 22 | def from_native(a): 23 | if not isinstance(a, Maybe): 24 | return Nothing if a is None else Just(a) 25 | else: 26 | return a 27 | 28 | 29 | class NothingType(Maybe): 30 | def __repr__(self): 31 | return 'Nothing' 32 | 33 | def bind(self, k): 34 | return Nothing 35 | 36 | def is_just(self): 37 | return False 38 | 39 | def match(self, n, j): 40 | return n 41 | 42 | 43 | Nothing = NothingType() 44 | 45 | 46 | class Just(Maybe): 47 | def __init__(self, value): 48 | self.value = value 49 | 50 | def __repr__(self): 51 | return f'Just({repr(self.value)})' 52 | 53 | def bind(self, k): 54 | return k(self.value) 55 | 56 | def is_just(self): 57 | return True 58 | 59 | def match(self, n, j): 60 | return j(self.value) 61 | -------------------------------------------------------------------------------- /monad_do/either.py: -------------------------------------------------------------------------------- 1 | from .monad import Monad 2 | 3 | 4 | class Either(Monad): 5 | @staticmethod 6 | def pure(b): 7 | return Right(b) 8 | 9 | def bind(self, k): 10 | raise NotImplementedError 11 | 12 | def is_left(self): 13 | return not self.is_right() 14 | 15 | def is_right(self): 16 | raise NotImplementedError 17 | 18 | def match(self, l, r): 19 | raise NotImplementedError 20 | 21 | @staticmethod 22 | def from_native(a): 23 | if not isinstance(a, Either): 24 | return Left(None) if a is None else Right(a) 25 | else: 26 | return a 27 | 28 | 29 | class Left(Either): 30 | def __init__(self, value): 31 | self.value = value 32 | 33 | def __repr__(self): 34 | return f'Left({repr(self.value)})' 35 | 36 | def bind(self, k): 37 | return Left(self.value) 38 | 39 | def is_right(self): 40 | return False 41 | 42 | def match(self, l, r): 43 | return l(self.value) 44 | 45 | 46 | class Right(Either): 47 | def __init__(self, value): 48 | self.value = value 49 | 50 | def __repr__(self): 51 | return f'Right({repr(self.value)})' 52 | 53 | def bind(self, k): 54 | return k(self.value) 55 | 56 | def is_right(self): 57 | return True 58 | 59 | def match(self, l, r): 60 | return r(self.value) 61 | -------------------------------------------------------------------------------- /monad_do/applicative.py: -------------------------------------------------------------------------------- 1 | from functools import partial, reduce 2 | 3 | from .combinators import * 4 | from .functor import Functor 5 | 6 | 7 | class Applicative(Functor): 8 | @staticmethod 9 | def pure(a): 10 | raise NotImplementedError 11 | 12 | def ap1(self, arg): 13 | raise NotImplementedError 14 | 15 | def product(self, other): 16 | return self.map(partial(partial, lambda a, b: (a, b))).ap1(other) 17 | 18 | def ap(self, *args, **kwargs): 19 | f1 = self.map(partial(partial, lambda f, args: partial(f, *args))) 20 | args_combined = reduce(lambda l, a: l.product(a).map( 21 | lambda t: t[0] + [t[1]]), args, self.pure([])) 22 | f2 = f1.ap1(args_combined).map(partial(partial, lambda f, kwargs: f(**kwargs))) 23 | kwargs_combined = reduce(lambda d, a: d.product(a[1]).map( 24 | lambda t: {**t[0], a[0]: t[1]}), kwargs.items(), self.pure({})) 25 | return f2.ap1(kwargs_combined) 26 | 27 | def map(self, f): 28 | return self.pure(f).ap1(self) 29 | 30 | 31 | def liftA(func): 32 | @wraps(func) 33 | def wrapped(*args, **kwargs): 34 | if args: 35 | representative = args[0] 36 | elif kwargs: 37 | representative = next(iter(kwargs.values())) 38 | else: 39 | raise NotImplementedError("No Applicative specified") 40 | return representative.pure(func).ap(*args, **kwargs) 41 | return wrapped 42 | -------------------------------------------------------------------------------- /monad_do/do_simple.py: -------------------------------------------------------------------------------- 1 | from functools import wraps 2 | 3 | from .combinators import * 4 | from .cont import * 5 | from .either import * 6 | 7 | 8 | class _Continuation: 9 | def __init__(self, generator_creator, history=[None]): 10 | self.generator_creator = generator_creator 11 | self.history = history 12 | 13 | def send(self, b): 14 | return _Continuation(self.generator_creator, self.history + [b]) 15 | 16 | def produce_value(self): 17 | g = self.generator_creator() 18 | try: 19 | for h in self.history: 20 | a = g.send(h) 21 | return Left(a) 22 | except StopIteration as exc: 23 | return Right(exc.value) 24 | 25 | 26 | def _transform(_cont): 27 | e = _cont.produce_value() 28 | 29 | def if_left(cont_b): 30 | def transformed(b): 31 | return _transform(_cont.send(b)) 32 | return cont_b.bind(transformed) 33 | 34 | return e.match(if_left, identity) 35 | 36 | 37 | def _do_impl_cont(f): 38 | @wraps(f) 39 | def wrapped(*args, **kwargs): 40 | return _transform(_Continuation(lambda: f(*args, **kwargs))) 41 | return wrapped 42 | 43 | 44 | def _to_cont(f, m): 45 | @wraps(f) 46 | def wrapped(*args, **kwargs): 47 | g = f(*args, **kwargs) 48 | a = None 49 | try: 50 | while True: 51 | a = yield Cont(m.from_native(g.send(a)).bind) 52 | except StopIteration as exc: 53 | return Cont.pure(m.from_native(exc.value)) 54 | return wrapped 55 | 56 | 57 | def do(m): 58 | if m is Cont: 59 | return _do_impl_cont 60 | else: 61 | return lambda f: compose(lambda c: c.run(identity), _do_impl_cont(_to_cont(f, m))) 62 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # py_monad_do 2 | 3 | A simple monad do notation implementation with nice syntax using generators. Also includes sample monads such as `Maybe` and `List`. 4 | 5 | Sample code: 6 | 7 | ```python 8 | from monad_do import * 9 | 10 | @do(List) 11 | def test_list(a, b): 12 | x = yield [a * 10, a] # Native lists are coerced into monad Lists here. 13 | y = yield [b * 100, b * 10, b] 14 | return [x + y] 15 | 16 | 17 | print(test_list(3, 4)) # List([430, 70, 34, 403, 43, 7]) 18 | ``` 19 | 20 | Monad instances derive from the `Monad` class and provide the methods `pure`(static) and `bind`. The `do` decorators binds the values yielded from a generator to its later computation. In a sense, `yield` works like `<-` in Haskell. 21 | 22 | Note that generators are uncopyable, so if some code needs to be run more than once (such as the case for the `List` monad), the generator is run from the beginning once again, with the values sent into it recorded to eliminate duplicate computation. This requires that the generators decorated by `do` to be more or less "pure". 23 | 24 | The `do` decorator is implemented inside `monad_do.do_cached`. There is also a simpler implementation in `monad_do.do_simple` which sketches the basic idea, but runs the generator from the beginning for each `yield`. 25 | 26 | The implementation is primarily inspired by these following materials: 27 | - [Monads and Do-Blocks in Python](https://blog.bede.io/do-notation-for-monads-in-python/) implements a do notation for the `List` monad through recording sent values. 28 | - [Monads in Python (with nice syntax!)](http://www.valuedlessons.com/2008/01/monads-in-python-with-nice-syntax.html) also implements a similar do notation which universally handle monads that only run the generator once. 29 | - [The Mother of all Monads](https://www.schoolofhaskell.com/school/to-infinity-and-beyond/pick-of-the-week/the-mother-of-all-monads) gives the idea of implementing other monads through the `Cont` monad. 30 | 31 | Type hints are not incorporated. For now, the weak support for function types (on arguments) makes type hinting more of a burden then something helpful. 32 | 33 | ### Acknowledgements 34 | 35 | Thank @danoneata for adding the `Reader` Monad. 36 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | pip-wheel-metadata/ 24 | share/python-wheels/ 25 | *.egg-info/ 26 | .installed.cfg 27 | *.egg 28 | MANIFEST 29 | 30 | # PyInstaller 31 | # Usually these files are written by a python script from a template 32 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 33 | *.manifest 34 | *.spec 35 | 36 | # Installer logs 37 | pip-log.txt 38 | pip-delete-this-directory.txt 39 | 40 | # Unit test / coverage reports 41 | htmlcov/ 42 | .tox/ 43 | .nox/ 44 | .coverage 45 | .coverage.* 46 | .cache 47 | nosetests.xml 48 | coverage.xml 49 | *.cover 50 | *.py,cover 51 | .hypothesis/ 52 | .pytest_cache/ 53 | 54 | # Translations 55 | *.mo 56 | *.pot 57 | 58 | # Django stuff: 59 | *.log 60 | local_settings.py 61 | db.sqlite3 62 | db.sqlite3-journal 63 | 64 | # Flask stuff: 65 | instance/ 66 | .webassets-cache 67 | 68 | # Scrapy stuff: 69 | .scrapy 70 | 71 | # Sphinx documentation 72 | docs/_build/ 73 | 74 | # PyBuilder 75 | target/ 76 | 77 | # Jupyter Notebook 78 | .ipynb_checkpoints 79 | 80 | # IPython 81 | profile_default/ 82 | ipython_config.py 83 | 84 | # pyenv 85 | .python-version 86 | 87 | # pipenv 88 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 89 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 90 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 91 | # install all needed dependencies. 92 | #Pipfile.lock 93 | 94 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 95 | __pypackages__/ 96 | 97 | # Celery stuff 98 | celerybeat-schedule 99 | celerybeat.pid 100 | 101 | # SageMath parsed files 102 | *.sage.py 103 | 104 | # Environments 105 | .env 106 | .venv 107 | env/ 108 | venv/ 109 | ENV/ 110 | env.bak/ 111 | venv.bak/ 112 | 113 | # Spyder project settings 114 | .spyderproject 115 | .spyproject 116 | 117 | # Rope project settings 118 | .ropeproject 119 | 120 | # mkdocs documentation 121 | /site 122 | 123 | # mypy 124 | .mypy_cache/ 125 | .dmypy.json 126 | dmypy.json 127 | 128 | # Pyre type checker 129 | .pyre/ 130 | 131 | .vscode/ 132 | -------------------------------------------------------------------------------- /examples.py: -------------------------------------------------------------------------------- 1 | from monad_do import * 2 | 3 | # from monad_do.do_simple import do 4 | 5 | 6 | def fdiv(a, b): 7 | if b == 0: 8 | return Left(f'{a} cannot be divided by 0') 9 | else: 10 | return Right(a / b) 11 | 12 | 13 | @do(Either) 14 | def test_either(a, b): 15 | print('run once') 16 | val1 = yield fdiv(2.0, a) 17 | val2 = yield fdiv(b, 1.0) 18 | val3 = yield fdiv(val1, val2) 19 | return val3 20 | 21 | 22 | print(test_either(2, 4)) 23 | print(test_either(0, 4)) 24 | print(test_either(2, 0)) 25 | print(test_either(0, 0)) 26 | 27 | 28 | @do(List) 29 | def test_list(a, b): 30 | x = yield [a * 10, a] 31 | y = yield [b * 100, b * 10, b] 32 | return [x + y] 33 | 34 | 35 | print(test_list(3, 4)) 36 | 37 | 38 | from collections import namedtuple 39 | 40 | CONFIG = { 41 | "first-name": "Napoléon", 42 | "last-name": "Bonaparte", 43 | "age": "52", 44 | "country": "France", 45 | } 46 | 47 | Person = namedtuple("Person", "name age") 48 | 49 | read_name = Reader(lambda config: config["first-name"] + " " + config["last-name"]) 50 | read_age = Reader(lambda config: int(config["age"])) 51 | 52 | @do(Reader) 53 | def test_reader(): 54 | name = yield read_name 55 | age = yield read_age 56 | env = yield ask 57 | print(env) 58 | return Reader.pure(Person(name, age)) 59 | 60 | print(test_reader().run(CONFIG)) 61 | 62 | 63 | def push(x): 64 | return modify(lambda l: l + [x]) 65 | 66 | 67 | pop = modify(lambda l: l[:-1]) 68 | 69 | 70 | @do(State) 71 | def test_state(x): 72 | not_empty = gets(bool) 73 | last = gets(lambda l: l[-1]) 74 | while (yield not_empty) and (yield last) < x: 75 | yield pop 76 | yield push(x) 77 | return State.pure(()) 78 | 79 | 80 | print(test_state(5).run([6, 7, 3, 1])) 81 | print(test_state(5).run([4, 3, 1])) 82 | 83 | 84 | def test_cont(n): 85 | def iter(n, p): 86 | @do(Cont) 87 | def body(return_): 88 | if n <= 1: 89 | yield return_(p) 90 | else: 91 | yield iter(n - 1, p * n)(return_) 92 | raise RuntimeError("Never reached") 93 | return body 94 | return callcc(iter(n, 1)) 95 | 96 | 97 | print(test_cont(10).run(identity)) 98 | 99 | 100 | @do(IO) 101 | def echo(): 102 | @do(IO) 103 | def loop(): 104 | s = yield io_input('echo> ') 105 | if s != 'exit': 106 | yield io_print(s) 107 | return loop() 108 | else: 109 | return IO.pure(()) 110 | 111 | yield io_print('-- The echo program --') 112 | yield io_print('Type "exit" to exit') 113 | return loop() 114 | 115 | e = echo() 116 | e.run() 117 | -------------------------------------------------------------------------------- /monad_do/do_cached.py: -------------------------------------------------------------------------------- 1 | from functools import wraps 2 | 3 | from fastcache import clru_cache 4 | 5 | from .combinators import * 6 | from .cont import * 7 | from .either import * 8 | 9 | 10 | class _Continuation: 11 | def __init__(self, generator_creator, history=[], value_to_feed=None, saved_generator=None): 12 | self.generator_creator = generator_creator 13 | self.history = history 14 | self.saved_generator = saved_generator 15 | self.value_to_feed = value_to_feed 16 | self.value_produced = None 17 | 18 | @clru_cache(typed=True, unhashable='ignore') 19 | def send(b): 20 | assert not isinstance(self.value_produced, Right) 21 | new_history = self.history + [self.value_to_feed] 22 | 23 | if self.saved_generator is None: 24 | return _Continuation(self.generator_creator, new_history, b) 25 | 26 | if self.value_produced is None: 27 | self._feed() 28 | ret = _Continuation(self.generator_creator, 29 | new_history, b, self.saved_generator) 30 | self._clear() 31 | return ret 32 | 33 | self.send = send 34 | 35 | def _create_generator(self): 36 | g = self.generator_creator() 37 | for h in self.history: 38 | g.send(h) 39 | self.saved_generator = g 40 | 41 | def _feed(self): 42 | try: 43 | a = self.saved_generator.send(self.value_to_feed) 44 | except StopIteration as exc: 45 | self.value_produced = Right(exc.value) 46 | else: 47 | self.value_produced = Left(a) 48 | 49 | def _clear(self): 50 | self.saved_generator = None 51 | 52 | def produce_value(self): 53 | if self.value_produced is None: 54 | if self.saved_generator is None: 55 | self._create_generator() 56 | self._feed() 57 | 58 | return self.value_produced 59 | 60 | 61 | def _transform(_cont): 62 | e = _cont.produce_value() 63 | 64 | def if_left(cont_b): 65 | def transformed(b): 66 | return _transform(_cont.send(b)) 67 | return cont_b.bind(transformed) 68 | 69 | return e.match(if_left, identity) 70 | 71 | 72 | def _do_impl_cont(f): 73 | @wraps(f) 74 | def wrapped(*args, **kwargs): 75 | return _transform(_Continuation(lambda: f(*args, **kwargs))) 76 | return wrapped 77 | 78 | 79 | def _to_cont(f, m): 80 | @wraps(f) 81 | def wrapped(*args, **kwargs): 82 | g = f(*args, **kwargs) 83 | a = None 84 | try: 85 | while True: 86 | a = yield Cont(m.from_native(g.send(a)).bind) 87 | except StopIteration as exc: 88 | return Cont.pure(m.from_native(exc.value)) 89 | return wrapped 90 | 91 | 92 | def do(m): 93 | if m is Cont: 94 | return _do_impl_cont 95 | else: 96 | return lambda f: compose(lambda c: c.run(identity), _do_impl_cont(_to_cont(f, m))) 97 | --------------------------------------------------------------------------------