Преимущество возможности передавать функции другим функциям заключается в том, что они позволяют писать более понятный и более общий код.Например, рассмотрим достаточно реалистичную функцию, которая суммирует все числа в списке:
sum [] = 0
sum (x:xs) = x + sum xs
Это синтаксис Haskell, который может быть вам незнаком;две строки определяют два разных случая, в зависимости от того, является ли ввод пустым списком, в этом случае сумма равна нулю, или она x
добавляется к списку xs
, в этом случае сумма равна x
плюссумма xs
.На более традиционном языке (не на каком-либо конкретном) у вас будет
function sum(xs) {
var total = 0
for x in xs {
total = x + total
}
return total
}
Теперь предположим, что мы хотим найти произведение чисел в списке, вместо этого:
product [] = 1
product (x:xs) = x * product xs
На традиционном языке это выглядело бы как
function product(xs) {
var total = 1
for x in xs {
total = x * total
}
return total
}
Интересно, что эти две функции выглядят практически одинаково.Единственное отличие состоит в том, что 0
заменяется на 1
, а +
заменяется на *
.И действительно, получается, что мы можем обобщить как sum
, так и +
:
foldr f i [] = i
foldr f i (x:xs) = f x (foldr f i xs)
Здесь foldr
принимает три аргумента: функция с двумя аргументами f
, константа i
и список.Если список пуст, мы возвращаем константу ( например , sum [] = 0
);в противном случае мы применяем функцию к (а) первому элементу списка и (б) результат свертывания остальной части списка.На более традиционном языке это будет выглядеть примерно так:
function foldr(f,i,xs)
var result = i
for x in xs {
result = f(x, result)
}
return result
}
Это означает, что мы можем упростить sum
и product
до простого
sum = foldr (+) 0
product = foldr (*) 1
(Здесь (+)
и (*)
- это функции с двумя аргументами, которые добавляют и умножают свои аргументы соответственно.) Вы не можете сделать это без функций первого класса.(Другая вещь, которую я делаю, - это опускание последнего аргумента; это называется каррированием, и это довольно удобно. Идея состоит в том, что если я не передам foldr
все его аргументы, он возвращает функцию, которая ожидаетостальные из них. Но если вас это смущает, представьте, что в определениях сказано: sum xs = foldr (+) 0 xs
.)
Но здесь все может стать интереснее.Предположим, у вас есть список чисел, и вы хотите вычислить квадрат каждого числа в списке:
squares [] = []
squares (x:xs) = (x^2) : squares xs
function squares(xs) {
var result = []
for x in xs {
result = result ++ [x^2]
}
return result
}
Но это явно абстрагируемо: почти точно такой же код будет работать, если вы хотите отрицать каждоеэлемент вашего списка, или если у вас есть список электронных писем и вы хотите получить отправителей, или что-то еще.Итак, мы абстрагируем:
map f [] = []
map f (x:xs) = f x : map f xs
function map(f,xs) {
var result = []
for x in xs {
result = result ++ [f(x)]
}
return result
}
squares = map (^2) # (^2) is the function which squares its argument.
negations = map negate
emailSenders = map getSender
Но что интересно, мы также можем реализовать map
в терминах нашего предыдущего foldr
.Во-первых, нам нужно определить состав функции, .
:
f . g = \x -> f (g x)
. Здесь говорится, что композиция f
и g
- это новая анонимная функция с одним аргументом, которая сама применяет g
кx
и f
к результату.И теперь мы можем определить map
:
map f = foldr ((:) . f) []
Здесь (:)
- это функция, которая берет элемент и список и возвращает этот элемент, добавленный к списку.(:) . f
- это то же самое, что и \x -> (:) (f x)
, которое (согласно упомянутому правилу каррирования) совпадает с \x xs -> f x : xs
.Другими словами, на каждом шаге сгиба добавьте f x
к тому, что мы имеем до сих пор.(Это не дает нам обратную сторону map
, потому что foldr
работает как бы "наизнанку".)
В этом определении map
используется функция высшего порядка .
для построения функции она переходит к foldr
.Так много функций передается в качестве параметров!И это позволяет нам делать такие вещи, как запись
f &&& g = \x -> (f x, g x)
, а затем использовать это для записи
sendersAndRecipients = map (getSender &&& getRecipient) . fetchEmails
Вы получаете большую мощность, имея возможность обрабатывать функции как значения.Вы можете написать общие процедуры, такие как map
или &&&
, которые позволят вам написать краткий, но читаемый код позже.Передача функций также полезна для обратных вызовов: sendMessage(theMessage,fn)
, где fn
- функция, которая запускается при получении ответа.Это просто очень естественный способ работы.
Что касается того, какие языки поддерживают это: честно, Википедия знала бы лучше, чем я. Но я сделаю это. C и C ++ делают что-то вроде: вы не можете писать функции in-line, но вы можете передавать указатели на функции. Это не очень распространено в любом языке (хотя и в C). Любой ОО-язык может, если вы определите класс, у которого есть единственный метод, который является той функцией, которую вы хотите, но это ужасно неуклюже. Тем не менее, это то, что делает Java. C #, с другой стороны, на самом деле имеет функции, которые могут быть переданы в качестве параметров. Ruby (который также имеет «блоки», которые являются специальным синтаксисом для определенных случаев использования), Python, Perl и JavaScript поддерживают передачу функций, как и любой функциональный язык программирования: Lisps (Scheme, Common Lisp, Clojure,… ); семья ML (SML, OCaml,…); Haskell (который может быть связан с семьей ML); Scala; и другие. Это полезная функция, поэтому неудивительно, что она так широко распространена.