Какие есть интересные применения функций высшего порядка?

В настоящее время я прохожу курс функционального программирования, и меня очень забавляет концепция функций высшего порядка и функций как граждан первого класса. Однако я пока не могу вспомнить много практически полезных, концептуально удивительных или просто интересных функций высшего порядка. (Кроме типичных и довольно скучных функций map, filter и т.д.).

Знаете ли вы примеры таких интересных функций?

Возможно, функции, которые возвращают функции, функции, которые возвращают списки функций (?) и т. д.

Я был бы признателен за примеры на Haskell, языке, который я сейчас изучаю :)


person Manuel Araoz    schedule 26.04.2011    source источник


Ответы (14)


Вы заметили, что в Haskell нет синтаксиса для циклов? Нет while или do или for. Потому что это всего лишь функции высшего порядка:

 map :: (a -> b) -> [a] -> [b]

 foldr :: (a -> b -> b) -> b -> [a] -> b

 filter :: (a -> Bool) -> [a] -> [a]

 unfoldr :: (b -> Maybe (a, b)) -> b -> [a]

 iterate :: (a -> a) -> a -> [a]

Функции высшего порядка заменяют необходимость встроенного синтаксиса языка для управляющих структур, а это означает, что почти каждая программа на Haskell использует эти функции, что делает их весьма полезными!

Это первый шаг к хорошей абстракции, потому что теперь мы можем включить пользовательское поведение в скелетную функцию общего назначения.

В частности, монады возможны только потому, что мы можем связывать их вместе и манипулировать функциями для создания программ.

Дело в том, что жизнь довольно скучна, когда она первоклассна. Программирование становится интересным только тогда, когда у вас есть более высокий порядок.

person Don Stewart    schedule 26.04.2011
comment
Ну, следуют монады, а затем следуют функции более высокого порядка над монадами. Это все черепахи! - person Don Stewart; 26.04.2011
comment
Очень хороший ответ. Некоторые структуры управления, которые придумали люди, весьма нетрадиционны, но чрезвычайно полезны. Отличным примером, который приходит мне на ум, является quickCheck — оператор управления, который запускает предоставленную функцию несколько раз с произвольными входными данными, пытаясь заставить ее не работать. Это такая простая концепция, а функции более высокого порядка (и классы типов) делают ее столь же простой в использовании. Просто quickCheck $ \n xs -> length (take n xs) <= n и наблюдайте (это ложно, кстати, для отрицательного n - легко сделать оплошность, но quickCheck легко ее ловит). - person mokus; 27.04.2011
comment
Другой, который только что пришел на ум (это не сразу, потому что я склонен считать это само собой разумеющимся), это «forkIO» или что-то еще, имеющее отношение к параллелизму. Создание потока — это настолько полезная функция высшего порядка, что люди используют ее даже в C, несмотря на отсутствие в языке какой-либо специальной поддержки для них. Просто посмотрите на тип, скажем, pthread_create. HOF настолько полезны, что люди готовы мириться с определением структур параметров, возни с указателями на них, обычно вручную управляя памятью для них и приведением к void * и обратно, просто чтобы иметь возможность использовать их в C. - person mokus; 29.04.2011
comment
жизнь скучна, когда это первый заказ. Что ж, когда вы оберегаете управляющую структуру императивного языка с помощью ее семантики, вы получаете нечто более высокого порядка, но вы жестко привязаны к определенному воплощению управляющей структуры (а именно, к диапазону примитивов которые предоставляет язык). Поведение более высокого порядка позволяет вам получить это непосредственно. И (как вы знаете) вы можете доказать, что все, что можно сделать с помощью циклов, можно сделать с помощью HOF, что делает программирование более высокого порядка расширением (типичного) императивного программирования. - person Kristopher Micinski; 25.09.2012

Многие методы, используемые в объектно-ориентированном программировании, являются обходными путями из-за отсутствия функций более высокого порядка.

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

Шаблон стратегия — это еще один пример схемы, которая часто передает объекты в качестве аргументов вместо того, что на самом деле предназначено, т. е. функций.

Точно так же внедрение зависимостей часто включает неуклюжую схему для передачи прокси для функций, когда часто было бы лучше просто передать функции непосредственно в качестве аргументов.

