├── amino ├── anon │ ├── __init__.py │ ├── error.py │ ├── prod │ │ ├── __init__.py │ │ ├── attr.py │ │ └── complex.py │ └── boot.py ├── tc │ ├── __init__.py │ ├── show.py │ ├── monoidal.py │ ├── tap.py │ ├── monoid.py │ ├── applicative.py │ ├── apply.py │ ├── functor.py │ ├── monad.py │ ├── traverse.py │ ├── zip.py │ ├── apply_n.py │ ├── optional.py │ ├── flat_map.py │ ├── context.py │ └── foldable.py ├── util │ ├── __init__.py │ ├── fs.py │ ├── random.py │ ├── tuple.py │ ├── mod.py │ ├── fun.py │ ├── numeric.py │ ├── ast.py │ ├── exception.py │ ├── tpe.py │ ├── string.py │ ├── trace.py │ └── coconut_mypy.py ├── instances │ ├── __init__.py │ ├── std │ │ ├── __init__.py │ │ └── datetime.py │ ├── io.py │ ├── map.py │ ├── maybe.py │ ├── either.py │ ├── list.py │ └── lazy_list.py ├── lenses │ ├── __init__.py │ ├── lens.py │ └── tree.py ├── data │ └── __init__.py ├── meta │ ├── __init__.py │ ├── gen.py │ └── gen_state.py ├── string │ ├── __init__.py │ └── hues.py ├── state │ ├── base.py │ └── __init__.py ├── test │ ├── __init__.py │ ├── sure.py │ ├── time.py │ ├── spec_spec.py │ ├── path.py │ ├── spec.py │ └── sure_ext.py ├── json │ ├── __init__.py │ ├── parse.py │ ├── encoder.py │ ├── decoder.py │ ├── encoders.py │ └── data.py ├── async.py ├── future.py ├── id.py ├── transformer.py ├── options.py ├── env_vars.py ├── __init__.py ├── mod.py ├── dispatch.py ├── do.py ├── typecheck.py ├── list.py ├── algebra.py ├── lazy.py ├── func.py ├── regex.py ├── eff.py ├── boolean.py ├── map.py ├── case.py ├── bi_rose_tree.py ├── eval.py └── lazy_list.py ├── unit ├── tc │ ├── __init__.py │ └── context_spec.py ├── instances │ ├── __init__.py │ └── std │ │ ├── __init__.py │ │ └── datetime_spec.py ├── __init__.py ├── id_spec.py ├── coconut_mypy_spec.py ├── algebra_spec.py ├── lazy_spec.py ├── dat_spec.py ├── regex_spec.py ├── case_spec.py ├── do_spec.py ├── bi_rose_tree_spec.py ├── eval_spec.py ├── either_spec.py ├── lens_spec.py ├── tree_spec.py ├── eff_spec.py ├── map_spec.py ├── state_spec.py ├── implicit_spec.py ├── maybe_spec.py ├── list_spec.py ├── io_spec.py ├── lazy_list_spec.py └── json_spec.py ├── requirements.txt ├── .gitlab-ci.yml ├── scripts └── generate.py ├── .travis.yml ├── setup.py └── LICENSE /amino/anon/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /amino/tc/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /amino/util/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /amino/instances/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /amino/lenses/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /unit/tc/__init__.py: -------------------------------------------------------------------------------- 1 | 2 | 3 | __all__ = () 4 | -------------------------------------------------------------------------------- /amino/data/__init__.py: -------------------------------------------------------------------------------- 1 | 2 | 3 | __all__ = () 4 | -------------------------------------------------------------------------------- /amino/meta/__init__.py: -------------------------------------------------------------------------------- 1 | 2 | 3 | __all__ = () 4 | -------------------------------------------------------------------------------- /amino/string/__init__.py: -------------------------------------------------------------------------------- 1 | 2 | 3 | __all__ = () 4 | -------------------------------------------------------------------------------- /unit/instances/__init__.py: -------------------------------------------------------------------------------- 1 | 2 | 3 | __all__ = () 4 | -------------------------------------------------------------------------------- /amino/instances/std/__init__.py: -------------------------------------------------------------------------------- 1 | 2 | 3 | __all__ = () 4 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | toolz 2 | spec 3 | sure 4 | lenses 5 | -------------------------------------------------------------------------------- /unit/instances/std/__init__.py: -------------------------------------------------------------------------------- 1 | 2 | 3 | __all__ = () 4 | -------------------------------------------------------------------------------- /amino/state/base.py: -------------------------------------------------------------------------------- 1 | class StateT: 2 | pass 3 | 4 | __all__ = ('StateT',) 5 | -------------------------------------------------------------------------------- /amino/anon/error.py: -------------------------------------------------------------------------------- 1 | 2 | class AnonError(Exception): 3 | pass 4 | 5 | __all__ = ('AnonError',) 6 | -------------------------------------------------------------------------------- /.gitlab-ci.yml: -------------------------------------------------------------------------------- 1 | before_script: 2 | - pip install -r requirements.txt 3 | 4 | unit: 5 | script: 6 | - spec unit 7 | -------------------------------------------------------------------------------- /scripts/generate.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | from amino.meta.gen_state import generate_states 4 | 5 | generate_states().fatal 6 | -------------------------------------------------------------------------------- /unit/__init__.py: -------------------------------------------------------------------------------- 1 | import subprocess 2 | 3 | subprocess.run(['coconut', '--target', '3.6', '--quiet', '--strict', '--line-numbers', 'amino']) 4 | 5 | __all__ = () 6 | -------------------------------------------------------------------------------- /amino/util/fs.py: -------------------------------------------------------------------------------- 1 | from amino import Path, IO 2 | 3 | 4 | def mkdir(path: Path) -> IO[None]: 5 | return IO.delay(path.mkdir, parents=True, exist_ok=True) 6 | 7 | 8 | __all__ = ('create_dir',) 9 | -------------------------------------------------------------------------------- /amino/test/__init__.py: -------------------------------------------------------------------------------- 1 | from .path import setup, create_temp_file, temp_file, temp_path, temp_dir, fixture_path, load_fixture 2 | 3 | __all__ = ('setup', 'create_temp_file', 'temp_file', 'temp_path', 'temp_dir', 'fixture_path', 'load_fixture') 4 | -------------------------------------------------------------------------------- /amino/json/__init__.py: -------------------------------------------------------------------------------- 1 | from amino.json.decoder import decode_json 2 | from amino.json.encoder import encode_json, dump_json 3 | import amino.json.encoders 4 | import amino.json.decoders 5 | 6 | __all__ = ('decode_json', 'encode_json', 'dump_json') 7 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: required 2 | dist: trusty 3 | language: python 4 | env: 5 | - TRAVIS=true 6 | python: 7 | - "3.5.1" 8 | script: 9 | - spec unit 10 | notifications: 11 | email: 12 | on_success: change 13 | on_failure: change 14 | -------------------------------------------------------------------------------- /amino/anon/prod/__init__.py: -------------------------------------------------------------------------------- 1 | from amino.anon.prod.method import MethodLambdaInst 2 | from amino.anon.prod.attr import AttrLambdaInst 3 | from amino.anon.prod.complex import ComplexLambdaInst 4 | 5 | __all__ = ('MethodLambdaInst', 'AttrLambdaInst', 'ComplexLambdaInst') 6 | -------------------------------------------------------------------------------- /amino/tc/show.py: -------------------------------------------------------------------------------- 1 | from typing import TypeVar, Generic 2 | 3 | from amino.tc.base import TypeClass 4 | 5 | A = TypeVar('A') 6 | 7 | 8 | class Show(TypeClass): 9 | 10 | def show(self, obj): 11 | return str(obj) 12 | 13 | __all__ = ('Show',) 14 | -------------------------------------------------------------------------------- /amino/test/sure.py: -------------------------------------------------------------------------------- 1 | from amino.test.sure_ext import install_assertion_builder, AssBuilder 2 | 3 | 4 | class SureSpec: 5 | 6 | def setup(self) -> None: 7 | install_assertion_builder(AssBuilder) # type: ignore 8 | 9 | __all__ = ('SureSpec',) 10 | -------------------------------------------------------------------------------- /amino/util/random.py: -------------------------------------------------------------------------------- 1 | import random 2 | import string 3 | 4 | 5 | class Random(object): 6 | _char_pool = string.ascii_uppercase + string.digits 7 | 8 | @staticmethod 9 | def string(n=10): 10 | return ''.join(random.choice(Random._char_pool) for _ in range(n)) 11 | 12 | __all__ = ('Random',) 13 | -------------------------------------------------------------------------------- /unit/id_spec.py: -------------------------------------------------------------------------------- 1 | from amino.test.spec_spec import Spec 2 | from amino.id import Id 3 | 4 | 5 | class IdSpec(Spec): 6 | 7 | def monad(self) -> None: 8 | Id(5).flat_map(lambda a: Id(a + 6)).should.equal(Id(11)) 9 | Id(8).map(lambda a: a / 2).should.equal(Id(4)) 10 | 11 | __all__ = ('IdSpec',) 12 | -------------------------------------------------------------------------------- /amino/tc/monoidal.py: -------------------------------------------------------------------------------- 1 | import abc 2 | from typing import TypeVar, Generic 3 | 4 | from amino.tc.base import TypeClass 5 | 6 | F = TypeVar('F') 7 | 8 | 9 | class Monoidal(TypeClass): 10 | 11 | @abc.abstractmethod 12 | def product(self, fa: F, fb: F) -> F: 13 | ... 14 | 15 | __all__ = ('Monoidal',) 16 | -------------------------------------------------------------------------------- /amino/tc/tap.py: -------------------------------------------------------------------------------- 1 | from typing import TypeVar, Generic, Callable, Any 2 | 3 | from amino.tc.base import TypeClass 4 | 5 | A = TypeVar('A') 6 | B = TypeVar('B') 7 | 8 | 9 | class Tap(Generic[A], TypeClass): 10 | 11 | def tap(self, a: A, f: Callable[[A], Any]) -> A: 12 | f(a) 13 | return a 14 | 15 | __all__ = ('Tap',) 16 | -------------------------------------------------------------------------------- /amino/tc/monoid.py: -------------------------------------------------------------------------------- 1 | import abc 2 | from typing import TypeVar 3 | 4 | from amino.tc.base import TypeClass 5 | 6 | A = TypeVar('A') 7 | 8 | 9 | class Monoid(TypeClass): 10 | 11 | @abc.abstractproperty 12 | def empty(self) -> A: 13 | ... 14 | 15 | @abc.abstractmethod 16 | def combine(self, a: A, b: A) -> A: 17 | ... 18 | 19 | __all__ = ('Monoid',) 20 | -------------------------------------------------------------------------------- /amino/state/__init__.py: -------------------------------------------------------------------------------- 1 | from amino.state.maybe import MaybeState 2 | from amino.state.either import EitherState 3 | from amino.state.io import IOState 4 | from amino.state.eval import EvalState 5 | from amino.state.id import IdState 6 | from amino.state.base import StateT 7 | 8 | State = IdState 9 | 10 | __all__ = ('MaybeState', 'EitherState', 'IOState', 'EvalState', 'IdState', 'StateT', 'State',) 11 | -------------------------------------------------------------------------------- /amino/util/tuple.py: -------------------------------------------------------------------------------- 1 | from typing import Tuple, TypeVar, Callable 2 | 3 | from amino import Maybe, Just, Nothing 4 | 5 | A = TypeVar('A') 6 | 7 | 8 | def lift_tuple(index: int) -> Callable[[Tuple[A, ...]], Maybe[A]]: 9 | def lift_tuple(data: Tuple[A, ...]) -> Maybe[A]: 10 | return Just(data[index]) if len(data) > index else Nothing 11 | return lift_tuple 12 | 13 | 14 | __all__ = ('lift_tuple',) 15 | -------------------------------------------------------------------------------- /amino/tc/applicative.py: -------------------------------------------------------------------------------- 1 | import abc 2 | from typing import TypeVar, Generic 3 | 4 | from amino.tc.apply import Apply 5 | 6 | F = TypeVar('F') 7 | A = TypeVar('A') 8 | 9 | 10 | class Applicative(Apply): 11 | 12 | @abc.abstractmethod 13 | def pure(self, a: A) -> F: 14 | ... 15 | 16 | @property 17 | def unit(self) -> F: 18 | return self.pure(None) 19 | 20 | __all__ = ('Applicative',) 21 | -------------------------------------------------------------------------------- /amino/util/mod.py: -------------------------------------------------------------------------------- 1 | import importlib 2 | from typing import Optional, Any 3 | 4 | 5 | def unsafe_import_name(modname: str, name: str) -> Optional[Any]: 6 | mod = importlib.import_module(modname) 7 | return getattr(mod, name) if hasattr(mod, name) else None 8 | 9 | 10 | def class_path(cls: type) -> str: 11 | return f'{cls.__module__}.{cls.__name__}' 12 | 13 | 14 | __all__ = ('unsafe_import_name', 'class_path') 15 | -------------------------------------------------------------------------------- /amino/async.py: -------------------------------------------------------------------------------- 1 | from amino import List 2 | 3 | 4 | async def gather_sync(data, f): 5 | results = List() 6 | for el in data: 7 | results = results + List(await f(el)) 8 | return results 9 | 10 | 11 | async def gather_sync_flat(data, f): 12 | results = List() 13 | for el in data: 14 | results = results + (await f(el)).to_list 15 | return results 16 | 17 | __all__ = ('gather_sync', 'gather_sync_flat') 18 | -------------------------------------------------------------------------------- /amino/test/time.py: -------------------------------------------------------------------------------- 1 | import time 2 | import inspect 3 | from contextlib import contextmanager 4 | from typing import Iterator, cast 5 | from types import FrameType 6 | 7 | from amino.logging import module_log 8 | 9 | log = module_log() 10 | 11 | 12 | @contextmanager 13 | def timed() -> Iterator[None]: 14 | mod = inspect.currentframe() 15 | caller = cast(FrameType, mod).f_back.f_back 16 | name = caller.f_globals['__name__'] 17 | start = time.time() 18 | yield 19 | total = time.time() - start 20 | log.info(f'>> {name}: {total}') 21 | 22 | 23 | __all__ = ('timed',) 24 | -------------------------------------------------------------------------------- /unit/coconut_mypy_spec.py: -------------------------------------------------------------------------------- 1 | from amino.util.coconut_mypy import process_output 2 | from amino import List, Map 3 | from amino.test.spec_spec import Spec 4 | 5 | 6 | class CoconutMypySpec(Spec): 7 | 8 | def substitute_lnums(self) -> None: 9 | lines = List( 10 | 'sooo...In module imported from: asdf', 11 | 'amino/maybe.py:116:5: error: broken', 12 | 'foo/bar/__coconut__.py:22: error: nutt' 13 | ) 14 | return 15 | process_output(lines).should.equal(List(Map(lnum=82, text='broken', valid=1, maker_name='mypy', col=5))) 16 | 17 | __all__ = ('CoconutMypySpec') 18 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup, find_packages 2 | 3 | version_parts = (13, 0, 1) 4 | version = '.'.join(map(str, version_parts)) 5 | 6 | setup( 7 | name='amino', 8 | description='functional data structures and type classes', 9 | version=version, 10 | author='Torsten Schmits', 11 | author_email='torstenschmits@gmail.com', 12 | license='MIT', 13 | url='https://github.com/tek/amino', 14 | packages=find_packages(exclude=['unit', 'unit.*']), 15 | install_requires=[ 16 | 'toolz', 17 | 'lenses==0.4.0', 18 | ], 19 | tests_require=[ 20 | 'kallikrein', 21 | ], 22 | ) 23 | -------------------------------------------------------------------------------- /amino/json/parse.py: -------------------------------------------------------------------------------- 1 | import json 2 | from typing import Any 3 | 4 | from amino import Either, Lists, Map, Try 5 | from amino.json.data import Json, JsonArray, JsonScalar, JsonObject, JsonNull, JsonError 6 | 7 | 8 | def to_json(a: Any) -> Json: 9 | return ( 10 | JsonArray(Lists.wrap(a) / to_json) 11 | if isinstance(a, (list, tuple)) else 12 | JsonObject(Map(a).valmap(to_json)) 13 | if isinstance(a, dict) else 14 | JsonNull(None) 15 | if a is None else 16 | JsonScalar(a) 17 | ) 18 | 19 | 20 | def parse_json(payload: str) -> Either[str, Json]: 21 | return (Try(json.loads, payload) / to_json).lmap(lambda e: JsonError(payload, e)) 22 | -------------------------------------------------------------------------------- /amino/instances/std/datetime.py: -------------------------------------------------------------------------------- 1 | from datetime import timedelta 2 | 3 | from amino.tc.base import TypeClass, ImplicitInstances 4 | from amino.tc.monoid import Monoid 5 | from amino import Map 6 | from amino.lazy import lazy 7 | 8 | 9 | class TimedeltaInstances(ImplicitInstances, tpe=timedelta): 10 | 11 | @lazy 12 | def _instances(self) -> Map[str, TypeClass]: 13 | return Map({Monoid: TimeDeltaMonoid()}) 14 | 15 | 16 | class TimeDeltaMonoid(Monoid): 17 | 18 | @property 19 | def empty(self) -> timedelta: 20 | return timedelta(seconds=0) 21 | 22 | def combine(self, a: timedelta, b: timedelta) -> timedelta: 23 | return a + b 24 | 25 | __all__ = ('TimedeltaInstances',) 26 | -------------------------------------------------------------------------------- /amino/tc/apply.py: -------------------------------------------------------------------------------- 1 | import abc 2 | from typing import TypeVar, Callable, Tuple, Generic 3 | 4 | from amino.tc.functor import Functor 5 | from amino.tc.monoidal import Monoidal 6 | 7 | F = TypeVar('F') 8 | A = TypeVar('A') 9 | B = TypeVar('B') 10 | Z = TypeVar('Z') 11 | 12 | 13 | class Apply(Functor, Monoidal): 14 | 15 | @abc.abstractmethod 16 | def ap(self, fa: F, f: F) -> F: 17 | ''' f should be an F[Callable[[A], B]] 18 | ''' 19 | ... 20 | 21 | def ap2(self, fa: F, fb: F, f: Callable[[A, B], Z]) -> F: 22 | def unpack(tp: Tuple[A, B]): 23 | return f(tp[0], tp[1]) 24 | return self.map(self.product(fa, fb), unpack) 25 | 26 | __all__ = ('Apply',) 27 | -------------------------------------------------------------------------------- /unit/algebra_spec.py: -------------------------------------------------------------------------------- 1 | from typing import Generic, TypeVar 2 | 3 | from amino.test.spec_spec import Spec 4 | from amino.algebra import Algebra 5 | from amino.case import Case 6 | from amino import ADT 7 | 8 | A = TypeVar('A') 9 | 10 | 11 | class Base(Generic[A], ADT['Base[A]']): 12 | pass 13 | 14 | 15 | class A1(Base[A]): 16 | pass 17 | 18 | 19 | class A2(Base[A]): 20 | pass 21 | 22 | 23 | class _tostr(Generic[A], Case[Base, str], alg=Base): 24 | 25 | def a1(self, a: A1[A]) -> str: 26 | return str(a) 27 | 28 | def a2(self, a: A2) -> str: 29 | return str(a) 30 | 31 | 32 | class AlgebraSpec(Spec): 33 | 34 | def subclasses(self) -> None: 35 | pass 36 | 37 | 38 | __all__ = ('AlgebraSpec',) 39 | -------------------------------------------------------------------------------- /amino/util/fun.py: -------------------------------------------------------------------------------- 1 | from types import FunctionType, MethodType 2 | from typing import Callable, Union 3 | 4 | 5 | def lambda_str(f: Union[Callable, str]) -> str: 6 | if isinstance(f, MethodType): 7 | return '{}.{}'.format(f.__self__.__class__.__name__, f.__name__) 8 | elif isinstance(f, FunctionType): 9 | return f.__name__ 10 | elif isinstance(f, str): 11 | return f 12 | else: 13 | return str(f) 14 | 15 | 16 | def format_funcall(fun: Union[Callable, str], args: tuple, kwargs: dict) -> str: 17 | from amino.map import Map 18 | kw = Map(kwargs).map2('{}={!r}'.format) 19 | a = list(map(repr, args)) + list(kw) 20 | args_fmt = ', '.join(a) 21 | return '{}({})'.format(lambda_str(fun), args_fmt) 22 | 23 | __all__ = ('lambda_str', 'format_funcall') 24 | -------------------------------------------------------------------------------- /amino/future.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | from typing import Generic, TypeVar, Callable 3 | 4 | A = TypeVar('A') 5 | B = TypeVar('B') 6 | 7 | 8 | class Future(Generic[A], asyncio.Future): 9 | 10 | def flat_map(self, f: Callable[[A], 'Future[B]']) -> 'Future[B]': 11 | wrapper = Future() # type: ignore 12 | def cb(future: asyncio.Future): 13 | res = future.result() 14 | if res.success: 15 | f(res).relay_result(wrapper) 16 | else: 17 | wrapper.cancel() 18 | self.add_done_callback(cb) 19 | return wrapper 20 | 21 | def relay_result(self, wrapper: 'Future[A]'): 22 | def setter(f: asyncio.Future): 23 | wrapper.set_result(f.result()) 24 | self.add_done_callback(setter) 25 | 26 | 27 | __all__ = ('Future',) 28 | -------------------------------------------------------------------------------- /unit/instances/std/datetime_spec.py: -------------------------------------------------------------------------------- 1 | from datetime import timedelta 2 | 3 | from amino.test.spec_spec import Spec 4 | from amino.tc.monoid import Monoid 5 | from amino.instances.std.datetime import TimedeltaInstances # NOQA 6 | from amino import List 7 | 8 | 9 | class TimedeltaSpec(Spec): 10 | 11 | def empty(self) -> None: 12 | Monoid.fatal(timedelta).empty.should.equal(timedelta(seconds=0)) 13 | 14 | def combine(self) -> None: 15 | a = timedelta(seconds=5) 16 | b = timedelta(seconds=12) 17 | Monoid.fatal(timedelta).combine(a, b).should.equal(a + b) 18 | 19 | def fold(self) -> None: 20 | secs = List(4, 23, 45, 71) 21 | result = secs.map(lambda a: timedelta(seconds=a)).fold(timedelta) 22 | result.should.equal(timedelta(seconds=sum(secs))) 23 | 24 | __all__ = ('TimedeltaSpec',) 25 | -------------------------------------------------------------------------------- /amino/lenses/lens.py: -------------------------------------------------------------------------------- 1 | from typing import Iterable, TypeVar, Any 2 | 3 | from lenses.ui import UnboundLens 4 | from lenses.optics import TrivialIso 5 | from lenses.hooks.hook_funcs import from_iter 6 | 7 | from amino import List, Lists 8 | 9 | 10 | class UnboundLensA(UnboundLens): 11 | 12 | def __getattr__(self, name): 13 | # type: (str) -> Any 14 | if name.startswith('__') and name.endswith('__') or name.startswith('call_mut_') or name.startswith('call_'): 15 | return super().__getattr__(name) 16 | else: 17 | return self.GetAttr(name) 18 | 19 | 20 | lens = UnboundLensA(TrivialIso()) 21 | 22 | A = TypeVar('A') 23 | 24 | 25 | def list_from_iter(self, iterable: Iterable[A]) -> List[A]: 26 | return Lists.wrap(iterable) 27 | 28 | 29 | from_iter.register(list)(list_from_iter) 30 | 31 | __all__ = ('lens',) 32 | -------------------------------------------------------------------------------- /amino/id.py: -------------------------------------------------------------------------------- 1 | from typing import Generic, TypeVar, Callable, Any 2 | 3 | from amino.tc.base import F 4 | from amino.tc.monad import Monad 5 | from amino.util.string import ToStr 6 | from amino import List 7 | 8 | A = TypeVar('A') 9 | B = TypeVar('B') 10 | 11 | 12 | class Id(Generic[A], F[A], ToStr, implicits=True, auto=True): 13 | 14 | def __init__(self, value: A) -> None: 15 | self.value = value 16 | 17 | def __eq__(self, other: Any) -> bool: 18 | return isinstance(other, Id) and self.value == other.value 19 | 20 | def _arg_desc(self) -> List[str]: 21 | return List(str(self.value)) 22 | 23 | 24 | class IdMonad(Monad, tpe=Id): 25 | 26 | def pure(self, a: A) -> Id[A]: 27 | return Id(a) 28 | 29 | def flat_map(self, fa: Id[A], f: Callable[[A], Id[B]]) -> Id[B]: 30 | return f(fa.value) 31 | 32 | 33 | __all__ = ('Id',) 34 | -------------------------------------------------------------------------------- /unit/lazy_spec.py: -------------------------------------------------------------------------------- 1 | from amino.lazy import lazy, Lazy 2 | from amino.test.spec_spec import Spec 3 | 4 | 5 | class A(Lazy): 6 | __slots__ = ['a'] 7 | i = 0 8 | 9 | @lazy 10 | def test(self) -> int: 11 | A.i += 1 12 | return A.i 13 | 14 | 15 | class B(object): 16 | i = 0 17 | 18 | @lazy 19 | def test(self) -> int: 20 | B.i += 1 21 | return B.i 22 | 23 | 24 | class LazySpec(Spec): 25 | 26 | def _impl(self, tpe): 27 | t = tpe() 28 | t2 = tpe() 29 | tpe.i.should.equal(0) 30 | t.test.should.equal(1) 31 | t2.test.should.equal(2) 32 | t.test.should.equal(1) 33 | t.test.should.equal(1) 34 | t.test.should.equal(1) 35 | tpe.i.should.equal(2) 36 | 37 | def slots(self): 38 | self._impl(A) 39 | 40 | def dict(self): 41 | self._impl(B) 42 | 43 | __all__ = ('LazySpec',) 44 | -------------------------------------------------------------------------------- /amino/util/numeric.py: -------------------------------------------------------------------------------- 1 | from numbers import Number 2 | from typing import Any, Callable 3 | 4 | from amino import may, Right, Left, Maybe, Either 5 | 6 | 7 | @may 8 | def try_convert_int(data: Any) -> Maybe[int]: 9 | if ( 10 | isinstance(data, Number) or 11 | (isinstance(data, str) and data.isdigit()) 12 | ): 13 | return int(data) 14 | 15 | 16 | def parse_int(i: Any) -> Either[str, int]: 17 | return Right(i) if isinstance(i, int) else ( 18 | Right(int(i)) if isinstance(i, str) and i.isdigit() else 19 | Left('could not parse int {}'.format(i)) 20 | ) 21 | 22 | 23 | def add(inc: int) -> Callable[[int], int]: 24 | def add(z: int) -> int: 25 | return z + inc 26 | return add 27 | 28 | 29 | def sub(inc: int) -> Callable[[int], int]: 30 | def sub(z: int) -> int: 31 | return z - inc 32 | return sub 33 | 34 | 35 | __all__ = ('try_convert_int', 'parse_int', 'add',) 36 | -------------------------------------------------------------------------------- /amino/anon/boot.py: -------------------------------------------------------------------------------- 1 | from typing import Any, cast 2 | 3 | from amino import options 4 | from amino.util.mod import unsafe_import_name 5 | from amino.anon.prod.attr import AttrLambda 6 | from amino.anon.prod.method import MethodLambda 7 | from amino.anon.prod.complex import ComplexLambda 8 | 9 | _: AttrLambda = cast(AttrLambda, None) 10 | __: MethodLambda = cast(MethodLambda, None) 11 | L: ComplexLambda = cast(ComplexLambda, None) 12 | 13 | 14 | def set(mod: str) -> None: 15 | def name(name: str) -> Any: 16 | return unsafe_import_name(mod, name) 17 | global _, __, L 18 | _ = name('AttrLambdaInst') 19 | __ = name('MethodLambdaInst') 20 | L = name('ComplexLambdaInst') 21 | 22 | 23 | def set_debug() -> None: 24 | set('amino.anon.debug') 25 | 26 | 27 | def set_prod() -> None: 28 | set('amino.anon.prod') 29 | 30 | if options.anon_debug: 31 | set_debug() 32 | else: 33 | set_prod() 34 | 35 | __all__ = ('set_debug', 'set_prod', '_', '__', 'L') 36 | -------------------------------------------------------------------------------- /amino/transformer.py: -------------------------------------------------------------------------------- 1 | import abc 2 | from typing import Generic, Callable, Any, TypeVar 3 | 4 | from amino import L, _ 5 | 6 | A = TypeVar('A') 7 | 8 | 9 | class Transformer(Generic[A], metaclass=abc.ABCMeta): 10 | 11 | def __init__(self, val: A) -> None: 12 | self.val = val 13 | 14 | @abc.abstractmethod 15 | def pure(self, b) -> A: 16 | ... 17 | 18 | def flat_map(self, f: Callable[[A], Any]) -> 'Transformer': 19 | return self.__class__(f(self.val)) # type: ignore 20 | 21 | __floordiv__ = flat_map 22 | 23 | def map(self, f: Callable[[A], Any]): 24 | return self.flat_map(L(f)(_) >> self.pure) 25 | 26 | __truediv__ = map 27 | 28 | def effect(self, f: Callable[[A], Any]): 29 | f(self.val) 30 | return self 31 | 32 | __mod__ = effect 33 | 34 | def effect0(self, f: Callable[[], Any]): 35 | f() 36 | return self 37 | 38 | __matmul__ = effect0 39 | 40 | __all__ = ('Transformer',) 41 | -------------------------------------------------------------------------------- /unit/dat_spec.py: -------------------------------------------------------------------------------- 1 | from amino.dat import Dat 2 | from amino.test.spec_spec import Spec 3 | 4 | from lenses import bind 5 | 6 | 7 | class V(Dat['V']): 8 | 9 | def __init__(self, a: int, b: str) -> None: 10 | self.a = a 11 | self.b = b 12 | 13 | 14 | class CC(Dat['CC']): 15 | 16 | def __init__(self, v: V, b: str='asdf', c: int=6) -> None: 17 | self.v = v 18 | self.b = b 19 | self.c = c 20 | 21 | 22 | c1 = CC(V(1, 'v'), 'cc') 23 | c2 = CC(V(2, 'v'), 'cc') 24 | 25 | 26 | class DatSpec(Spec): 27 | 28 | def copy(self) -> None: 29 | c3: CC = c1.copy(b='foo') 30 | c3.b.should.equal('foo') 31 | c4 = c3.set.c(7) 32 | c4.c.should.equal(7) 33 | 34 | def eq(self) -> None: 35 | c1.should.equal(CC(V(1, 'v'), 'cc')) 36 | c1.should_not.equal(c2) 37 | (c1 == c2).should_not.be.ok 38 | 39 | def lens(self) -> None: 40 | bind(c1).v.a.set(2).should.equal(c2) 41 | 42 | __all__ = ('DatSpec',) 43 | -------------------------------------------------------------------------------- /amino/tc/functor.py: -------------------------------------------------------------------------------- 1 | import abc 2 | from typing import TypeVar, Callable, Any, List 3 | 4 | from amino.tc.base import TypeClass, tc_prop 5 | from amino.func import ReplaceVal 6 | from amino.tc.apply_n import ApplyN 7 | 8 | F = TypeVar('F') 9 | A = TypeVar('A') 10 | B = TypeVar('B') 11 | 12 | 13 | class Functor(TypeClass, ApplyN): 14 | 15 | def apply_n_funcs(self) -> List[str]: 16 | return ['map'] 17 | 18 | @abc.abstractmethod 19 | def map(self, fa: F, f: Callable[[A], B]) -> F: 20 | ... 21 | 22 | def __truediv__(self, fa, f): 23 | return self.map(fa, f) 24 | 25 | def replace(self, fa: F, b: B) -> F: 26 | return self.map(fa, ReplaceVal(b)) 27 | 28 | @tc_prop 29 | def void(self, fa: F) -> F: 30 | return self.replace(fa, None) 31 | 32 | def foreach(self, fa: F, f: Callable[[A], Any]) -> F: 33 | def effect(a: A) -> A: 34 | f(a) 35 | return a 36 | return self.map(fa, effect) 37 | 38 | __mod__ = foreach 39 | 40 | __all__ = ('Functor',) 41 | -------------------------------------------------------------------------------- /amino/options.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | 4 | class EnvOption: 5 | 6 | def __init__(self, name: str) -> None: 7 | self.name = name 8 | 9 | @property 10 | def exists(self) -> bool: 11 | return self.name in os.environ 12 | 13 | def __bool__(self) -> bool: 14 | return self.exists 15 | 16 | @property 17 | def value(self) -> 'amino.Maybe[str]': 18 | import amino 19 | return amino.env[self.name] 20 | 21 | def __str__(self) -> str: 22 | value = self.value.map(lambda a: f'={a}') | ' is unset' 23 | return f'{self.name}{value}' 24 | 25 | def set(self, value: str) -> None: 26 | import amino 27 | amino.env[self.name] = value 28 | 29 | 30 | development = EnvOption('AMINO_DEVELOPMENT') 31 | integration_test = EnvOption('AMINO_INTEGRATION') 32 | anon_debug = EnvOption('AMINO_ANON_DEBUG') 33 | io_debug = EnvOption('AMINO_IO_DEBUG') 34 | env_xdg_data_dir = EnvOption('XDG_DATA_DIR') 35 | 36 | __all__ = ('development', 'integration_test', 'anon_debug', 'io_debug', 'env_xdg_data_dir') 37 | -------------------------------------------------------------------------------- /amino/instances/io.py: -------------------------------------------------------------------------------- 1 | from typing import Callable, TypeVar, Type 2 | 3 | from amino import Just, L, _, Nothing, Eval, Map 4 | from amino.tc.monad import Monad 5 | from amino.tc.base import ImplicitInstances, TypeClass 6 | from amino.lazy import lazy 7 | from amino.io import IO 8 | from amino.util.fun import lambda_str 9 | 10 | A = TypeVar('A') 11 | B = TypeVar('B') 12 | 13 | 14 | class IOInstances(ImplicitInstances): 15 | 16 | @lazy 17 | def _instances(self) -> Map[Type[TypeClass], TypeClass]: 18 | from amino.map import Map 19 | return Map({Monad: IOMonad()}) 20 | 21 | 22 | class IOMonad(Monad): 23 | 24 | def pure(self, a: A) -> IO[A]: 25 | return IO.now(a) 26 | 27 | def flat_map(self, fa: IO[A], f: Callable[[A], IO[B]]) -> IO[B]: 28 | return fa.flat_map(f) 29 | 30 | def map(self, fa: IO[A], f: Callable[[A], B]) -> IO[B]: 31 | s = Eval.later(lambda: f'map({lambda_str(f)})') 32 | mapper = L(f)(_) >> L(IO.now)(_) 33 | return fa.flat_map(mapper, Nothing, Just(s)) 34 | 35 | __all__ = ('IOInstances',) 36 | -------------------------------------------------------------------------------- /unit/regex_spec.py: -------------------------------------------------------------------------------- 1 | from amino.test.spec_spec import Spec 2 | from amino.regex import Regex 3 | from amino import List, Left 4 | 5 | 6 | class RegexSpec(Spec): 7 | 8 | def match(self): 9 | n1 = 'grp1' 10 | n2 = 'grp2' 11 | r = Regex('pre:(?P<{}>\w+)/(\w+)%(\w+)(?=:)'.format(n1)) 12 | g1 = List.random_string() 13 | g2 = List.random_string() 14 | g3 = List.random_string() 15 | suf = 'suf' 16 | whole = 'pre:{}/{}%{}'.format(g1, g2, g3) 17 | s = '{}:{}'.format(whole, suf) 18 | res = r.match(s) 19 | res.should.be.right 20 | m = res.value 21 | m.g(n1).should.contain(g1) 22 | m.g(n2).should.be.empty 23 | m.l.should.equal(List(g1, g2, g3)) 24 | m.match.should.equal(whole) 25 | 26 | def optional(self) -> None: 27 | r = Regex('(?P.*?\.py)(:(?P\d+))?$') 28 | m = r.match('/foo/bar/file.py') 29 | m.should.be.right 30 | m.value.group('lnum').should.equal(Left('group `lnum` did not match')) 31 | 32 | __all__ = ('RegexSpec',) 33 | -------------------------------------------------------------------------------- /unit/case_spec.py: -------------------------------------------------------------------------------- 1 | from amino import ADT 2 | from amino.case import CaseRec, Term 3 | from amino.test.spec_spec import Spec 4 | 5 | 6 | class _Num(ADT['_Num']): 7 | pass 8 | 9 | 10 | class _Int(_Num): 11 | 12 | def __init__(self, i: int) -> None: 13 | self.i = i 14 | 15 | 16 | class _Float(_Num): 17 | 18 | def __init__(self, f: float) -> None: 19 | self.f = f 20 | 21 | 22 | class _Prod(_Num): 23 | 24 | def __init__(self, p: int) -> None: 25 | self.p = p 26 | 27 | 28 | class _rec(CaseRec[int, int], alg=_Num): 29 | 30 | def __init__(self, base: int) -> None: 31 | self.base = base 32 | 33 | def _int(self, n: _Int) -> int: 34 | return self(_Prod(self.base * n.i)) 35 | 36 | def _float(self, n: _Float) -> int: 37 | return self(_Int(int(n))) 38 | 39 | def _prod(self, n: _Prod) -> int: 40 | return Term(n.p + 7) 41 | 42 | 43 | class CaseSpec(Spec): 44 | 45 | def _rec(self) -> None: 46 | r = _rec(5)(_Int(6)).eval() 47 | r.should.equal(37) 48 | 49 | 50 | __all__ = ('CaseSpec',) 51 | -------------------------------------------------------------------------------- /amino/tc/monad.py: -------------------------------------------------------------------------------- 1 | from typing import TypeVar, Callable 2 | 3 | from amino.tc.flat_map import FlatMap 4 | from amino.tc.applicative import Applicative 5 | from amino.data.list import List 6 | 7 | F = TypeVar('F') 8 | A = TypeVar('A') 9 | B = TypeVar('B') 10 | 11 | 12 | class Monad(FlatMap, Applicative): 13 | 14 | def map(self, fa: F, f: Callable[[A], B]) -> F: 15 | return self.flat_map(fa, lambda a: self.pure(f(a))) 16 | 17 | def eff(self, fa: F, tpe: type=None): 18 | from amino.eff import Eff 19 | tpes = List() if tpe is None else List(tpe) 20 | return Eff(fa, tpes, 1) 21 | 22 | def effs(self, fa: F, *args): 23 | from amino.eff import Eff 24 | types = List.wrap(args) 25 | c = lambda a, b: Eff(fa, a, depth=b) 26 | with_depth = lambda d, t: c(t, d) 27 | types_only = lambda: c(types, depth=len(types)) 28 | def try_depth(h, t): 29 | return with_depth(int(h), t) if isinstance(h, int) else types_only() 30 | return types.detach_head.map2(try_depth) | types_only 31 | 32 | __all__ = ('Monad',) 33 | -------------------------------------------------------------------------------- /amino/util/ast.py: -------------------------------------------------------------------------------- 1 | from types import FunctionType 2 | from typing import Type, Tuple, Any 3 | 4 | from amino import List, Map 5 | 6 | 7 | def type_tail(a: Any) -> str: 8 | return str(a).split('.')[-1] 9 | 10 | 11 | def typename(a: Any) -> str: 12 | return getattr(a, __name__, type_tail(a)) 13 | 14 | 15 | def synth_method(name: str, params: List[Tuple[str, Type]], statements: List[str], _globals: dict) -> FunctionType: 16 | id = f'synth_{name}__' 17 | params_s = params.map2(lambda n, t: f'{n}: {typename(t)}').join_comma 18 | param_globals = Map(params.map2(lambda n, t: (typename(t), t))) 19 | globs = Map(_globals) ** param_globals 20 | code = f'''\ 21 | def {name}(self, {params_s}) -> None: 22 | {statements.indent(4).join_lines} 23 | globals()['{id}'] = {name} 24 | ''' 25 | exec(code, globs) 26 | return globs.pop(id) 27 | 28 | 29 | def synth_init(params: List[Tuple[str, Type]], _globals: dict) -> FunctionType: 30 | assign = params.map2(lambda k, v: f'self.{k} = {k}') 31 | return synth_method('__init__', params, assign, _globals) 32 | 33 | 34 | __all__ = ('synth_method', 'synth_init') 35 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Torsten Schmits 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of 6 | this software and associated documentation files (the "Software"), to deal in 7 | the Software without restriction, including without limitation the rights to 8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies 9 | of the Software, and to permit persons to whom the Software is furnished to do 10 | 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. 22 | -------------------------------------------------------------------------------- /unit/do_spec.py: -------------------------------------------------------------------------------- 1 | from typing import Any 2 | 3 | from amino.test.spec_spec import Spec 4 | 5 | from amino import Just, Nothing, Maybe, do, Do 6 | from amino.state import EvalState 7 | 8 | 9 | class DoSpec(Spec): 10 | 11 | def just(self) -> None: 12 | @do(Maybe[int]) 13 | def run(i: int) -> Do: 14 | a = yield Just(i) 15 | b = yield Just(a + 5) 16 | c = yield Just(b + 7) 17 | d = yield Just(c * 3) 18 | return d 19 | run(3).should.equal(Just(45)) 20 | 21 | def nothing(self) -> None: 22 | @do(Maybe[int]) 23 | def run(i: int) -> Do: 24 | yield Just(i) 25 | b = yield Nothing 26 | c = yield Just(b + 7) 27 | yield Just(c) 28 | run(3).should.equal(Nothing) 29 | 30 | def eval_state(self) -> None: 31 | @do(EvalState[str, Any]) 32 | def run() -> Do: 33 | a = yield EvalState.pure(1) 34 | yield EvalState.set('state') 35 | yield EvalState.inspect(lambda s: f'{s}: {a}') 36 | run().run_a('init').value.should.equal('state: 1') 37 | 38 | 39 | __all__ = ('DoSpec',) 40 | -------------------------------------------------------------------------------- /amino/anon/prod/attr.py: -------------------------------------------------------------------------------- 1 | from typing import Any, Callable 2 | 3 | from amino.anon.prod.method import Opers, Anon, AnonMethodCall 4 | 5 | 6 | class AttrLambda(Opers, Anon): 7 | 8 | def __init__(self, pre: str) -> None: 9 | self.__pre = pre 10 | 11 | def __pre__(self) -> str: 12 | return self.__pre 13 | 14 | def __getattr__(self, name: str) -> 'AttrLambda': 15 | return AttrLambda(f'{self.__pre}.{name}') 16 | 17 | def __getitem__(self, key: Any) -> AnonMethodCall: 18 | return AnonMethodCall(f'{self.__pre}.__getitem__', (key,), {}) 19 | 20 | def __repr__(self) -> str: 21 | return f'lambda a: {self.__pre}' 22 | 23 | def __call__(self, a: Any) -> Any: 24 | return self.__eval__()(a) 25 | 26 | def __substitute_object__(self, obj: Any) -> Callable: 27 | return self.__call__(obj) 28 | 29 | def __lop__(self, op: Callable, s: str, a: Any) -> AnonMethodCall: 30 | return AnonMethodCall(f'(lambda b: {self.__pre__()} {s} b)', (a,), {}) 31 | 32 | def __rop__(self, op: Callable[[Any, Any], Any], s: str, a: Any) -> AnonMethodCall: 33 | return AnonMethodCall(f'(lambda b: b {s} {self.__pre__()})', (a,), {}) 34 | 35 | AttrLambdaInst = AttrLambda('a') 36 | 37 | __all__ = ('AttrLambdaInst',) 38 | -------------------------------------------------------------------------------- /amino/env_vars.py: -------------------------------------------------------------------------------- 1 | import os 2 | from typing import Any 3 | 4 | from amino.map import Map 5 | from amino import Either 6 | from amino.io import IO 7 | 8 | 9 | class EnvVars: 10 | 11 | @property 12 | def vars(self): 13 | return Map(os.environ) 14 | 15 | def __contains__(self, name: str) -> bool: 16 | return self.contains(name) 17 | 18 | def contains(self, name: str) -> bool: 19 | return name in self.vars 20 | 21 | def __getitem__(self, name: str) -> Either[str, str]: 22 | return self.get(name) 23 | 24 | def get(self, name: str) -> Either[str, str]: 25 | return self.vars.lift(name).to_either(f'env var {name} is unset') 26 | 27 | def __setitem__(self, name: str, value: str) -> None: 28 | self.set(name, value) 29 | 30 | def set(self, name: str, value: str) -> None: 31 | os.environ[name] = str(value) 32 | 33 | def __delitem__(self, name: str) -> None: 34 | return self.delete(name) 35 | 36 | def delete(self, name: str) -> None: 37 | if name in os.environ: 38 | del os.environ[name] 39 | 40 | env = EnvVars() 41 | 42 | 43 | def set_env(name: str, value: Any) -> IO[None]: 44 | return IO.delay(env.set, name, str(value)) 45 | 46 | 47 | __all__ = ('EnvVars', 'env') 48 | -------------------------------------------------------------------------------- /amino/__init__.py: -------------------------------------------------------------------------------- 1 | import pathlib 2 | 3 | Path = pathlib.Path 4 | 5 | # from amino.typecheck import boot 6 | from amino.maybe import Maybe, Just, Empty, may, flat_may, Nothing 7 | from amino.either import Left, Right, Either, Try 8 | from amino.data.list import List, Lists, Nil 9 | from amino.boolean import Boolean 10 | from amino.anon.boot import __, L, _ 11 | from amino.lazy_list import LazyList 12 | from amino.map import Map 13 | from amino.future import Future 14 | from amino.func import curried, I 15 | from amino.env_vars import env 16 | from amino.io import IO 17 | from amino.logging import Logger, amino_root_logger as amino_log, with_log 18 | from amino.eff import Eff 19 | from amino.eval import Eval 20 | from amino.regex import Regex 21 | from amino.options import integration_test, development 22 | from amino.do import do, Do 23 | from amino.id import Id 24 | from amino.dat import Dat, ADT 25 | 26 | # boot() 27 | 28 | __all__ = ('Maybe', 'Just', 'Empty', 'may', 'List', 'Map', '_', 'Future', 'Boolean', 'development', 'flat_may', 29 | 'curried', '__', 'Left', 'Right', 'Either', 'env', 'Try', 'LazyList', 'Logger', 'I', 'L', 'Eff', 30 | 'IO', 'Eval', 'Regex', 'Nothing', 'integration_test', 'Lists', 'do', 'IO', 'Id', 'Nil', 'amino_log', 31 | 'with_log', 'Do', 'Dat', 'ADT') 32 | -------------------------------------------------------------------------------- /unit/bi_rose_tree_spec.py: -------------------------------------------------------------------------------- 1 | from typing import Any 2 | 3 | from amino.test.spec_spec import Spec 4 | from amino.bi_rose_tree import nodes, leaves, from_tree_default, RoseTreeRoot 5 | from amino import _, __, List 6 | from amino.tree import Node, LeafNode 7 | 8 | from unit.tree_spec import mtree 9 | 10 | 11 | simple_tree = RoseTreeRoot(1, nodes((2, leaves(3, 4)))) 12 | 13 | 14 | class BiRoseTreeSpec(Spec): 15 | 16 | def show(self) -> None: 17 | simple_tree.show.should.equal('1\n 2\n 3\n 4') 18 | 19 | def map(self) -> None: 20 | t1 = simple_tree.map(_ + 2) 21 | t1[0].flat_map(__[1]).map(_.data).should.contain(6) 22 | t1[0].flat_map(__[1]).map(_.parent.parent).should.contain(t1) 23 | 24 | def filter(self) -> None: 25 | t1 = simple_tree.filter(_ <= 3) 26 | t1[0].map(__.sub.drain.map(_.data)).should.contain(List(3)) 27 | 28 | def from_tree(self) -> None: 29 | def trans(n: Node[str, Any], parent) -> int: 30 | return (len(n.data) if isinstance(n, LeafNode) else 0) 31 | t1 = from_tree_default(mtree, trans) 32 | t1[0].flat_map(lambda a: a.parent[0]).map(_.sub.drain.length).should.contain(2) 33 | t1[0].flat_map(__[1]).flat_map(__[1]).map(_.data).should.contain(4) 34 | 35 | __all__ = ('BiRoseTreeSpec',) 36 | -------------------------------------------------------------------------------- /unit/eval_spec.py: -------------------------------------------------------------------------------- 1 | from amino import eval 2 | from amino.test.spec_spec import Spec 3 | from amino.lazy_list import LazyLists 4 | from amino.eval import Eval 5 | 6 | 7 | class EvalSpec(Spec): 8 | 9 | def now(self) -> None: 10 | v = 'amino' 11 | e = eval.Now(v) 12 | e.value.should.be(v) 13 | 14 | def always(self) -> None: 15 | vs = iter(range(2)) 16 | f = lambda: next(vs) 17 | e = eval.Always(f) 18 | e.value.should.be(0) 19 | e.value.should.be(1) 20 | 21 | def later(self) -> None: 22 | flag = 0 23 | def f() -> int: 24 | nonlocal flag 25 | flag = flag + 1 26 | return flag 27 | e = eval.Later(f) 28 | e.value.should.be(1) 29 | e.value.should.be(1) 30 | 31 | def map(self) -> None: 32 | eval.Later(lambda: 5).map(lambda a: a + 4)._value().should.equal(9) 33 | 34 | def flat_map(self) -> None: 35 | def f(a: int) -> Eval[int]: 36 | return eval.Now(a * 2) 37 | e = eval.Now(1) 38 | e1 = e.flat_map(f) 39 | e1._value().should.be(2) 40 | 41 | def loop(self) -> None: 42 | e = (LazyLists.range(1000).fold_left(eval.Now(0))(lambda z, a: z.flat_map(lambda a: eval.Now(a + 1)))) 43 | e._value().should.equal(1000) 44 | 45 | __all__ = ('EvalSpec',) 46 | -------------------------------------------------------------------------------- /amino/tc/traverse.py: -------------------------------------------------------------------------------- 1 | import abc 2 | from typing import TypeVar, Generic, Callable, List 3 | 4 | from amino.tc.base import TypeClass 5 | from amino.func import I 6 | from amino import _ 7 | from amino.tc.apply_n import ApplyN 8 | 9 | A = TypeVar('A') 10 | B = TypeVar('B') 11 | 12 | 13 | class TraverseF(Generic[A], abc.ABC): 14 | pass 15 | 16 | 17 | class TraverseG(Generic[A], abc.ABC): 18 | pass 19 | 20 | F = TraverseF 21 | G = TraverseG 22 | 23 | F0 = TypeVar('F0', bound=TraverseF) 24 | G0 = TypeVar('G0', bound=TraverseG) 25 | 26 | 27 | class Traverse(TypeClass, ApplyN): 28 | # FIXME lens functions return index lenses, which is not a property of Traverse 29 | 30 | def apply_n_funcs(self) -> list: 31 | return ['traverse', 'flat_traverse'] 32 | 33 | @abc.abstractmethod 34 | def traverse(self, fa: F[G[A]], f: Callable[[A], B], tpe: type) -> G[F[B]]: 35 | ... 36 | 37 | def flat_traverse(self, fa: F[G[A]], f: Callable[[A], F[B]], tpe: type) -> G[F[B]]: 38 | return self.traverse(fa, f, tpe).map(_.join) # type: ignore 39 | 40 | def sequence(self, fa: F[G[A]], tpe: type) -> G[F[A]]: 41 | return self.traverse(fa, I, tpe) 42 | 43 | def flat_sequence(self, fa: F[G[A]], tpe: type) -> G[F[B]]: 44 | return self.sequence(fa, tpe).map(_.join) # type: ignore 45 | 46 | __all__ = ('Traverse',) 47 | -------------------------------------------------------------------------------- /amino/meta/gen.py: -------------------------------------------------------------------------------- 1 | import re 2 | from typing import Tuple 3 | 4 | from amino import Path, IO, do, Do, Dat, List 5 | 6 | 7 | class CodegenTask(Dat['CodegenTask']): 8 | 9 | @staticmethod 10 | def cons( 11 | template: Path, 12 | subs: List[Tuple[str, str]], 13 | imports: List[str], 14 | ) -> 'CodegenTask': 15 | return CodegenTask( 16 | template, 17 | subs, 18 | imports, 19 | ) 20 | 21 | def __init__( 22 | self, 23 | template: Path, 24 | subs: List[Tuple[str, str]], 25 | imports: List[str], 26 | ) -> None: 27 | self.template = template 28 | self.subs = subs 29 | self.imports = imports 30 | 31 | 32 | @do(IO[str]) 33 | def codegen(task: CodegenTask) -> Do: 34 | template_path = yield IO.delay(task.template.absolute) 35 | template_code = yield IO.delay(template_path.read_text) 36 | replaced = task.subs.fold_left(template_code)(lambda z, a: re.sub(a[0], a[1], z)) 37 | return task.imports.cat(replaced).join_lines 38 | 39 | 40 | @do(IO[str]) 41 | def codegen_write(task: CodegenTask, outpath: Path) -> Do: 42 | code = yield codegen(task) 43 | yield IO.delay(outpath.write_text, code) 44 | return code 45 | 46 | 47 | __all__ = ('CodegenTask', 'codegen', 'codegen_write',) 48 | -------------------------------------------------------------------------------- /amino/json/encoder.py: -------------------------------------------------------------------------------- 1 | import abc 2 | import json 3 | from typing import TypeVar, Generic, Type 4 | 5 | from amino.tc.base import TypeClass 6 | from amino import Either, L, _, Map, do, Do, Lists 7 | from amino.json.data import JsonError, Json, JsonObject, JsonScalar, tpe_key, JsonArray 8 | from amino.logging import module_log 9 | from amino.util.tpe import qualname 10 | 11 | A = TypeVar('A') 12 | log = module_log() 13 | 14 | 15 | class Encoder(Generic[A], TypeClass): 16 | 17 | @abc.abstractmethod 18 | def encode(self, a: A) -> Either[JsonError, Json]: 19 | ... 20 | 21 | 22 | @do(Either[JsonError, Json]) 23 | def encode_json(data: A) -> Do: 24 | enc = yield Encoder.e_for(data).lmap(L(JsonError)(data, _)) 25 | yield enc.encode(data) 26 | 27 | 28 | @do(Either[JsonError, str]) 29 | def dump_json(data: A) -> Do: 30 | js = yield encode_json(data) 31 | return json.dumps(js.native) 32 | 33 | 34 | def json_type(tpe: Type) -> Json: 35 | mod = '__builtins__' if tpe.__module__ == 'builtins' else tpe.__module__ 36 | names = Lists.split(qualname(tpe), '.').map(JsonScalar) 37 | return JsonObject(Map(module=JsonScalar(mod), names=JsonArray(names))) 38 | 39 | 40 | def json_object_with_type(fields: Map[str, JsonObject], tpe: Type[A]) -> Json: 41 | return JsonObject(fields.cat((tpe_key, json_type(tpe)))) 42 | 43 | 44 | __all__ = ('Encoder', 'encode_json', 'dump_json') 45 | -------------------------------------------------------------------------------- /amino/tc/zip.py: -------------------------------------------------------------------------------- 1 | import abc 2 | from typing import TypeVar, Generic, Callable, Tuple 3 | 4 | from amino.tc.base import TypeClass, tc_prop 5 | from amino.tc.monoid import Monoid 6 | from amino.tc.foldable import Foldable 7 | from amino.tc.applicative import Applicative 8 | from amino.tc.functor import Functor 9 | 10 | F = TypeVar('F') 11 | A = TypeVar('A') 12 | B = TypeVar('B') 13 | 14 | 15 | class Zip(Generic[F], TypeClass): 16 | 17 | @abc.abstractmethod 18 | def zip(self, fa: F, fb: F, *fs) -> F: 19 | ... 20 | 21 | def __and__(self, fa: F, fb: F): 22 | return self.zip(fa, fb) 23 | 24 | def apzip(self, fa: F, f: Callable[[A], B]) -> F: 25 | return self.zip(fa, Functor.fatal(type(fa)).map(fa, f)) 26 | 27 | def zip_const(self, fa: F, b: B) -> F: 28 | return self.apzip(fa, lambda a: b) 29 | 30 | def flat_apzip(self, fa: F, f: Callable[[A], F]) -> F: 31 | return self.zip(fa, Functor.fatal(type(fa)).flat_map(fa, f)) 32 | 33 | @tc_prop 34 | def unzip(self, fa: F) -> Tuple[F, F]: 35 | tpe = type(fa) 36 | f = Foldable.fatal(tpe) 37 | m = Monoid.fatal(tpe) 38 | a = Applicative.fatal(tpe) 39 | def folder(z, b): 40 | l, r = z 41 | x, y = b 42 | return m.combine(l, a.pure(x)), m.combine(r, a.pure(y)) 43 | return f.fold_left(fa, (m.empty, m.empty), folder) 44 | 45 | __all__ = ('Zip',) 46 | -------------------------------------------------------------------------------- /amino/tc/apply_n.py: -------------------------------------------------------------------------------- 1 | import re 2 | import abc 3 | from typing import Callable, Any, List, TypeVar 4 | 5 | from amino.tc.base import TypeClass 6 | 7 | A = TypeVar('A') 8 | B = TypeVar('B') 9 | F = TypeVar('F') 10 | 11 | 12 | def apply_n(self: TypeClass, num: int, fa: F, f: Callable[..., A], g: Callable[..., B], *a: Any) -> B: 13 | def wrapper(args: tuple): 14 | if len(args) != num: 15 | name = self.__class__.__name__ 16 | msg = f'passed {len(args)} args to {name}.{g.__name__}{num}' 17 | raise TypeError(msg) 18 | return f(*args) 19 | return g(fa, wrapper, *a) 20 | 21 | 22 | apply_n_rex = re.compile('^(.*)(\d+)$') 23 | 24 | 25 | class ApplyN: 26 | 27 | @abc.abstractclassmethod 28 | def apply_n_funcs(self) -> List[str]: 29 | ... 30 | 31 | def __getattr__(self, name: str) -> Callable: 32 | def error() -> None: 33 | raise AttributeError(f'''`{self.__class__.__name__}` object has no attribute `{name}`''') 34 | match = apply_n_rex.match(name) 35 | if match is None: 36 | error() 37 | func = match.group(1) 38 | num = int(match.group(2)) 39 | n_name = f'{func}_n' 40 | if hasattr(self, n_name): 41 | return lambda fa, f, *a: getattr(self, n_name)(num, fa, f, *a) 42 | if func not in self.apply_n_funcs(): 43 | error() 44 | return lambda fa, f, *a: apply_n(self, num, fa, f, getattr(self, func), *a) 45 | 46 | 47 | __all__ = ('apply_n', 'ApplyN') 48 | -------------------------------------------------------------------------------- /amino/test/spec_spec.py: -------------------------------------------------------------------------------- 1 | import abc 2 | import time 3 | from typing import Union, Any, Callable 4 | from datetime import datetime 5 | 6 | import spec 7 | 8 | from amino.test.spec import SpecBase, IntegrationSpecBase, default_timeout 9 | from amino.test.sure import SureSpec 10 | 11 | 12 | def later(ass: Callable[..., bool], *a: Any, timeout: Union[float, None]=None, intval: float=0.1, **kw: Any) -> bool: 13 | timeout = default_timeout if timeout is None else timeout 14 | start = datetime.now() 15 | ok = False 16 | while not ok and (datetime.now() - start).total_seconds() < timeout: 17 | try: 18 | ass(*a, **kw) 19 | ok = True 20 | except AssertionError: 21 | time.sleep(intval) 22 | return ass(*a, **kw) 23 | 24 | 25 | class SpecMeta(spec.InnerClassParser, abc.ABCMeta): 26 | pass 27 | 28 | 29 | class Spec(SureSpec, SpecBase, spec.Spec, metaclass=SpecMeta): 30 | 31 | def setup(self) -> None: 32 | SureSpec.setup(self) 33 | SpecBase.setup(self) 34 | 35 | def _wait_for(self, pred: Callable[[], bool], timeout: float=default_timeout, intval: float=0.1) -> None: 36 | start = datetime.now() 37 | while (not pred() and (datetime.now() - start).total_seconds() < timeout): 38 | time.sleep(intval) 39 | pred().should.be.ok 40 | 41 | 42 | class IntegrationSpec(IntegrationSpecBase, Spec): 43 | 44 | def setup(self) -> None: 45 | IntegrationSpecBase.setup(self) 46 | Spec.setup(self) 47 | 48 | __all__ = ('Spec', 'IntegrationSpec') 49 | -------------------------------------------------------------------------------- /amino/instances/map.py: -------------------------------------------------------------------------------- 1 | from typing import TypeVar, Callable, Any, Type, Tuple 2 | 3 | from amino import Map 4 | from amino.lazy import lazy 5 | from amino.tc.functor import Functor 6 | from amino.tc.base import ImplicitInstances, F 7 | from amino.tc.traverse import Traverse 8 | from amino.tc.monoid import Monoid 9 | from amino.tc.monad import Monad 10 | 11 | A = TypeVar('A', covariant=True) 12 | B = TypeVar('B') 13 | C = TypeVar('C') 14 | G = TypeVar('G', bound=F) 15 | 16 | 17 | class MapInstances(ImplicitInstances): 18 | 19 | @lazy 20 | def _instances(self): 21 | from amino import Map 22 | return Map( 23 | { 24 | Functor: MapFunctor(), 25 | Traverse: MapTraverse(), 26 | Monoid: MapMonoid(), 27 | } 28 | ) 29 | 30 | 31 | class MapFunctor(Functor): 32 | 33 | def map(self, fa: Map[Any, A], f: Callable[[A], B]) -> Map[Any, B]: 34 | return fa.valmap(f) 35 | 36 | 37 | class MapTraverse(Traverse): 38 | 39 | def traverse(self, fa: Map[Any, A], f: Callable[[A], B], tpe: Type[G]) -> G: 40 | monad = Monad.fatal(tpe) 41 | def folder(z, kv: Tuple[A, B]): 42 | k, v = kv 43 | return monad.map2(z.product(f(v)), lambda l, b: l.cat((k, b))) 44 | return fa.to_list.fold_left(monad.pure(Map()))(folder) 45 | 46 | 47 | class MapMonoid(Monoid): 48 | 49 | @property 50 | def empty(self): 51 | return Map() 52 | 53 | def combine(self, fa: Map, fb: Map): 54 | return fa ** fb 55 | 56 | __all__ = ('MapInstances',) 57 | -------------------------------------------------------------------------------- /amino/mod.py: -------------------------------------------------------------------------------- 1 | from typing import Any, Callable, TypeVar, Type 2 | from types import ModuleType 3 | 4 | from amino.do import do, Do 5 | from amino import Maybe, Lists, L, _, Either, List 6 | 7 | 8 | @do(Either[str, List[Any]]) 9 | def objects_from_module(mod: ModuleType, pred: Callable[[Any], bool]) -> Do: 10 | all = yield Maybe.getattr(mod, '__all__').to_either(f'module `{mod.__name__}` does not define `__all__`') 11 | return ( 12 | Lists.wrap(all) 13 | .flat_map(L(Maybe.getattr)(mod, _)) 14 | .filter(pred) 15 | ) 16 | 17 | 18 | @do(Either[str, List[Any]]) 19 | def object_from_module(mod: ModuleType, pred: Callable[[Any], bool], desc: str) -> Do: 20 | all = yield objects_from_module(mod, pred) 21 | yield all.head.to_either(f'no {desc} in `{mod.__name__}.__all__`') 22 | 23 | 24 | AS = TypeVar('AS') 25 | A = TypeVar('A', bound=AS) 26 | 27 | 28 | def cls_from_module(mod: ModuleType, tpe: Type[AS]) -> Either[str, Type[A]]: 29 | pred = lambda a: isinstance(a, type) and issubclass(a, tpe) 30 | return object_from_module(mod, pred, f'subclass of `{tpe}`') 31 | 32 | 33 | def instances_from_module(mod: ModuleType, tpe: Type[A]) -> Either[str, List[A]]: 34 | pred = lambda a: isinstance(a, tpe) 35 | return objects_from_module(mod, pred) 36 | 37 | 38 | def instance_from_module(mod: ModuleType, tpe: Type[A]) -> Either[str, A]: 39 | pred = lambda a: isinstance(a, tpe) 40 | return object_from_module(mod, pred, f'instance of `{tpe.__name__}`') 41 | 42 | 43 | __all__ = ('object_from_module', 'cls_from_module', 'instance_from_module', 'instances_from_module',) 44 | -------------------------------------------------------------------------------- /unit/either_spec.py: -------------------------------------------------------------------------------- 1 | import operator 2 | 3 | from amino.either import Left, Right 4 | from amino import Empty, Just, Maybe, List, Either, _ 5 | from amino.test.spec_spec import Spec 6 | from amino.list import Lists 7 | 8 | 9 | class EitherSpec(Spec): 10 | 11 | def map(self) -> None: 12 | a = 'a' 13 | b = 'b' 14 | Right(a).map(_ + b).value.should.equal(a + b) 15 | Left(a).map(_ + b).value.should.equal(a) 16 | 17 | def optional(self) -> None: 18 | a = 'a' 19 | b = 'b' 20 | Right(a).to_maybe.should.just_contain(a) 21 | Left(a).to_maybe.should.be.a(Empty) 22 | Right(a).to_either(b).should.equal(Right(a)) 23 | Left(a).to_either(b).should.equal(Left(a)) 24 | 25 | def ap2(self) -> None: 26 | a = 'a' 27 | b = 'b' 28 | Right(a).ap2(Right(b), operator.add).should.equal(Right(a + b)) 29 | 30 | def traverse(self) -> None: 31 | a = 'a' 32 | Right(Just(a)).sequence(Maybe).should.equal(Just(Right(a))) 33 | Left(Just(a)).sequence(Maybe).should.equal(Just(Left(Just(a)))) 34 | List(Right(a)).sequence(Either).should.equal(Right(List(a))) 35 | List(Right(a), Left(a)).sequence(Either).should.equal(Left(a)) 36 | 37 | def fold_m(self) -> None: 38 | def f(z: int, a: int) -> Either[str, int]: 39 | return Right(z + a) if a < 5 else Left('too large') 40 | Lists.range(5).fold_m(Right(8))(f).should.contain(18) 41 | Lists.range(6).fold_m(Right(8))(f).should.be.left 42 | 43 | def list_flat_map(self) -> None: 44 | (List(Right(1), Left(2), Right(3)).join).should.equal(List(1, 3)) 45 | 46 | __all__ = ('EitherSpec',) 47 | -------------------------------------------------------------------------------- /amino/dispatch.py: -------------------------------------------------------------------------------- 1 | from functools import singledispatch 2 | import typing 3 | from typing import Callable, Any, Dict, TypeVar, Type 4 | 5 | from amino.util.string import snake_case 6 | from amino.algebra import Algebra 7 | 8 | A = TypeVar('A') 9 | B = TypeVar('B') 10 | R = TypeVar('R') 11 | Alg = TypeVar('Alg', bound=Algebra) 12 | 13 | 14 | def dispatch(obj: B, tpes: typing.List[A], prefix: str, default: Callable[[A], R]=None) -> Callable[[A], R]: 15 | def error(o: A) -> None: 16 | msg = 'no dispatcher defined for {} on {} {}' 17 | raise TypeError(msg.format(o, obj.__class__.__name__, prefix)) 18 | @singledispatch 19 | def main(o: A, *a: Any, **kw: Any) -> R: 20 | if default is None: 21 | error(o) 22 | else: 23 | return default(o, *a, **kw) 24 | for tpe in tpes: 25 | fun = getattr(obj, '{}{}'.format(prefix, snake_case(tpe.__name__)), None) 26 | if fun is None: 27 | error(tpe) 28 | main.register(tpe)(fun) 29 | return main 30 | 31 | 32 | def dispatch_alg(obj: B, alg: Type[Alg], prefix: str='', default: Callable[[Alg], R]=None) -> Callable[[Alg], R]: 33 | return dispatch(obj, alg.__algebra_variants__, prefix, default) 34 | 35 | 36 | def dispatch_with(rules: Dict[type, Callable], default: Callable=None): 37 | @singledispatch 38 | def main(o, *a, **kw): 39 | if default is None: 40 | msg = 'no dispatcher defined for {} {} ({})' 41 | raise TypeError(msg.format(type(o), o, rules)) 42 | else: 43 | default(o, *a, **kw) 44 | for tpe, fun in rules.items(): 45 | main.register(tpe)(fun) 46 | return main 47 | 48 | 49 | __all__ = ('dispatch', 'dispatch_alg', 'dispatch_with') 50 | -------------------------------------------------------------------------------- /amino/do.py: -------------------------------------------------------------------------------- 1 | from types import GeneratorType 2 | from typing import TypeVar, Callable, Any, Generator, Type 3 | import functools 4 | 5 | from amino.tc.monad import Monad 6 | 7 | A = TypeVar('A') 8 | B = TypeVar('B') 9 | Do = Generator 10 | 11 | 12 | # NOTE ostensibly, this cannot be tailrecced without separating strictly evaluated monadic composition from lazy ones. 13 | # itr.gi_frame.f_lasti is the instruction pointer and could be used to detect laziness. 14 | # NOTE due to the nature of generators, a do with a lazily evaluated monad cannot be executed twice. 15 | # NOTE Lists don't work properly because the generator will be consumed by the first element 16 | def do_impl(tpe: type, f: Callable[..., Generator[A, B, A]]) -> Callable[..., A]: 17 | @functools.wraps(f) 18 | def do_loop(*a: Any, **kw: Any) -> A: 19 | m = Monad.fatal(tpe) 20 | itr = f(*a, **kw) 21 | if not isinstance(itr, GeneratorType): 22 | raise Exception(f'function `{f.__qualname__}` decorated with `do` does not produce a generator') 23 | @functools.wraps(f) 24 | def loop(val: B) -> A: 25 | try: 26 | return m.flat_map(itr.send(val), loop) 27 | except StopIteration as e: 28 | return m.pure(val if e.value is None else e.value) 29 | return m.flat_map(m.pure(None), loop) 30 | return do_loop 31 | 32 | 33 | def do(tpe: Type[A]) -> Callable[[Callable[..., Generator[A, B, A]]], Callable[..., A]]: 34 | def deco(f: Callable[..., Generator[A, B, A]]) -> Callable[..., A]: 35 | f.tpe = tpe # type: ignore 36 | f.__do = None # type: ignore 37 | f.__do_original = f # type: ignore 38 | return functools.wraps(f)(do_impl)(tpe, f) 39 | return deco 40 | 41 | tdo = do 42 | 43 | __all__ = ('do', 'tdo', 'Do') 44 | -------------------------------------------------------------------------------- /unit/tc/context_spec.py: -------------------------------------------------------------------------------- 1 | from typing import TypeVar, Callable, Any, Generic 2 | 3 | from amino.tc.monad import Monad 4 | from amino import List, ADT 5 | from amino.case import Case 6 | from amino.algebra import Algebra 7 | from amino.test.spec_spec import Spec 8 | from amino.tc.context import context, Bindings 9 | 10 | A = TypeVar('A') 11 | B = TypeVar('B') 12 | X = TypeVar('X') 13 | Alg = TypeVar('Alg', bound=Algebra) 14 | 15 | 16 | @context(A=Monad) 17 | def monad_constraint(a: A, f: Callable[[A], A]) -> A: 18 | return a.flat_map(f) 19 | 20 | 21 | @context(**monad_constraint.bounds) 22 | def monad_constraint_wrap(bindings: Bindings, a: A, f: Callable[[A], A]) -> None: 23 | return monad_constraint(**bindings.bindings)(a, f) 24 | 25 | 26 | class Al(Generic[A], ADT['Al']): 27 | pass 28 | 29 | 30 | class Ala(Al[A]): 31 | 32 | def __init__(self, a: A) -> None: 33 | self.a = a 34 | 35 | 36 | @context(A=Monad) 37 | class MonadConstraint(Case, alg=Al): 38 | 39 | def __init__(self, bindings: Bindings, i: int) -> None: 40 | self.bindings = bindings 41 | self.i = i 42 | 43 | def ala(self, a: Ala[A], f: Callable[[Any], B]) -> B: 44 | return monad_constraint(self.bindings)(a.a, f) 45 | 46 | 47 | target = List(2, 6, 3, 7) 48 | 49 | 50 | class ContextSpec(Spec): 51 | 52 | def _f(self, a: int) -> List[int]: 53 | return List(a + 1, a + 5) 54 | 55 | def func(self) -> None: 56 | r = monad_constraint_wrap(A=List)(List(1, 2), self._f) 57 | r.should.equal(target) 58 | 59 | def case(self) -> None: 60 | r = MonadConstraint(A=List)(1)(Ala(List(1, 2)), self._f) 61 | r.should.equal(target) 62 | 63 | def err(self) -> None: 64 | MonadConstraint.when.called_with().should.throw(Exception) 65 | 66 | 67 | __all__ = ('ContextSpec',) 68 | -------------------------------------------------------------------------------- /amino/util/exception.py: -------------------------------------------------------------------------------- 1 | import inspect 2 | from typing import Tuple, Callable 3 | 4 | from traceback import format_list, format_exception_only, FrameSummary, extract_tb 5 | 6 | from amino.data.list import List, Lists 7 | from amino.func import I 8 | 9 | 10 | def sanitize_tb(tb: List[str]) -> List[str]: 11 | return tb.flat_map(lambda a: Lists.wrap(a.splitlines())) / (lambda a: a.rstrip()) 12 | 13 | 14 | def format_exception_error(exc: Exception) -> List[str]: 15 | return sanitize_tb(Lists.wrap(format_exception_only(type(exc), exc))) 16 | 17 | 18 | def format_one_exception( 19 | exc: Exception, 20 | tb_filter: Callable[[List[FrameSummary]], List[FrameSummary]]=I, 21 | tb_formatter: Callable[[List[str]], List[str]]=I, 22 | exc_formatter: Callable[[List[str]], List[str]]=I, 23 | ) -> Tuple[List[str], List[str]]: 24 | e_str = exc_formatter(format_exception_error(exc)) 25 | tb = tb_filter(Lists.wrap(extract_tb(exc.__traceback__))) 26 | tb_str = tb_formatter(sanitize_tb(Lists.wrap(format_list(tb)))) 27 | return e_str, tb_str 28 | 29 | 30 | def format_cause(exc: Exception, **kw) -> List[str]: 31 | from amino import Maybe 32 | return Maybe.optional(exc.__cause__) / (lambda a: format_exception(a, **kw)) / (lambda a: a.cons('Cause:')) | List() 33 | 34 | 35 | def format_exception(exc: Exception, **kw) -> List[str]: 36 | e, tb = format_one_exception(exc, **kw) 37 | main = tb + e 38 | return main + format_cause(exc, **kw) 39 | 40 | 41 | def format_stack(stack: List[inspect.FrameInfo]) -> List[str]: 42 | data = stack.map(lambda a: a[1:-2] + tuple(a[-2] or [''])) 43 | return sanitize_tb(Lists.wrap(format_list(list(data)))) 44 | 45 | 46 | def format_current_stack() -> List[str]: 47 | return format_stack(Lists.wrap(inspect.stack())) 48 | 49 | 50 | __all__ = ('sanitize_tb', 'format_exception', 'format_stack', 'format_current_stack') 51 | -------------------------------------------------------------------------------- /amino/lenses/tree.py: -------------------------------------------------------------------------------- 1 | from typing import TypeVar, Callable 2 | 3 | from lenses import bind 4 | from lenses.ui import UnboundLens, BoundLens 5 | 6 | from amino import Maybe, List, __, Boolean, _, L 7 | from amino.lenses.lens import lens 8 | 9 | A = TypeVar('A') 10 | 11 | _add = lambda l: __.map(lens.add_lens(l).add_lens) 12 | 13 | 14 | def path_lens_pred(a: A, sub: Callable[[A], List[A]], lsub, f: Callable[[A], bool]) -> Maybe[BoundLens]: 15 | g = lambda a: Boolean(f(lsub(a))).maybe(lsub(lens)) 16 | return path_lens(a, sub, g) 17 | 18 | 19 | def path_lens(a: A, sub: Callable[[A], List[A]], f: Callable[[A], Maybe[UnboundLens]]) -> Maybe[BoundLens]: 20 | return _path_lens(a, sub, f).map(lambda b: bind(a).tuple_(*b)) 21 | 22 | 23 | def path_lens_unbound_pre(a: A, sub: Callable[[A], List[A]], f: Callable[[A], Maybe[UnboundLens]], pre: Callable 24 | ) -> Maybe[UnboundLens]: 25 | return ( 26 | _path_lens(pre(a), sub, f) / 27 | (_ / pre(lens).add_lens) / 28 | __.cons(lens) 29 | ).map(lambda b: lens.tuple_(*b)) 30 | 31 | 32 | def path_lens_unbound(a: A, sub: Callable[[A], List[A]], f: Callable[[A], Maybe[UnboundLens]]) -> Maybe[UnboundLens]: 33 | return _path_lens(a, sub, f).map(lambda b: lens.tuple_(*b)) 34 | 35 | 36 | def _path_lens(a: A, sub: Callable[[A], List[A]], f: Callable[[A], Maybe[UnboundLens]]) -> Maybe[UnboundLens]: 37 | def go_sub(): 38 | l, s = sub(lens), sub(a) 39 | g = lambda b: _path_lens(b, sub, f) 40 | return _path_lens_list(s, g) / _add(l).cons(lens) 41 | return (f(a) / L(List)(lens, _)).or_else(go_sub) 42 | 43 | 44 | def _path_lens_list(fa: List[A], f: Callable[[A], Maybe[UnboundLens]]) -> Maybe[UnboundLens]: 45 | check = lambda a: f(a[1]) / (lambda b: (a[0], b)) 46 | cat = lambda i, l: _add(lens[i])(l) 47 | return fa.with_index.find_map(check).map2(cat) 48 | 49 | __all__ = ('path_lens',) 50 | -------------------------------------------------------------------------------- /amino/typecheck.py: -------------------------------------------------------------------------------- 1 | import os 2 | import sys 3 | import inspect 4 | import logging 5 | from types import ModuleType 6 | from typing import List, Any 7 | from importlib.machinery import SourceFileLoader, SOURCE_SUFFIXES, FileFinder 8 | 9 | _enable_var = 'AMINO_TYPECHECK' 10 | _checker_var = 'AMINO_TYPECHECKER' 11 | _mods_var = 'AMINO_TYPECHECK_MODS' 12 | check_mods: List[str] = [] 13 | tc: Any = None 14 | 15 | if _enable_var in os.environ: 16 | mods = os.environ.get(_mods_var) or 'amino' 17 | check_mods = mods.split(',') 18 | 19 | 20 | class Loader(SourceFileLoader): 21 | 22 | def exec_module(self, module: ModuleType) -> None: 23 | from enforce import runtime_validation 24 | super().exec_module(module) 25 | if module.__package__ and module.__package__.split('.')[0] in check_mods: 26 | for clsname, cls in inspect.getmembers(module, inspect.isclass): 27 | for funname, fun in list((k, v) for k, v in cls.__dict__.items() if inspect.isfunction(v)): 28 | try: 29 | module.__dict__[funname] = runtime_validation(fun) 30 | except Exception as e: 31 | logging.debug(f'failed to apply runtime validation on {fun}: {e}') 32 | 33 | 34 | def init_enforce() -> None: 35 | loader_details = (Loader, SOURCE_SUFFIXES) 36 | sys.path_hooks.insert(0, FileFinder.path_hook(loader_details)) # type: ignore 37 | 38 | 39 | def init_typeguard() -> None: 40 | from typeguard import TypeChecker 41 | global tc 42 | tc = TypeChecker(check_mods) 43 | tc.start() 44 | 45 | 46 | def boot() -> bool: 47 | if check_mods: 48 | if os.environ.get(_checker_var) == 'enforce': 49 | init_enforce() 50 | else: 51 | init_typeguard() 52 | return True 53 | else: 54 | return False 55 | 56 | __all__ = ('boot', 'init_enforce', 'init_typeguard') 57 | -------------------------------------------------------------------------------- /amino/list.py: -------------------------------------------------------------------------------- 1 | from typing import TypeVar, Callable, Tuple 2 | 3 | from amino.data.list import List, Lists, Nil 4 | from amino import Eval, Maybe, Nothing, Just, __, Either 5 | 6 | A = TypeVar('A') 7 | B = TypeVar('B') 8 | 9 | 10 | def replace_one(fa: List[A], pred: Callable[[A], bool], new: A) -> Eval[Maybe[List[A]]]: 11 | def loop(head: A, tail: List[A]) -> Eval[Maybe[List[A]]]: 12 | def flatten(memla: Maybe[Eval[Maybe[List[A]]]]) -> Eval[Maybe[List[A]]]: 13 | return ( 14 | memla 15 | .map(lambda emla: emla.map(lambda mla: mla.map(lambda la: la.cons(head)))) | 16 | (lambda: Eval.now(Nothing)) 17 | ) 18 | return ( 19 | Eval.now(Just(tail.cons(new))) 20 | if pred(head) else 21 | Eval.later(tail.detach_head.map2, loop) // flatten 22 | ) 23 | return fa.detach_head.map2(loop) | Eval.now(Nothing) 24 | 25 | 26 | def split_by_status_zipped(data: List[Tuple[A, bool]]) -> Tuple[List[A], List[A]]: 27 | def decide(z: Tuple[List[A], List[A]], item: Tuple[A, bool]) -> Tuple[List[A], List[A]]: 28 | l, r = z 29 | a, status = item 30 | return ( 31 | (l.cat(a), r) 32 | if status else 33 | (l, r.cat(a)) 34 | ) 35 | return data.fold_left((Nil, Nil))(decide) 36 | 37 | 38 | def split_by_status(data: List[A], statuses: List[bool]) -> Tuple[List[A], List[A]]: 39 | return split_by_status_zipped(data.zip(statuses)) 40 | 41 | 42 | def split_either_list(data: List[Either[A, B]]) -> Tuple[List[A], List[B]]: 43 | def split(z: Tuple[List[A], List[B]], a: Either[A, B]) -> Tuple[List[A], List[B]]: 44 | ls, rs = z 45 | return a.cata((lambda e: (ls.cat(e), rs)), (lambda v: (ls, rs.cat(v)))) 46 | return data.fold_left((Nil, Nil))(split) 47 | 48 | 49 | __all__ = ('List', 'Lists', 'Nil', 'split_by_status', 'split_by_status_zipped', 'split_either_list',) 50 | -------------------------------------------------------------------------------- /amino/tc/optional.py: -------------------------------------------------------------------------------- 1 | import abc 2 | from typing import TypeVar, Callable, Union, cast, Any 3 | 4 | from amino.tc.base import TypeClass, tc_prop 5 | from amino import maybe # NOQA 6 | from amino.boolean import Boolean 7 | 8 | F = TypeVar('F') 9 | A = TypeVar('A') 10 | B = TypeVar('B') 11 | 12 | 13 | class Optional(TypeClass): 14 | 15 | @abc.abstractmethod 16 | def to_maybe(self, fa: F) -> 'maybe.Maybe[B]': 17 | ... 18 | 19 | def get_or_else(self, fa: F, a: Union[A, Callable[[], A]]): 20 | return self.to_maybe(fa).get_or_else(a) 21 | 22 | def get_or_strict(self, fa: F, a: A) -> A: 23 | return self.to_maybe(fa).get_or_strict(a) 24 | 25 | def get_or(self, fa: F, f: Callable[[], A], *a: Any, **kw: Any) -> A: 26 | return self.to_maybe(fa).get_or(f, *a, **kw) 27 | 28 | @abc.abstractmethod 29 | def to_either(self, fb: F, left: Union[A, Callable[[], A]]) -> 'Either[A, B]': 30 | ... 31 | 32 | def to_either_f(self, fb: F, left: Callable[[], A]) -> 'Either[A, B]': 33 | return self.to_either(fb, left) 34 | 35 | __or__ = get_or_else 36 | 37 | @abc.abstractmethod 38 | def present(self, fa: F) -> Boolean: 39 | ... 40 | 41 | def or_else(self, fa: F, a: Union[F, Callable[[], F]]): 42 | return fa if self.present(fa) else maybe.call_by_name(a) 43 | 44 | o = or_else 45 | 46 | def or_else_call(self, fa: F, f: Callable[[], F], *a: Any, **kw: Any): 47 | return fa if self.present(fa) else f(*a, **kw) 48 | 49 | def io(self, fa: F, err: str=''): 50 | from amino.io import IO 51 | return IO.from_either(self.to_either(fa, err)) 52 | 53 | @tc_prop 54 | def true(self, fa: F) -> Boolean: 55 | return self.to_maybe(fa).exists(bool) 56 | 57 | @tc_prop 58 | def _unsafe_value(self, fa: F) -> A: 59 | return cast(A, self.to_maybe(fa)._get) 60 | 61 | def absent(self, msg: str=None) -> F: 62 | ... 63 | 64 | 65 | __all__ = ('Optional',) 66 | -------------------------------------------------------------------------------- /amino/algebra.py: -------------------------------------------------------------------------------- 1 | import abc 2 | from typing import Any, Generic, TypeVar 3 | from types import SimpleNamespace 4 | 5 | from amino import List, Lists, Nil, Maybe 6 | from amino.util.string import ToStr 7 | 8 | A = TypeVar('A') 9 | 10 | 11 | def is_algebra(bases: List[type]) -> bool: 12 | return bases.exists(lambda a: hasattr(a, '__algebra_base__')) 13 | 14 | 15 | def find_algebra(name: str, bases: List[type]) -> Maybe[type]: 16 | return bases.find(lambda a: hasattr(a, '__algebra_variants__')) 17 | 18 | 19 | def setup_algebra(name: str, inst: type, bases: List[type]) -> None: 20 | if is_algebra(bases): 21 | inst.__algebra_variants__ = List() 22 | else: 23 | raise Exception(f'algebra subclass has no algebra superclass: {name}') 24 | 25 | 26 | def setup_variant(name: str, inst: type, bases: List[type], algebra: type) -> None: 27 | inst.__algebra_index__ = len(algebra.__algebra_variants__) 28 | algebra.__algebra_variants__.append(inst) 29 | 30 | 31 | def setup_algebraic_type(name: str, inst: type, bases: List[type]) -> None: 32 | return ( 33 | find_algebra(name, bases) 34 | .cata_f( 35 | lambda a: setup_variant(name, inst, bases, a), 36 | lambda: setup_algebra(name, inst, bases) 37 | ) 38 | ) 39 | 40 | 41 | class AlgebraMeta(abc.ABCMeta): 42 | 43 | def __new__( 44 | cls, 45 | name: str, 46 | bases: list, 47 | namespace: SimpleNamespace, 48 | algebra_base: bool=False, 49 | **kw: Any, 50 | ) -> None: 51 | inst = super().__new__(cls, name, bases, namespace, **kw) 52 | if not hasattr(inst, '__args__') or inst.__args__ is None: 53 | if algebra_base: 54 | inst.__algebra_base__ = None 55 | else: 56 | setup_algebraic_type(name, inst, Lists.wrap(bases)) 57 | return inst 58 | 59 | 60 | class Algebra(Generic[A], ToStr, metaclass=AlgebraMeta, algebra_base=True): 61 | pass 62 | 63 | 64 | __all__ = ('AlgebraMeta', 'Algebra') 65 | -------------------------------------------------------------------------------- /amino/meta/gen_state.py: -------------------------------------------------------------------------------- 1 | from typing import Any 2 | 3 | from amino import Path, List, IO, do, Do, Nil 4 | from amino.meta.gen import codegen, CodegenTask, codegen_write 5 | 6 | here = Path(__file__).absolute().parent 7 | template_path = here.parent.parent / 'templates' / 'state.py' 8 | 9 | 10 | def state_task( 11 | tpe: str, 12 | path: str, 13 | tvar: List[str]=Nil, 14 | class_extra: str='', 15 | meta_extra: str='', 16 | ctor_extra: str='', 17 | extra_import: List[str]=Nil, 18 | extra: str='', 19 | ) -> CodegenTask: 20 | tpar = tvar.map(lambda a: f'{a}, ').mk_string('') 21 | tvars = tvar.map(lambda a: f'{a} = TypeVar(\'{a}\')').join_lines 22 | tpe_import = f'from {path} import {tpe}' 23 | subs = List( 24 | (r'\bF\[', f'{tpe}[{tpar}'), 25 | (r'\bF\b', tpe), 26 | ('StateT\[', f'{tpe}State[{tpar}'), 27 | ('StateT', f'{tpe}State'), 28 | ('{tpar}', tpar), 29 | ('{tvar}', tvars), 30 | ('{f_import}', tpe_import), 31 | ('{class_extra}', class_extra), 32 | ('{meta_extra}', meta_extra), 33 | ('{ctor_extra}', ctor_extra), 34 | ('{extra_import}', extra_import.join_lines), 35 | ('{extra}', extra), 36 | ('StateBase', 'StateT'), 37 | ) 38 | return CodegenTask(template_path, subs, Nil) 39 | 40 | 41 | def generate_state(write: bool, name: str, *a: Any, **kw: Any) -> IO[str]: 42 | task = state_task(*a, **kw) 43 | outpath = here.parent / 'state' / f'{name}.py' 44 | return codegen_write(task, outpath) if write else codegen(task) 45 | 46 | 47 | @do(IO[None]) 48 | def generate_states() -> Do: 49 | yield generate_state(True, 'maybe', 'Maybe', 'amino.maybe') 50 | yield generate_state(True, 'eval', 'Eval', 'amino.eval') 51 | yield generate_state(True, 'io', 'IO', 'amino.io') 52 | yield generate_state(True, 'id', 'Id', 'amino.id') 53 | yield generate_state(True, 'either', 'Either', 'amino.either', tvar=List('E')) 54 | 55 | 56 | __all__ = ('generate_state', 'generate_states',) 57 | -------------------------------------------------------------------------------- /amino/test/path.py: -------------------------------------------------------------------------------- 1 | import sys 2 | from typing import Union 3 | 4 | from amino import env, Path 5 | 6 | __base_dir__ = None 7 | 8 | 9 | class TestEnvError(Exception): 10 | pass 11 | 12 | 13 | def setup(path: Union[str, Path]) -> None: 14 | ''' Use the supplied path to initialise the tests base dir. 15 | If _path is a file, its dirname is used. 16 | ''' 17 | if not isinstance(path, Path): 18 | path = Path(path) 19 | if not path.is_dir(): 20 | path = path.parent 21 | global __base_dir__ 22 | __base_dir__ = path 23 | container = str(pkg_dir()) 24 | if container not in sys.path: 25 | sys.path.insert(0, container) 26 | env['PYTHONPATH'] = '{}:{}'.format(container, env['PYTHONPATH'] | '') 27 | 28 | 29 | def _check() -> Path: 30 | if __base_dir__ is None: 31 | msg = 'Test base dir not set! Call amino.test.setup(dir).' 32 | raise TestEnvError(msg) 33 | else: 34 | return __base_dir__ 35 | 36 | 37 | def temp_path(*components: str) -> Path: 38 | dir = _check() 39 | return Path(dir, '_temp', *components) 40 | 41 | 42 | def temp_dir(*components: str) -> Path: 43 | _dir = temp_path(*components) 44 | _dir.mkdir(exist_ok=True, parents=True) 45 | return _dir 46 | 47 | 48 | def temp_file(*components: str) -> Path: 49 | return temp_dir(*components[:-1]).joinpath(*components[-1:]) 50 | 51 | 52 | def create_temp_file(*components: str) -> Path: 53 | _file = temp_file(*components) 54 | _file.touch() 55 | return _file 56 | 57 | 58 | def fixture_path(*components: str) -> Path: 59 | dir = _check() 60 | return Path(dir, '_fixtures', *components) 61 | 62 | 63 | def load_fixture(*components: str) -> str: 64 | with fixture_path(*components).open() as f: 65 | return f.read() 66 | 67 | 68 | def base_dir() -> Path: 69 | return _check() 70 | 71 | 72 | def pkg_dir() -> Path: 73 | return base_dir().parent 74 | 75 | __all__ = ('setup', 'create_temp_file', 'temp_file', 'temp_path', 'temp_dir', 'fixture_path', 'load_fixture', 76 | 'base_dir', 'pkg_dir') 77 | -------------------------------------------------------------------------------- /amino/tc/flat_map.py: -------------------------------------------------------------------------------- 1 | import abc 2 | from typing import Callable, Iterable, TypeVar, Generic, List 3 | 4 | import amino # NOQA 5 | from amino.tc.apply import Apply 6 | from amino.func import I 7 | from amino.tc.base import tc_prop 8 | from amino.tc.apply_n import ApplyN 9 | 10 | F = TypeVar('F') 11 | G = TypeVar('G') 12 | A = TypeVar('A') 13 | B = TypeVar('B') 14 | 15 | 16 | class FlatMap(Apply, ApplyN): 17 | 18 | def apply_n_funcs(self) -> List[str]: 19 | return super().apply_n_funcs() + ['flat_map', 'product'] 20 | 21 | def ap(self, fa: F, ff: F): 22 | f = lambda f: self.map(fa, f) 23 | return self.flat_map(ff, f) 24 | 25 | @abc.abstractmethod 26 | def flat_map(self, fa: F, f: Callable[[A], G]) -> G: 27 | ... 28 | 29 | def __floordiv__(self, fa, f): 30 | return self.flat_map(fa, f) 31 | 32 | def product(self, fa: F, fb: F) -> F: 33 | f = lambda a: self.map(fb, lambda b: (a, b)) 34 | return self.flat_map(fa, f) 35 | 36 | __and__ = product 37 | 38 | def product_n(self, num: int, fa: F, *fs: Iterable[F]): 39 | from amino.list import List 40 | if len(fs) != num: 41 | msg = 'passed {} args to {}.product{}' 42 | name = self.__class__.__name__ 43 | raise TypeError(msg.format(len(fs), name, num)) 44 | def add(a, b): 45 | return self.flat_map(a, lambda a: self.map(b, lambda b: a + (b,))) 46 | init = self.map(fa, lambda a: (a,)) 47 | return List.wrap(fs).fold_left(init)(add) 48 | 49 | def flat_pair(self, fa: F, f: Callable[[A], 'amino.maybe.Maybe[B]']) -> F: 50 | cb = lambda a: f(a).map(lambda b: (a, b)) 51 | return self.flat_map(fa, cb) 52 | 53 | def flat_replace(self, fa: F, fb: F) -> F: 54 | cb = lambda a: fb 55 | return self.flat_map(fa, cb) 56 | 57 | def and_then(self, fa: F, fb: F) -> F: 58 | cb = lambda a: fb 59 | return self.flat_map(fa, cb) 60 | 61 | @tc_prop 62 | def join(self, fa: F) -> F: 63 | return self.flat_map(fa, I) 64 | 65 | __all__ = ('FlatMap',) 66 | -------------------------------------------------------------------------------- /amino/util/tpe.py: -------------------------------------------------------------------------------- 1 | from typing import Iterable, Any, TypeVar, _GenericAlias 2 | 3 | from amino import do, Either, Do, Maybe, Right, Left, Lists 4 | from amino.logging import module_log 5 | 6 | A = TypeVar('A') 7 | log = module_log() 8 | 9 | 10 | def normalize_generic_type(tpe: type) -> type: 11 | return ( 12 | tpe.__origin__ 13 | if isinstance(tpe, _GenericAlias) else 14 | tpe 15 | ) 16 | 17 | 18 | def qualified_type(tpe: type) -> str: 19 | ntpe = normalize_generic_type(tpe) 20 | return ntpe.__name__ if ntpe.__module__ == 'builtins' else f'{ntpe.__module__}.{ntpe.__name__}' 21 | 22 | 23 | def qualified_name(inst: Any) -> str: 24 | return inst.__name__ if inst.__module__ == 'builtins' else f'{inst.__module__}.{inst.__name__}' 25 | 26 | 27 | @do(Either[str, type]) 28 | def type_arg(tpe: type, index: int) -> Do: 29 | def error() -> str: 30 | return f'{tpe} has no type args' 31 | raw = yield Maybe.getattr(tpe, '__args__').to_either_f(error) 32 | types = yield Right(Lists.wrap(raw)) if isinstance(raw, Iterable) else Left(error()) 33 | yield types.lift(index).to_either_f(lambda: f'{tpe} has less than {index + 1} args') 34 | 35 | 36 | def first_type_arg(tpe: type) -> Either[str, type]: 37 | return type_arg(tpe, 0) 38 | 39 | 40 | def qualname(a: Any) -> str: 41 | return ( 42 | a.__qualname__ 43 | if hasattr(a, '__qualname__') else 44 | a.__origin__.__qualname__ 45 | if hasattr(a, '__origin__') else 46 | a.__qualname__ 47 | ) 48 | 49 | 50 | def is_subclass(tpe: Any, base: type) -> bool: 51 | def error(): 52 | raise TypeError(f'argument `{tpe}` to `is_subclass({base})` is neither TypeVar nor type') 53 | return ( 54 | False 55 | if isinstance(tpe, TypeVar) else 56 | issubclass(normalize_generic_type(tpe), base) 57 | if isinstance(tpe, (type, _GenericAlias)) else 58 | error() 59 | ) 60 | 61 | 62 | __all__ = ('qualified_type', 'type_arg', 'first_type_arg', 'qualified_name', 'qualname', 'normalize_generic_type', 63 | 'is_subclass',) 64 | -------------------------------------------------------------------------------- /amino/test/spec.py: -------------------------------------------------------------------------------- 1 | import os 2 | import time 3 | import shutil 4 | import inspect 5 | import warnings 6 | import traceback 7 | from functools import wraps 8 | from contextlib import contextmanager 9 | 10 | import amino 11 | from amino.logging import amino_stdout_logging, Logging, amino_root_file_logging 12 | from amino.test import path 13 | from amino import Path, List 14 | 15 | 16 | default_timeout = 20 if 'TRAVIS' in os.environ else 5 17 | 18 | 19 | class SpecBase(Logging): 20 | 21 | @property 22 | def _warnings(self) -> bool: 23 | return True 24 | 25 | def setup(self) -> None: 26 | if path.__base_dir__: 27 | shutil.rmtree(str(path.temp_path()), ignore_errors=True) 28 | if self._warnings: 29 | warnings.resetwarnings() 30 | amino.development.set(True) 31 | amino_stdout_logging() 32 | amino_root_file_logging() 33 | 34 | def teardown(self) -> None: 35 | warnings.simplefilter('ignore') 36 | 37 | def _wait(self, seconds: float) -> None: 38 | time.sleep(seconds) 39 | 40 | 41 | class IntegrationSpecBase(SpecBase): 42 | 43 | def setup(self) -> None: 44 | SpecBase.setup(self) 45 | os.environ['AMINO_INTEGRATION'] = '1' 46 | amino.integration_test = True 47 | 48 | def teardown(self): 49 | SpecBase.teardown(self) 50 | 51 | 52 | def profiled(sort='time'): 53 | fname = 'prof' 54 | def dec(f): 55 | import cProfile 56 | import pstats 57 | @wraps(f) 58 | def wrap(*a, **kw): 59 | cProfile.runctx('f(*a, **kw)', dict(), dict(f=f, a=a, kw=kw), 60 | filename=fname) 61 | stats = pstats.Stats(fname) 62 | stats.sort_stats(sort).print_stats(30) 63 | Path(fname).unlink() 64 | return wrap 65 | return dec 66 | 67 | 68 | def callers(limit=20): 69 | stack = (List.wrap(inspect.stack()) 70 | .filter_not(lambda a: 'amino' in a.filename)) 71 | data = stack[:limit] / (lambda a: a[1:-2] + tuple(a[-2])) 72 | return ''.join(traceback.format_list(data)) 73 | 74 | 75 | __all__ = ('SpecBase', 'profiled', 'IntegrationSpecBase') 76 | -------------------------------------------------------------------------------- /amino/util/string.py: -------------------------------------------------------------------------------- 1 | import re 2 | import abc 3 | from typing import Any, Sized 4 | from functools import singledispatch 5 | 6 | import amino 7 | 8 | 9 | def snake_case(name: str) -> str: 10 | s1 = re.sub('([^_])([A-Z][a-z]+)', r'\1_\2', name) 11 | return re.sub('([a-z0-9])([A-Z])', r'\1_\2', s1).lower() 12 | 13 | 14 | @singledispatch 15 | def decode(value: Any) -> Any: 16 | return value 17 | 18 | 19 | @decode.register(bytes) 20 | def decode_bytes(value: bytes) -> str: 21 | return value.decode() 22 | 23 | 24 | @decode.register(list) 25 | def decode_list(value: list) -> 'amino.List[str]': 26 | return amino.List.wrap(value).map(decode) 27 | 28 | 29 | @decode.register(tuple) 30 | def decode_tuple(value: tuple) -> 'amino.List[str]': 31 | return decode_list(value) 32 | 33 | 34 | @decode.register(dict) 35 | def decode_dict(value: dict) -> 'amino.Map[str, str]': 36 | return amino.Map.wrap(value).keymap(decode).valmap(decode) 37 | 38 | 39 | @decode.register(Exception) 40 | def decode_exc(value: Exception) -> str: 41 | return decode_list(value.args).head | str(value) 42 | 43 | 44 | def camelcase(name: str, sep: str='', splitter: str='_') -> str: 45 | return sep.join([n.capitalize() for n in re.split(splitter, name)]) 46 | 47 | camelcaseify = camelcase 48 | 49 | 50 | def safe_string(value: Any) -> str: 51 | try: 52 | return str(value) 53 | except Exception: 54 | try: 55 | return repr(value) 56 | except Exception: 57 | return 'invalid' 58 | 59 | 60 | class ToStr(abc.ABC): 61 | 62 | @abc.abstractmethod 63 | def _arg_desc(self) -> 'amino.List[str]': 64 | ... 65 | 66 | def __str__(self) -> str: 67 | args = self._arg_desc().join_comma 68 | return f'{self.__class__.__name__}({args})' 69 | 70 | def __repr__(self) -> str: 71 | return str(self) 72 | 73 | 74 | def plural_s(items: Sized) -> str: 75 | return '' if len(items) == 1 else 's' 76 | 77 | 78 | def indent(data: str, count: int=2) -> str: 79 | ws = ' ' * count 80 | return f'{ws}{data}' 81 | 82 | 83 | __all__ = ('snake_case', 'decode', 'camelcaseify', 'camelcase', 'plural_s', 'indent',) 84 | -------------------------------------------------------------------------------- /amino/test/sure_ext.py: -------------------------------------------------------------------------------- 1 | from pathlib import Path 2 | 3 | import sure 4 | 5 | from amino import Maybe, Empty, Just, Left, Right, Either 6 | 7 | 8 | class AssBuilder(sure.AssertionBuilder): 9 | 10 | @sure.assertionmethod 11 | def contain(self, what): 12 | if isinstance(self.obj, Maybe): 13 | return self.just_contain(what) 14 | else: 15 | return super().contain(what) 16 | 17 | @sure.assertionmethod 18 | def just_contain(self, what): 19 | self.obj.should.be.a(Maybe) 20 | if self.negative: 21 | msg = "{} contains {}, expected {}" 22 | assert self.obj.is_empty or self.obj._get != what,\ 23 | msg.format(self.obj, self.obj._get, what) 24 | else: 25 | self.be.just 26 | self.obj._get.should.equal(what) 27 | return True 28 | 29 | def _bool(self, pred, agent, action): 30 | no = ('not ' if self.negative else '') + 'to ' 31 | assert pred(self.obj) ^ self.negative,\ 32 | 'expected {} \'{}\' {} {}'.format(agent, self.obj, no, action) 33 | return True 34 | 35 | @sure.assertionproperty 36 | def just(self): 37 | return self.be.a(Just) 38 | 39 | @sure.assertionproperty 40 | def empty(self): 41 | if isinstance(self.obj, Maybe): 42 | return self.be.a(Empty) 43 | elif isinstance(self.obj, Either): 44 | return self.be.a(Left) 45 | else: 46 | return super().empty 47 | 48 | @sure.assertionproperty 49 | def exist(self): 50 | err = "can only check existence of Path, not {}" 51 | assert isinstance(self.obj, Path), err.format(self.obj) 52 | return self._bool(lambda a: a.exists(), "path", "exist") 53 | 54 | @sure.assertionmethod 55 | def start_with(self, prefix): 56 | return self.match('^{}'.format(prefix)) 57 | 58 | @sure.assertionproperty 59 | def right(self): 60 | return self.be.a(Right) 61 | 62 | @sure.assertionproperty 63 | def left(self): 64 | return self.be.a(Left) 65 | 66 | 67 | def install_assertion_builder(builder): 68 | sure.AssertionBuilder = builder 69 | 70 | __all__ = ('install_assertion_builder', 'AssBuilder') 71 | -------------------------------------------------------------------------------- /unit/lens_spec.py: -------------------------------------------------------------------------------- 1 | from lenses import lens, bind 2 | 3 | from amino import List, _ 4 | from amino.test.spec_spec import Spec 5 | from amino.lenses.tree import path_lens_pred 6 | 7 | 8 | class B: 9 | 10 | def __init__(self, a, b=1) -> None: 11 | self.a = a 12 | self.b = b 13 | 14 | def __repr__(self): 15 | return 'B({}, {})'.format(self.a, self.b) 16 | 17 | def __eq__(self, other): 18 | return self.a == other.a and self.b == other.b 19 | 20 | 21 | class A: 22 | 23 | def __init__(self, b: List[B]) -> None: 24 | self.b = b 25 | 26 | def __repr__(self): 27 | return 'A({!r})'.format(self.b) 28 | 29 | def __eq__(self, other): 30 | return self.b == other.b 31 | 32 | 33 | class C: 34 | 35 | def __init__(self, a, c: List['C']=List()) -> None: 36 | self.a = a 37 | self.c = c 38 | 39 | def __repr__(self): 40 | sub = '' if self.c.is_empty else ', {!r}'.format(self.c) 41 | return 'C({}{})'.format(self.a, sub) 42 | 43 | def __eq__(self, other): 44 | return self.a == other.a and self.c == other.c 45 | 46 | 47 | class LensSpec(Spec): 48 | 49 | def tuple_(self): 50 | x, y, z = 11, 22, 33 51 | def mod(a): 52 | a[0][1].a = B(y) 53 | return a[0], B(x), bind(a[2]).b.set(z) 54 | b = List(B(B(0)), B(B(1)), B(B(2))) 55 | a = A(b) 56 | l1 = lens.GetAttr('b') 57 | l2 = lens.GetAttr('b')[0].GetAttr('a') 58 | l3 = lens.GetAttr('b')[2] 59 | l = lens.Tuple(l1, l2, l3) 60 | target = A(List(B(B(x)), B(B(y)), B(B(2), z))) 61 | l.modify(mod)(a).should.equal(target) 62 | 63 | def path(self): 64 | c = C(1, List(C(2), C(3, List(C(4, List(C(5))), C(6))), C(7))) 65 | t = path_lens_pred(c, _.c, _.a, _ == 5).x 66 | mod = lambda a: (a + 10 if isinstance(a, int) else bind(a).a.modify(_ + 20)) 67 | m = t.modify(lambda a: map(mod, a)) 68 | target = C(21, List(C(2), C(23, List(C(24, List(C(15))), C(6))), C(7))) 69 | m.should.equal(target) 70 | m2 = t.get() 71 | t.set(map(mod, m2)).should.equal(target) 72 | 73 | def list(self) -> None: 74 | fa = List(1, 2, 3) 75 | lns = lens.Each().modify(lambda a: a + 1) 76 | lns(fa).should.be.a(List) 77 | 78 | 79 | __all__ = ('LensSpec',) 80 | -------------------------------------------------------------------------------- /amino/lazy.py: -------------------------------------------------------------------------------- 1 | from typing import TypeVar, Generic, Callable, Any, cast 2 | import functools 3 | 4 | A = TypeVar('A') 5 | _attr_fmt = '_{}__value' 6 | 7 | 8 | class LazyMeta(type): 9 | 10 | def __new__(mcs: type, name: str, bases: tuple, dct: dict) -> type: 11 | lazies = [k for k, v in dct.items() if isinstance(v, lazy)] 12 | attrs = tuple(map('_{}__value'.format, lazies)) 13 | inst = super().__new__(mcs, name, bases, dct) 14 | inst.__slots__ = attrs 15 | return inst 16 | 17 | 18 | class LazyError(Exception): 19 | 20 | def __init__(self, name: str, inst: Any) -> None: 21 | self.name = name 22 | self.inst = inst 23 | 24 | def __str__(self) -> str: 25 | msg = ( 26 | 'class {} with lazy attribute must have a __dict__, a {}' + 27 | ' slot or use LazyMeta as metaclass' 28 | ) 29 | return msg.format(type(self.inst).__name__, self.name) 30 | 31 | 32 | class LazyAttributeError(Exception): 33 | 34 | def __init__(self, cause: AttributeError) -> None: 35 | self.cause = cause 36 | 37 | 38 | class lazy(Generic[A]): 39 | 40 | def __init__(self, func: Callable[..., A], name: str=None) -> None: 41 | self.func = func 42 | self._attr_name = _attr_fmt.format(name or self.func.__name__) 43 | functools.wraps(self.func)(self) # type: ignore 44 | 45 | def __get__(self, inst: Any, inst_cls: type) -> A: 46 | return cast(A, self if inst is None else self._get(inst, inst_cls)) 47 | 48 | def _get(self, inst: Any, inst_cls: type) -> A: 49 | try: 50 | self._check(inst) 51 | except AttributeError as e: 52 | raise LazyAttributeError(e) 53 | return getattr(inst, self._attr_name) 54 | 55 | def _valid_slot(self, inst: Any) -> bool: 56 | return hasattr(inst, '__slots__') and self._attr_name in inst.__slots__ 57 | 58 | def _check(self, inst: Any) -> None: 59 | if not (hasattr(inst, '__dict__') or self._valid_slot): 60 | self._complain() 61 | if not hasattr(inst, self._attr_name): 62 | object.__setattr__(inst, self._attr_name, self.func(inst)) 63 | 64 | def _complain(self, inst: Any) -> LazyError: 65 | raise LazyError(self._attr_name, inst) 66 | 67 | 68 | class Lazy(metaclass=LazyMeta): 69 | pass 70 | 71 | __all__ = ('lazy',) 72 | -------------------------------------------------------------------------------- /unit/tree_spec.py: -------------------------------------------------------------------------------- 1 | from typing import Generic, TypeVar, Any 2 | 3 | from amino.test.spec_spec import Spec 4 | from amino.tree import ListNode, LeafNode, Node, MapNode 5 | from amino import _, Map, LazyList 6 | from amino.lazy_list import LazyLists 7 | 8 | 9 | Sub = TypeVar('Sub') 10 | 11 | 12 | class StrNode(Generic[Sub], Node[str, Sub]): 13 | pass 14 | 15 | 16 | class StrLeaf(LeafNode[str], StrNode[None]): 17 | pass 18 | 19 | 20 | class StrListNode(ListNode[str], StrNode[LazyList[Node[str, Any]]]): 21 | pass 22 | 23 | 24 | class StrMapNode(MapNode[str], StrNode[Map[str, Node[str, Any]]]): 25 | pass 26 | 27 | 28 | tree = StrListNode(LazyLists.cons(StrLeaf("leaf1"), StrListNode(LazyLists.cons(StrLeaf("sub1"), StrLeaf("sub2"))))) 29 | mtree = StrMapNode(Map(first=tree, second=StrLeaf('leaf2'))) 30 | 31 | 32 | show_target = '''[] 33 | leaf1 34 | [] 35 | sub1 36 | sub2''' 37 | 38 | 39 | class TreeSpec(Spec): 40 | 41 | def sub(self) -> None: 42 | t1 = tree.lift(1) 43 | t1.head.data.data.should.equal('sub1') 44 | 45 | def flat_map(self) -> None: 46 | def f(n: str) -> Node: 47 | return StrLeaf(f'changed: {n}') 48 | t1 = tree.flat_map(f) 49 | t1.head.e.map(_.data).should.contain('changed: leaf1') 50 | 51 | def map(self) -> None: 52 | def f(n: str) -> Node: 53 | return f"changed: {n}" 54 | t1 = tree.map(f) 55 | t1.head.e.map(_.data).should.contain('changed: leaf1') 56 | 57 | def map_node(self) -> None: 58 | def f(n: str) -> Node: 59 | return f"changed: {n}" 60 | t1 = mtree.map(f) 61 | t1.s.first.head.e.map(_.data).should.contain('changed: leaf1') 62 | t1.s.second.e.map(_.data).should.contain('changed: leaf2') 63 | 64 | def show(self) -> None: 65 | tree.show.should.equal(show_target) 66 | 67 | def fold_left(self) -> None: 68 | def folder(z: str, a: StrNode) -> str: 69 | return ( 70 | f'{z} {a.data}' 71 | if isinstance(a, StrLeaf) else 72 | z 73 | ) 74 | tree.fold_left('start:')(folder).should.equal('start: leaf1 sub1 sub2') 75 | 76 | def filter(self) -> None: 77 | target = 'sub1' 78 | def filt(node: StrNode) -> bool: 79 | return isinstance(node, StrLeaf) and node.data == target 80 | mtree.filter(filt).s.first.head.head.e.map(_.data).should.contain(target) 81 | 82 | __all__ = ('TreeSpec',) 83 | -------------------------------------------------------------------------------- /unit/eff_spec.py: -------------------------------------------------------------------------------- 1 | import random 2 | 3 | from amino import List, Right, _, Just, Empty, Maybe, Either, Left 4 | 5 | from amino.test.spec_spec import Spec 6 | from amino.io import IO 7 | 8 | 9 | class EffSpec(Spec): 10 | 11 | @property 12 | def _r(self): 13 | a, b = random.randint(0, 10), random.randint(0, 10) 14 | return (a, b) if a != b else self._r 15 | 16 | def eff_map(self): 17 | a, b = self._r 18 | c = -1 19 | t = Right(List(Just(Right(a)), Just(Left(c)), Empty())) 20 | target = Right(List(Just(Right(a + b)), Just(Left(c)), Empty())) 21 | res = t.effs(3).map(_ + b) 22 | res.value.should.equal(target) 23 | 24 | def eff_map_io(self): 25 | a, b = self._r 26 | t = IO.now(List(Just(a), Just(b), Empty())) 27 | target = List(Just(a + b), Just(b + b), Empty()) 28 | res = t.effs(2).map(_ + b) 29 | res.value.run().should.equal(target) 30 | 31 | def eff_flat(self): 32 | a, b = self._r 33 | t = List(Just(Right(Just(a)))) 34 | target = List(Just(Right(Just(a + b)))) 35 | res = (t.effs(3, Maybe, Either, Maybe) 36 | .flat_map(lambda x: List(Just(Right(Just(x + b)))))) 37 | res.value.should.equal(target) 38 | 39 | def eff_flat_empty(self): 40 | t = List(Right(Empty())) 41 | target = List(Right(Empty())) 42 | res = (t.effs(3, Either, Maybe, Maybe) 43 | .flat_map(lambda x: List(Right(Just(Just(1)))))) 44 | res.value.should.equal(target) 45 | 46 | def eff_flat_io(self): 47 | a, b = self._r 48 | t = IO.now(List(Right(Just(Right(a))))) 49 | target = List(Right(Just(Right(a + b)))) 50 | res = (t.effs(4, List, Either, Maybe, Either) 51 | .flat_map(lambda x: IO.now(List(Right(Just(Right(x + b))))))) 52 | res.value.run().should.equal(target) 53 | 54 | def eff_flat_io_empty(self): 55 | t = IO.now(List(Right(Empty()))) 56 | target = List(Right(Empty())) 57 | res = (t.effs(4, List, Either, Maybe, Either) 58 | .flat_map(lambda x: IO.now(List(Right(Just(Right(1))))))) 59 | res.value.run().should.equal(target) 60 | 61 | def eff_flat_io_left(self): 62 | a, b = self._r 63 | t = IO.now(Left(Just(a))) 64 | target = Left(Just(a)) 65 | res = t.effs(1, Either, Maybe) // (lambda x: IO.now(Right(Just(b)))) 66 | res.value.run().should.equal(target) 67 | 68 | __all__ = ('EffSpec',) 69 | -------------------------------------------------------------------------------- /unit/map_spec.py: -------------------------------------------------------------------------------- 1 | from amino.test.spec_spec import Spec 2 | 3 | from amino import Map, Empty, Just, _, Right, Either 4 | 5 | 6 | class MapSpec(Spec): 7 | 8 | def lift(self): 9 | key = 'key' 10 | val = 'value' 11 | m = Map({key: val}) 12 | m.lift(key).should.equal(Just(val)) 13 | m.lift(key + key).should.equal(Empty()) 14 | 15 | def add(self): 16 | key = 'key' 17 | val = 'value' 18 | k2 = 'key2' 19 | v2 = 'value2' 20 | m = Map({key: val}) 21 | m2 = m + (k2, v2) 22 | m2.lift(k2).should.equal(Just(v2)) 23 | m.lift(k2).should.equal(Empty()) 24 | 25 | def add_multi(self): 26 | key = 'key' 27 | val = 'value' 28 | k2 = 'key2' 29 | v2 = 'value2' 30 | m = Map({key: val}) 31 | m2 = m ** Map({k2: v2}) 32 | m2.lift(k2).should.equal(Just(v2)) 33 | m.lift(k2).should.equal(Empty()) 34 | 35 | def find(self): 36 | k1 = 'key' 37 | v1 = 'value' 38 | k2 = 'key2' 39 | v2 = 'value2' 40 | m = Map({k1: v1, k2: v2}) 41 | m.find(_ == v1).should.equal(Just((k1, v1))) 42 | m.find_key(_ == k2).should.equal(Just((k2, v2))) 43 | m.find(_ == 'invalid').should.equal(Empty()) 44 | m.find_key(_ == 'invalid').should.equal(Empty()) 45 | 46 | def map(self): 47 | k1 = 'key' 48 | v1 = 'value' 49 | k2 = 'key2' 50 | v2 = 'value2' 51 | m = Map({k1: v1, k2: v2}) 52 | res = m.map(lambda a, b: (len(a), len(b))) 53 | res.should.have.key(len(k1)).being.equal(len(v1)) 54 | res.should.have.key(len(k2)).being.equal(len(v2)) 55 | 56 | def keymap(self): 57 | k1 = 'key' 58 | v1 = 'value' 59 | k2 = 'key2' 60 | v2 = 'value2' 61 | m = Map({k1: v1, k2: v2}) 62 | res = m.keymap(lambda a: len(a)) 63 | res.should.have.key(len(k1)).being.equal(v1) 64 | res.should.have.key(len(k2)).being.equal(v2) 65 | 66 | def flat_map(self): 67 | k1 = 'key' 68 | v1 = 'value' 69 | k2 = 'key2' 70 | v2 = 'value2' 71 | m = Map({k1: v1, k2: v2}) 72 | res = m.flat_map(lambda a, b: Just((a, b)) if a == k1 else Empty()) 73 | res.should.have.key(k1).being.equal(v1) 74 | res.should_not.have.key(k2) 75 | 76 | def bimap(self): 77 | f = _ + 1 78 | g = _ + 2 79 | Map({1: 2}).bimap(f, g).should.equal(Map({2: 4})) 80 | 81 | def traverse(self) -> None: 82 | def f(a: int) -> Either[str, int]: 83 | return Right(a * 2) 84 | Map({1: 2, 3: 4}).traverse(f, Either).should.equal(Right(Map({1: 4, 3: 8}))) 85 | 86 | __all__ = ('MapSpec') 87 | -------------------------------------------------------------------------------- /unit/state_spec.py: -------------------------------------------------------------------------------- 1 | from typing import Tuple 2 | 3 | from amino.state.maybe import MaybeState 4 | from amino import Maybe, Just, Left, Right, Either, Id, List 5 | from amino.state.either import EitherState 6 | from amino.state.eval import EvalState 7 | from amino.state.id import IdState 8 | from amino.test.spec_spec import Spec 9 | 10 | 11 | class State3Spec(Spec): 12 | 13 | def types(self) -> None: 14 | s: MaybeState[int, int] = MaybeState.pure(5) 15 | x: Maybe[Tuple[int, int]] = s.run(2) 16 | x 17 | 18 | def pure(self) -> None: 19 | assert(MaybeState.s(str).pure(1).run('state') == Just(('state', 1))) 20 | 21 | def flat_map(self) -> None: 22 | s = MaybeState.s(str).pure(1) 23 | def f(a: int) -> MaybeState[str, int]: 24 | return MaybeState.s(str).inspect(lambda s: len(s) + a) 25 | s1 = s.flat_map(f) 26 | assert(s1.run('str') == Just(('str', 4))) 27 | 28 | def modify(self) -> None: 29 | MaybeState.s(str).modify((lambda s: s + ' updated')).run_s('state').should.equal(Just('state updated')) 30 | 31 | def modify_f(self) -> None: 32 | MaybeState.s(str).modify_f((lambda s: Just(s + ' updated'))).run_s('state').should.equal(Just('state updated')) 33 | 34 | def flat_map_f(self) -> None: 35 | l = Left('boing') 36 | EitherState.pure(1).flat_map_f(lambda a: l).run('start').should.equal(l) 37 | 38 | def zip(self) -> None: 39 | EvalState.pure(1).zip(EvalState.pure(2)).run_a(None)._value().should.equal(List(1, 2)) 40 | 41 | def eff(self) -> None: 42 | def f(a: int) -> EvalState[int, Either[str, int]]: 43 | return EvalState.pure(Right(2)) 44 | s0: EvalState[int, Either[str, int]] = EvalState.s(int).pure(Right(1)) 45 | s0.eff(Either).flat_map(f).value.run(1)._value().should.equal((1, Right(2))) 46 | assert ((s0 // EvalState.s(int).modify(lambda a: a).replace).eff(Either).flat_map(f).value.run(1)._value() == 47 | (1, Right(2))) 48 | 49 | def id(self) -> None: 50 | s = IdState.s(int).inspect(lambda s0: s0 * 2).flat_map(lambda a: IdState.pure(a + 4)) 51 | s.run(5).should.equal(Id((5, 14))) 52 | 53 | def transform_s(self) -> None: 54 | def trans_from(r: str) -> int: 55 | return int(r) 56 | def trans_to(r: str, s: int) -> str: 57 | return str(s) 58 | s1 = IdState(Id(lambda s: Id((s + 1, None)))).transform_s(trans_from, trans_to) 59 | s1.run_s('2').value.should.equal('3') 60 | 61 | def transform_f(self) -> None: 62 | MaybeState.pure(7).transform_f(EitherState, lambda m: m.to_either('none')).run_a(None).should.equal(Right(7)) 63 | 64 | def lift_left(self) -> None: 65 | EitherState.lift(Left(1)).run_a(None).should.equal(Left(1)) 66 | 67 | 68 | __all__ = ('State3Spec',) 69 | -------------------------------------------------------------------------------- /unit/implicit_spec.py: -------------------------------------------------------------------------------- 1 | from typing import Callable, Any 2 | import collections.abc 3 | 4 | from amino import Just, Map, List 5 | from amino.test.spec_spec import Spec 6 | from amino.instances.maybe import MaybeMonad 7 | from amino.tc.base import (ImplicitInstances, Instances, InstancesMetadata, 8 | TypeClass) 9 | from amino.tc.monoid import Monoid 10 | from amino.lazy import lazy 11 | from amino.instances.list import ListMonoid 12 | from amino.tc.functor import Functor 13 | 14 | 15 | class A: 16 | 17 | def __init__(self, a: int) -> None: 18 | self.a = a 19 | 20 | 21 | class AMonoid(Monoid): 22 | 23 | @property 24 | def empty(self) -> A: 25 | return A(0) 26 | 27 | def combine(self, l: A, r: A) -> A: 28 | return A(l.a + r.a) 29 | 30 | 31 | class AInstances(ImplicitInstances): 32 | 33 | @lazy 34 | def _instances(self) -> Map[str, TypeClass]: 35 | return Map({Monoid: AMonoid()}) 36 | 37 | 38 | Instances.add(InstancesMetadata('A', 'unit.implicit_spec', 'AInstances')) 39 | 40 | 41 | class B: 42 | pass 43 | 44 | 45 | class BFunctor(Functor): 46 | 47 | def map(self, fa: B, f: Callable) -> B: 48 | return fa 49 | 50 | 51 | class BInstances(ImplicitInstances, tpe=B): 52 | 53 | @lazy 54 | def _instances(self) -> Map[type, TypeClass]: 55 | return Map({Functor: BFunctor()}) 56 | 57 | 58 | class C: 59 | pass 60 | 61 | 62 | class CMonoid(Monoid, tpe=C): 63 | 64 | @property 65 | def empty(self) -> C: 66 | return C() 67 | 68 | def combine(self, l: C, r: C) -> C: 69 | return C() 70 | 71 | 72 | class CFunctor(Functor, tpe=C): 73 | 74 | def map(self, fa: C, f: Callable) -> C: 75 | return fa 76 | 77 | 78 | def _pred_d_functor(tpe: type) -> bool: 79 | return issubclass(tpe, collections.abc.Iterable) 80 | 81 | 82 | class DFunctor(Functor, pred=_pred_d_functor): 83 | 84 | def map(a: Any, f: Callable) -> Any: 85 | return a 86 | 87 | 88 | class ImplicitSpec(Spec): 89 | 90 | def set_attr(self) -> None: 91 | Just(1).map.__wrapped__.__self__.should.be.a(MaybeMonad) 92 | 93 | def manual(self) -> None: 94 | Monoid.fatal(A).should.be.a(AMonoid) 95 | Monoid.fatal(List).should.be.a(ListMonoid) 96 | Functor.fatal(B).should.be.a(BFunctor) 97 | 98 | def auto_type_class(self) -> None: 99 | Monoid.fatal(C).should.be.a(CMonoid) 100 | Functor.fatal(C).should.be.a(CFunctor) 101 | 102 | def pred_type_class(self) -> None: 103 | Functor.fatal(list).should.be.a(DFunctor) 104 | Functor.fatal(str).should.be.a(DFunctor) 105 | Functor.fatal(str).should.be.a(DFunctor) 106 | Functor.m(int).should.be.empty 107 | 108 | __all__ = ('ImplicitSpec',) 109 | -------------------------------------------------------------------------------- /amino/util/trace.py: -------------------------------------------------------------------------------- 1 | import inspect 2 | from types import FrameType 3 | from typing import Any, Tuple 4 | 5 | from amino import List, Lists, Path, Either, _, Try, Right, Maybe, L, Nil 6 | from amino.do import do, Do 7 | from amino.func import tailrec 8 | 9 | 10 | def cframe() -> FrameType: 11 | return inspect.currentframe() 12 | 13 | 14 | @do(Either[Exception, str]) 15 | def file_line(file: str, lineno: int) -> Do: 16 | path = yield Try(Path, file) 17 | text = yield Try(path.read_text) 18 | yield Lists.lines(text).lift(lineno).to_either('frame_data: invalid line number in frame') 19 | 20 | 21 | def frame_data(frame: FrameType) -> Tuple[str, str]: 22 | file = inspect.getsourcefile(frame.f_code) 23 | code = file_line(file, frame.f_lineno - 1).value_or(lambda err: f'invalid file entry `{file}`: {err}') 24 | return file, code 25 | 26 | 27 | def traceback_entry(frame: FrameType, file: str, code: str) -> List[str]: 28 | line = frame.f_lineno 29 | fun = frame.f_code.co_name 30 | clean = code.strip() 31 | return List(f' File "{file}", line {line}, in {fun}', f' {clean}') 32 | 33 | 34 | def frame_traceback_entry(frame: FrameType) -> List[str]: 35 | file, code = frame_data(frame) 36 | return traceback_entry(frame, file, code) 37 | 38 | 39 | default_internal_packages = List('amino') 40 | 41 | 42 | def internal_frame(frame: FrameType, pkgs: List[str]=None) -> bool: 43 | pkg = frame.f_globals.get('__package__') 44 | name = frame.f_code.co_filename or '' 45 | return ( 46 | (not isinstance(pkg, str)) or 47 | pkg == '' or 48 | (pkgs or default_internal_packages).exists(pkg.startswith) or 49 | not name.endswith('.py') 50 | ) 51 | 52 | 53 | def non_internal_frame(frame: FrameType, pkgs: List[str]=None) -> bool: 54 | return not internal_frame(frame, pkgs) 55 | 56 | 57 | def tb_callsite(tb: List[FrameType], pkgs: List[str]=None) -> Either[str, FrameType]: 58 | return tb.find(L(non_internal_frame)(_, pkgs)).to_either('no non-internal frame found') 59 | 60 | 61 | def frame_callsite(frame: FrameType, pkgs: List[str]=None) -> FrameType: 62 | @tailrec 63 | def loop(f: FrameType) -> FrameType: 64 | return (True, (f.f_back,)) if not (f is None or f.f_back is None) and internal_frame(f, pkgs) else (False, f) 65 | return loop(frame) 66 | 67 | 68 | def callsite_traceback_entry(frame: FrameType, pkgs: List[str]=None) -> List[str]: 69 | return frame_traceback_entry(frame_callsite(frame, pkgs)) 70 | 71 | 72 | def callsite_info(frame: FrameType, pkgs: List[str]=None) -> List[str]: 73 | return Maybe.optional(frame) / L(callsite_traceback_entry)(_, pkgs) | List(' ') 74 | 75 | 76 | def callsite_source(frame: FrameType, pkgs: List[str]=None) -> str: 77 | return Maybe.optional(frame) / (lambda f: frame_data(frame_callsite(f, pkgs))) / _[1] | '' 78 | 79 | 80 | __all__ = ('cframe', 'callsite_info', 'callsite_source', 'tb_callsite', 'non_internal_frame', 'internal_frame') 81 | -------------------------------------------------------------------------------- /amino/func.py: -------------------------------------------------------------------------------- 1 | from functools import wraps, partial 2 | from inspect import getfullargspec 3 | from typing import Callable, Union, Any, TypeVar, Tuple, Generic, cast 4 | 5 | A = TypeVar('A') 6 | B = TypeVar('B') 7 | C = TypeVar('C') 8 | 9 | 10 | def curried(func: Callable[..., B]) -> Callable[[A], Callable[..., Union[Callable, B]]]: 11 | @wraps(func) 12 | def _curried(*args: Any, **kwargs: Any) -> Union[B, Callable[..., Union[Callable, B]]]: 13 | f = func 14 | count = 0 15 | while isinstance(f, partial): 16 | if f.args: 17 | count += len(f.args) 18 | f = f.func 19 | spec = getfullargspec(f) 20 | if count == len(spec.args) - len(args): 21 | return func(*args, **kwargs) 22 | else: 23 | return curried(partial(func, *args, **kwargs)) 24 | return _curried 25 | 26 | 27 | class Identity: 28 | 29 | def __init__(self) -> None: 30 | self.__name__ = 'identity' 31 | 32 | def __call__(self, a: A) -> A: 33 | return a 34 | 35 | def __str__(self) -> str: 36 | return '(a => a)' 37 | 38 | I = Identity() 39 | 40 | 41 | class Val(Generic[A]): 42 | 43 | def __init__(self, value: A) -> None: 44 | self.value = value 45 | self.__name__ = self.__class__.__name__ 46 | 47 | def __call__(self) -> A: 48 | return self.value 49 | 50 | def __str__(self) -> str: 51 | return '{}({})'.format(self.__class__.__name__, self.value) 52 | 53 | 54 | class ReplaceVal(Generic[A], Val[A]): 55 | 56 | def __call__(self, *a: Any, **kw: Any) -> A: 57 | return super().__call__() 58 | 59 | 60 | def flip(a: A, b: B) -> Tuple[B, A]: 61 | return b, a 62 | 63 | 64 | CallByName = Union[Any, Callable[[], Any]] 65 | 66 | 67 | def call_by_name(b: Union[A, Callable[[], A]]) -> A: 68 | return b() if callable(b) else cast(A, b) 69 | 70 | 71 | def is_not_none(a: Any) -> bool: 72 | return a is not None 73 | 74 | 75 | def tupled2(f: Callable[[A, B], C]) -> Callable[[Tuple[A, B]], C]: 76 | def wrap(a: Tuple[A, B]) -> C: 77 | return f(a[0], a[1]) 78 | return wrap 79 | 80 | 81 | TailrecResult = Tuple[bool, Union[A, tuple]] 82 | 83 | 84 | class tailrec: 85 | __slots__ = 'func', 86 | 87 | def __init__(self, func: Callable[..., Tuple[bool, Union[A, tuple]]]) -> None: 88 | self.func = func 89 | 90 | def __call__(self, *a: Any) -> A: 91 | args: Union[A, tuple] = a 92 | while True: 93 | cont, args = self.func(*cast(tuple, args)) 94 | if not cont: 95 | break 96 | return cast(A, args) 97 | 98 | 99 | tco = tailrec 100 | 101 | 102 | class mtailrec: 103 | __slots__ = 'func', 104 | 105 | 106 | def const(a: A) -> Callable[[B], A]: 107 | return lambda b: a 108 | 109 | 110 | __all__ = ('curried', 'I', 'flip', 'call_by_name', 'Val', 'ReplaceVal', 'is_not_none', 'tupled2', 'tailrec', 'mtailrec', 111 | 'const', 'TailrecResult',) 112 | -------------------------------------------------------------------------------- /amino/util/coconut_mypy.py: -------------------------------------------------------------------------------- 1 | from typing import Tuple, Generator, Any 2 | 3 | from amino import List, Map, Lists, _, Regex, Either, Right, Left, Id, Path 4 | from amino.state import EitherState, State 5 | from amino.regex import Match 6 | from amino.do import do 7 | from amino.util.numeric import parse_int 8 | 9 | Files = Map[Path, List[str]] 10 | Entry = Map[str, Any] 11 | excludes = List(Regex('In module imported'), Regex('__coconut__')) 12 | error_rex = Regex('(?P[^:]*):(?P\d+)(:(?P\d+))?: (error|note): (?P.*)') 13 | lnum_rex = Regex('# line (?P\d+)') 14 | 15 | 16 | @do(Either[str, Tuple[str, int, Either[str, int], str]]) 17 | def extract(match: Match) -> Generator: 18 | path = yield match.group('path') 19 | lnum = yield match.group('lnum') 20 | lnum_i = yield parse_int(lnum) 21 | col = match.group('col') // parse_int 22 | error = yield match.group('error') 23 | yield Right((Path(path), lnum_i, col, error)) 24 | 25 | 26 | def update_for(path: Path, files: Files) -> Files: 27 | return files if path in files else files + (path, Lists.lines(path.read_text())) 28 | 29 | 30 | @do(Either[str, Entry]) 31 | def substitute(files: Files, path: Path, lnum: int, col: Either[str, int], error: str, coco_path: Path) -> Generator: 32 | lines = yield files.lift(path).to_either('corrupt state') 33 | line = yield lines.lift(lnum - 1).to_either(f'invalid line number {lnum} for {path}') 34 | lnum_match = yield lnum_rex.search(line) 35 | coco_lnum = yield lnum_match.group('lnum') 36 | coco_lnum_i = yield parse_int(coco_lnum) 37 | col_map = col / (lambda a: Map(col=a)) | Map() 38 | yield Right(Map(lnum=coco_lnum_i, text=error, valid=1, maker_name='mypy') ** col_map) 39 | 40 | 41 | @do(EitherState[str, Files, Entry]) 42 | def handle_coco(path: Path, lnum: int, col: Either[str, int], error: str, coco_path: Path) -> Generator: 43 | yield EitherState.modify(lambda s: update_for(path, s)) 44 | files = yield EitherState.get() 45 | yield EitherState.lift(substitute(files, path, lnum, col, error, coco_path)) 46 | 47 | 48 | @do(EitherState[str, Files, Entry]) 49 | def line(l: str) -> Generator: 50 | r = excludes.traverse(lambda a: a.search(l).swap, Either) 51 | yield EitherState.lift(r) 52 | match = yield EitherState.lift(error_rex.match(l)) 53 | path, lnum, col, error = yield EitherState.lift(extract(match)) 54 | coco_path = path.with_suffix('.coco') 55 | yield ( 56 | handle_coco(path, lnum, col, error, coco_path) 57 | if coco_path.exists() else 58 | EitherState.failed('not a coconut') 59 | ) 60 | 61 | 62 | def recover(est: EitherState[str, Files, Entry]) -> State[Map, Either[str, Entry]]: 63 | def fix(s: Map, r: Map) -> Id[Tuple[Map, Either[str, Map]]]: 64 | return Id((s, Right(r))) 65 | return State.apply(lambda s: est.run(s).map2(fix).value_or(lambda err: Id((s, Left(err))))) 66 | 67 | 68 | def process_output(output: list) -> List[str]: 69 | s, result = Lists.wrap(output).map(line).traverse(recover, State).run(Map()).value 70 | return result.flat_map(_.to_list) 71 | 72 | __all__ = ('process_output',) 73 | -------------------------------------------------------------------------------- /amino/regex.py: -------------------------------------------------------------------------------- 1 | import re 2 | import typing 3 | from typing import Any, Union, Pattern 4 | 5 | from amino import L, _, Maybe, Map, List, Either, Boolean 6 | 7 | 8 | class Regex: 9 | 10 | @staticmethod 11 | def cons(pattern: Union[str, Pattern]) -> 'Regex': 12 | spec = pattern.pattern if isinstance(pattern, Pattern) else pattern 13 | rex = pattern if isinstance(pattern, Pattern) else re.compile(pattern) 14 | return Regex(spec, rex) 15 | 16 | def __init__(self, spec: str, rex: Union[None, Pattern]=None) -> None: 17 | self.spec = spec 18 | self.rex = re.compile(spec) if rex is None else rex 19 | 20 | def __getattr__(self, name: str) -> Any: 21 | if hasattr(self.rex, name): 22 | return getattr(self.rex, name) 23 | else: 24 | raise AttributeError('Regex has no attribute \'{}\''.format(name)) 25 | 26 | def match(self, data: str, *a: Any, **kw: Any) -> Either[str, 'Match']: 27 | return ( 28 | Maybe.optional(self.rex.match(data, *a, **kw)) 29 | .to_either('`{}` does not match `{}`'.format(data, self.spec)) / 30 | L(Match)(self, _, data) 31 | ) 32 | 33 | def matches(self, data: str, *a: Any, **kw: Any) -> Boolean: 34 | return self.match(data, *a, **kw).is_right 35 | 36 | def search(self, data: str, *a: Any, **kw: Any) -> Either[str, 'Match']: 37 | return ( 38 | Maybe.optional(self.rex.search(data, *a, **kw)) 39 | .to_either('`{}` does not contain `{}`'.format(data, self.spec)) / 40 | L(Match)(self, _, data) 41 | ) 42 | 43 | def contains(self, data: str, *a: Any, **kw: Any) -> Boolean: 44 | return self.search(data, *a, **kw).is_right 45 | 46 | def __str__(self) -> str: 47 | return 'Regex({})'.format(self.spec) 48 | 49 | 50 | class Match: 51 | 52 | def __init__(self, regex: Regex, internal: typing.Match, data: str) -> None: 53 | self.regex = regex 54 | self.internal = internal 55 | self.data = data 56 | 57 | @property 58 | def group_map(self) -> Map[str, str]: 59 | return Map(self.internal.groupdict()) 60 | 61 | m = group_map 62 | 63 | def group(self, id: str) -> Either[str, str]: 64 | return ( 65 | self.group_map 66 | .lift(id) 67 | .flat_map(lambda a: Maybe.optional(a).to_either_f(lambda: f'group `{id}` did not match')) 68 | .to_either('no group `{}` in {}'.format(id, self)) 69 | ) 70 | 71 | g = group 72 | 73 | @property 74 | def groups(self) -> List[str]: 75 | return List.wrap(self.internal.groups()) 76 | 77 | l = groups 78 | 79 | def all_groups(self, *ids: str) -> Either[str, List[str]]: 80 | return (self.group_map.lift_all(*ids) 81 | .to_either('not all groups `{}` in {}'.format(ids, self))) 82 | 83 | @property 84 | def match(self) -> str: 85 | return self.internal.group(0) 86 | 87 | def __str__(self) -> str: 88 | return 'Match({}, {}, {})'.format(self.regex, self.data, self.group_map) 89 | 90 | @property 91 | def string(self) -> str: 92 | return self.internal.string 93 | 94 | __all__ = ('Regex',) 95 | -------------------------------------------------------------------------------- /amino/eff.py: -------------------------------------------------------------------------------- 1 | from typing import Callable, Generic, TypeVar 2 | 3 | from amino import List, I, _ 4 | from amino.lazy import lazy 5 | from amino.tc.base import Implicits 6 | from amino.tc.flat_map import FlatMap 7 | 8 | A = TypeVar('A') 9 | B = TypeVar('B') 10 | 11 | 12 | class F(Generic[A]): 13 | pass 14 | 15 | 16 | class Eff(Generic[A], Implicits, implicits=True, auto=True): 17 | ''' An effect stack transformer. 18 | Wraps arbitrarily nested effects, like IO[List[Maybe[A]]]. 19 | ''' 20 | 21 | def __init__(self, value: F[A], effects: List[type]=List(), depth: int=1) -> None: 22 | self.value = value 23 | self.effects = effects 24 | self.depth = depth 25 | 26 | def __repr__(self): 27 | return 'Eff[{}]({!r})'.format(self.effects.mk_string(','), self.value) 28 | 29 | def copy(self, value): 30 | return Eff(value, self.effects, self.depth) 31 | 32 | @lazy 33 | def all_effects(self): 34 | return self.effects.cons(type(self.value)) 35 | 36 | def _map(self, f: Callable): 37 | g = List.wrap(range(self.depth)).fold_left(f)(lambda z, i: lambda a: a.map(z)) 38 | return g(self.value) 39 | 40 | def map(self, f: Callable): 41 | return self.copy(self._map(lambda a: a.map(f))) 42 | 43 | __truediv__ = map 44 | 45 | def _flat_map(self, f: Callable): 46 | ''' **f** must return the same stack type as **self.value** has. 47 | Iterates over the effects, sequences the inner instance 48 | successively to the top and joins with the outer instance. 49 | Example: 50 | List(Right(Just(1))) => List(Right(Just(List(Right(Just(5)))))) 51 | => List(List(Right(Just(Right(Just(5)))))) 52 | => List(Right(Just(Right(Just(5))))) 53 | => List(Right(Right(Just(Just(5))))) 54 | => List(Right(Just(Just(5)))) 55 | => List(Right(Just(5))) 56 | Note: IO works only as outermost effect, as it cannot sequence 57 | ''' 58 | index = List.range(self.depth + 1) 59 | g = index.fold_left(f)(lambda z, i: lambda a: a.map(z)) 60 | nested = g(self.value) 61 | def sequence_level(z, depth, tpe): 62 | nesting = lambda z, i: lambda a: a.map(z).sequence(tpe) 63 | lifter = List.range(depth).fold_left(I)(nesting) 64 | return z // lifter 65 | def sequence_type(z, data): 66 | return lambda a: sequence_level(a, *data).map(z) 67 | h = self.all_effects.reversed.with_index.fold_left(I)(sequence_type) 68 | return h(nested) 69 | 70 | def flat_map(self, f: Callable): 71 | ''' Calls **f** with the inner values, if present, and sequences 72 | the result, which is assumed to be a complete stack, one by one 73 | through the old stack. 74 | ''' 75 | return self.copy(self._flat_map(f)) 76 | 77 | __floordiv__ = flat_map 78 | 79 | def flat_map_inner(self, f: Callable): 80 | return self.copy(self.value.map(lambda a: a.flat_map(f))) 81 | 82 | 83 | class EffFlatMap(FlatMap, tpe=Eff): 84 | 85 | def map(self, fa: Eff[A], f: Callable[[A], B]) -> Eff[B]: 86 | return Eff.map(fa, f) 87 | 88 | def flat_map(self, fa: Eff[A], f: Callable[[A], Eff[B]]) -> Eff[B]: 89 | return Eff.flat_map(fa, f) 90 | 91 | __all__ = ('Eff',) 92 | -------------------------------------------------------------------------------- /amino/instances/maybe.py: -------------------------------------------------------------------------------- 1 | from typing import TypeVar, Callable, Union, Type, Tuple 2 | 3 | from amino.tc.base import tc_prop, ImplicitInstances, F 4 | from amino.tc.monad import Monad 5 | from amino.tc.optional import Optional 6 | from amino import either, Just, Empty, Maybe, curried, List, Nothing 7 | from amino.lazy import lazy 8 | from amino.maybe import call_by_name 9 | from amino.tc.applicative import Applicative 10 | from amino.tc.traverse import Traverse 11 | from amino.tc.foldable import Foldable 12 | from amino.tc.zip import Zip 13 | from amino.instances.list import ListTraverse 14 | from amino.func import CallByName 15 | 16 | A = TypeVar('A') 17 | B = TypeVar('B') 18 | C = TypeVar('C') 19 | 20 | 21 | class MaybeInstances(ImplicitInstances): 22 | 23 | @lazy 24 | def _instances(self): 25 | from amino import Map 26 | return Map( 27 | { 28 | Monad: MaybeMonad(), 29 | Optional: MaybeOptional(), 30 | Traverse: MaybeTraverse(), 31 | Foldable: MaybeFoldable(), 32 | Zip: MaybeZip(), 33 | } 34 | ) 35 | 36 | 37 | class MaybeMonad(Monad): 38 | 39 | def pure(self, a: A): 40 | return Just(a) 41 | 42 | def flat_map(self, fa: Maybe[A], f: Callable[[A], Maybe[B]]) -> Maybe[B]: 43 | return fa.cata(lambda v: f(v), Nothing) 44 | 45 | 46 | class MaybeOptional(Optional): 47 | 48 | @tc_prop 49 | def to_maybe(self, fa: Maybe[A]): 50 | return fa 51 | 52 | def to_either(self, fa: Maybe[A], left: Union[B, Callable[[], B]] 53 | ) -> either.Either[A, B]: 54 | from amino.either import Left, Right 55 | return fa.cata(Right, lambda: Left(call_by_name(left))) 56 | 57 | @tc_prop 58 | def present(self, fa: Maybe): 59 | return fa.is_just 60 | 61 | def absent(self, msg: str) -> Maybe[A]: 62 | return Nothing 63 | 64 | 65 | class MaybeTraverse(Traverse): 66 | 67 | def traverse(self, fa: Maybe[A], f: Callable, tpe: type): 68 | monad = Applicative.fatal(tpe) 69 | r = lambda a: monad.map(f(a), Just) 70 | return fa.cata(r, monad.pure(Empty())) 71 | 72 | 73 | class MaybeFoldable(Foldable): 74 | 75 | @tc_prop 76 | def with_index(self, fa: Maybe[A]) -> Maybe[Tuple[int, A]]: 77 | return Just(0) & fa 78 | 79 | def filter(self, fa: Maybe[A], f: Callable[[A], bool]): 80 | return fa // (lambda a: Just(a) if f(a) else Empty()) 81 | 82 | @curried 83 | def fold_left(self, fa: Maybe[A], z: B, f: Callable[[B, A], B]) -> B: 84 | return fa / (lambda a: f(z, a)) | z 85 | 86 | def find(self, fa: Maybe[A], f: Callable[[A], bool]): 87 | return self.filter(fa, f) 88 | 89 | def find_map_optional(self, fa: Maybe[A], tpe: Type[F], f: Callable[[B], F[C]], msg: CallByName=None) -> F[C]: 90 | return fa / f | (lambda: fa.absent(call_by_name(msg))) 91 | 92 | def index_where(self, fa: Maybe[A], f: Callable[[A], bool]): 93 | return fa / f // (lambda a: Just(0) if a else Empty()) 94 | 95 | 96 | class MaybeZip(Zip): 97 | 98 | def zip(self, fa: Maybe[A], fb: Maybe[B], *fs: Maybe) -> Maybe: 99 | return ListTraverse().sequence(List(fa, fb, *fs), Maybe) 100 | 101 | __all__ = ('MaybeInstances',) 102 | -------------------------------------------------------------------------------- /unit/maybe_spec.py: -------------------------------------------------------------------------------- 1 | import operator 2 | 3 | from amino.test.spec_spec import Spec 4 | from amino import Maybe, Empty, Just, Left, Right, _, L 5 | from amino.tc.monad import Monad 6 | 7 | 8 | class MaybeSpec(Spec): 9 | 10 | def none(self) -> None: 11 | Maybe.optional(None).is_just.should_not.be.ok 12 | 13 | def just(self) -> None: 14 | Maybe.optional('value').is_just.should.be.ok 15 | 16 | def map(self) -> None: 17 | a = 'start' 18 | b = 'end' 19 | Maybe.optional(a).map(_ + b)._get.should.equal(a + b) 20 | (Maybe.optional(a) / (_ + b))._get.should.equal(a + b) 21 | 22 | def flat_map(self) -> None: 23 | a = 'start' 24 | b = 'end' 25 | Maybe.optional(a).flat_map(lambda v: Maybe.optional(v + b)).should.contain(a + b) 26 | f = lambda a: Maybe.optional(a).flat_map(lambda c: Monad.fatal(Maybe).pure(c + b)) 27 | f(a).should.contain(a + b) 28 | 29 | def join(self) -> None: 30 | Just(Just(1)).join.should.equal(Just(1)) 31 | 32 | def contains(self) -> None: 33 | a = 'start' 34 | Maybe.optional(a).contains(a).should.be.ok 35 | Maybe.optional(a + a).contains(a).should_not.be.ok 36 | Empty().contains(a).should_not.be.ok 37 | 38 | def get_or_else(self) -> None: 39 | e = Empty() 40 | a = 1 41 | e.get_or_else(a).should.equal(a) 42 | (e | a).should.equal(a) 43 | 44 | def get_or_else_call(self) -> None: 45 | e = Empty() 46 | a = 5 47 | ac = lambda: a 48 | e.get_or_else(ac).should.equal(a) 49 | (e | ac).should.equal(a) 50 | 51 | def or_else(self) -> None: 52 | e = Empty() 53 | a = Just(1) 54 | e.or_else(a).should.equal(a) 55 | 56 | def or_else_call(self) -> None: 57 | e = Empty() 58 | a = Just(5) 59 | ac = lambda: a 60 | e.or_else(ac).should.equal(a) 61 | 62 | def foreach(self) -> None: 63 | a = 1 64 | b = 6 65 | def setter(c: int) -> None: 66 | nonlocal a 67 | a = c + 1 68 | (Just(b) % setter).should.contain(b) 69 | a.should.equal(b + 1) 70 | 71 | def ap2(self) -> None: 72 | a = 17 73 | b = 13 74 | ja = Just(a) 75 | jb = Just(b) 76 | ja.ap2(jb, operator.add).should.contain(a + b) 77 | 78 | def optional(self) -> None: 79 | a = 'a' 80 | b = 'b' 81 | Maybe.optional(a).to_maybe.should.just_contain(a) 82 | Empty().to_maybe.should.be.a(Empty) 83 | Maybe.optional(a).to_either(b).should.equal(Right(a)) 84 | Empty().to_either(b).should.equal(Left(b)) 85 | Empty().to_either(lambda: b).should.equal(Left(b)) 86 | 87 | def map_n(self) -> None: 88 | m = Just((1, 2, 3, 4)) 89 | m.map4(lambda a, b, c, d: b + d).should.contain(6) 90 | m.map5.when.called_with(lambda a: a).should.throw(TypeError) 91 | 92 | def flat_map_n(self) -> None: 93 | m = Just((1, 2, 3, 4)) 94 | m.flat_map4(lambda a, b, c, d: Just(b + d)).should.contain(6) 95 | m.flat_map5.when.called_with(lambda a: a).should.throw(TypeError) 96 | 97 | def product_n(self) -> None: 98 | m = Just(1).product3(Just(2), Just(3), Just(4)) 99 | m.flat_map4(lambda a, b, c, d: Just(b + d)).should.contain(6) 100 | e = Just(1).product3(Just(2), Empty(), Just(4)) 101 | e.should.be.empty 102 | 103 | __all__ = ('MaybeSpec',) 104 | -------------------------------------------------------------------------------- /amino/anon/prod/complex.py: -------------------------------------------------------------------------------- 1 | from typing import Callable, Tuple, Any, Generic, TypeVar 2 | from types import FunctionType 3 | 4 | from amino.anon.prod.attr import AttrLambdaInst 5 | from amino.anon.prod.method import Anon, MethodRef, MethodLambdaInst, MethodChain, AnonChain 6 | from amino.util.fun import format_funcall 7 | 8 | A = TypeVar('A') 9 | B = TypeVar('B') 10 | 11 | 12 | def make_complex(args: tuple) -> Tuple[list, list, str, Any, int]: 13 | i_strict, i_lambda, i_param = 0, 0, 0 14 | ordered = '' 15 | stricts = '' 16 | lambdas = '' 17 | params = '' 18 | rest = [] 19 | lambda_args = [] 20 | for a in args: 21 | if a is AttrLambdaInst: 22 | i_param += 1 23 | arg = f'y{i_param},' 24 | ordered += arg 25 | params += arg 26 | elif isinstance(a, Anon): 27 | i_param += 1 28 | i_lambda += 1 29 | arg = f'y{i_param},' 30 | ordered += f'l{i_lambda}.__substitute_object__(y{i_param}),' 31 | params += arg 32 | lambdas += f'l{i_lambda},' 33 | lambda_args.append(a) 34 | else: 35 | i_strict += 1 36 | arg = f'x{i_strict},' 37 | ordered += arg 38 | stricts += arg 39 | rest.append(a) 40 | rep = f'lambda f: lambda {lambdas}: lambda {params}: lambda {stricts}**kw: f({ordered}**kw)' 41 | return lambda_args, rest, rep, eval(rep), i_param 42 | 43 | 44 | class ComplexLambda(Generic[A]): 45 | 46 | def __init__(self, func: Callable[..., A], a: tuple, kw: dict) -> None: 47 | self.__func = func 48 | self.__args = a 49 | self.__kwargs = kw 50 | self.__name__ = ( 51 | self.__func.__name__ 52 | if isinstance(self.__func, FunctionType) else 53 | self.__func.__class__.__name__ 54 | ) 55 | self.__qualname__ = self.__name__ 56 | self.__annotations__ = {} 57 | self.__lambda_args, self.__rest, self.__repr, self.__lambda, self.__param_count = make_complex(self.__args) 58 | 59 | def __call__(self, *a: Any, **kw: Any) -> Callable[..., A]: 60 | return self.__lambda(self.__func)(*self.__lambda_args)(*a)(*self.__rest, **self.__kwargs, **kw) 61 | 62 | def __readable(self) -> str: 63 | return format_funcall(self.__func, self.__args, self.__kwargs) 64 | 65 | def __str__(self) -> str: 66 | return self.__readable() 67 | 68 | def __rshift__(self, f): 69 | return AnonChain(self, f, self.__param_count) 70 | 71 | def __getattr__(self, name): 72 | return MethodChain(self, f'a.{name}') 73 | 74 | 75 | class LazyMethod(Anon): 76 | 77 | def __init__(self, obj, attr: MethodRef) -> None: 78 | self.__obj = obj 79 | self.__attr = attr 80 | self.__name__ = self.__attr.__name__ 81 | 82 | def __call__(self, *a, **kw): 83 | return self.__attr(*a, **kw)(self.__obj) 84 | 85 | def __getattr__(self, name: str): 86 | return LazyMethod(self.__obj, getattr(self.__attr, name)) 87 | 88 | 89 | class ComplexLambdaInst(Generic[A]): 90 | 91 | def __init__(self, func: Callable[..., A]) -> None: 92 | self.__func = func 93 | 94 | def __call__(self, *a: Any, **kw: Any) -> ComplexLambda[A]: 95 | return ComplexLambda(self.__func, a, kw) 96 | 97 | def __getattr__(self, name): 98 | return ComplexLambdaInst(LazyMethod(self.__func, getattr(MethodLambdaInst, name))) 99 | 100 | __all__ = ('ComplexLambdaInst',) 101 | -------------------------------------------------------------------------------- /amino/instances/either.py: -------------------------------------------------------------------------------- 1 | from typing import TypeVar, Callable, Tuple, Any, Type 2 | 3 | from amino.tc.base import tc_prop, ImplicitInstances, F 4 | from amino.tc.optional import Optional 5 | from amino.tc.monad import Monad 6 | from amino.tc.traverse import Traverse 7 | from amino.lazy import lazy 8 | from amino.maybe import Just, Empty 9 | from amino.either import Right, Either, Left 10 | from amino.map import Map 11 | from amino.tc.applicative import Applicative 12 | from amino.tc.foldable import Foldable 13 | from amino import curried, Maybe, Boolean, List 14 | from amino.tc.zip import Zip 15 | from amino.instances.list import ListTraverse 16 | from amino.func import CallByName, call_by_name 17 | 18 | A = TypeVar('A') 19 | B = TypeVar('B') 20 | C = TypeVar('C') 21 | D = TypeVar('D') 22 | 23 | 24 | class EitherInstances(ImplicitInstances): 25 | 26 | @lazy 27 | def _instances(self) -> Map: 28 | from amino.map import Map 29 | return Map( 30 | { 31 | Monad: EitherMonad(), 32 | Optional: EitherOptional(), 33 | Traverse: EitherTraverse(), 34 | Foldable: EitherFoldable(), 35 | Zip: EitherZip(), 36 | } 37 | ) 38 | 39 | 40 | class EitherMonad(Monad): 41 | 42 | def pure(self, b: B) -> Either[A, B]: 43 | return Right(b) 44 | 45 | def flat_map(self, fa: Either[A, B], f: Callable[[B], Either[A, C]] 46 | ) -> Either[A, C]: 47 | return f(fa.value) if isinstance(fa, Right) else fa 48 | 49 | 50 | class EitherOptional(Optional): 51 | 52 | @tc_prop 53 | def to_maybe(self, fa: Either[A, B]) -> Maybe[A]: 54 | return Just(fa.value) if fa.is_right else Empty() 55 | 56 | def to_either(self, fa: Either[A, B], left: C) -> Either[C, B]: 57 | return fa 58 | 59 | @tc_prop 60 | def present(self, fa: Either) -> Boolean: 61 | return fa.is_right 62 | 63 | def absent(self, msg: str) -> Either[str, B]: 64 | return Left(msg or 'not found') 65 | 66 | 67 | class EitherTraverse(Traverse): 68 | 69 | def traverse(self, fa: Either[A, B], f: Callable, tpe: type) -> Any: 70 | monad = Applicative.fatal(tpe) 71 | r = lambda a: monad.map(f(a), Right) 72 | return fa.cata(lambda a: monad.pure(Left(a)), r) 73 | 74 | 75 | class EitherFoldable(Foldable): 76 | 77 | @tc_prop 78 | def with_index(self, fa: Either[A, B]) -> Either[A, Tuple[int, B]]: 79 | return Right(0) & fa 80 | 81 | def filter(self, fa: Either[A, B], f: Callable[[B], bool]) -> Either[Any, B]: 82 | return fa // (lambda a: Right(a) if f(a) else Left('filtered')) 83 | 84 | @curried 85 | def fold_left(self, fa: Either[A, B], z: C, f: Callable[[C, B], C]) -> C: 86 | return fa / (lambda a: f(z, a)) | z 87 | 88 | def find(self, fa: Either[A, B], f: Callable[[B], bool]) -> Maybe[B]: 89 | return fa.to_maybe.find(f) 90 | 91 | def find_map_optional(self, fa: Either[A, B], tpe: Type[F], f: Callable[[B], F[C]], msg: CallByName=None) -> F[C]: 92 | return fa / f | (lambda: fa.absent(call_by_name(msg))) 93 | 94 | def index_where(self, fa: Either[A, B], f: Callable[[B], bool] 95 | ) -> Maybe[int]: 96 | return fa.to_maybe.index_where(f) 97 | 98 | 99 | class EitherZip(Zip): 100 | 101 | def zip(self, fa: Either[A, B], fb: Either[C, D], *fs: Either) -> Either: 102 | return ListTraverse().sequence(List(fa, fb, *fs), Either) 103 | 104 | __all__ = ('EitherInstances',) 105 | -------------------------------------------------------------------------------- /amino/tc/context.py: -------------------------------------------------------------------------------- 1 | import inspect 2 | from typing import TypeVar, Callable, Any, Generic, Type 3 | from functools import reduce 4 | 5 | from amino import Map, Boolean 6 | from amino.case import Case 7 | from amino.algebra import Algebra 8 | 9 | A = TypeVar('A') 10 | B = TypeVar('B') 11 | X = TypeVar('X') 12 | Alg = TypeVar('Alg', bound=Algebra) 13 | 14 | 15 | class Bindings: 16 | 17 | def __init__(self, bindings: Map[str, type]) -> None: 18 | self.bindings = bindings 19 | 20 | 21 | def function_bindings_relay(f: Callable) -> Boolean: 22 | target = f.__do_original if hasattr(f, '__do') else f 23 | anno = inspect.getfullargspec(target).annotations 24 | return Boolean(anno.get('bindings', None) is Bindings) 25 | 26 | 27 | def case_bindings_relay(pm: Type[Case[Alg, B]]) -> Boolean: 28 | return function_bindings_relay(pm.__init__) 29 | 30 | 31 | def check_bound(rep: str, t: Type[A], b: Type[B], bindings: Map) -> None: 32 | binding = bindings.lift(t).get_or_fail(lambda: f'no binding for {t} => {b} in {rep}') 33 | b.m(binding).get_or_fail(lambda: f'no instance of {t} => {b} for {binding} in {rep}') 34 | 35 | 36 | def check_bounds(rep: str, bounds: Map, bindings: Map) -> None: 37 | for t, b in bounds.items(): 38 | check_bound(rep, t, b, bindings) 39 | 40 | 41 | class BoundedFunction(Generic[A]): 42 | 43 | def __init__(self, f: Callable, bindings: Bindings, relay: Boolean) -> None: 44 | self.f = f 45 | self.bindings = bindings 46 | self.relay = relay 47 | 48 | def __call__(self, *a, **kw) -> Callable: 49 | return self.f(self.bindings, *a, **kw) if self.relay else self.f(*a, **kw) 50 | 51 | 52 | class ContextBound(Generic[A]): 53 | 54 | def __init__(self, f: Callable, bounds: Map) -> None: 55 | self.f = f 56 | self.bounds = bounds 57 | self.relay = function_bindings_relay(f) 58 | 59 | def __call__(self, *a: Bindings, **kw) -> Callable: 60 | bindings = reduce(lambda a, b: a ** b.bindings, a, Map(kw)) 61 | check_bounds(str(self.f), self.bounds, bindings) 62 | return BoundedFunction(self.f, Bindings(bindings), self.relay) 63 | 64 | def call(self, *a, **kw) -> A: 65 | return self.f(*a, **kw) 66 | 67 | 68 | class BoundedCase(Generic[Alg, B]): 69 | 70 | def __init__(self, pm: Case[Alg, B], bindings: Bindings, relay: Boolean) -> None: 71 | self.pm = pm 72 | self.bindings = bindings 73 | self.relay = relay 74 | 75 | def __call__(self, *a, **kw) -> Callable: 76 | return self.pm(self.bindings, *a, **kw) if self.relay else self.pm(*a, **kw) 77 | 78 | 79 | class CaseContextBound(Generic[Alg, B]): 80 | 81 | def __init__(self, pm: Type[Case[Alg, B]], bounds: Map) -> None: 82 | self.pm = pm 83 | self.bounds = bounds 84 | self.relay = case_bindings_relay(pm) 85 | 86 | def __call__(self, *a: Bindings, **kw: type) -> Case[Alg, B]: 87 | bindings = reduce(lambda a, b: a ** b.bindings, a, Map(kw)) 88 | check_bounds(self.pm.__name__, self.bounds, bindings) 89 | return BoundedCase(self.pm, Bindings(bindings), self.relay) 90 | 91 | def call(self, *a, **kw) -> A: 92 | return self.pm(*a, **kw) 93 | 94 | 95 | def context(*bindings: ContextBound, **tpes: Any) -> Callable: 96 | def dec(f: Callable) -> Callable: 97 | return ( 98 | CaseContextBound(f, Map(tpes)) 99 | if isinstance(f, type) and issubclass(f, Case) else 100 | ContextBound(f, Map(tpes)) 101 | ) 102 | return dec 103 | 104 | __all__ = ('context',) 105 | -------------------------------------------------------------------------------- /unit/list_spec.py: -------------------------------------------------------------------------------- 1 | from amino.test.spec_spec import Spec 2 | 3 | from amino import List, Empty, Just, Maybe, _, Lists, __ 4 | from amino.list import replace_one 5 | 6 | 7 | class ListSpec(Spec): 8 | 9 | def map(self): 10 | List(1, 2, 3) \ 11 | .map(_ + 1) \ 12 | .should.equal(List(2, 3, 4)) 13 | 14 | def flat_map(self): 15 | List(1, 2, 3) \ 16 | .flat_map(lambda v: List(v, v + 1)) \ 17 | .should.equal(List(1, 2, 2, 3, 3, 4)) 18 | 19 | def join_maybes(self): 20 | List(Just(4), Empty(), Just(5), Empty())\ 21 | .join\ 22 | .should.equal(List(4, 5)) 23 | 24 | def flat_map_maybe(self): 25 | List(1, 2, 3)\ 26 | .flat_map(lambda a: Empty() if a % 2 == 0 else Just(a + 1))\ 27 | .should.equal(List(2, 4)) 28 | 29 | def find(self): 30 | l = List(1, 6, 9) 31 | l.find(_ % 2 == 0).should.equal(Just(6)) 32 | l.find(_ == 3).should.equal(Empty()) 33 | 34 | def contains(self): 35 | l = List(1, 6, 9) 36 | l.contains(6).should.be.ok 37 | l.contains(5).should_not.be.ok 38 | 39 | def lift(self): 40 | l = List(1, 4, 7) 41 | l.lift(1).should.equal(Just(4)) 42 | l.lift(3).should.equal(Empty()) 43 | 44 | def is_empty(self): 45 | List(1).is_empty.should_not.be.ok 46 | List().empty.should.be.ok 47 | 48 | def head_and_last(self): 49 | List(1, 2, 3).head.contains(1).should.be.ok 50 | List(1, 2, 3).last.contains(3).should.be.ok 51 | 52 | def distinct(self): 53 | List(1, 3, 6, 3, 6, 9, 5, 3, 6, 7, 1, 2, 5).distinct.should.equal( 54 | List(1, 3, 6, 9, 5, 7, 2) 55 | ) 56 | 57 | def distinct_by(self): 58 | List(1, 2, 3, 4, 5, 4).distinct_by(_ % 2).should.equal(List(1, 2)) 59 | 60 | def split(self): 61 | z = List(5, 2, 8, 2, 9, 4, 1, 7) 62 | l, r = z.split(_ >= 5) 63 | l.should.equal(List(5, 8, 9, 7)) 64 | r.should.equal(List(2, 2, 4, 1)) 65 | 66 | def split_type(self): 67 | z = List('a', 2, 'b', 'c', 3) 68 | l, r = z.split_type(str) 69 | l.should.equal(List('a', 'b', 'c')) 70 | r.should.equal(List(2, 3)) 71 | 72 | def fold_left(self): 73 | List(1, 2, 3).fold_left('')(lambda a, b: str(b) + a)\ 74 | .should.equal('321') 75 | 76 | def fold_map(self): 77 | List(1, 2, 3).fold_map(5, _ * 2).should.equal(17) 78 | 79 | def traverse(self): 80 | n = 3 81 | target = Just(List.wrap(range(n))) 82 | List.wrap(map(Just, range(n))).sequence(Maybe).should.equal(target) 83 | 84 | def sort_by(self): 85 | l = List(3, 2, 1) 86 | l.sort_by(_ + 5).should.equal(l.reversed) 87 | 88 | def max_min(self): 89 | l = List(3, 2, 1, 5, 6, 4, 3, 2) 90 | l.max_by(_ + 4).should.contain(6) 91 | l.min_by(_ + 4).should.contain(1) 92 | 93 | def unzip(self): 94 | a = 13 95 | b = 29 96 | l = List((a, a), (b, b), (a, a)) 97 | l1, l2 = l.unzip 98 | l1.should.equal(l2) 99 | l1.should.equal(List(a, b, a)) 100 | 101 | def with_index(self): 102 | l = List(1, 2, 3) 103 | l.with_index.unzip.should.equal((List.range(3), l)) 104 | 105 | def replace_one(self) -> None: 106 | l = Lists.range(102) 107 | index = 100 108 | pred = lambda a: a == index 109 | l1 = replace_one(l, pred, 23)._value() 110 | l1.flat_map(__.lift(index)).should.just_contain(23) 111 | 112 | __all__ = ('ListSpec',) 113 | -------------------------------------------------------------------------------- /amino/instances/list.py: -------------------------------------------------------------------------------- 1 | import itertools 2 | from typing import TypeVar, Callable, Tuple, Optional as TOptional, Type 3 | from functools import reduce 4 | 5 | from amino import maybe, List, Maybe, Lists 6 | from amino.func import curried, call_by_name, CallByName 7 | from amino.lazy import lazy 8 | from amino.tc.monad import Monad 9 | from amino.tc.base import ImplicitInstances, tc_prop, F 10 | from amino.tc.traverse import Traverse, TraverseF, TraverseG 11 | from amino.tc.applicative import Applicative 12 | from amino.tc.foldable import Foldable, FoldableABC 13 | from amino.tc.zip import Zip 14 | from amino.tc.monoid import Monoid 15 | from amino.tc.optional import Optional 16 | 17 | A = TypeVar('A', covariant=True) 18 | B = TypeVar('B') 19 | 20 | 21 | class ListInstances(ImplicitInstances): 22 | 23 | @lazy 24 | def _instances(self): 25 | from amino import Map 26 | return Map( 27 | { 28 | Monad: ListMonad(), 29 | Traverse: ListTraverse(), 30 | Foldable: ListFoldable(), 31 | Zip: ListZip(), 32 | Monoid: ListMonoid(), 33 | } 34 | ) 35 | 36 | 37 | class ListMonad(Monad): 38 | 39 | def pure(self, b: B) -> List[B]: 40 | return List(b) 41 | 42 | def map(self, fa: List[A], f: Callable[[A], B]) -> List[B]: 43 | return List.wrap(map(f, fa)) 44 | 45 | def flat_map(self, fa: List[A], f: Callable[[A], List[B]]) -> List[B]: 46 | acc = [] 47 | for a in fa: 48 | fb = f(a) 49 | acc.append(fb if isinstance(fb, List) else fb.to_list) 50 | return Lists.wrap(itertools.chain.from_iterable(acc)) 51 | 52 | 53 | class ListTraverse(Traverse): 54 | 55 | def traverse(self, fa: List[A], f: Callable, tpe: type): 56 | monad = Applicative.fatal(tpe) 57 | def folder(z, a): 58 | return monad.map2(z.product(f(a)), lambda l, b: l.cat(b)) 59 | return fa.fold_left(monad.pure(List()))(folder) 60 | 61 | 62 | FoldableABC.register(List) 63 | TraverseF.register(List) 64 | TraverseG.register(List) 65 | 66 | 67 | def _find(fa: List[A], f: Callable[[A], bool]) -> TOptional[A]: 68 | return next(filter(f, fa), None) 69 | 70 | 71 | class ListFoldable(Foldable): 72 | 73 | @tc_prop 74 | def with_index(self, fa: List[A]) -> List[Tuple[int, A]]: 75 | return List.wrap(enumerate(fa)) 76 | 77 | def filter(self, fa: List[A], f: Callable[[A], bool]): 78 | return List.wrap(filter(f, fa)) 79 | 80 | @curried 81 | def fold_left(self, fa: List[A], z: B, f: Callable[[B, A], B]) -> B: 82 | return reduce(f, fa, z) 83 | 84 | def find(self, fa: List[A], f: Callable[[A], bool]): 85 | return Maybe.optional(_find(fa, f)) 86 | 87 | def find_map_optional(self, fa: List[A], tpe: Type[F], f: Callable[[A], F[B]], msg: CallByName=None) -> F[B]: 88 | for el in fa: 89 | found = f(el) 90 | if found.present: 91 | return found 92 | return Optional.fatal(tpe).absent(call_by_name(msg)) 93 | 94 | def index_where(self, fa: List[A], f: Callable[[A], bool]): 95 | gen = (maybe.Just(i) for i, a in enumerate(fa) if f(a)) 96 | return next(gen, maybe.Nothing) 97 | 98 | 99 | class ListZip(Zip): 100 | 101 | def zip(self, fa: List[A], fb: List[B], *fs) -> List: 102 | return List.wrap(zip(fa, fb, *fs)) 103 | 104 | 105 | class ListMonoid(Monoid): 106 | 107 | @property 108 | def empty(self): 109 | return List() 110 | 111 | def combine(self, fa: List, fb: List): 112 | return fa + fb 113 | 114 | __all__ = ('ListInstances',) 115 | -------------------------------------------------------------------------------- /amino/instances/lazy_list.py: -------------------------------------------------------------------------------- 1 | from typing import TypeVar, Callable, Tuple, Type 2 | import itertools 3 | 4 | from amino import Maybe, LazyList, _, __ 5 | from amino.list import List 6 | from amino.func import curried, call_by_name, CallByName 7 | from amino.tc.monad import Monad 8 | from amino.tc.base import ImplicitInstances, tc_prop, F 9 | from amino.lazy import lazy 10 | from amino.tc.traverse import Traverse 11 | from amino.tc.foldable import Foldable, FoldableABC 12 | from amino.tc.zip import Zip 13 | from amino.instances.list import ListZip 14 | from amino.tc.optional import Optional 15 | 16 | A = TypeVar('A') 17 | B = TypeVar('B') 18 | 19 | 20 | class LazyListInstances(ImplicitInstances): 21 | 22 | @lazy 23 | def _instances(self): 24 | from amino.map import Map 25 | return Map( 26 | { 27 | Monad: LazyListMonad(), 28 | Traverse: LazyListTraverse(), 29 | Foldable: LazyListFoldable(), 30 | Zip: LazyListZip(), 31 | } 32 | ) 33 | 34 | 35 | class LazyListMonad(Monad): 36 | 37 | def pure(self, a: A): 38 | return LazyList([], List(a)) 39 | 40 | def map(self, fa: LazyList[A], f: Callable[[A], B]) -> LazyList[B]: 41 | return fa.copy(lambda a: map(f, a), __.map(f)) 42 | 43 | def flat_map(self, fa: LazyList[A], f: Callable[[A], LazyList[B]]) -> LazyList[B]: 44 | a, b = itertools.tee(fa.source) 45 | fa.source = a 46 | strict_m = fa.strict.map(f) 47 | lazy_m = map(f, b) 48 | mapped = itertools.chain(strict_m, lazy_m) 49 | source = itertools.chain.from_iterable(mapped) 50 | return LazyList(source, List(), fa._chunk_size) 51 | 52 | 53 | class LazyListTraverse(Traverse): 54 | 55 | def traverse(self, fa: LazyList[A], f: Callable, tpe: type): 56 | return fa.drain.traverse(f, tpe) / LazyList 57 | 58 | 59 | FoldableABC.register(LazyList) 60 | 61 | 62 | class LazyListFoldable(Foldable): 63 | 64 | @tc_prop 65 | def with_index(self, fa: LazyList[A]) -> List[Tuple[int, A]]: 66 | return LazyList(enumerate(fa.source), fa.strict, fa._chunk_size) 67 | 68 | def filter(self, fa: LazyList[A], f: Callable[[A], bool]) -> LazyList[A]: 69 | return fa.copy(lambda l: filter(f, l), __.filter(f)) 70 | 71 | @curried 72 | def fold_left(self, fa: LazyList[A], z: B, f: Callable[[B, A], B]) -> B: 73 | return Foldable.fatal(List).fold_left(fa.drain, z, f) 74 | 75 | def find(self, fa: List[A], f: Callable[[A], bool]) -> Maybe[A]: 76 | return fa.strict.find(f).o(fa._drain_find(f)) 77 | 78 | def find_map(self, fa: LazyList[A], f: Callable[[A], Maybe[B]]) -> Maybe[B]: 79 | return fa.map(f).find(_.is_just) 80 | 81 | def index_where(self, fa: LazyList[A], f: Callable[[A], bool] 82 | ) -> Maybe[int]: 83 | return fa.strict.index_where(f) | ( 84 | fa._drain_find(f) / (lambda a: len(fa.strict) - 1)) 85 | 86 | def find_map_optional(self, fa: List[A], tpe: Type[F], f: Callable[[A], F[B]], msg: CallByName=None) -> F[B]: 87 | a = fa.map(f).find(_.present) 88 | return a | (lambda: Optional.fatal(tpe).absent(call_by_name(msg))) 89 | 90 | 91 | class LazyListZip(Zip): 92 | 93 | def _zip(self, fs): 94 | return zip(*map(_.source, fs)) 95 | 96 | def zip(self, fa: LazyList[A], fb: LazyList[B], *fs) -> LazyList: 97 | fss = (fa, fb) + fs 98 | maxlen = max(map(lambda a: len(a.strict), fss)) 99 | for f in fss: 100 | f._fetch(maxlen - 1) 101 | stricts = map(_.strict, fss) 102 | strict = ListZip().zip(*stricts) 103 | return LazyList(self._zip(fss), init=strict) 104 | 105 | __all__ = ('LazyListInstances',) 106 | -------------------------------------------------------------------------------- /amino/json/decoder.py: -------------------------------------------------------------------------------- 1 | import abc 2 | from typing import TypeVar, Type, Generic 3 | 4 | from amino.tc.base import TypeClass 5 | from amino import Either, Map, Right, Lists, Do, _, L, Left, Dat, List, Maybe 6 | from amino.do import do 7 | from amino.json.data import JsonError, JsonObject, JsonArray, JsonScalar, Json, JsonAbsent, JsonNull, tpe_key 8 | from amino.json.parse import parse_json 9 | from amino.case import Case 10 | from amino.logging import module_log 11 | 12 | A = TypeVar('A') 13 | log = module_log() 14 | 15 | 16 | class TypeInfo(Dat['TypeInfo']): 17 | 18 | def __init__(self, module: str, names: List[str]) -> None: 19 | self.module = module 20 | self.names = names 21 | 22 | 23 | class Decoder(Generic[A], TypeClass): 24 | 25 | @abc.abstractmethod 26 | def decode(self, tpe: Type[A], data: Json) -> Either[JsonError, A]: 27 | ... 28 | 29 | 30 | @do(Either[str, A]) 31 | def decode_json_object(data: dict) -> Do: 32 | m = Map(data) 33 | tpe_s = yield m.lift(tpe_key).to_either(f'no `{tpe_key}` attr in json object {m}') 34 | tpe = yield Either.import_path(tpe_s) 35 | dec = yield Decoder.e(tpe) 36 | yield dec.decode(tpe, m) 37 | 38 | 39 | @do(Either[str, type]) 40 | def builtin_type(type_info: TypeInfo) -> Do: 41 | name = yield type_info.names.head.to_either(f'no name specified for builtin type') 42 | yield ( 43 | Right(type(None)) 44 | if name == 'NoneType' else 45 | Maybe.optional(__builtins__.get(name)).to_either(f'no builtin type named `{name}`') 46 | ) 47 | 48 | 49 | @do(Either[str, type]) 50 | def select_type(type_info: TypeInfo) -> Do: 51 | mod = yield Either.import_module(type_info.module) 52 | yield type_info.names.fold_m(Right(mod))(Either.getattr) 53 | 54 | 55 | def decode_type_info(json: Json) -> Either[JsonError, TypeInfo]: 56 | return decode_json_type_json(json, TypeInfo) 57 | 58 | 59 | @do(Either[JsonError, type]) 60 | def type_info(json: JsonObject) -> Do: 61 | jtpe = yield json.data.lift(tpe_key).to_either(json.error(f'no `{tpe_key}` field in json object')) 62 | type_info = yield decode_type_info(jtpe) 63 | yield ( 64 | builtin_type(type_info) 65 | if type_info.module == '__builtins__' else 66 | select_type(type_info) 67 | ).lmap(json.error) 68 | 69 | 70 | class decode(Generic[A], Case[Json, Either[JsonError, A]], alg=Json): 71 | 72 | @do(Either[JsonError, A]) 73 | def decode_json_object(self, json: JsonObject) -> Do: 74 | tpe = yield type_info(json) 75 | dec = yield Decoder.e(tpe).lmap(L(JsonError)(json, _)) 76 | yield dec.decode(tpe, json) 77 | 78 | def decode_json_array(self, json: JsonArray) -> Either[JsonError, A]: 79 | return Lists.wrap(json.data).traverse(self, Either) 80 | 81 | def decode_json_scalar(self, json: JsonScalar) -> Either[JsonError, A]: 82 | return Right(json.data) 83 | 84 | def decode_json_null(self, json: JsonNull) -> Either[JsonError, A]: 85 | return Right(None) 86 | 87 | def decode_json_absent(self, json: JsonAbsent) -> Either[JsonError, A]: 88 | return Left(JsonError(json.data), 'json absent') 89 | 90 | 91 | @do(Either[JsonError, A]) 92 | def decode_json(data: str) -> Do: 93 | json = yield parse_json(data) 94 | yield decode.match(json) 95 | 96 | 97 | @do(Either[JsonError, A]) 98 | def decode_json_type_json(json: Json, tpe: Type[A]) -> Do: 99 | dec = yield Decoder.e(tpe).lmap(L(JsonError)(json, _)) 100 | yield dec.decode(tpe, json) 101 | 102 | 103 | @do(Either[JsonError, A]) 104 | def decode_json_type(data: str, tpe: Type[A]) -> Do: 105 | json = yield parse_json(data) 106 | yield decode_json_type_json(json, tpe) 107 | 108 | 109 | __all__ = ('Decoder', 'decode_json_object', 'decode_json', 'decode', 'decode_json_type', 'decode_json_type_json') 110 | -------------------------------------------------------------------------------- /unit/io_spec.py: -------------------------------------------------------------------------------- 1 | from amino.io import IO 2 | from amino.test.spec_spec import Spec 3 | from amino import List, Right, L, _, Just 4 | 5 | 6 | class IOSpec(Spec): 7 | 8 | def sequence(self) -> None: 9 | f = lambda: 3 10 | t = List(IO.now(1), IO.now(2), IO.delay(f)).sequence(IO) 11 | t.attempt.should.equal(Right(List(1, 2, 3))) 12 | 13 | def zip(self) -> None: 14 | t = IO.now(1) & IO.now(2) 15 | t.attempt.should.equal(Right((1, 2))) 16 | 17 | def trampoline(self) -> None: 18 | t = (List.range(1000).fold_left(IO.now(1))(lambda z, a: z.flat_map(IO.now, fs=Just('now')))) 19 | t.attempt.should.contain(1) 20 | 21 | def now(self) -> None: 22 | v = 5 23 | t = IO.now(v) 24 | t.attempt.should.contain(v) 25 | 26 | def delay(self) -> None: 27 | v = 13 28 | w = 29 29 | f = _ + v 30 | t = IO.delay(f, w) 31 | t.run().should.equal(v + w) 32 | 33 | def suspend(self) -> None: 34 | v = 13 35 | w = 29 36 | f = lambda a: IO.now(a + v) 37 | t = IO.suspend(f, w) 38 | t.run().should.equal(v + w) 39 | 40 | def flat_map_now(self) -> None: 41 | v = 13 42 | w = 29 43 | t = IO.now(v).flat_map(L(IO.now)(_ + w)) 44 | t.attempt.should.contain(v + w) 45 | 46 | def flat_map_delay(self) -> None: 47 | v = 13 48 | w = 29 49 | x = 17 50 | f = _ + x 51 | t = IO.delay(f, v).flat_map(L(IO.now)(_ + w)) 52 | t.attempt.should.contain(v + w + x) 53 | 54 | def flat_map_delay_2(self) -> None: 55 | v = 13 56 | w = 29 57 | x = 17 58 | f = _ + w 59 | g = _ + x 60 | t = IO.delay(f, v).flat_map(lambda a: IO.delay(g, a)) 61 | t.attempt.should.contain(v + w + x) 62 | 63 | def flat_map_twice(self) -> None: 64 | v = 13 65 | w = 29 66 | x = 17 67 | t = ( 68 | IO.now(v) 69 | .flat_map(L(IO.now)(_ + w)) 70 | .flat_map(L(IO.now)(_ + x)) 71 | ) 72 | t.attempt.should.contain(v + w + x) 73 | 74 | def flat_map_thrice(self) -> None: 75 | v = 13 76 | w = 29 77 | x = 17 78 | y = 11 79 | t = ( 80 | IO.now(v) 81 | .flat_map(L(IO.now)(_ + w)) 82 | .flat_map(L(IO.now)(_ + x)) 83 | .flat_map(L(IO.now)(_ + y)) 84 | ) 85 | t.attempt.should.contain(v + w + x + y) 86 | 87 | def map(self) -> None: 88 | v = 13 89 | w = 29 90 | t = IO.now(v).map(_ + w) 91 | t.attempt.should.contain(v + w) 92 | 93 | def and_then(self) -> None: 94 | v = 7 95 | f = lambda: v 96 | t = IO.now(1).and_then(IO.delay(f)) 97 | t.attempt.should.contain(v) 98 | 99 | 100 | class IOStringSpec(Spec): 101 | 102 | def now(self) -> None: 103 | str(IO.now(5)).should.equal('Pure(5)') 104 | 105 | def now_flat_map(self) -> None: 106 | t = IO.now(5) // IO.now 107 | target = 'Suspend(Pure(5).flat_map(now))' 108 | str(t).should.equal(target) 109 | 110 | def suspend_flat_map(self) -> None: 111 | t = IO.suspend(IO.now, 5) // IO.now 112 | target = 'BindSuspend(now(5).flat_map(now))' 113 | str(t).should.equal(target) 114 | 115 | def now_map(self) -> None: 116 | t = IO.now(5) / (_ + 1) 117 | target = 'Suspend(Pure(5).map(lambda a: (lambda b: a + b)(1)))' 118 | str(t).should.equal(target) 119 | str(t.step()).should.equal('Pure(6)') 120 | 121 | def suspend_map(self) -> None: 122 | t = IO.suspend(IO.now, 5) / (_ + 1) 123 | target = 'Suspend(Pure(5).map(lambda a: (lambda b: a + b)(1)))' 124 | str(t.step()).should.equal(target) 125 | 126 | 127 | __all__ = ('IOSpec', 'IOStringSpec') 128 | -------------------------------------------------------------------------------- /amino/json/encoders.py: -------------------------------------------------------------------------------- 1 | from typing import Union, TypeVar, Mapping, Any, List as TList, Callable, Tuple, Type 2 | from numbers import Number 3 | from uuid import UUID 4 | from types import FunctionType 5 | 6 | from amino import Either, List, L, _, Right, Lists, Maybe, Path, Map, Boolean, do, Do, Try, Dat 7 | from amino.json.encoder import Encoder, encode_json, json_object_with_type 8 | from amino.json.data import JsonError, Json, JsonArray, JsonScalar, JsonObject, JsonNull 9 | 10 | A = TypeVar('A') 11 | B = TypeVar('B') 12 | Sub = TypeVar('Sub', bound=Dat) 13 | 14 | 15 | class ScalarEncoder(Encoder[Union[Number, str, None]], pred=L(issubclass)(_, (Number, str, type(None)))): 16 | 17 | def encode(self, a: Union[Number, str, None]) -> Either[JsonError, Json]: 18 | return Right(JsonScalar(a)) 19 | 20 | 21 | class MapEncoder(Encoder[List], pred=L(issubclass)(_, Mapping)): 22 | 23 | def encode(self, a: Map[str, Any]) -> Either[JsonError, Json]: 24 | return Map(a).traverse(encode_json, Either) / JsonObject 25 | 26 | 27 | class ListEncoder(Encoder[List], pred=L(issubclass)(_, TList)): 28 | 29 | def encode(self, a: TList) -> Either[JsonError, Json]: 30 | return Lists.wrap(a).traverse(encode_json, Either) / JsonArray 31 | 32 | 33 | class MaybeEncoder(Encoder[Maybe], tpe=Maybe): 34 | 35 | def encode(self, a: Maybe[A]) -> Either[JsonError, Json]: 36 | return a.map(encode_json) | (lambda: Right(JsonNull.cons())) 37 | 38 | 39 | class EitherEncoder(Encoder[Either], tpe=Either): 40 | 41 | @do(Either[JsonError, Json]) 42 | def encode(self, a: Either[B, A]) -> Do: 43 | json = yield encode_json(a.value) 44 | yield Right(json_object_with_type(Map(value=json), type(a))) 45 | 46 | 47 | class UUIDEncoder(Encoder[UUID], tpe=UUID): 48 | 49 | def encode(self, a: UUID) -> Either[JsonError, Json]: 50 | return Right(JsonScalar(str(a))) 51 | 52 | 53 | class PathEncoder(Encoder[Path], tpe=Path): 54 | 55 | def encode(self, a: Path) -> Either[JsonError, Json]: 56 | return Right(JsonScalar(str(a))) 57 | 58 | 59 | class BooleanEncoder(Encoder[Boolean], tpe=Boolean): 60 | 61 | def encode(self, a: Boolean) -> Either[JsonError, Json]: 62 | return Right(JsonScalar(a.value)) 63 | 64 | 65 | @do(Either[JsonError, Json]) 66 | def encode_instance(a: A, tpe: type, module: str, names: list) -> Do: 67 | mod_json = yield encode_json(module) 68 | names_json = yield encode_json(names) 69 | return json_object_with_type(Map(module=mod_json, names=names_json), tpe) 70 | 71 | 72 | @do(Either[JsonError, Json]) 73 | def encode_instance_simple(data: A, tpe: type) -> Do: 74 | mod = yield Try(lambda: data.__module__) 75 | names = yield Try(lambda: data.__qualname__.split('.')) 76 | yield encode_instance(data, tpe, mod, names) 77 | 78 | 79 | class FunctionEncoder(Encoder[Callable], tpe=FunctionType): 80 | 81 | @do(Either[JsonError, Json]) 82 | def encode(self, data: Callable) -> Do: 83 | yield encode_instance_simple(data, Callable) 84 | 85 | 86 | class TupleEncoder(Encoder[Tuple], tpe=tuple): 87 | 88 | @do(Either[JsonError, Json]) 89 | def encode(self, data: Tuple) -> Do: 90 | array = yield encode_json(Lists.wrap(data)) 91 | return json_object_with_type(Map(data=array), tuple) 92 | 93 | 94 | class TypeEncoder(Encoder[Type], tpe=type): 95 | 96 | def encode(self, data: Type) -> Either[JsonError, Json]: 97 | return encode_instance_simple(data, Type) 98 | 99 | 100 | class DatEncoder(Encoder, tpe=Dat): 101 | 102 | @do(Either[JsonError, Map]) 103 | def encode(self, a: Sub) -> Do: 104 | jsons = yield a._dat__values.traverse(encode_json, Either) 105 | yield Right(json_object_with_type(Map(a._dat__names.zip(jsons)), type(a))) 106 | 107 | 108 | __all__ = ('ListEncoder', 'ScalarEncoder', 'MaybeEncoder', 'UUIDEncoder', 'PathEncoder', 'MapEncoder', 'EitherEncoder', 109 | 'BooleanEncoder', 'encode_instance', 'FunctionEncoder', 'TupleEncoder', 'TypeEncoder', 'DatEncoder',) 110 | -------------------------------------------------------------------------------- /amino/json/data.py: -------------------------------------------------------------------------------- 1 | import abc 2 | from typing import TypeVar, Union, Generic 3 | from numbers import Number 4 | 5 | from amino import Map, List, _, Either, Right, Left, Boolean 6 | from amino.util.string import ToStr 7 | from amino.algebra import Algebra 8 | 9 | A = TypeVar('A') 10 | tpe_key = '__type__' 11 | 12 | 13 | class JsonError(ToStr): 14 | 15 | def __init__(self, data: Union['Json', str], error: Union[str, Exception]) -> None: 16 | self.data = data 17 | self.error = error 18 | 19 | def _arg_desc(self) -> List[str]: 20 | return List(self.data, str(self.error)) 21 | 22 | @property 23 | def exception(self) -> Exception: 24 | return self.error if isinstance(self.error, Exception) else Exception(self.error) 25 | 26 | 27 | class Json(Algebra[A]): 28 | 29 | def __init__(self, data: A) -> None: 30 | self.data = data 31 | 32 | @abc.abstractproperty 33 | def native(self) -> Union[dict, list, str, Number, None]: 34 | ... 35 | 36 | @abc.abstractmethod 37 | def field(self, key: str) -> Either[JsonError, 'Json']: 38 | ... 39 | 40 | def _arg_desc(self) -> List[str]: 41 | return List(str(self.data)) 42 | 43 | @property 44 | def scalar(self) -> Boolean: 45 | return Boolean.isinstance(self, JsonScalar) 46 | 47 | @property 48 | def array(self) -> Boolean: 49 | return Boolean.isinstance(self, JsonArray) 50 | 51 | @property 52 | def object(self) -> Boolean: 53 | return Boolean.isinstance(self, JsonObject) 54 | 55 | @property 56 | def absent(self) -> Boolean: 57 | return Boolean.isinstance(self, JsonAbsent) 58 | 59 | @property 60 | def null(self) -> Boolean: 61 | return Boolean.isinstance(self, JsonNull) 62 | 63 | def error(self, msg: Union[str, Exception]) -> JsonError: 64 | return JsonError(self.data, msg) 65 | 66 | @property 67 | def as_scalar(self) -> Either[JsonError, 'JsonScalar']: 68 | return Right(self) if self.scalar else Left(self.error('not a scalar')) 69 | 70 | @property 71 | def as_array(self) -> Either[JsonError, 'JsonArray']: 72 | return Right(self) if self.array else Left(self.error('not an array')) 73 | 74 | @property 75 | def as_object(self) -> Either[JsonError, 'JsonObject']: 76 | return Right(self) if self.object else Left(self.error('not an object')) 77 | 78 | 79 | class JsonObject(Json[Map[str, Json]]): 80 | 81 | @property 82 | def native(self) -> Union[dict, list, str, Number, None]: 83 | return self.data.valmap(_.native) 84 | 85 | def field(self, key: str) -> Either[JsonError, Json]: 86 | return Right(self.data.lift(key) | JsonAbsent(self.error(f'no field `{key}`'))) 87 | 88 | @property 89 | def has_type(self) -> Boolean: 90 | return self.data.contains(tpe_key) 91 | 92 | 93 | class JsonArray(Json[List[Json]]): 94 | 95 | @property 96 | def native(self) -> Union[dict, list, str, Number, None]: 97 | return self.data.map(_.native) 98 | 99 | def field(self, key: str) -> Either[JsonError, Json]: 100 | return Left(self.error('JsonArray has no fields')) 101 | 102 | 103 | class JsonScalar(Json[Union[str, Number]]): 104 | 105 | @property 106 | def native(self) -> Union[dict, list, str, Number]: 107 | return self.data 108 | 109 | def field(self, key: str) -> Either[JsonError, Json]: 110 | return Left(self.error('JsonScalar has no fields')) 111 | 112 | 113 | class JsonNull(Json[None]): 114 | 115 | @staticmethod 116 | def cons() -> 'Json[None]': 117 | return JsonNull(None) 118 | 119 | @property 120 | def native(self) -> None: 121 | None 122 | 123 | def field(self, key: str) -> Either[JsonError, Json]: 124 | return Left(self.error('JsonNull has no fields')) 125 | 126 | 127 | class JsonAbsent(Json[JsonError]): 128 | 129 | @property 130 | def native(self) -> Union[dict, list, str, Number, None]: 131 | return None 132 | 133 | def field(self, key: str) -> Either[JsonError, Json]: 134 | return Right(self) 135 | 136 | 137 | __all__ = ('JsonError', 'Json', 'JsonObject', 'JsonArray', 'JsonScalar', 'JsonAbsent') 138 | -------------------------------------------------------------------------------- /amino/boolean.py: -------------------------------------------------------------------------------- 1 | from typing import Union, Any, TypeVar, Type, Callable, Tuple 2 | 3 | import amino 4 | from amino import maybe 5 | from amino.either import Right, Left, Either 6 | from amino.func import call_by_name 7 | 8 | A = TypeVar('A') 9 | B = TypeVar('B') 10 | 11 | 12 | class Boolean: 13 | 14 | def __init__(self, value: Union['Boolean', bool]) -> None: 15 | self.value = bool(value) 16 | 17 | @staticmethod 18 | def wrap(value): 19 | return Boolean(value) 20 | 21 | @staticmethod 22 | def issubclass(value: Type[A], tpe: Type[B]) -> 'Boolean': 23 | return Boolean(isinstance(value, type) and issubclass(value, tpe)) 24 | 25 | @staticmethod 26 | def isinstance(value: A, tpe: Union[Type[Any], Tuple[Type[Any], ...]]) -> 'Boolean': 27 | return Boolean(isinstance(value, tpe)) 28 | 29 | @staticmethod 30 | def is_a(tpe: Union[Type[Any], Tuple[Type[Any], ...]]) -> Callable[[Any], 'Boolean']: 31 | return lambda a: Boolean.isinstance(a, tpe) 32 | 33 | @staticmethod 34 | def is_a_class(tpe: Type[A]) -> Callable[[Any], 'Boolean']: 35 | return lambda a: Boolean.issubclass(a, tpe) 36 | 37 | def maybe(self, value): 38 | return maybe.Maybe.optional(value) if self else maybe.Empty() 39 | 40 | def flat_maybe(self, value: 'Maybe'): # type: ignore 41 | return value if self else maybe.Empty() 42 | 43 | def maybe_call(self, f, *a, **kw): 44 | return maybe.Just(f(*a, **kw)) if self else maybe.Empty() 45 | 46 | def m(self, v): 47 | return maybe.Maybe.optional(call_by_name(v)) if self else maybe.Empty() 48 | 49 | def flat_maybe_call(self, f, *a, **kw): 50 | return f(*a, **kw) if self else maybe.Empty() 51 | 52 | def flat_m(self, v): 53 | return call_by_name(v) if self else maybe.Empty() 54 | 55 | def either(self, l, r): 56 | return self.either_call(l, lambda: r) 57 | 58 | def either_call(self, l, r): 59 | return Right(r()) if self else Left(l) 60 | 61 | def flat_either_call(self, l, r): 62 | return r() if self else Left(l) 63 | 64 | def e(self, f: A, t: B) -> Either[A, B]: 65 | return Right(call_by_name(t)) if self else Left(call_by_name(f)) 66 | 67 | def flat_e(self, l, r): 68 | return call_by_name(r) if self else Left(call_by_name(l)) 69 | 70 | def l(self, v: A) -> 'amino.List[A]': 71 | return self.m(v) / amino.List | amino.Nil 72 | 73 | def cata(self, t: A, f: A) -> A: 74 | return t if self.value else f 75 | 76 | def cata_call(self, t, f): 77 | return t() if self.value else f() 78 | 79 | def c(self, t: Callable[[], A], f: Callable[[], A]) -> A: 80 | return call_by_name(t) if self.value else call_by_name(f) 81 | 82 | def __bool__(self): 83 | return self.value 84 | 85 | def __str__(self): 86 | return '⊤' if self.value else '⊥' 87 | 88 | def __repr__(self): 89 | return '{}({})'.format(self.__class__.__name__, self.value) 90 | 91 | def __eq__(self, other): 92 | return ( 93 | self.value == other 94 | if isinstance(other, bool) else 95 | self.value == other.value 96 | if isinstance(other, Boolean) else 97 | False 98 | ) 99 | 100 | def __and__(self, other: Any) -> 'Boolean': 101 | return Boolean(self and other) 102 | 103 | def __or__(self, other: Any) -> 'Boolean': 104 | return Boolean(self or other) 105 | 106 | def __invert__(self) -> 'Boolean': 107 | return Boolean(not self.value) 108 | 109 | def __xor__(self, other: Any) -> 'Boolean': 110 | return Boolean(bool(self.value ^ bool(other))) 111 | 112 | def __rxor__(self, other: Any) -> 'Boolean': 113 | return Boolean(bool(self.value ^ bool(other))) 114 | 115 | def __hash__(self) -> int: 116 | return hash(self.value) 117 | 118 | @property 119 | def no(self): 120 | return Boolean(not self.value) 121 | 122 | @property 123 | def json_repr(self): 124 | return self.value 125 | 126 | @property 127 | def to_int(self) -> int: 128 | return 1 if self else 0 129 | 130 | 131 | true = Boolean(True) 132 | false = Boolean(False) 133 | 134 | __all__ = ('Boolean', 'true', 'false') 135 | -------------------------------------------------------------------------------- /unit/lazy_list_spec.py: -------------------------------------------------------------------------------- 1 | import itertools 2 | 3 | from amino.test.spec_spec import Spec 4 | from amino import LazyList, List, _, Just, Maybe, IO, I, Nothing 5 | from amino.lazy_list import LazyLists 6 | from amino.func import tupled2 7 | 8 | 9 | class LazyListSpec(Spec): 10 | 11 | def slice_infinite(self) -> None: 12 | l = LazyList(itertools.count(), chunk_size=20) 13 | l[:15].should.have.length_of(15) 14 | l.strict.should.have.length_of(20) 15 | l[:20].should.have.length_of(20) 16 | l.strict.should.have.length_of(20) 17 | l[:21].should.have.length_of(21) 18 | l.strict.should.have.length_of(40) 19 | 20 | def slice_finite(self) -> None: 21 | l = LazyList(range(30), chunk_size=20) 22 | l[:15].should.have.length_of(15) 23 | l.strict.should.have.length_of(20) 24 | l[:20].should.have.length_of(20) 25 | l.strict.should.have.length_of(20) 26 | l[:21].should.have.length_of(21) 27 | l.strict.should.have.length_of(30) 28 | 29 | def single(self) -> None: 30 | l = LazyList(range(30), chunk_size=20) 31 | l[19].should.equal(19) 32 | l.strict.should.have.length_of(20) 33 | l[20].should.equal(20) 34 | l.strict.should.have.length_of(30) 35 | 36 | def map(self) -> None: 37 | l = LazyList(itertools.count(), chunk_size=20) 38 | l.lift(5) 39 | l2 = l.map(_ * 10) 40 | l2[:5].should.equal(List.wrap(range(0, 50, 10))) 41 | 42 | def with_index(self) -> None: 43 | l = LazyList(itertools.count(), chunk_size=20) 44 | l2 = l.map(_ * 5).with_index 45 | l2[:2].should.equal(List((0, 0), (1, 5))) 46 | 47 | def index_of(self) -> None: 48 | l = LazyList(range(30), chunk_size=20) 49 | l.index_of(21).should.contain(21) 50 | l.index_of(49).should.be.empty 51 | 52 | def find(self) -> None: 53 | l = LazyList(range(30), chunk_size=20) 54 | l.find(_ == 21).should.contain(21) 55 | l.find(_ == 49).should.be.empty 56 | 57 | def deep(self) -> None: 58 | n = int(1e4) 59 | l = LazyList(List.wrap(range(n))) 60 | l.index_of(n - 1).should.contain(n - 1) 61 | 62 | def filter(self) -> None: 63 | l = LazyList(range(30)) 64 | l2 = l.filter(_ % 2 == 0) 65 | l2.strict.should.have.length_of(0) 66 | l3 = LazyList(range(30)) 67 | l3[29] 68 | l4 = l3.filter(_ % 2 == 0) 69 | l4.strict.should.have.length_of(15) 70 | l4.drain.should.equal(List.wrap(range(0, 30, 2))) 71 | 72 | def fold_left(self) -> None: 73 | LazyList((1, 2, 3)).fold_left('')(lambda a, b: str(b) + a)\ 74 | .should.equal('321') 75 | 76 | def fold_map(self) -> None: 77 | LazyList((1, 2, 3)).fold_map(5, _ * 2).should.equal(17) 78 | 79 | def sequence(self) -> None: 80 | n = 3 81 | l = LazyList(map(Just, range(n))) 82 | target = LazyList(List.wrap(range(n))) 83 | (l.sequence(Maybe) / _.drain).should.contain(target.drain) 84 | 85 | def traverse_io(self) -> None: 86 | n = 3 87 | l = LazyList(range(n)) 88 | result = l.traverse(IO.now, IO).attempt / _.drain 89 | result.should.contain(l.drain) 90 | 91 | def zip(self) -> None: 92 | a = 1 93 | b = 2 94 | ab = (a, b) 95 | l1 = LazyList((a, a, a, a), chunk_size=1) 96 | l2 = LazyList((b, b, b, b), chunk_size=1) 97 | l1[1] 98 | z = l1.zip(l2) 99 | z.strict.should.equal(List(ab, ab)) 100 | z.drain.should.equal(List(ab, ab, ab, ab)) 101 | 102 | def apzip(self) -> None: 103 | l = LazyList((1, 2, 3), chunk_size=1) 104 | l[0] 105 | z = l.apzip(_ + 2) 106 | z.drain.should.equal(List((1, 3), (2, 4), (3, 5))) 107 | 108 | def flat_map(self) -> None: 109 | l = LazyLists.cons(LazyList((1, 2, 3)), LazyList((4, 5, 6)), LazyList((1, 2, 3))) 110 | l.lift(1) 111 | l.flat_map(I).drain.should.equal(List(1, 2, 3, 4, 5, 6, 1, 2, 3)) 112 | 113 | def collect(self) -> None: 114 | def f(a: int, n: str) -> Maybe[str]: 115 | return Just(n) if a % 2 == 0 else Nothing 116 | l: LazyList = LazyList((a, str(a)) for a in range(10)) 117 | l2 = l.collect(tupled2(f)) 118 | l2.drain.should.equal(List('0', '2', '4', '6', '8')) 119 | 120 | __all__ = ('LazyListSpec',) 121 | -------------------------------------------------------------------------------- /unit/json_spec.py: -------------------------------------------------------------------------------- 1 | import abc 2 | from typing import Any, Callable, TypeVar, Type, Generic 3 | 4 | from amino.test.spec_spec import Spec 5 | from amino.dat import Dat 6 | from amino import Right, Maybe, List, Either, Left, do, Do, ADT, Map, Nothing, Just 7 | from amino.json import dump_json, decode_json 8 | from amino.json.data import JsonError, tpe_key 9 | 10 | A = TypeVar('A') 11 | 12 | 13 | @do(Either[JsonError, A]) 14 | def _code_json(a: A) -> Do: 15 | json = yield dump_json(a) 16 | yield decode_json(json) 17 | 18 | 19 | class E(Dat['E']): 20 | 21 | def __init__(self, a: int, b: str) -> None: 22 | self.a = a 23 | self.b = b 24 | 25 | 26 | class D(Dat['D']): 27 | 28 | def __init__(self, e: E, a: int, b: str) -> None: 29 | self.e = e 30 | self.a = a 31 | self.b = b 32 | 33 | 34 | class M(Dat['M']): 35 | 36 | def __init__(self, a: Maybe[int]) -> None: 37 | self.a = a 38 | 39 | 40 | class Li(Dat['Li']): 41 | 42 | def __init__(self, a: List[Any]) -> None: 43 | self.a = a 44 | 45 | 46 | class Ei(Dat['Ei']): 47 | 48 | def __init__(self, a: Either[str, E], b: Either[str, List[int]], c: Either[str, E]) -> None: 49 | self.a = a 50 | self.b = b 51 | self.c = c 52 | 53 | 54 | def encode_me() -> int: 55 | return 5 56 | 57 | 58 | class Fun(Dat['Fun']): 59 | 60 | def __init__(self, f: Callable[[], int]) -> None: 61 | self.f = f 62 | 63 | 64 | mod = 'unit.json_spec' 65 | 66 | 67 | class Tpe(Dat['Tpe']): 68 | 69 | def __init__(self, tpe: Type[A], none: type) -> None: 70 | self.tpe = tpe 71 | self.none = none 72 | 73 | 74 | class _Abstr(ADT['_Abstr']): 75 | 76 | @abc.abstractmethod 77 | def abs(self, a: int) -> None: 78 | ... 79 | 80 | 81 | class _Sub1(_Abstr): 82 | 83 | def __init__(self, a: int) -> None: 84 | self.a = a 85 | 86 | def abs(self, a: int) -> None: 87 | pass 88 | 89 | 90 | class _Cont(Dat['_Cont']): 91 | 92 | def __init__(self, a: _Abstr) -> None: 93 | self.a = a 94 | 95 | 96 | Ab = TypeVar('Ab', bound=_Abstr) 97 | 98 | 99 | class TV(Generic[Ab], Dat['Ab']): 100 | 101 | def __init__(self, a: Ab) -> None: 102 | self.a = a 103 | 104 | 105 | class _SM: 106 | 107 | @staticmethod 108 | def cons() -> int: 109 | return 65 110 | 111 | 112 | class _Gen(Generic[A], Dat['_Gen[A]']): 113 | 114 | def __init__(self, a: A) -> None: 115 | self.a = a 116 | 117 | 118 | class _MM(Dat['_MM']): 119 | 120 | def __init__(self, m: Maybe[Map[str, str]]) -> None: 121 | self.m = m 122 | 123 | 124 | class JsonSpec(Spec): 125 | 126 | def codec_dat(self) -> None: 127 | a = D(E(2, 'E'), 1, 'D') 128 | _code_json(a).should.equal(Right(a)) 129 | 130 | def codec_maybe(self) -> None: 131 | json = f'{{"a": null, "{tpe_key}": {{"module": "{mod}", "names": ["M"]}}}}' 132 | decoded = decode_json(json) 133 | (decoded // dump_json).should.equal(Right(json)) 134 | 135 | def codec_list(self) -> None: 136 | json = f'{{"a": [1, 2, "string"], "{tpe_key}": {{"module": "{mod}", "names": ["Li"]}}}}' 137 | decoded = decode_json(json) 138 | (decoded // dump_json).should.equal(Right(json)) 139 | 140 | def codec_either(self) -> None: 141 | a = Ei(Right(E(7, 'value')), Right(List(5, 9)), Left('error')) 142 | _code_json(a).should.equal(Right(a)) 143 | 144 | def function(self) -> None: 145 | @do(Either[str, int]) 146 | def run() -> Do: 147 | decoded = yield _code_json(Fun(encode_me)) 148 | return decoded.f() 149 | run().should.equal(Right(5)) 150 | 151 | def staticmethod(self) -> None: 152 | @do(Either[str, int]) 153 | def run() -> Do: 154 | decoded = yield _code_json(_SM.cons) 155 | return decoded() 156 | run().should.equal(Right(65)) 157 | 158 | def tuple(self) -> None: 159 | t = (4, E(9, 'nine'), 6) 160 | _code_json(t).should.equal(Right(t)) 161 | 162 | def tpe(self) -> None: 163 | t = Tpe(Tpe, type(None)) 164 | _code_json(t).should.equal(Right(t)) 165 | 166 | def adt(self) -> None: 167 | a = _Cont(_Sub1(1)) 168 | _code_json(a).should.equal(Right(a)) 169 | 170 | def type_var(self) -> None: 171 | a = TV(_Sub1(1)) 172 | _code_json(a).should.equal(Right(a)) 173 | 174 | def generic(self) -> None: 175 | a = _Gen(E(5, '55')) 176 | _code_json(a).should.equal(Right(a)) 177 | 178 | def maybe_map(self) -> None: 179 | a = _MM(Just(Map(a='a'))) 180 | _code_json(a).should.equal(Right(a)) 181 | 182 | 183 | __all__ = ('JsonSpec',) 184 | -------------------------------------------------------------------------------- /amino/map.py: -------------------------------------------------------------------------------- 1 | from typing import TypeVar, Dict, Generic, Tuple, Callable 2 | 3 | from toolz import dicttoolz 4 | 5 | from amino import Maybe, may, Just, Nothing 6 | from amino.data.list import List, Lists 7 | from amino.boolean import Boolean 8 | from amino.tc.base import Implicits, ImplicitsMeta 9 | 10 | A = TypeVar('A') 11 | B = TypeVar('B', covariant=True) 12 | C = TypeVar('C') 13 | D = TypeVar('D', covariant=True) 14 | 15 | 16 | class Map(Generic[A, B], Dict[A, B], Implicits, metaclass=ImplicitsMeta, implicits=True): 17 | 18 | @staticmethod 19 | def wrap(d: Dict[A, B]) -> 'Map[A, B]': 20 | return Map(d) 21 | 22 | def lift(self, key: str) -> Maybe[B]: 23 | return Just(Dict.get(self, key)) if key in self else Nothing 24 | 25 | def lift_item(self, key): 26 | return self.lift(key) / (lambda a: (key, a)) 27 | 28 | def lift_all(self, *keys): 29 | def append(zm, k): 30 | return zm // (lambda z: self.lift(k) / z.cat) 31 | return List.wrap(keys).fold_left(Just(List()))(append) 32 | 33 | def get_all_map(self, *keys): 34 | def append(zm, k): 35 | return zm // (lambda z: self.get_item(k) / z.cat) 36 | return List.wrap(keys).fold_left(Just(Map()))(append) 37 | 38 | def lift_or_else(self, key, default: Callable[[], C]): 39 | return self.lift(key).get_or_else(default) 40 | 41 | def set_if_missing(self, key: A, default: Callable[[], B]): 42 | if key in self: 43 | return self 44 | else: 45 | return self + (key, default()) 46 | 47 | def __str__(self): 48 | return str(dict(self)) 49 | 50 | def cat(self, item: Tuple[A, B]): 51 | return Map(dicttoolz.assoc(self, *item)) 52 | 53 | __add__ = cat 54 | 55 | def merge(self, other: 'Map[A, B]'): 56 | return Map(dicttoolz.merge(self, other)) 57 | 58 | __pow__ = merge 59 | 60 | def __sub__(self, key: A): 61 | return Map(dicttoolz.dissoc(self, key)) 62 | 63 | def find(self, f: Callable[[B], bool]) -> Maybe[Tuple[A, B]]: 64 | return self.to_list.find(lambda item: f(item[1])) 65 | 66 | def find_key(self, f: Callable[[A], bool]) -> Maybe[Tuple[A, B]]: 67 | return self.to_list.find(lambda item: f(item[0])) 68 | 69 | def valfilter(self, f: Callable[[B], bool]) -> 'Map[A, B]': 70 | return Map(dicttoolz.valfilter(f, self)) 71 | 72 | def keyfilter(self, f: Callable[[A], bool]) -> 'Map[A, B]': 73 | return Map(dicttoolz.keyfilter(f, self)) 74 | 75 | def filter(self, f: Callable[[Tuple[A, B]], bool]) -> 'Map[A, B]': 76 | return Map(dicttoolz.itemfilter(f, self)) 77 | 78 | def valmap(self, f: Callable[[B], C]) -> 'Map[A, C]': 79 | return Map(dicttoolz.valmap(f, dict(self))) 80 | 81 | def keymap(self, f: Callable[[A], C]) -> 'Map[C, B]': 82 | return Map(dicttoolz.keymap(f, dict(self))) 83 | 84 | def map(self, f: Callable[[A, B], Tuple[C, D]]) -> 'Map[C, D]': 85 | wrap = lambda a: f(*a) 86 | return Map(dicttoolz.itemmap(wrap, self)) 87 | 88 | def flat_map( 89 | self, 90 | f: Callable[[A, B], Maybe[Tuple[C, D]]] 91 | ) -> 'Map[C, D]': 92 | filtered = List.wrap([f(a, b) for a, b in self.items()])\ 93 | .join 94 | return Map(filtered) 95 | 96 | def bimap(self, fa: Callable[[A], C], fb: Callable[[B], D]) -> 'Map[C, D]': 97 | return self.map(lambda a, b: (fa(a), fb(b))) 98 | 99 | def map2(self, f: Callable[[A, B], C]) -> List[C]: 100 | return List.wrap([f(a, b) for a, b in self.items()]) 101 | 102 | def flat_map2(self, f: Callable[[A, B], List[C]]) -> List[C]: 103 | return self.to_list.flat_map2(f) 104 | 105 | @property 106 | def to_list(self): 107 | return List.wrap(self.items()) 108 | 109 | @property 110 | def head(self): 111 | return self.to_list.head 112 | 113 | @property 114 | def keys_view(self): 115 | return Dict.keys(self) 116 | 117 | @property 118 | def values_view(self): 119 | return Dict.values(self) 120 | 121 | @property 122 | def k(self): 123 | return List(*Dict.keys(self)) 124 | 125 | @property 126 | def v(self) -> List[B]: 127 | return Lists.wrap(Dict.values(self)) 128 | 129 | @property 130 | def is_empty(self): 131 | return self.k.is_empty 132 | 133 | empty = is_empty 134 | 135 | def at(self, *keys): 136 | return self.keyfilter(lambda a: a in keys) 137 | 138 | def values_at(self, *keys): 139 | return List.wrap(keys) // self.lift 140 | 141 | def has_key(self, name): 142 | return Boolean(name in self) 143 | 144 | contains = has_key 145 | 146 | def add_maybe(self, key: A, value: Maybe[B]): 147 | return value / (lambda a: self + (key, a)) | self 148 | 149 | def insert_if_absent(self, key: A, value: Callable[[], B]) -> 'Map[A, B]': 150 | return self if key in self else self + (key, value()) 151 | 152 | 153 | __all__ = ('Map',) 154 | -------------------------------------------------------------------------------- /amino/case.py: -------------------------------------------------------------------------------- 1 | from types import SimpleNamespace 2 | from typing import Type, Generic, Any, TypeVar, Callable, Tuple, _GenericAlias, get_type_hints 3 | import inspect 4 | 5 | from amino.util.string import snake_case 6 | from amino import ADT, Map, List, Lists, do, Do, Either, Try, Just, Nothing, Maybe 7 | from amino.algebra import Algebra 8 | from amino.logging import module_log 9 | 10 | log = module_log() 11 | Alg = TypeVar('Alg', bound=Algebra) 12 | C = TypeVar('C', bound=Alg) 13 | A = TypeVar('A') 14 | B = TypeVar('B') 15 | 16 | 17 | class CaseMeta(type): 18 | 19 | def __new__(cls: type, name: str, bases: tuple, namespace: SimpleNamespace, alg: Type[Alg]=None, **kw: Any) -> type: 20 | inst = super().__new__(cls, name, bases, namespace, **kw) 21 | if alg is not None: 22 | inst.case = case_dispatch(inst, alg) 23 | return inst 24 | 25 | 26 | class Case(Generic[Alg, B], metaclass=CaseMeta): 27 | 28 | @classmethod 29 | def match(cls, scrutinee: Alg, *a: Any, **kw: Any) -> B: 30 | return cls().case(scrutinee, *a, **kw) 31 | 32 | def __call__(self, scrutinee: Alg, *a: Any, **kw: Any) -> B: 33 | return self.case(scrutinee, *a, **kw) 34 | 35 | 36 | def normalize_type(tpe: Type[C]) -> Type[C]: 37 | return getattr(tpe, '__origin__', tpe) 38 | 39 | 40 | def case_list( 41 | cls: Type[Case[C, B]], 42 | variants: List[Type[C]], 43 | alg: Type[C], 44 | has_default: bool, 45 | ) -> Map[Type[C], Callable[[Case[C, B]], B]]: 46 | @do(Maybe[Tuple[Type[C], Callable[[Case[C, B]], B]]]) 47 | def is_handler(name: str, f: Callable) -> Do: 48 | effective = getattr(f, '__do_original', f) 49 | hints = yield Try(get_type_hints, effective).to_maybe 50 | spec = yield Try(inspect.getfullargspec, effective).to_maybe 51 | param_name = yield Lists.wrap(spec.args).lift(1) 52 | param_type = yield Map(hints).lift(param_name) 53 | yield ( 54 | Just((normalize_type(param_type), f)) 55 | if isinstance(param_type, type) and issubclass(param_type, alg) else 56 | Just((normalize_type(param_type), f)) 57 | if isinstance(param_type, _GenericAlias) and issubclass(param_type.__origin__, alg) else 58 | Nothing 59 | ) 60 | handlers = Lists.wrap(inspect.getmembers(cls, inspect.isfunction)).flat_map2(is_handler) 61 | def find_handler(variant: Type[C]) -> Callable[[Case[C, B]], B]: 62 | def not_found() -> None: 63 | if not has_default: 64 | raise Exception(f'no case defined for {variant} on {cls.__name__}') 65 | def match_handler(tpe: type, f: Callable) -> Maybe[Callable[[Case[C, B]], B]]: 66 | return Just(f) if issubclass(tpe, variant) else Nothing 67 | return handlers.find_map2(match_handler).get_or_else(not_found) 68 | return variants.map(find_handler) 69 | 70 | 71 | # TODO determine default case from param type being the ADT 72 | def case_dispatch(cls: Type[Case[C, B]], alg: Type[C]) -> Callable[[Case[C, B], C], B]: 73 | def error(func: Case[C, B], variant: Alg, *a: Any, **kw: Any) -> None: 74 | raise TypeError(f'no case defined for {variant} on {cls.__name__}') 75 | default = getattr(cls, 'case_default', error) 76 | has_default = default is not error 77 | cases = case_list(cls, alg.__algebra_variants__.sort_by(lambda a: a.__algebra_index__), alg, has_default) 78 | length = cases.length 79 | def case(func: Case[C, B], scrutinee: C, *a: Any, **kw: Any) -> B: 80 | index = getattr(scrutinee, '__algebra_index__', None) 81 | handler = (cases[index] or default) if index is not None and index < length else default 82 | return handler(func, scrutinee, *a, **kw) 83 | return case 84 | 85 | 86 | class CaseRecMeta(type): 87 | 88 | def __new__(cls, name: str, bases: tuple, namespace: dict, alg: Type[Alg]=None, **kw: Any) -> type: 89 | inst = super().__new__(cls, name, bases, namespace, **kw) 90 | if alg is not None: 91 | inst.case = case_dispatch(inst, alg) 92 | return inst 93 | 94 | 95 | class CaseRec(Generic[Alg, A], metaclass=CaseRecMeta): 96 | 97 | def __call__(self, scrutinee: Alg, *a: Any, **kw: Any) -> 'Rec[Alg, A]': 98 | return Rec(self, scrutinee, a, kw) 99 | 100 | 101 | class RecStep(Generic[Alg, A], ADT['RecStep[Alg, A]']): 102 | pass 103 | 104 | 105 | class Rec(Generic[Alg, A], RecStep[Alg, A]): 106 | 107 | def __init__(self, func: CaseRec[Alg, A], scrutinee: Alg, args: list, kwargs: dict) -> None: 108 | self.func = func 109 | self.scrutinee = scrutinee 110 | self.args = args 111 | self.kwargs = kwargs 112 | 113 | def eval(self) -> A: 114 | return eval_case_rec(self) 115 | 116 | 117 | class Term(Generic[Alg, A], RecStep[Alg, A]): 118 | 119 | def __init__(self, result: A) -> None: 120 | self.result = result 121 | 122 | 123 | def eval_case_rec(step: Rec[Alg, A]) -> A: 124 | while True: 125 | step = step.func.case(step.scrutinee, *step.args, **step.kwargs) 126 | if isinstance(step, Term): 127 | return step.result 128 | elif not isinstance(step, Rec): 129 | raise Exception(f'invalid result in CaseRec step: {step}') 130 | 131 | 132 | __all__ = ('Case', 'CaseRec', 'Term') 133 | -------------------------------------------------------------------------------- /amino/string/hues.py: -------------------------------------------------------------------------------- 1 | '''Stolen from https://github.com/prashnts/hues 2 | ''' 3 | 4 | from typing import Union, Any, Callable, Type 5 | from collections import namedtuple 6 | from functools import reduce, partial 7 | 8 | 9 | ANSIColors = namedtuple( 10 | 'ANSIColors', 11 | [ 12 | 'black', 13 | 'red', 14 | 'green', 15 | 'yellow', 16 | 'blue', 17 | 'magenta', 18 | 'cyan', 19 | 'white', 20 | ] 21 | ) 22 | 23 | ANSIStyles = namedtuple( 24 | 'ANSIStyles', 25 | [ 26 | 'reset', 27 | 'bold', 28 | 'italic', 29 | 'underline', 30 | 'defaultfg', 31 | 'defaultbg', 32 | ] 33 | ) 34 | 35 | # Style Codes 36 | STYLE = ANSIStyles(0, 1, 3, 4, 39, 49) 37 | 38 | # Regular Colors 39 | FG = ANSIColors(*range(30, 38)) 40 | BG = ANSIColors(*range(40, 48)) 41 | 42 | # High Intensity Colors 43 | HI_FG = ANSIColors(*range(90, 98)) 44 | HI_BG = ANSIColors(*range(100, 108)) 45 | 46 | # Terminal sequence format 47 | SEQ = '\033[%sm' 48 | 49 | 50 | def gen_keywords(*args: Union[ANSIColors, ANSIStyles], **kwargs: Union[ANSIColors, ANSIStyles]) -> tuple: 51 | '''generate single escape sequence mapping.''' 52 | fields: tuple = tuple() 53 | values: tuple = tuple() 54 | for tpl in args: 55 | fields += tpl._fields 56 | values += tpl 57 | for prefix, tpl in kwargs.items(): 58 | fields += tuple(map(lambda x: '_'.join([prefix, x]), tpl._fields)) 59 | values += tpl 60 | return namedtuple('ANSISequences', fields)(*values) 61 | 62 | KEYWORDS = gen_keywords(STYLE, FG, bg=BG, bright=HI_FG, bg_bright=HI_BG) 63 | 64 | 65 | def zero_break(stack: tuple) -> tuple: 66 | '''Handle Resets in input stack. 67 | Breaks the input stack if a Reset operator (zero) is encountered. 68 | ''' 69 | reducer = lambda x, y: tuple() if y == 0 else x + (y,) 70 | return reduce(reducer, stack, tuple()) 71 | 72 | 73 | def annihilate(predicate: tuple, stack: tuple) -> tuple: 74 | '''Squash and reduce the input stack. 75 | Removes the elements of input that match predicate and only keeps the last 76 | match at the end of the stack. 77 | ''' 78 | extra = tuple(filter(lambda x: x not in predicate, stack)) 79 | head = reduce(lambda x, y: y if y in predicate else x, stack, None) 80 | return extra + (head,) if head else extra 81 | 82 | 83 | def annihilator(predicate: tuple) -> Callable[[tuple], tuple]: 84 | '''Build a partial annihilator for given predicate.''' 85 | return partial(annihilate, predicate) 86 | 87 | 88 | def dedup(stack: tuple) -> tuple: 89 | '''Remove duplicates from the stack in first-seen order.''' 90 | # Initializes with an accumulator and then reduces the stack with first match 91 | # deduplication. 92 | reducer = lambda x, y: x if y in x else x + (y,) 93 | return reduce(reducer, stack, tuple()) 94 | 95 | 96 | def apply(funcs: tuple, stack: tuple) -> tuple: 97 | '''Apply functions to the stack, passing the resulting stack to next state.''' 98 | return reduce(lambda x, y: y(x), funcs, stack) 99 | 100 | 101 | OPTIMIZATION_STEPS = ( 102 | zero_break, # Handle Resets using `reset`. 103 | annihilator(FG + HI_FG), # Squash foreground colors to the last value. 104 | annihilator(BG + HI_BG), # Squash background colors to the last value. 105 | dedup, # Remove duplicates in (remaining) style values. 106 | ) 107 | optimize = partial(apply, OPTIMIZATION_STEPS) 108 | 109 | 110 | def colorize(string: str, stack: tuple) -> str: 111 | '''Apply optimal ANSI escape sequences to the string.''' 112 | codes = optimize(stack) 113 | if len(codes): 114 | prefix = SEQ % ';'.join(map(str, codes)) 115 | suffix = SEQ % STYLE.reset 116 | return prefix + string + suffix 117 | else: 118 | return string 119 | 120 | 121 | class HueString(str): 122 | 123 | def __new__(cls, string: str, hue_stack: tuple=None) -> Type['HueString']: 124 | return super(HueString, cls).__new__(cls) 125 | 126 | def __init__(self, string: str, hue_stack: tuple=tuple()) -> None: 127 | self.__string = string 128 | self.__hue_stack = hue_stack 129 | 130 | def __getattr__(self, attr: str) -> 'HueString': 131 | try: 132 | code = getattr(KEYWORDS, attr) 133 | hues = self.__hue_stack + (code,) 134 | return HueString(self.__string, hue_stack=hues) 135 | except AttributeError as e: 136 | raise e 137 | 138 | @property 139 | def colorized(self) -> str: 140 | return colorize(self.__string, self.__hue_stack) 141 | 142 | 143 | def huestr(s: str) -> HueString: 144 | return HueString(s) 145 | 146 | 147 | def col(a: Any, c: Callable[[HueString], HueString]) -> str: 148 | return c(huestr(str(a))).colorized 149 | 150 | 151 | def red(a: Any) -> str: 152 | return col(a, lambda a: a.red) 153 | 154 | 155 | def green(a: Any) -> str: 156 | return col(a, lambda a: a.green) 157 | 158 | 159 | def yellow(a: Any) -> str: 160 | return col(a, lambda a: a.yellow) 161 | 162 | 163 | def blue(a: Any) -> str: 164 | return col(a, lambda a: a.blue) 165 | 166 | 167 | def cyan(a: Any) -> str: 168 | return col(a, lambda a: a.cyan) 169 | 170 | 171 | def magenta(a: Any) -> str: 172 | return col(a, lambda a: a.magenta) 173 | 174 | 175 | __all__ = ('huestr', 'red', 'green', 'yellow', 'blue', 'cyan', 'magenta') 176 | -------------------------------------------------------------------------------- /amino/bi_rose_tree.py: -------------------------------------------------------------------------------- 1 | import abc 2 | from typing import Generic, TypeVar, Callable, Tuple, Any 3 | 4 | from amino import LazyList, Maybe, I, Empty, Just, Boolean 5 | from amino.lazy import lazy 6 | from amino.lazy_list import LazyLists 7 | from amino.tree import indent, Node 8 | from amino.boolean import true, false 9 | 10 | 11 | Data = TypeVar('Data') 12 | A = TypeVar('A') 13 | B = TypeVar('B') 14 | 15 | 16 | class RoseTree(Generic[Data]): 17 | 18 | @abc.abstractproperty 19 | def parent(self) -> 'RoseTree': 20 | ... 21 | 22 | @abc.abstractproperty 23 | def is_root(self) -> Boolean: 24 | ... 25 | 26 | def __init__(self, data: Data, sub_cons: Callable[['RoseTree[Data]'], LazyList['RoseTree[Data]']]) -> None: 27 | self.data = data 28 | self._sub_cons = sub_cons 29 | 30 | @lazy 31 | def sub(self) -> LazyList['RoseTree[Data]']: 32 | return self._sub_cons(self) 33 | 34 | @property 35 | def desc(self) -> str: 36 | num = self.sub._drain().length 37 | return f'{self.data}, {num} children' 38 | 39 | def __str__(self) -> str: 40 | return f'{self.__class__.__name__}({self.desc})' 41 | 42 | def __repr__(self) -> str: 43 | return f'{self.__class__.__name__}({self.data})' 44 | 45 | @property 46 | def show(self) -> str: 47 | return self.strings._drain().mk_string('\n') 48 | 49 | @property 50 | def strings(self) -> LazyList[str]: 51 | return self._strings() 52 | 53 | def _strings(self) -> LazyList[str]: 54 | return indent(self.sub.flat_map(lambda a: a._strings())).cons(str(self.data)) 55 | 56 | def map(self, f: Callable[[Data], A]) -> 'RoseTree[A]': 57 | return self.copy(f, I) 58 | 59 | def copy(self, f: Callable[[Data], A], g: Callable[[LazyList['RoseTree[Data]']], LazyList['RoseTree[A]']] 60 | ) -> 'RoseTree[A]': 61 | return RoseTreeRoot(f(self.data), self._copy_sub(f, g)) 62 | 63 | def _copy_sub(self, f: Callable[[Data], A], g: Callable[[LazyList['RoseTree[Data]']], LazyList['RoseTree[A]']] 64 | ) -> Callable[['RoseTree[A]'], LazyList['RoseTree[A]']]: 65 | def sub(node: RoseTree[Data]) -> Callable[[RoseTree[A]], RoseTree[A]]: 66 | return lambda parent: BiRoseTree(f(node.data), parent, node._copy_sub(f, g)) 67 | return lambda parent: g(self.sub).map(lambda a: sub(a)(parent)) 68 | 69 | def __getitem__(self, key: int) -> Maybe['RoseTree[Data]']: 70 | return self.sub.lift(key) 71 | 72 | def filter(self, f: Callable[[Data], bool]) -> 'RoseTree[Data]': 73 | return self.copy(I, lambda a: a.filter(lambda n: f(n.data))) 74 | 75 | def _drain(self) -> None: 76 | self.sub._drain().foreach(lambda a: a._drain()) 77 | 78 | 79 | class RoseTreeRoot(Generic[Data], RoseTree[Data]): 80 | 81 | @property 82 | def parent(self) -> RoseTree[Data]: 83 | return self 84 | 85 | @property 86 | def is_root(self) -> Boolean: 87 | return true 88 | 89 | 90 | class BiRoseTree(Generic[Data], RoseTree[Data]): 91 | 92 | def __init__( 93 | self, 94 | data: Data, 95 | parent: RoseTree[Data], 96 | sub_cons: Callable[[RoseTree[Data]], LazyList[RoseTree[Data]]] 97 | ) -> None: 98 | super().__init__(data, sub_cons) 99 | self._parent = parent 100 | 101 | @property 102 | def parent(self) -> RoseTree[Data]: 103 | return self._parent 104 | 105 | @property 106 | def is_root(self) -> Boolean: 107 | return false 108 | 109 | 110 | def node(data: Data, sub_cons: Callable[[RoseTree[Data]], LazyList[RoseTree[Data]]] 111 | ) -> Callable[[RoseTree[Data]], RoseTree[Data]]: 112 | return lambda parent: BiRoseTree(data, parent, sub_cons) 113 | 114 | 115 | def nodes(*s: Tuple[Data, Callable[[RoseTree[Data]], RoseTree[Data]]] 116 | ) -> Callable[[RoseTree[Data]], LazyList[RoseTree[Data]]]: 117 | return lambda parent: LazyList(s).map2(node).map(lambda f: f(parent)) 118 | 119 | 120 | def leaf(data: Data) -> Callable[[RoseTree[Data]], RoseTree[Data]]: 121 | return lambda parent: BiRoseTree(data, parent, lambda a: LazyLists.empty()) 122 | 123 | 124 | def leaves(*data: Data) -> Callable[[RoseTree[Data]], LazyList[RoseTree[Data]]]: 125 | return lambda parent: LazyList(data).map(leaf).map(lambda f: f(parent)) 126 | 127 | 128 | def from_tree( 129 | tree: Node[A, Any], 130 | f: Callable[[Node[A, Any], Maybe[RoseTree[B]]], B], 131 | cons_root: Callable[[B, Callable[[RoseTree[Node[A, Any]]], LazyList[RoseTree[Node[A, Any]]]]], RoseTree[B]], 132 | cons_node: Callable[ 133 | [B, RoseTree[Node[A, Any]], Callable[[RoseTree[Node[A, Any]]], LazyList[RoseTree[Node[A, Any]]]]], 134 | RoseTree[B] 135 | ] 136 | ) -> RoseTree[B]: 137 | def sub(node: Node[A, Any]) -> Callable[[RoseTree[Node[B, Any]]], LazyList[RoseTree[Node[B, Any]]]]: 138 | def cons_sub(parent: RoseTree[Node[B, Any]], a: A) -> RoseTree[B]: 139 | return cons_node(f(a, Just(parent)), parent, sub(a)) 140 | return lambda parent: node.sub_l.map(lambda a: cons_sub(parent, a)) 141 | return cons_root(f(tree, Empty()), sub(tree)) 142 | 143 | 144 | def from_tree_default(tree: Node[A, Any], f: Callable[[Node[A, Any], Maybe[RoseTree[B]]], B]) -> RoseTree[B]: 145 | return from_tree(tree, f, RoseTreeRoot, BiRoseTree) 146 | 147 | __all__ = ('BiRoseTree', 'RoseTree', 'node', 'leaf', 'leaves', 'from_tree') 148 | -------------------------------------------------------------------------------- /amino/eval.py: -------------------------------------------------------------------------------- 1 | import abc 2 | from typing import Generic, TypeVar, Callable, Tuple, Any, Union 3 | 4 | from amino.tc.base import F 5 | from amino.tc.monad import Monad 6 | from amino import List 7 | from amino.func import tailrec 8 | 9 | A = TypeVar('A') 10 | B = TypeVar('B') 11 | 12 | 13 | class Eval(Generic[A], F[A], implicits=True, auto=True): 14 | 15 | @staticmethod 16 | def now(a: A) -> 'Eval[A]': 17 | return Now(a) 18 | 19 | @staticmethod 20 | def later(f: Callable[..., A], *a: Any, **kw: Any) -> 'Eval[A]': 21 | return Later(lambda: f(*a)) 22 | 23 | @staticmethod 24 | def always(f: Callable[..., A], *a: Any, **kw: Any) -> 'Eval[A]': 25 | return Always(lambda: f(*a)) 26 | 27 | @abc.abstractmethod 28 | def _value(self) -> A: 29 | ... 30 | 31 | @property 32 | def value(self) -> A: 33 | return self._value() 34 | 35 | @abc.abstractproperty 36 | def memoize(self) -> 'Eval[A]': 37 | ... 38 | 39 | def evaluate(self) -> A: 40 | return self._value() 41 | 42 | 43 | class Now(Generic[A], Eval[A]): 44 | 45 | def __init__(self, value: A) -> None: 46 | self.strict = value 47 | 48 | def _value(self) -> A: 49 | return self.strict 50 | 51 | @property 52 | def memoize(self) -> Eval[A]: 53 | return self 54 | 55 | def __str__(self) -> str: 56 | return f'Now({self.strict})' 57 | 58 | 59 | class Later(Generic[A], Eval[A]): 60 | 61 | def __init__(self, f: Callable[[], A]) -> None: 62 | self.f = f 63 | self._memo = None # type: Union[A, None] 64 | 65 | def _memoized(self) -> A: 66 | if self._memo is None: 67 | self._memo = self.f() 68 | return self._memo 69 | 70 | def _value(self) -> A: 71 | return self._memoized() 72 | 73 | @property 74 | def memoize(self) -> Eval[A]: 75 | return self 76 | 77 | def __str__(self) -> str: 78 | return f'Later({self.f})' 79 | 80 | 81 | class Always(Generic[A], Eval[A]): 82 | 83 | def __init__(self, f: Callable[[], A]) -> None: 84 | self.f = f 85 | 86 | def _value(self) -> A: 87 | return self.f() 88 | 89 | @property 90 | def memoize(self) -> Eval[A]: 91 | return Later(self.f) 92 | 93 | def __str__(self) -> str: 94 | return f'Always({self.f})' 95 | 96 | 97 | class Call(Generic[A], Eval[A]): 98 | 99 | def __init__(self, thunk: Callable[[], Eval[A]]) -> None: 100 | self.thunk = thunk 101 | 102 | @staticmethod 103 | def _loop(ev: Eval[A]) -> Eval[A]: 104 | def loop1(s: Eval[A]) -> Eval[A]: 105 | return loop(s.run) 106 | @tailrec 107 | def loop(e: Eval[A]) -> Tuple[bool, Tuple[Eval[A]]]: 108 | if isinstance(e, Call): 109 | return True, (e.thunk(),) 110 | elif isinstance(e, Compute): 111 | return False, (Compute(e.start, loop1, e.start_str),) 112 | else: 113 | return False, e 114 | return loop(ev) 115 | 116 | @property 117 | def memoize(self) -> Eval[A]: 118 | return Later(lambda: self._value()) 119 | 120 | def _value(self) -> A: 121 | return Call._loop(self).value 122 | 123 | def __str__(self) -> str: 124 | return f'Call({self.thunk})' 125 | 126 | 127 | class Compute(Generic[A, B], Eval[A]): 128 | 129 | def __init__(self, start: Callable[[], Eval[B]], run: Callable[[B], Eval[A]], start_str: str) -> None: 130 | self.start = start 131 | self.run = run 132 | self.start_str = start_str 133 | 134 | def _value(self) -> A: 135 | C = Callable[[Any], Eval[Any]] 136 | def loop_compute(c: Compute[Any, Any], fs: List[C]) -> Tuple[bool, Union[Tuple[Eval[Any], List[C]], Any]]: 137 | cc = c.start() 138 | return ( 139 | (True, (cc.start(), fs.cons(c.run).cons(cc.run))) 140 | if isinstance(cc, Compute) else 141 | (True, (c.run(cc._value()), fs)) 142 | ) 143 | def loop_other(e: Eval[Any], fs: List[C]) -> Tuple[bool, Union[Tuple[Eval[Any], List[C]], Any]]: 144 | return fs.detach_head.map2(lambda fh, ft: (True, (fh(e._value()), ft))) | (False, e._value()) 145 | @tailrec 146 | def loop(e: Eval[Any], fs: List[C]) -> Tuple[bool, Union[Tuple[Eval[Any], List[C]], Any]]: 147 | return ( 148 | loop_compute(e, fs) 149 | if isinstance(e, Compute) else 150 | loop_other(e, fs) 151 | ) 152 | return loop(self, List()) 153 | 154 | @property 155 | def memoize(self) -> Eval[A]: 156 | return Later(self) 157 | 158 | def __str__(self) -> str: 159 | return f'Compute({self.start_str})' 160 | 161 | 162 | class EvalMonad(Monad, tpe=Eval): 163 | 164 | def pure(self, a: A) -> Eval[A]: 165 | return Now(a) 166 | 167 | def flat_map(self, fa: Eval[A], f: Callable[[A], Eval[B]]) -> Eval[B]: 168 | def f1(s: A) -> Eval[B]: 169 | return Compute(lambda: fa.run(s), f, '') 170 | start, run, start_str = ( 171 | (fa.start, f1, fa.start_str) 172 | if isinstance(fa, Compute) else 173 | (fa.thunk, f, fa.thunk_str) 174 | if isinstance(fa, Call) else 175 | (lambda: fa, f, str(fa)) 176 | ) 177 | return Compute(start, run, start_str) 178 | 179 | 180 | def defer_eval(f: Callable[[], Eval[A]]) -> Eval[A]: 181 | return Call(f) 182 | 183 | __all__ = ('Eval', 'defer_eval') 184 | -------------------------------------------------------------------------------- /amino/tc/foldable.py: -------------------------------------------------------------------------------- 1 | import abc 2 | from typing import TypeVar, Generic, Callable, Type, List, Tuple 3 | import operator 4 | 5 | from lenses import lens, UnboundLens 6 | 7 | from amino.tc.base import TypeClass, tc_prop 8 | from amino.tc.functor import Functor 9 | from amino.func import curried, I, CallByName 10 | from amino.maybe import Maybe, Empty, Just 11 | from amino.boolean import Boolean 12 | from amino import _, Either, Nothing 13 | from amino.tc.monoid import Monoid 14 | from amino.tc.monad import Monad 15 | from amino.tc.apply_n import ApplyN 16 | 17 | G = TypeVar('G') 18 | H = TypeVar('H') 19 | A = TypeVar('A') 20 | B = TypeVar('B') 21 | C = TypeVar('C') 22 | Z = TypeVar('Z') 23 | 24 | 25 | class FoldableABC(Generic[A], abc.ABC): 26 | pass 27 | 28 | 29 | F = FoldableABC 30 | 31 | 32 | class Foldable(TypeClass, ApplyN): 33 | # FIXME lens functions return index lenses, which is not a property of 34 | # Foldable 35 | 36 | def apply_n_funcs(self) -> list: 37 | return ['filter', 'find_map'] 38 | 39 | @abc.abstractmethod 40 | def with_index(self, fa: F[A]) -> F[A]: 41 | ... 42 | 43 | @abc.abstractmethod 44 | def filter(self, fa: F[A], f: Callable[[A], bool]) -> F: 45 | ... 46 | 47 | def filter_not(self, fa: F[A], f: Callable[[A], bool]) -> F: 48 | pred = lambda a: not f(a) 49 | return self.filter(fa, pred) 50 | 51 | def filter_type(self, fa: F[A], tpe: type) -> F: 52 | pred = lambda a: isinstance(a, tpe) 53 | return self.filter(fa, pred) 54 | 55 | @abc.abstractmethod 56 | def find(self, fa: F[A], f: Callable[[A], bool]) -> Maybe[A]: 57 | ... 58 | 59 | @abc.abstractmethod 60 | @curried 61 | def fold_left(self, fa: F[A], z: Z, f: Callable[[Z, A], Z]) -> Z: 62 | ... 63 | 64 | def fold_map(self, fa: F[A], z: B, f: Callable[[A], B], g: Callable[[Z, B], Z]=operator.add) -> Z: 65 | ''' map `f` over the traversable, then fold over the result 66 | using the supplied initial element `z` and operation `g`, 67 | defaulting to addition for the latter. 68 | ''' 69 | mapped = Functor.fatal(type(fa)).map(fa, f) 70 | return self.fold_left(mapped)(z)(g) 71 | 72 | @curried 73 | def fold_m(self, fa: F[A], z: G, f: Callable[[B, A], G]) -> G: 74 | monad = Monad.fatal(type(z)) 75 | def folder(z1: G, a: A) -> G: 76 | return monad.flat_map(z1, lambda b: f(b, a)) 77 | return self.fold_left(fa)(z)(folder) 78 | 79 | def fold(self, fa: F[A], tpe: type) -> A: 80 | mono = Monoid.fatal(tpe) 81 | return self.fold_left(fa)(mono.empty)(mono.combine) 82 | 83 | @abc.abstractmethod 84 | def find_map_optional(self, fa: F[A], tpe: Type[F], f: Callable[[A], F[B]], msg: CallByName=None) -> Maybe[B]: 85 | ... 86 | 87 | def find_map(self, fa: F[A], f: Callable[[A], Maybe[B]], msg: CallByName=None) -> Maybe[B]: 88 | return self.find_map_optional(fa, Maybe, f, msg) 89 | 90 | def find_map_e(self, fa: F[A], f: Callable[[A], Either[C, B]], msg: CallByName=None) -> Either[C, B]: 91 | return self.find_map_optional(fa, Either, f, msg) 92 | 93 | def find_type(self, fa: F[A], tpe: type) -> Maybe[A]: 94 | pred = lambda a: isinstance(a, tpe) 95 | return self.find(fa, pred) 96 | 97 | def find_fold(self, fa: F[A], z: B) -> Callable[[Callable[[B, A], Tuple[B, Maybe[C]]]], Maybe[C]]: 98 | def find_fold(f: Callable[[B, A], Tuple[B, Maybe[C]]]) -> Maybe[C]: 99 | def g(zz: Tuple[B, Maybe[C]], a: A) -> Tuple[B, Maybe[C]]: 100 | z, c = zz 101 | return c.map(lambda c0: (z, Just(c0))).get_or(lambda: f(z, a)) 102 | return self.fold_left(fa)((z, Nothing))(g) 103 | return find_fold 104 | 105 | @abc.abstractmethod 106 | def index_where(self, fa: F[A], f: Callable[[A], bool]) -> Maybe[int]: 107 | ... 108 | 109 | def index_of(self, fa: F[A], a: A) -> Maybe[int]: 110 | return self.index_where(fa, _ == a) 111 | 112 | def exists(self, fa: F[A], f: Callable[[A], bool]) -> Boolean: 113 | return Boolean(self.find(fa, f).is_just) 114 | 115 | def contains(self, fa: F[A], v: A) -> Boolean: 116 | return self.exists(fa, _ == v) 117 | 118 | def lens(self, fa: F[A], f: Callable[[A], bool]) -> Maybe[UnboundLens]: 119 | return self.index_where(fa, f) / (lambda i: lens()[i]) 120 | 121 | def find_lens(self, fa: F[A], f: Callable[[A], Maybe[UnboundLens]]) -> Maybe[UnboundLens]: 122 | check = lambda a: f(a[1]) / (lambda b: (a[0], b)) 123 | index = lambda i, l: lens()[i].add_lens(l) 124 | wi = self.with_index(fa) 125 | return self.find_map(wi, check).map2(index) 126 | 127 | def find_lens_pred(self, fa: F[A], f: Callable[[A], bool]) -> Maybe[UnboundLens]: 128 | g = lambda a: Boolean(f(a)).maybe(lens()) 129 | return self.find_lens(fa, g) 130 | 131 | def _min_max(self, fa: F[A], f: Callable[[A], int], pred: Callable[[int, int], int]) -> Maybe[int]: 132 | def folder(z: Maybe[A], a: A) -> Maybe[int]: 133 | return ( 134 | z.map(lambda b: b if pred(f(b), f(a)) else a) 135 | .or_else(Just(a)) 136 | ) 137 | return self.fold_left(fa, Empty())(folder) 138 | 139 | def max_by(self, fa: F[A], f: Callable[[A], int]) -> Maybe[A]: 140 | return self._min_max(fa, f, operator.gt) 141 | 142 | def min_by(self, fa: F[A], f: Callable[[A], int]) -> Maybe[A]: 143 | return self._min_max(fa, f, operator.lt) 144 | 145 | @tc_prop 146 | def max(self, fa: F[A]) -> Maybe[A]: 147 | return self.max_by(fa, I) 148 | 149 | @tc_prop 150 | def min(self, fa: F[A]) -> Maybe[A]: 151 | return self.min_by(fa, I) 152 | 153 | __all__ = ('Foldable',) 154 | -------------------------------------------------------------------------------- /amino/lazy_list.py: -------------------------------------------------------------------------------- 1 | import itertools 2 | from functools import wraps 3 | from typing import Generic, TypeVar, Callable, Union, Any, Iterable, Generator 4 | 5 | from toolz import concatv 6 | 7 | from amino.data.list import List 8 | from amino.maybe import Just, Empty, Maybe 9 | from amino.tc.base import Implicits 10 | from amino.func import I 11 | from amino.util.string import safe_string 12 | 13 | A = TypeVar('A') 14 | B = TypeVar('B') 15 | 16 | 17 | class LazyList(Generic[A], Implicits, implicits=True): 18 | _default_chunk_size = 20 19 | 20 | def fetch(f: Callable) -> Callable: 21 | @wraps(f) 22 | def wrapper(self: 'LazyList', index: Union[slice, int]) -> Any: 23 | if isinstance(index, int) and index < 0: 24 | self._drain() 25 | else: 26 | self._fetch(index) 27 | return f(self, index) 28 | return wrapper 29 | 30 | def __init__(self, source: Iterable, init: List[A]=List(), chunk_size: Union[int, None]=None) -> None: 31 | self.source = iter(source) 32 | self.strict = init 33 | self._chunk_size = chunk_size or self._default_chunk_size 34 | 35 | @fetch 36 | def __getitem__(self, index: int) -> Maybe[A]: 37 | return self.strict.__getitem__(index) 38 | 39 | def __len__(self) -> int: 40 | return self.length 41 | 42 | @property 43 | def length(self) -> int: 44 | return self.drain.length 45 | 46 | @property 47 | def _one(self) -> Generator: 48 | try: 49 | yield next(self.source) 50 | except StopIteration: 51 | pass 52 | 53 | def _fetch(self, index: Union[slice, int, float]) -> None: 54 | count = index.stop if isinstance(index, slice) else index + 1 55 | def chunk() -> List[A]: 56 | for i in range(self._chunk_size): 57 | yield from self._one 58 | def gen() -> None: 59 | while True: 60 | if self.strict.length < count: 61 | c = list(chunk()) 62 | self.strict = self.strict + c 63 | if len(c) == self._chunk_size: 64 | continue 65 | break 66 | gen() 67 | 68 | @property 69 | def drain(self) -> List[A]: 70 | return self._drain() 71 | 72 | def _drain(self) -> List[A]: 73 | self._fetch(float('inf')) 74 | return self.strict 75 | 76 | def copy(self, wrap_source: Callable, trans_strict: Callable[[List[A]], List[B]]) -> 'LazyList[B]': 77 | a, b = itertools.tee(self.source) 78 | self.source = a 79 | return LazyList(wrap_source(b), trans_strict(self.strict), self._chunk_size) 80 | 81 | @fetch 82 | def lift(self, index: int) -> Maybe[A]: 83 | return self.strict.lift(index) 84 | 85 | @property 86 | def head(self) -> Maybe[A]: 87 | return self.lift(0) 88 | 89 | @property 90 | def last(self) -> Maybe[A]: 91 | return self.lift(-1) 92 | 93 | def _drain_find(self, abort: Callable[[A], bool]) -> Maybe[A]: 94 | culprit = Empty() 95 | def gen() -> Generator: 96 | nonlocal culprit 97 | while True: 98 | try: 99 | el = next(self.source) 100 | yield el 101 | if abort(el): 102 | culprit = Just(el) 103 | break 104 | except StopIteration: 105 | break 106 | drained = List.wrap(list(gen())) 107 | self.strict = self.strict + drained 108 | return culprit 109 | 110 | def foreach(self, f: Callable[[A], None]) -> None: 111 | self.drain.foreach(f) 112 | 113 | @fetch 114 | def min_length(self, index: int) -> bool: 115 | return self.strict.length >= index 116 | 117 | @fetch 118 | def max_length(self, index: int) -> bool: 119 | return self.strict.length <= index 120 | 121 | @property 122 | def empty(self) -> bool: 123 | return self.max_length(0) 124 | 125 | def append(self, other: 'LazyList[A]') -> 'LazyList[A]': 126 | return self.copy(lambda s: concatv(s, self.strict, other.source, other.strict), lambda s: List()) 127 | 128 | __add__ = append 129 | 130 | def __repr__(self) -> str: 131 | strict = (self.strict / safe_string).mk_string(', ') 132 | return '{}({} {!r})'.format(self.__class__.__name__, strict, 133 | self.source) 134 | 135 | def mk_string(self, sep: str='') -> str: 136 | return sep.join((self / str).drain) 137 | 138 | @property 139 | def join_lines(self) -> str: 140 | return self.mk_string('\n') 141 | 142 | @property 143 | def join_comma(self) -> str: 144 | return self.mk_string(',') 145 | 146 | def cons(self, a: A) -> 'LazyList[A]': 147 | return self.copy(I, lambda s: s.cons(a)) 148 | 149 | def cat(self, a: A) -> 'LazyList[A]': 150 | return self.copy(lambda xs: itertools.chain(xs, (a,)), I) 151 | 152 | def collect(self, f: Callable[[A], Maybe[B]]) -> 'LazyList[B]': 153 | return self.copy(lambda s: (a._unsafe_value for a in map(f, s) if a.present), lambda a: a.collect(f)) 154 | 155 | 156 | def lazy_list(f: Callable) -> Callable: 157 | @wraps(f) 158 | def w(*a: Any, **kw: Any) -> LazyList: 159 | return LazyList(f(*a, **kw)) 160 | return w 161 | 162 | 163 | def lazy_list_prop(f: Callable) -> property: 164 | return property(lazy_list(f)) 165 | 166 | 167 | class LazyLists: 168 | 169 | @staticmethod 170 | def cons(*a: A) -> LazyList[A]: 171 | return LazyList(a) 172 | 173 | @staticmethod 174 | def empty() -> LazyList[A]: 175 | return LazyLists.cons() 176 | 177 | @staticmethod 178 | def range(*rng: int) -> LazyList[int]: 179 | return LazyList(range(*rng)) 180 | 181 | __all__ = ('LazyList', 'lazy_list', 'LazyLists') 182 | --------------------------------------------------------------------------------