Кои са някои интересни приложения на функции от по-висок ред?

В момента водя курс по функционално програмиране и съм доста забавен от концепцията за функции от по-висок ред и функции като първокласни граждани. Все още обаче не мога да се сетя за много практически полезни, концептуално невероятни или просто интересни функции от по-висок ред. (Освен типичните и доста скучни функции 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 и гледайте (това е фалшиво, BTW, за отрицателно 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

Много техники, използвани в OO програмирането, са заобиколни решения за липсата на функции от по-висок ред.

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

Моделът strategy е друг пример за схема, която често предава обекти като аргументи като заместител на това, което всъщност е предназначено, функции.

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

Така че моят отговор би бил, че функциите от по-висок ред често се използват за изпълнение на същите видове задачи, които изпълняват OO програмистите, но директно и с много по-малко шаблони.

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

Наистина започнах да усещам силата, когато научих, че една функция може да бъде част от структура от данни. Ето една "потребителска монада" (technobabble: безплатна монада над (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

Едно нещо, което е доста забавно, макар и не особено практично, е Church Numerals. Това е начин за представяне на цели числа, като се използват само функции. Луд, знам. ‹shamelessPlug>Ето една имплементация в JavaScript, която направих. Може да е по-лесно за разбиране от реализация на Lisp/Haskell. (Но вероятно не, ако трябва да бъда честен. JavaScript всъщност не е предназначен за подобни неща.)‹/shamelessPlug>

person MatrixFrog    schedule 27.04.2011

Беше споменато, че Javascript поддържа определени функции от по-висок порядък, включително есе от Джоел Сполски. Марк Джейсън Доминус написа цяла книга, наречена Higher–Order 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 няма вградено currying, въпреки че има модул за това. Perl 6 обаче има къри и първокласни продължения, вградени направо в него, плюс много повече.

person tchrist    schedule 27.04.2011