Поэтому мой ответ будет заключаться в том, что функции более высокого порядка часто используются для выполнения тех же задач, что и объектно-ориентированные программисты, но напрямую и с гораздо меньшим количеством шаблонов.

person sigfpe    schedule 27.04.2011
comment
Как примечание, в ООП нет ничего, что запрещающие функции были бы первоклассными объектами. И в SmallTalk, и в Ruby есть первоклассные функции. - person John F. Miller; 03.05.2011
comment
@Джон Абсолютно. Мое первое знакомство с функциями как объектами первого класса произошло много лет назад, когда я изучал Smalltalk. - person sigfpe; 17.05.2011

Я действительно почувствовал силу, когда узнал, что функция может быть частью структуры данных. Вот "потребительская монада" (техноболтовня: бесплатная монада над (i ->)).

data Coro i a
    = Return a
    | Consume (i -> Coro i a)

Таким образом, Coro может либо мгновенно дать значение, либо быть другим Coro в зависимости от некоторого ввода. Например, это Coro Int Int:

Consume $ \x -> Consume $ \y -> Consume $ \z -> Return (x+y+z)

Это потребляет три целочисленных входа и возвращает их сумму. Вы также можете заставить его вести себя по-разному в зависимости от входных данных:

sumStream :: Coro Int Int
sumStream = Consume (go 0)
    where
    go accum 0 = Return accum
    go accum n = Consume (\x -> go (accum+x) (n-1))

Это потребляет Int, а затем потребляет еще столько же Int, прежде чем получить их сумму. Это можно представить как функцию, которая принимает произвольное количество аргументов, созданную без какой-либо языковой магии, просто функции более высокого порядка.

Функции в структурах данных — очень мощный инструмент, который не был частью моего лексикона до того, как я начал работать с Haskell.

person luqui    schedule 26.04.2011
comment
Его Return accum в первой ветке. Кроме того, я нахожу это немного грязным, что первый потребляемый элемент как семантика так отличается от других. Я бы предпочел сделать длину параметром subStream. - person gasche; 27.04.2011
comment
@gasche, спасибо исправлено. Также я согласен с тем, что это грязно - дело в том, что сигнатура этой функции может зависеть от ее параметров, демонстрируя силу конструкции функции в структуре данных. - person luqui; 27.04.2011

Ознакомьтесь с документом 'Даже функции высшего порядка для синтаксического анализа или почему Кто-нибудь когда-нибудь хотел использовать функцию шестого порядка?» Криса Окасаки. Он написан с использованием ML, но идеи в равной степени применимы и к Haskell.

person yatima2975    schedule 26.04.2011

Джоэл Спольски написал известное эссе, демонстрирующее, как Map-Reduce работает с использованием функций высшего порядка JavaScript. Обязательна к прочтению всем, кто задается этим вопросом.

person Kurtosis    schedule 27.04.2011

Функции более высокого порядка также требуются для каррирования, которое Haskell использует повсеместно. По сути, функция, принимающая два аргумента, эквивалентна функции, принимающей один аргумент и возвращающей другую функцию, принимающую один аргумент. Когда вы видите такую ​​сигнатуру типа в Haskell:

f :: A -> B -> C

... (->) можно считать правоассоциативным, показывая, что на самом деле это функция более высокого порядка, возвращающая функцию типа B -> C:

f :: A -> (B -> C)

Некаррированная функция с двумя аргументами вместо этого имела бы такой тип:

f' :: (A, B) -> C

Таким образом, каждый раз, когда вы используете частичное приложение в Haskell, вы работаете с функциями более высокого порядка.

person C. A. McCann    schedule 26.04.2011


Одна интересная и немного сумасшедшая вещь, которую вы можете сделать, — это имитировать объектно-ориентированную систему, используя функцию и сохраняя данные в области видимости функции (т. е. в замыкании). Это более высокий порядок в том смысле, что функция генератора объектов — это функция, которая возвращает объект (другую функцию).

Мой Haskell довольно ржавый, поэтому я не могу легко привести вам пример Haskell, но вот упрощенный пример Clojure, который, надеюсь, передает концепцию:

(defn make-object [initial-value]
  (let [data (atom {:value initial-value})]
      (fn [op & args]
        (case op 
          :set (swap! data assoc :value (first args))
          :get (:value @data)))))

