8451 / labrea

A framework for declarative, functional dataset definitions.
MIT License
11 stars 0 forks source link

Add a `.map` (or `.apply`) method to Evaluatable #6

Closed austinwarner-8451 closed 17 hours ago

austinwarner-8451 commented 1 month ago

Sometimes we just want to perform simple transformations of Evaluatables, and creating a new dataset can be cumbersome. The proposal is to add a .map method that returns a new Evaluatable which is equivalent to evaluating the original Evaluatable then applying the function.

The use of the name .map comes from pure functional languages and Monads, but may be confusing to those unfamiliar with the theory since map is used for iterables in Python. We could alternatively call it .apply, which has a similar but different meaning in most pure functional languages, but would be clearer for those only familiar with Python.

from labrea import Option

mapped = Option('X').map(str.lower)

mapped({'X': 'Hello World!'}) == 'hello world!'
austinwarner-8451 commented 1 month ago

A couple thoughts:

For this reason I'm leaning towards apply for the name. It's more intuitive for Python programmers, and still technically works in the same way that you'd expect if you were coming from a pure functional language that uses Monads for everything

austinwarner-8451 commented 1 month ago

Adding a .bind method as well. Bind takes a function of type A -> Evaluatable[B] and returns a new Evaluatable[B]. This is useful for when the evaluation is dependent on the result of a different dataset/options

austinwarner-8451 commented 1 month ago

Adding a .bind method as well. Bind takes a function of type A -> Evaluatable[B] and returns a new Evaluatable[B]. This is useful for when the evaluation is dependent on the result of a different dataset/options

The motivation for this is two-fold

  1. To complete the "monad interface" a bind method is required.
  2. This will simplify/generalize the implementation of Iterations in #4
austinwarner-8451 commented 1 month ago

Also realizing that Switch object could (and arguable should) be expressed in terms of .bind

The following are equivalent

@dataset
def foo():
    ...

@dataset
def bar():
    ...

Switch(
    Option('X'),
    {
        True: foo,
        False: bar,
    },
    None
)
Option('X').bind(lambda x: {True: foo, False: bar}.get(x, Value(None)))

Not proposing that Switch should be abandoned for this syntax, as the current syntax is much more concise. However, it could be beneficial to recreate Switch like so

MaybeEvaluatable = Union[T, Evaluatable[T]]

def _ensure_evaluatable(x: MaybeEvaluatable[T]) -> Evaluatable[T]:
    return Value(val) if not isinstance(val, Evaluatable) else val

def switch(
    evaluatable: Evaluatable[A],
    lookup: Dict[A, MaybeEvaluatable[B]],
    default: Optional[MaybeEvaluatable[B]]
) -> Evaluatable[B]:
    return evaluatable.bind(lambda x: _ensure_evaluatable(lookup.get(x, default)))

If we add switch as a function like this we could deprecate Switch the class

austinwarner-8451 commented 1 month ago

This would also allow us to add a more general case function as well

def case(
    evaluatable: Evaluatable[A],
    *cases: Tuple[Callable[[A], bool], MaybeEvaluatable[B]]
) -> Evaluatable[B]:
    def _case(a: A) -> Evaluatable[B]:
        for predicate, target in cases:
            if predicate(a):
                return _ensure_evaluatable(target)

    return evaluatable.bind(_case)
austinwarner-8451 commented 1 month ago

Wait I'm an idiot we can't add new methods to Evaluatable because it is a breaking change.

We can add new Apply and Bind types that do this, it just means we cant chain everything together like before.

austinwarner-8451 commented 17 hours ago

This is coming in Labrea 2.0