justanr / pynads

Dumb implementation of monads in Python.
MIT License
15 stars 3 forks source link

Implement monoidal helpers as classes implementing `pynads.abc.Monoid` or as a functions? #7

Closed justanr closed 9 years ago

justanr commented 9 years ago

The current implementation of First, Last, Sum, Product and Endo (here) strikes me partially as a clean implementation of these and partially as "what the hell are these things that are a bag of staticmethods?"

For example, First can be completely replaced by a function:

def first(*maybes):
     # Nothing or a provided default instead of None?
    return next(filter(bool, maybes), None)

And since Last can be defined in terms of First:

def last(*maybes):
    return first(*reversed(maybes))

However, Haskell actually implements these as full wrappers around Maybe:

newtype First a = First {getFirst :: Maybe a} deriving (Eq, Ord, Read, Show)

instance Monoid (First a) where
    mempty = Nothing
    r@(First (Just x)) `mappend` _ = r
    First Nothing `mappend` r = r

Similarly:

newtype Last a = Last {getLast :: Maybe a} deriving (Eq, Ord, Read, Show)

instance Monoid (Last a) where
    mempty = Nothing
    _ `mappend` r@(Last (Just x)) = r
    r `mappend` Last Nothing = r

To pull the Maybe out of First or Last, the selector function is used:

getFirstJust :: [Maybe a] -> Maybe a
getFirstJust maybes = getFirst . mconcat $ map First maybes
getFirstJust [Nothing, Just 2, Nothing, Just 4]
Just 2

getLastJust can similarly defined.

Implementing this with Pynads is pretty much free as Monoid inherits from Container, which implements Container.v for us, so it can be done like this:

from pynads.utils.internal import Instance
class First(Monoid):
    mempty = Instance(Nothing)

    def __bool__(self):
        return bool(self.v)

    def mappend(self, other):
        # ugly, but whatever, parens help some
        return self if self else (other if other else Nothing)

    @classmethod
    def mconcat(cls, *maybes):
        return next(filter(bool, *maybes), cls.mempty)

#Again, define Last in terms of First
class Last(First):
    mempty = Instance(Nothing)

    def mappend(self, other):
        return First.mappend(other, self)

    @classmethod
    def mconcat(cls, *maybes):
        return First.mconcat(*reversed(maybes))

Use then changes to this:

# use Maybe's checker field to create a distribution of Justs and Nothings
maybes = [Maybe(x, lambda x: not x%3) for x in range(1,7)]
firsts = [First(m) for m in maybes]  # First Just 3
lasts = [Last(m) for m in maybes]  # Last Just 6

x = First.mconcat(*firsts).v
y = Last.mconcat(*lasts).v

Which is more inline with Haskell's implementation. Similarly, Endo, Sum and Product can be redefined to follow this.

justanr commented 9 years ago

Hm. Perhaps implementing this is something better left to end users. The framework is already in place: inheriting from pynads.Monoid and the pynads.funcs.monoids module. Rather than attempting to cover this base, I'm simply closing the issue as it's non-critical. Perhaps I'll revisit it in the future.