Использование:

(def a (make-object 10))

(a :get)
=> 10

(a :set 40)

(a :get)
=> 40

Тот же принцип будет работать в Haskell (за исключением того, что вам, вероятно, потребуется изменить операцию set, чтобы вернуть новую функцию, поскольку Haskell является чисто функциональным)

person mikera    schedule 26.04.2011
comment
Я не думаю, что вы можете сказать, что он имитирует объектно-ориентированное программирование. Это зависит от вашего определения ООП: является ли наследование (открытая рекурсия) требованием или нет? Если это так, то то, что вы показали, не является объектом, а просто обычной формой сокрытия состояния. Если это не так, то то, что вы показали, является объектом (а не его симуляцией), только следующим объектному протоколу, который не стандартизирован языком. - person gasche; 26.04.2011
comment
@gasche - конечно, это всего лишь упрощенный пример. но если вы тоже хотите добавить наследование, это будет легко - например. вы можете сделать наследование на основе прототипа с реализациями методов, хранящимися внутри хеш-таблицы (что также было бы хорошим примером функций первого порядка, хранящихся в структуре данных для OP!) - person mikera; 26.04.2011
comment
Не очень просто в Haskell, так как он очень строго относится к безопасности типов и не имеет внутреннего универсального понятия подтипов. Использование наследования в лучшем случае неудобно, когда язык не предоставляет встроенных средств, например, для принятия решения о том, можно ли (безопасно и автоматически) заменить один тип другим типом. В остальном записи функций в Haskell создают очень удобную, хотя и минималистскую, объектную систему на основе прототипов. Это, конечно, не является препятствием для языков с динамической типизацией или встроенным подтипом. - person C. A. McCann; 26.04.2011
comment
Haskell предпочитает делать вещи, подобные наследованию, с классами типов. См., например, различные классы и экземпляры числовых типов. - person Phob; 13.05.2011
comment
Пожалуйста, не увековечивайте этот миф. Типовые классы не предназначены для вещей, подобных наследованию, и на самом деле совершенно не подходят для такой задачи. Достаточно простого функтора. - person nomen; 07.06.2013

Я особый поклонник мемоизации более высокого порядка:

memo :: HasTrie t => (t -> a) -> (t -> a)

(Для любой функции возвращайте запомненную версию этой функции. Ограничено тем фактом, что аргументы функции должны иметь возможность кодироваться в трие.)

Это из http://hackage.haskell.org/package/MemoTrie.

person Darius Jahandarie    schedule 27.04.2011

Здесь есть несколько примеров: http://www.haskell.org/haskellwiki/Higher_order_function

Я также рекомендую эту книгу: http://www.cs.nott.ac.uk/~gmh/book.html, который является отличным введением во все аспекты Haskell и охватывает функции более высокого порядка.

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

person Nick Brunt    schedule 26.04.2011

Вот небольшой перефразированный фрагмент кода:

rays :: ChessPieceType -> [[(Int, Int)]]
rays Bishop = do
  dx <- [1, -1]
  dy <- [1, -1]
  return $ iterate (addPos (dx, dy)) (dx, dy)
...  -- Other piece types

-- takeUntilIncluding is an inclusive version of takeUntil
takeUntilIncluding :: (a -> Bool) -> [a] -> [a]

possibleMoves board piece = do
  relRay <- rays (pieceType piece)
  let ray = map (addPos src) relRay
  takeUntilIncluding (not . isNothing . pieceAt board)
    (takeWhile notBlocked ray)
  where
    notBlocked pos =
      inBoard pos &&
      all isOtherSide (pieceAt board pos)
    isOtherSide = (/= pieceSide piece) . pieceSide

При этом используются несколько функций «высшего порядка»:

iterate :: (a -> a) -> a -> [a]
takeUntilIncluding  -- not a standard function
takeWhile :: (a -> Bool) -> [a] -> [a]
all :: (a -> Bool) -> [a] -> Bool
map :: (a -> b) -> [a] -> [b]
(.) :: (b -> c) -> (a -> b) -> a -> c
(>>=) :: Monad m => m a -> (a -> m b) -> m b

(.) — это оператор ., а (>>=) — это do-нотация «оператор разрыва строки».

