from pynads import Writer
from pynads.funcs import multibind
from itertools import repeat
from functools import partial
pairs = ((5, 'fizz'), (3, 'buzz'))
def fizzer(n, pairs):
fizzed = ''.join([buzz for fizz, buzz in pairs if not n % fizz]) or n
return Writer(n+1, {n:fizzed})
res = multibind(Writer(1, {}), *repeat(partial(fizzer, pairs=pairs), 15))
print(res)
Truly an 11x Developer solution to the world's universe's premier code interview question!
What is "pynads"¶
All joking aside, I've been hacking together a collection of Haskell-esque tools for Python over the last few weeks. It started as a "Well, I'll learn Haskell better this way." and has become...well, honestly a tool for me learning Haskell still.
Check it out, I think it's nifty.
A Quick Tour of Pynads¶
pynads strives to be a pythonic form of Haskell. Which is buzzword for I did some stuff. There's:
- Functors
- Applicatives
- Monads
- Monoids
- Helpers
All of the base classes are implemented as Abstract Base Classes which makes inheritance easy. Well, that's a lie, the root object of pynads is a concrete class that just serves as an endpoint for __new__
and __init__
.
Container¶
pynads.Container
is the root object of every pynads
class. It serves as a final endpoint for __new__
and __init__
as well as providing a consistent name for accessing the values held by objects in pynads
. Some would say that it's a silly idea, but it works! Every class in pynads
is also slotted for memory reasons since it's built around the idea of not manipulating a container but creating a new one.
The only important thing to know about Container is that it defines v
as a property which actually delagates to the _get_val
method. Meaning that's all that needs to be overriden to get multiple values out of a container.
For most subclasses of Container, the provided __init__
is fine, but it's a-ok to override it as well as the only setup that happens is setting a single attribute _v
.
from pynads import Container
class Person(Container):
__slots__ = ('name', 'age')
def __init__(self, name, age):
self.name = name
self.age = age
def _get_val(self):
return {'name': self.name, 'age': self.age}
def __repr__(self):
return "Person(name={!s}, age={!s})".format(self.name, self.age)
print(Person(name="Alec", age=26).v)
Functors¶
Functors are data types that can be mapped over with the fmap
method. But calling methods isn't very Haskell like, so there's an operator that does the same thing: %
.
from pynads import Functor
class FmapPerson(Person, Functor):
__slots__ = ()
def fmap(self, f):
return self.__class__(f(self.name), self.age)
print(str.upper % FmapPerson(name="Alec", age=26))
Of course, you could just use FmapPerson.fmap
but that's not very Haskellic. pynads
also exports a funcs
name space that contains functional shortcuts to some of these (though f % functor
doesn't get much shorter). In this case, there's also:
from pynads import funcs
print(funcs.fmap(str.upper, FmapPerson(name="Alec", age=26)))
Every class in the pynads.concrete
namespace is a functor except for Mempty
(we'll get there!).
Applicatives¶
Applicative Functors are functors that hold functions and can then be applied to other Applicatives/Functors. Every class (except for Mempty
) in pynads.concrete
is also an Applicative!
from pynads import Just
print(Just(lambda x: x+2) * Just(2))
Yeah, like Haskell there's an infix operator. Except of <*>
I just dropped the angle brackets because Python doesn't let you define custom operators (easily). It also combines nicely with %
because they have the same precedence level!
print((lambda x: lambda y: x+y) % Just(4) * Just(6))
BOOM! Mind blown! Whaaaaaat. Applicative uses the abstract method apply
to determine how *
operates. Just inherit from Applicative, define fmap
and apply
and boom, you're on your way. Well, you also need the unit
method -- which is a class method for all pynad
types, but that's not a requirement -- which knows how to put a value in a minimal context.
But wait, what if you have a curried function and you stuffed it into a Just and now you don't want to write out just_f * just_v1 * just_v2 ...
. Sure, you could think "Well, what if I used reduce(operator.mul, *justs)
" But I thought of that already.
add_three_together = lambda x: lambda y: lambda z: x+y+z
print(funcs.multiapply(Just(add_three_together), *[Just(x) for x in range(1,4)]))
If you're mind isn't blown yet, it's because I haven't revealed...
MOOOOOOOOONAAAAAAAADS!!!!!¶
Monads get a bad rap because they get all sorts of overblown explainations. You want to know a what a monad is? It's another way to compute things. It's a glorified container with a special method. You have a value in a monad, a function that takes a regular value and returns a monad and you bind them together. That's it. Literally all there is to it.
from pynads import Nothing
inc_if_odd_else_nothing = lambda x: Just(x+1) if not x&1 else Nothing
print(Just(2) >> inc_if_odd_else_nothing)
The Maybe monad (which consists of the Just and Nothing data types) is basically a glorified if expression. That's it! The bind operation will detect if you have a failure in your computation and short circuit it. It's essentially an abstraction over this:
def safe_func(x):
if x is None:
return None
else:
return x+1
print(safe_func(1), safe_func(None))
Notice how that didn't cause a nasty AttributeError, because None doesn't have attributes? That's all Maybe lets you do (this behavior is mimicked in its fmap and apply: fmap(f, Nothing)
and apply(ap_f, Nothing)
both return you a Nothing). Nothing is extraspecialsauce because it's a singleton. It's basically a monadic None. Actually, it is a monadic None because it represents...well, Nothing.
If you've got more binds than editor columns, then there's something for you as well!
print(multibind(Just(1), *repeat(lambda x: Just(x+1), 5)))
Before you start thinking, "Well, monads are just glorified if expressions" becuase that's missing the point, Monads are the abstraction abstraction. They represent a way to compute something by abstracting away how it's computated.
There's the Writer monad above which is a value in a monadic context but it also keeps some extra side-output as well. Instead of us trying to track this extra information ourselves, Writer says, "Hey, I'll handle it for you!" It just wants a function that accepts a value and returns a Writer. But here's the really cool thing, I didn't have to use a dictionary. It could have a list, or a string or an integer, or a custom class! I hear, "But how!" Through the power of...
Monoids!¶
So monoids are pretty cool. They're a set of something, a "zero" and a binary operator that's transative. Lots of things form monoids. Numbers are monoids! There's two ways to make numbers monoids: with 0
and +
, with 1
and *
. However, pynads
is lazy and only defines the first...sorry, hard choices were made.
Wait, "a zero value" but 1 != 0
. That's missing the point, a zero value is a value that doesn't change the input when combined with the binary operator. x * 1 == x
.
But Python's "primitive" types all form monads!
list
is a monoid. That's the zero value right there and it's binary operator would belist.extend
if it was actually a binop.dict
is a monoid with{}
anddict.update
bool
is a monoid withFalse
and|
\or
set
andfrozenset
are also monoids with their empty instances and|
str
is a monoid with''
and+
(boooo using+
to combine strings! but whatever)
Tuple, Complex and Float are also monoids in exactly the ways you expect. There's a catch though: When combining tuples, you actually get a list back. I'll probably rectify this at a later point, but it's just something to live with right now.
pynads
also defines two three monoids of its own: List
(the monadic form of tuple
), Map
(the applicative form of dict
) and Mempty
(which we're getting to!).
Making your own monoid is easy, and you've probably done it before (just not in this fashion). Just inherit from pynads.Monoid
create a mempty
attribute (it won't let you without it through __new__
hackery) and the mappend
method for combining two instances of your monoid. Let's assume we want a real tuple Monoid. We'd do it like this:
from pynads import Monoid
# also inherits from Container
# so we get all the Container goodness for free
class MTuple(Monoid):
mempty = ()
def __init__(self, *vs):
super(MTuple, self).__init__(vs)
def mappend(self, other):
# type checking optional
if not isinstance(other, MTuple):
raise TypeError("Can only mappend MTuple with MTuple")
return MTuple(*(self.v + other.v))
def __repr__(self):
return "MTuple{!r}".format(self.v)
print(MTuple(4,5) + MTuple(6,7))
pynads.Monoid
overrides +
to be a shortcut to mappend
. That's all well and good, but why other than have a unified way of combining values?! Because we get a way to reduce a list of monoids into a single value for free!
print(MTuple.mconcat(MTuple(1), MTuple(2), MTuple(3)))
Monoid.mconcat
actually delegates to the mappend
method and essentially looks like reduce(cls.mappend, monoids)
. That's it. That's all there is. But you can define your own mconcat
to get performace bonuses if you need to.
from itertools import chain
class CMTuple(MTuple):
def __iter__(self):
return iter(self.v)
@classmethod
def mconcat(cls, *MTuples):
return CMTuple(*chain.from_iterable(MTuples))
print(CMTuple.mconcat(CMTuple(1), CMTuple(2), CMTuple(3)))
pynads.List
and pynads.Map
take advantage of this to create only one intermediate object rather than a bunch. pynads
will also let you treat the builtin types as monoids as well through pynads.funcs.monoid
namespace which has four functions we're interested in: mempty
, mappend
, mconcat
and is_monoid
. mempty
returns the "zero" value for a type, mappend
knows how to combine types, mconcat
knows how to combine an iter of types into a single one and is_monoid
knows if something is monoidal or not (generally, it doesn't declare decimal.Decimal
to be a monoid but this is because I didn't want to add a special case -- special cases beget special cases).
This is done through introspection of types and abstract base classes (to make the type introspection more acceptable less painful).
from pynads.funcs import monoid
print(monoid.mempty(list()))
print(monoid.mappend({'a':10}, {'b': 7}))
print(monoid.mconcat("hello", " ", "world"))
print(monoid.is_monoid(set()))
The monoid namespace is just a nice interface to the nasty plumbing that lives in pynads.utils.monoidal
. It's pretty gross and actually probably pretty fragile, but it works! IT WORKS!!!
Mempty¶
So here's the thing that lets Writer
do its little trick by accept any monoid as a potential log. I can't know ahead of time what you're going to use to keep track of stuff with Writer
-- I've drank plenty of spice water, but I've yet to develop prescient abilities. And rather than making a bunch of subclasses specialized to handle a dictionary and a list and a string and a blah blah blah and forcing you to make your own for WriterLogWithMyFirstMonoid
I decided to create a mempty monoid -- Mempty
. It's not an original idea. Really, it's kind of a dumb object.
It's a singleton, so that's a strike against it (two singletons in my library, my god!). It doesn't actually do anything. It just sits around, taking up space until a real monoid comes along and scares it off. It's mempty value is actually itself! It's mappend just returns whatever its mappended with. And it's mconcat filters out any Mempty values before trying to mconcat the remaining values (you get a Mempty if mconcat an iter of Mempties). There's even an __iter__
method that yields from an empty tuple! What's going on!
In Haskell, mempty
can be used a place holder and Haskell knows to do the right thing already. However, we have to teach Python how to use a placeholder and silently step it out of the way when a real value comes along. I suspect that this is similar, maybe, to how Haskell handles it, but I've not dug at all.
from pynads import Mempty
print(monoid.mempty(Mempty))
print(Mempty + 4)
print(monoid.mconcat(Mempty, {4}, {5}, Mempty))
So Writer's little trick of allowing any Monoid be the log is really just, "I have this dumb thing that pretends to be a Monoid as a default log."
Helpers¶
I won't explore this section indepth because pynads.funcs
has a whole host of helpers, with more being added as needed. There's helpers for making constants, composing functions, an identity function (as in lambda x: x
not id(x)
), turning values into Monads. There's also pynads.utils
if you need to plumb some of the internal workings like compatibility (did I mention all the tests pass 3.4 and 2.7?!) or how monoids are determined on things that aren't explicitly monoids.
That's it!¶
Well, not really. There's about five other monads implemented that I didn't cover (like Reader, which was a pain but also a lot of fun to implement). There's more coming as well. Not just monads, but other things. A proof of concept for do notation via abusing coroutines exists in the repo (lifted from a blog called Valued Lessons which served as partial inspiration for this project). And maybe Monad Transformers if I work the gumption up for it (probably yes). And who knows what else.
No comments:
Post a Comment