Awesome
About
This library provides some functional data structures, utilities and a typeclass system that resemble those concepts in scala. Some of the implementations have considerable overhead and aren't suitable for performance critical applications; their purpose is nicer and familiar syntax and composability.
Anonymous functions
These are an alternative to lambda
s with parameter placeholder wildcards.
If the env var AMINO_ANON_DEBUG
is set, a different implementation with severe performance penalties but literal
string representation is used.
from amino import L, _, __
f = L(isinstance)(_, str)
f('amino')
# True
# for methods
f = __.index('n')
f('amino')
# 3
# for attributes
f = _.parent
f(Path('/home/user'))
# Path('/home')
Data
List
A wrapper for stdlib list
with extended interface.
from amino import List, _
l = List(1, 2, 3, 4)
l.map(_ + 5)
# List(6, 7, 8, 9)
l = List(List(1, 2), List(3, 4))
l.flat_map(__.map(_ - 1))
# List(0, 1, 2, 3)
Maybe
An optional ADT with subtypes Just
and Empty
.
j = Just(1)
e = Empty()
j.map(List)
# Just(List(1))
e.map(_ + 2)
# Empty()
j.flat_map(lambda a: Just(a - 2))
# Just(-1)
Either
A simple coproduct type that can be inhabited by two types.
r = Right(5)
l = Left('error')
r.map(_ + 1)
# Right(6)
l.map(_ + 1)
# Left('error')
l.lmap(_ + ' in test')
# Left('error in test')
r.flat_map(lambda a: Left(a + 3))
# Left(8)
do notation
The function decorator do
allows to use generators as do-blocks with any class that responds to flat_map
.
It is implemented by looping until the generator is exhausted, calling flat_map
on each yielded effect and sending
its value into the generator.
from amino import do
@do(Either[int, int])
def compute() -> Do:
user = yield Right('root')
content = yield IO.delay(Path('/etc/passwd').read_text).attempt
yield Lists.lines(content).index_where(lambda l: user in l).to_either('not found')
All yielded values produce an Either, the return value is the found index or the error message from the last statement
or the IO
call.
return a
behaves similar to Haskell, being equivalent to yield Monad[F].pure(a)
.
Typeclasses
Although these make a lot more sense with a real type system, they provide a nice abstraction for functionality.
The map
and flat_map
operations, for instance, are implemented in the typeclasses Functor
and FlatMap
, for which
instances are provided for arbitrary types, among them List
and Maybe
.
The typeclasses define methods that are looked up in the data class' __getattr__
, which is provided by the Implicits
base class inherited by List
and Maybe
.
The typeclass instances are stored in a global registry, which can be used separately from the Implicits
concept:
from amino.tc.monad import Monad
Monad.lookup(List).flat_map(List(1, 2), lambda a: List(a + 5))
# List(6, 7)
Instances can be registered in several ways, the easiest of which is by passing the target type to the metaclass:
from typing import TypeVar, Callable
from amino.tc.functor import Functor
A = TypeVar('A')
B = TypeVar('B')
class ListFunctor(Functor, tpe=List):
def map(l: List[A], f: Callable[[A], B]) -> List[B]:
return List.wrap(map(f, l))
After importing the instance's module, the map
method can be used as shown
above.
IO
IO
is a trampolined algebra for computation abstraction that catches errors:
t = IO.pure(Path('/var/log/dmesg')).flat_map(L(IO.delay)(__.read_text()))
# IO(Pure(/var/log/dmesg).flat_map(L(delay)(__.read_text())))
t.attempt
# Right('...'): Either[IOException, str]
# or
# Left(IOException('Pure(/var/log/dmesg).flat_map(L(delay)(__.read_text()))', [], PermissionError(13, 'Permission denied')))
StateT
StateT
abstracts F[S => F[S, A]]
, implemented for several effects as EitherState
etc.
It offers the usual monadic constructors:
from amino.state import StateT, EitherState
@do
def state(x: int) -> typing.Generator[StateT[Either, str, int], Any, None]:
i = yield EitherState.inspect_f(lambda s: Try(int, s))
yield EitherState.modify(lambda a: len(a) * x + i)
yield EitherState.pure(x)
s = state(5)
s.run('15') # -> Right((25, 5))
Data
The base class Dat
makes working with pure data types simpler by analyzing a class' __init__
and creating class attributes containing info about its fields.
This allows for simple copying (optionally with type check) without intrusive descriptor magic – a Dat
instance is
still a plain old python object.
class Data(Dat['Data'])
def __init__(a: int, b: List[str]) -> None:
self.a = a
self.b = b
Data(7, List('one', 'two')).copy(a=3)
Json
amino provides a framework for Json de/encoding based on typeclasses.
The functions encode_json
, dump_json
and decode_json
look for instances of the typeclasses Encoder
and Decoder
to
process data.
Codecs for some builtins like Path
and UUID
as well as *amino's standard types like Either
and Maybe
are provided.
The most convenient aspect of this is that Dat
will automatically call the corresponding De/Encoder for all its
fields, making serialization of nested data structures trivial.