Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Adding implementation for the fn.monad.Option #21

Open
grayfall opened this issue Oct 27, 2016 · 18 comments
Open

Adding implementation for the fn.monad.Option #21

grayfall opened this issue Oct 27, 2016 · 18 comments
Milestone

Comments

@grayfall
Copy link

I've been using fn for quite some time, so I'm glad you guys are trying to keep it going. Are you planning on implementing that monad? Do you need any contributions?

@grayfall grayfall changed the title Adding implementation for the fn.monad.Optional Adding implementation for the fn.monad.Option Oct 27, 2016
@jacobbridges
Copy link

Hi @grayfall, thanks for reporting the issue. That's what we are here for -- to keep fn.py maintained. There are no current plans for building out fn.monad.Option, but then again I haven't put together a good road map yet.

If you would like to work on this, please feel free to fork and make a PR. If you are not comfortable adding the code, I can work on it or @low-ghost would probably enjoy working on it. He likes all things monad. :)

For now I will add this to our v1.0 milestone, since it doesn't make sense to release a "version 1" with NotImplementedErrors being thrown.

@jacobbridges jacobbridges added this to the v1.0.0 milestone Oct 28, 2016
@ExpHP
Copy link

ExpHP commented Feb 10, 2017

fn.monad.Option IS implemented. It is an abstract base class.

See for yourself; here's an example from the docs, slightly modified to produce testable output

from operator import methodcaller
from fn.monad import optionable

class Request(dict):
    @optionable
    def parameter(self, name):
        return self.get(name, None)

def get_uppercased(r, name):
    return (r.parameter(name)
             .map(methodcaller("strip"))
             .filter(len)
             .map(methodcaller("upper"))
             .get_or("-"))


r = Request(testing="Fixed", empty="   ")

print(get_uppercased(r, "not-present"))  # => -
print(get_uppercased(r, "testing"))      # => FIXED

That said; I don't think the offerings of fn's Option are very impressive, and quickly dropped it after only toying around with it for a short while.

Some actual problems with Option

No interop with exceptions

When I think of Option types in other languages, the I think of is the mantra, "it is better to beg for forgiveness than to ask for permission." But the Python community already understands this, and the language already has its own way of eliminating problematic if statements:

try:
    x = int(s)
except ValueError:
    # do something else

In order for Option to really be useful, it needs some sort of way to wrap these existing functions that throw exceptions. Maybe something like a decorator @option_catch(ValueError), though I'm still not certain if even that is enough to make it useful.

It's not much of a monad!

I hate to get pedantic, but I think it is generally well-agreed upon that a monad is something which provides at least either one of these two things: (either can be used to implement the other; Option provides neither)

  • A "flattening" function of type m[m[A]] -> m[A], often called join.
  • A "chaining" function of type (m[A], (A -> m[B])) -> m[B]. This is the bread and butter of monads; in the case of Option, it lets you chain together multiple operations that can fail.

The chaining function goes by many names in other languages; in Haskell it is some funny operator pronounced "bind"; Scala calls it flatMap; I've seen JS libraries call it chain. Yet another possible name (though it doesn't work for other monads) is and_call, to emphasize its relationship to Option.or_call. (Rust does this)

@grayfall
Copy link
Author

grayfall commented Feb 10, 2017

@ExpHP I've already figured out that Option is merely an abstract class and that the real work is done by its subclasses. Anyway, just like you I've found little practical comfort in using this Maybe-like functionality. First of all, as you've already mentioned, the optionable decorator does nothing to wrap code that can raise an exception, instead a user must manually wrap all function calls in a try/except block and return None for the decorator to work properly. Secondly, from my understanding Empty is supposed to be a unit value (which can be implemented as a singleton), but it isn't. Finally, unlike the Maybe monad, Option does little to allow subsequent chaining. And I don't like the method-based syntax.

I've played around it for a while and came up with my own purely experimental stuff, which mostly addresses the first and the second issues https://github.com/grayfall/evaluate/blob/master/evaluate/

Here are the important parts

from collections import Callable, Sequence
from functools import reduce, wraps

