Функции высшего порядка: автоматическая генерация против ручного определения

Я пытаюсь немного отложить оценку, поэтому предпочитаю работать с функциями как можно дольше.

У меня есть class Function, который определяет композицию и поточечную арифметику для функций:

from functools import reduce
def compose(*funcs):
    '''
    Compose a group of functions f1, f2, f3, ... into (f1(f2(f3(...))))
    '''
    result = reduce(lambda f, g: lambda *args, **kaargs: f(g(*args, **kaargs)), funcs))
    return Function(result)

class Function:
    ''' 
    >>> f = Function(lambda x : x**2)
    >>> g = Function(lambda x : x + 4)
    >>> h = f/g
    >>> h(6)
    3.6
    >>> (f + 1)(5)
    26
    >>> (2 * f)(3)
    18
    # >> means composition, but in the order opposite to the mathematical composition
    >>> (f >> g)(6) # g(f(6))
    40
    # | means apply function: x | f is the same as f(x)
    >>> 6 | f | g # g(f(6))
    40
    '''

    # implicit type conversion from a non-callable arg to a function that returns arg
    def __init__(self, arg):
        if isinstance(arg, Function):
            # would work without this special case, but I thought long chains
            # of nested functions are best avoided for performance reasons (??)
            self._func = arg._func
        elif callable(arg):
            self._func = arg
        else:
            self._func = lambda *args, **kwargs : arg

    def __call__(self, *args, **kwargs):
        return self._func(*args, **kwargs)

    def __add__(lhs, rhs):
        # implicit type conversions, to allow expressions like f + 1
        lhs = Function(lhs)
        rhs = Function(rhs)

        new_f = lambda *args, **kwargs: lhs(*args, **kwargs) + rhs(*args, **kwargs)
        return Function(new_f)

    # same for __sub__, __mul__, __truediv__, and their reflected versions
    # ...

    # function composition
    # similar to Haskell's ., but with reversed order
    def __rshift__(lhs, rhs):
        return compose(rhs, lhs)
    def __rrshift__(rhs, lhs):
        return compose(rhs, lhs)

    # function application
    # similar to Haskell's $, but with reversed order and left-associative
    def __or__(lhs, rhs):
        return rhs(lhs)
    def __ror__(rhs, lhs):
        return rhs(lhs)

Изначально все мои функции имели одинаковую сигнатуру: они принимали в качестве единственного аргумента экземпляр class Data и возвращали float. Тем не менее, моя реализация Function не зависела от этой подписи.

Затем я начал добавлять различные функции более высокого порядка. Например, мне часто нужно создать ограниченную версию существующей функции, поэтому я написал функцию cap_if:

from operator import le
def cap_if(func, rhs):
    '''
    Input arguments:
      func: function that determines if constraint is violated
      rhs: Function(rhs) is the function to use if constraint is violated
    Output:
      function that
        takes as an argument function f, and
        returns a function with the same signature as f
    >>> f = Function(lambda x : x * 2)
    >>> 5 | (f | cap_if(le, 15))
    15
    >>> 10 | (f | cap_if(le, 15))
    20
    >>> 5 | (f | cap_if(le, lambda x : x ** 2))
    25
    >>> 1.5 | (f | cap_if(le, lambda x : x ** 2))
    3.0
    '''
    def transformation(original_f):
        def transformed_f(*args, **kwargs):
            lhs_value = original_f(*args, **kwargs)
            rhs_value = rhs(*args, **kwargs) if callable(rhs) else rhs
            if func(lhs_value, rhs_value):
                return rhs_value
            else:
                return lhs_value
        return Function(transformed_f)
    return Function(transformation)

Вот в чем проблема. Теперь я хочу представить функции, которые принимают "вектор" из Data экземпляров и возвращают "вектор" из чисел. На первый взгляд, я мог бы оставить свой существующий фреймворк без изменений. В конце концов, если я реализую вектор как, скажем, numpy.array, векторы будут поддерживать поточечную арифметику, и поэтому поточечная арифметика в функциях будет работать, как задумано, без каких-либо изменений в приведенном выше коде.

Но приведенный выше код не работает с функциями более высокого порядка, такими как cap_if (которые должны ограничивать каждый отдельный элемент в векторе). Я вижу три варианта:

  1. Создайте новую версию cap_if, скажем, vector_cap_if, для функции векторов. Но тогда мне нужно было бы сделать это для всех других функций более высокого порядка, что кажется нежелательным. Однако преимущество такого подхода заключается в том, что в будущем я мог бы заменить реализацию этих функций, скажем, на numpy функций для значительного прироста производительности.

  2. Реализовать функции, которые «повышают» тип функции с «число -> число» на «‹функция из данных в число> в ‹функция из данных в число>» и с «число -> число» в «‹функция из вектор данных в число> в ‹функция из вектора данных в число>". Назовем эти функции raise_to_data_function и raise_to_vector_function. Затем я могу определить basic_cap_if как функцию отдельных чисел (а не функцию более высокого порядка); Я делаю то же самое для других подобных вспомогательных функций, которые мне нужны. Затем я использую raise_to_data_function(basic_cap_if) вместо cap_if и raise_to_vector_function(basic_cap_if) вместо cap_if_vector. Этот подход кажется более элегантным в том смысле, что мне нужно реализовать каждую базовую функцию только один раз. Но при этом теряется возможный прирост производительности, описанный выше, а также получается код с большим количеством вызовов функций.

  3. Я мог бы следовать подходу в 2, но автоматически применять функции raise_to_data_function, raise_to_vector_function всякий раз, когда это необходимо, в зависимости от контекста. Предположительно, я могу реализовать это внутри метода __or__ (приложение функции): если он обнаружит, что функция передается simple_cap_if, он проверит подпись передаваемой функции и применит соответствующую функцию raise_to к правой стороне. (Сигнатуры могут быть раскрыты, например, путем создания функций с разными сигнатурами членами разных подклассов Function или с помощью назначенного метода в Function). Это может показаться очень хакерским, поскольку может произойти много неявных преобразований типов; но это уменьшает беспорядок кода.

Мне не хватает лучшего подхода и/или некоторых аргументов за/против этих?


comment
Вариант номер 2 звучит для меня как функция Functor fmap (в Haskell); Вы рассматривали возможность использования Functor?   -  person paul    schedule 12.12.2012
comment
Я не знаю, что такое Functor, но попытаюсь выяснить. Я полагаю, что с таким подходом ничего нельзя сделать, чтобы лишить себя основного прироста производительности (я не смогу использовать специальные функции numpy, которые обрабатывают весь вектор сразу)?   -  person max    schedule 13.12.2012
comment
Учебник по функторам: learnyouahaskell.com/ и learnyouahaskell.com/functors-applicative-functors-and-monoids -- И нет, вы не сможете использовать векторные версии функций, если пойдете по маршруту Functor :/   -  person paul    schedule 13.12.2012
comment
Кстати, у вас есть rhs = Function(lhs) в вашей функции __add__, вы хотели поставить rhs = Function(rhs)?   -  person paul    schedule 13.12.2012
comment
@Paul, спасибо, опечатка в моем коде! Что касается векторных версий: я полагаю, это проблема только потому, что у python есть возможность использовать гораздо более быструю векторную библиотеку. Это действительно не подходит для функциональных языков?   -  person max    schedule 14.12.2012
comment
нет проблем :) Я уверен, что проблема действительно возникает в функциональных языках, особенно при использовании таких библиотек, как BLAS/LAPAK, но я сам еще не занимался производительными вычислениями на функциональном языке, поэтому я не могу сказать много о Это.   -  person paul    schedule 14.12.2012