При программировании на Haskell вы просто используете их. Там, где у вас нет функций высшего порядка, вы понимаете, насколько невероятно полезными они были.

person yairchu    schedule 26.04.2011

Вот шаблон, о котором я еще не видел, чтобы кто-то еще упоминал, который действительно удивил меня, когда я впервые узнал о нем. Рассмотрим пакет статистики, в котором у вас есть список выборок в качестве входных данных, и вы хотите рассчитать по ним множество различных статистических данных (есть также множество других способов мотивировать это). Суть в том, что у вас есть список функций, которые вы хотите запустить. Как вы их всех запускаете?

statFuncs :: [ [Double] -> Double ]
statFuncs = [minimum, maximum, mean, median, mode, stddev]

runWith funcs samples = map ($samples) funcs

Здесь происходят все виды добра высшего порядка, некоторые из которых упоминались в других ответах. Но я хочу указать на функцию '$'. Когда я впервые увидел такое использование «$», я был поражен. До этого я не считал это очень полезным, кроме как удобной заменой скобок... но это было почти волшебно...

person mightybyte    schedule 27.04.2011
comment
($) :: (a -> b) -> a -> b Это просто оператор для применения простой функции. f $ a такое же, как f a. Он правоассоциативен и имеет приоритет 0. - person mightybyte; 28.04.2011
comment
Для получения более подробной информации ознакомьтесь с этим вопросом stackoverflow.com/questions/940382/ - person mightybyte; 28.04.2011

Одна вещь, которая довольно забавна, хотя и не особенно практична, — это Церковные числительные. Это способ представления целых чисел с использованием только функций. Сумасшедший, я знаю. ‹shamelessPlug>Вот реализация на JavaScript, которую я сделал. Это может быть проще для понимания, чем реализация Lisp/Haskell. (Но, если честно, вероятно, нет. JavaScript не был предназначен для таких вещей.)‹/shamelessPlug>

person MatrixFrog    schedule 27.04.2011

Было упомянуто, что Javascript поддерживает некоторые функции более высокого порядка, включая эссе Джоэла Спольски< /а>. Марк Джейсон Доминус написал целую книгу под названием Perl высшего порядка; исходный код книги доступен для бесплатной загрузки в различных форматах, включая PDF.

По крайней мере, начиная с Perl 3, Perl поддерживал функциональность, больше напоминающую Lisp, чем C, но только в Perl 5 была доступна полная поддержка замыканий и всего, что из этого следует. И ни одна из первых реализаций Perl 6 не была написана на Haskell, что оказало большое влияние на развитие дизайна этого языка.

Примеры подходов функционального программирования на Perl проявляются в повседневном программировании, особенно с map и grep:

@ARGV    = map { /\.gz$/ ? "gzip -dc < $_ |" : $_ } @ARGV;

@unempty = grep { defined && length } @many;

Поскольку sort также допускает замыкание, шаблон map/sort/map очень распространен:

@txtfiles = map { $_->[1] }
            sort { 
                    $b->[0]  <=>     $a->[0]
                              ||
                 lc $a->[1]  cmp  lc $b->[1]
                              ||
                    $b->[1]  cmp     $a->[1]
            }
            map  { -s => $_ } 
            grep { -f && -T }
            glob("/etc/*");

or

@sorted_lines = map { $_->[0] }
                sort {
                     $a->[4] <=> $b->[4] 
                             ||
                    $a->[-1] cmp $b->[-1]
                             ||
                     $a->[3] <=> $b->[3]
                             ||
                     ...
                }
                map { [$_ => reverse split /:/] } @lines;

Функция reduce упрощает взлом списка без зацикливания:

$sum = reduce { $a + $b } @numbers;

$max = reduce { $a > $b ? $a : $b } $MININT, @numbers;

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

try {
   something();
} catch {
   oh_drat();
};

не является встроенным. Однако это почти тривиально определено: try — это функция, которая принимает два аргумента: замыкание в первом аргументе и функцию, которая принимает замыкание во втором.

В Perl 5 нет встроенного каррирования, хотя для этого есть модуль. Однако в Perl 6 прямо встроены каррирование и первоклассные продолжения, а также многое другое.

person tchrist    schedule 27.04.2011