from ..bottom import Empty


# Note that there can only be one `Empty`, because the constructor `EmptyType()` always returns the same instance. 


class Optional:
    def __init__(self, value=Empty):
        self._value = value

    @property
    def value(self):
        return Empty if self.isempty else self._value

    @property
    def isempty(self):
        return isinstance(self._value, BaseException) or self._value is Empty

    def __bool__(self):
        return not self.isempty


def optionable(*exception_types):
    def build_wrapper(func):
        @wraps(func)
        def wrapper(*args, **kwargs):
            try:
                retval = Optional(func(*args, **kwargs))
                return (retval.value if isinstance(retval.value, Optional)
                        else retval)
            except exception_types as e:
                return Optional(e)
        wrapper.__isoptional__ = True
        return wrapper
    return build_wrapper

So a user can simply decide which exceptions to catch, which is similar to your proposal. Then I improvised on the monadic functionality

class Evaluation:
    """
    >>> from functools import partial
    >>> @optional(ArithmeticError)
    ... def rdiv(a, b):
    ...     return b // a
    >>> (Evaluation() | (rdiv, 0) |  partial(rdiv, 1))(1)
    1
    >>> (Evaluation() | (rdiv, 0) | (rdiv, 1))(1)
    1
    >>> (Evaluation() | rdiv | rdiv)(0, 1) is Empty
    True
    """
    def __init__(self, steps=None):
        self._steps = tuple(steps) if steps is not None else ()

    def _add_step(self, step):
        fn, *step_args = step if isinstance(step, Sequence) else (step, )
        return type(self)(steps=self._steps + ((fn, step_args), ))

    def __or__(self, step) -> "Evaluation":
        return self._add_step(step)

    def _evaluate(self, *args) -> Optional:
        def caller(carried: Optional, step):
            fn, step_args = step
            return fn(*(*step_args, *args)) if carried.isempty else carried
        return reduce(caller, self._steps, Optional())

    def __call__(self, *args):
        return self._evaluate(*args).value

This was supposed to replace the Option class. Basically, the idea was that you could describe your programme in terms of evaluation, i.e. several alternative ways to end up with a nonempty value (see the dummy examples in the doctest string). An evaluation can be either full or empty, but its type signature should be well defined, so that one could chain further functions using another operator (e.g. >> like in fn.F).

@ExpHP
Copy link

ExpHP commented Feb 10, 2017

@grayfall looks like we're definitely on the same page! That is a neat approach (also, I wasn't aware of F's sugar for partial application)

| is a really good choice of operator since its behavior is like a short-circuited or. In fact, it creates an excellent opportunity to use & as the monadic bind operator (because its behavior is a short-circuited and):

# suppose 'item' is an optionable version of x[i].
# this gets x[0]['a'] or x[1]['b'], with preference to the former
let f = (Evaluation()
    | (item, 0) & (item, 'a')
    | (item, 1) & (item, 'b')
)
f([]) # Empty
f([{}]) # Empty
f([{'a': 3}]) # Filled(3)
f([{}, {'b':2}]) # Filled(2)
f([{'a':3}, {'b':2}]) # Filled(3)

@grayfall
Copy link
Author

grayfall commented Feb 11, 2017

@ExpHP I failed to decide, whether Evaluation should decorate unoptionable functions to make them optionable automatically or should that be something a user will have to do manually? Automatic decoration limits control over the exception types to be suppressed.

@grayfall
Copy link
Author

grayfall commented Feb 11, 2017

@ExpHP using & to bind is a neat idea. I believe we would have to allow nested chains, too.

@ExpHP
Copy link

ExpHP commented Feb 11, 2017

I believe the expressed exceptions should always be explicit, which would mean that optionable wrappers must be constructed by the user ahead of time. It would be unfortunate for automatic annotations to hide obvious bugs (such as most NameErrors or AttributeErrors), and also, exceptions may be used for many other things, such as the end of iteration.

But not having direct experience using it, I'm not sure how much boilerplate this would actually entail.


