Function pattern/predicate matching in Python
Asked Answered
M

4

6

I'd like to be able to dispatch different implementations of a function, based not only on the type of the first parameter, but based on arbitrary predicates. Currently I have to do it like so:

def f(param):
    try:
        if param > 0:
            # do something
    except TypeError:
        pass
    try:
        if all(isinstance(item, str) for item in param):
            # do something else
    except TypeError:
        raise TypeError('Illegal input.')

Here's something in the spirit of what I'd like to be able to do:

@generic
def f(param):
    raise TypeError('Illegal input.')  # default

@f.when(lambda param: param > 0)
def f_when_param_positive(param):
    # do something

@f.when(lambda param: all(isinstance(item, str) for item in param))
def f_when_param_iterable_of_strings(param):
    # do something else

It is similar to Python 3's singledispatch, however singledispatch only supports dispatch on types, not on arbitrary predicates.

TL;DR: Is there a library that allows a predicate-based dispatch of a function based on arbitrary predicates (not only the parameter's type)?

Multipara answered 22/12, 2015 at 22:39 Comment(0)
M
2

Thanks to the repliers. After asking this question it seemed there was no existing module that does exactly this. So I wrote my own :) It is inspired by @Elazar's suggestion.

Please feel free to check it out. It's on PyPI and you can install it using:

pip install genericfuncs

It's also hosted on GitHub and I plan to continue development and add features while trying to keep the API simple. Contributions are welcome.

Multipara answered 18/1, 2016 at 21:47 Comment(0)
S
3

Here's a solution adjusted from @Yam's to fit your syntax, and to be used as a library. The decision (which is a common one) is that the first predicate wins:

class guarded:
    def __init__(self, default):
        self.funcs = []
        self.default = default

    def when(self, pred):
        def add(func):
            self.funcs.append( (pred, func) )
            return func
        return add

    def __call__(self, *args, **kwargs):
        for pred, func in self.funcs:
            try:  
                match = pred(*args, **kwargs)
            except Exception:
                match = False
            if match:
                return func(*args, **kwargs)
        return self.default(*args, **kwargs)

User code:

@guarded
def f(param):
    raise TypeError('Illegal input')

@f.when(lambda param: param > 0)
def f_when_param_positive(param):
    return 'param_positive'

@f.when(lambda param: all(isinstance(item, str) for item in param))
def f_when_param_iterable_of_strings(param):
    return 'param_iterable_of_strings'

Trying it, we get something like:

>>> print(f(123))
param_positive
>>> print(f(['a', 'b']))
param_iterable_of_strings
>>> print(f(-123))
Traceback (most recent call last):
...
TypeError: Illegal input
Stretcher answered 23/12, 2015 at 11:20 Comment(1)
Oooh. How a demonstrably-accurate, complete answer gets downvoted? I wonder.Stretcher
M
2

Thanks to the repliers. After asking this question it seemed there was no existing module that does exactly this. So I wrote my own :) It is inspired by @Elazar's suggestion.

Please feel free to check it out. It's on PyPI and you can install it using:

pip install genericfuncs

It's also hosted on GitHub and I plan to continue development and add features while trying to keep the API simple. Contributions are welcome.

Multipara answered 18/1, 2016 at 21:47 Comment(0)
M
1

I don't know of a library, but here's a basic implementation skeleton. The real problem which prevents this from being a practical solution, in my opinion, is that I have no idea how to make specialized resolution work here1. When that's the case, it will probably lead to a lot of maintenance hardships.

#!/usr/bin/python3  

class when(object):
  funcs = {}

  def __init__(self, pred):
    self.pred = pred

  def __call__(self, func):
    if func.__qualname__ not in when.funcs:
        when.funcs[func.__qualname__] = {}

    when.funcs[func.__qualname__][self.pred] = func

    return lambda *args, **kwargs: when.__match(func, *args, **kwargs)

  @staticmethod
  def __match(f, *args, **kwargs):
    for pred, func in when.funcs[f.__qualname__].items():
      if pred(*args, **kwargs):
          return func(*args, **kwargs)
    raise NotImplementedError()


@when(lambda x: x < 0)
def my_func(x):
  return "smaller!"

@when(lambda x: x > 0)
def my_func(x):
  return "greater!"


print(my_func(-123))
print(my_func(123))

[1]: The problem with resolution is that it's not easy to get right. Here are some alternatives to consider, all of which are severely lacking good reasons to implement and use.

  1. Specializing predicates which apply can be complex, and will probably be best laid at the hands of the user so as to manually define ranks/weights for each. This is clumsy, and generally a maintenance/boilerplate headache that's not worth the initial charm of this mechanism.
  2. The user can always add more overloads as the program is being run (Python being interpreted), and this can cause surprising temporal behavior. Spreading this around in your codebase is complacent. When it's not spread around, why not just if/else and be done?
  3. You can somehow restrict the usage, and enforce it so that only one predicate must return True for a given call. This is both weird, inefficient, and useless in many ways, e.g. what if you want to catch all instances of A or its subclasses, but treat subclass C in a special way? Or if you want to further specialize a predicate with an extra condition. How are you going to classify this kind of model?
Marketa answered 23/12, 2015 at 8:24 Comment(0)
F
-1

you can use isintance and combined it with a ABC to check the characteristic of the input like this:

from collections.abc import Iterable

def foo(param):
    if isinstance(param,int) and param > 0:
        #do something 
    elif isinstance(param,Iterable) and all(isinstance(item, str) for item in param):
        # do something else
    else:
        raise TypeError('Illegal input.')

The ABC tell you to what kind of interface the param has, so you can use the appropriate one depending of what you do if you don't care if it is a particular type or not, so defined like that param may be a set, list or tuple of strings and always will past the second check so you can processed it accordingly. There is also and ABC for numbers is you want to be general in that case too.

Frigidarium answered 22/12, 2015 at 23:6 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.