Edit: To some extent the wrappers can also be constructed on demand by using optionable on functions inside the evalutation (since decorators are just functions), though admittedly I just tried constructing an example and found that it wasn't entirely straightforward to write one, nor is it very readable

Evaluation() | (optionable(ValueError)(f), 3)

(though maybe optionable could also support the tuple-for-partial shorthand):

Evaluation() | optionable(ValueError)(f, 3)

This manner of using adapters reminds me of Railway oriented programming | F# for fun and profit, which uses simple function composition together with adapter functions to construct pipelines (as opposed to in Haskell where I find myself using a variety of different types of composition or application; fmap vs $ vs >>= etc.).

@grayfall
Copy link
Author

@ExpHP Okay, then we've got some serious technical issues to address. If we are going to use the ampersand operator to express the bind operation, we would have to create a class to wrap all nodes in the evaluation to overcome the inability to overload operator precedence in Python. In other words, we would have to end up with a symbolic representation of the entire expression before building the expression object. Alternatively, we can use a lower-precedence operator for the bind operation (e.g. >=), but that has its limitations.

@ExpHP
Copy link

ExpHP commented Feb 13, 2017 via email

@grayfall
Copy link
Author

@ExpHP then we need to develop a wrapper, otherwise your expression

f = (Evaluation()
    | (item, 0) & (item, 'a')
    | (item, 1) & (item, 'b')
)

will raise a TypeError.

@ExpHP
Copy link

ExpHP commented Feb 13, 2017

...oh. Okay, yes, I see what you mean now. That is a serious issue. :/

(have a simpler idea, will prototype something)

@grayfall
Copy link
Author

grayfall commented Feb 14, 2017

@ExpHP My idea is to rename Evaluation into E and make it F-like. Something like this

class E:
    def __init__(self, f: Callable=identity, *args, **kwargs):
        self.steps = [...]

    def __or__(self, other: "E") -> "E":
        """
        Add an `|` (or) node to the evaluation
        """
        pass

    def __and__(self, other: "E") -> "E":
        """
        Add an `&` (and) node to the evaluation (i.e. bind)
        """
        pass

    def __str__(self):
        """
        It will return something like:
        E(f0, *args, **kwargs) | E(f1, *args, **kwargs) & E(f2, *args, **kwargs)
                               | E(f3, *args, **kwargs) & E(f4, *args, **kwargs)
        """
        pass
    
    def __call__(self, ...):
        """
        Evaluate the entire expression. 
        """

@grayfall
Copy link
Author

grayfall commented Feb 14, 2017

@ExpHP Since the & operator binds before the | operator, those two branches will be created first and will be added to the root from left to right. Such a scheme allows to write and evaluate deeply nested expressions without any problem, e.g.

E(f0, ...) | (E(f1, ...) & E(f2, ...) | E(f5, ...)
                                      | E(f6, ...))
           | E(f3, ...) & E(f4, ...)

@grayfall
Copy link
Author

@jacobbridges Haven't heard from you for quite a while. Are you still planning to maintain the package?

@jacobbridges
Copy link

@grayfall yes, I still keep track of the issues and what you guys are talking about. My plans are to reorganize the README and make it clear this is a fork; reorganize the tests into separate files which align with the modules; and finally start working towards our version 1 milestone.

I've been busy with life stuff like moving and looking for a new job, so I apologize for not adding to these discussions. But I'll be here to review the pull requests when needed.

@matanagasauce
Copy link

Hi @grayfall @ExpHP , are you still working on the issue?
Personally I like @grayfall 's last comment to make Evaluation F-like, and it would be better to have _ for a placeholder like E(f, _, 1) or E(f)(_, 1).
Anyway it seems there have been many discussions and maybe we can give an implementation first to get more feedbacks from the community. What do you think about it?

@ExpHP
Copy link

ExpHP commented May 1, 2018

In the time since this discussion took place I have not used fn.py or even python very much at all. I no longer have any strong feelings about this.

@grayfall
Copy link
Author

grayfall commented May 2, 2018

@yangyangxcf ok, I'll make a prototype.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

4 participants