Практическое использование карриных функций?
Есть тонны учебников о том, как выполнять функции curry, и как много вопросов здесь, в stackoverflow. Однако после прочтения The Little Schemer, нескольких книг, учебников, сообщений в блогах и потоков stackoverflow я до сих пор не знаю ответа на простой вопрос: "Какой смысл карри?" Я действительно понимаю, как выполнять функцию, а не "почему?". за ним.
Может ли кто-нибудь объяснить мне практическое использование карриных функций (за пределами языков, которые допускают только один аргумент за функцию, где необходимость использования каррирования, конечно, совершенно очевидна.)
изменить: Принимая во внимание некоторые примеры из TLS, какое преимущество
(define (action kind)
(lambda (a b)
(kind a b)))
в отличие от
(define (action kind a b)
(kind a b))
Я могу видеть только больше кода и без дополнительной гибкости...
Ответы
Ответ 1
Одним эффективным использованием карри-функций является уменьшение количества кода.
Рассмотрим три функции, две из которых почти идентичны:
(define (add a b)
(action + a b))
(define (mul a b)
(action * a b))
(define (action kind a b)
(kind a b))
Если ваш код вызывает add
, он, в свою очередь, вызывает action
с видом +
. То же самое с mul
.
Вы определили эти функции, как вы делали бы во многих императивных популярных языках (некоторые из них включали лямбды, карри и другие функции, обычно встречающиеся в функциональном мире, потому что все это очень удобно).
Все add
и sum
do, завершают вызов action
с помощью соответствующего kind
. Теперь рассмотрим кардинальные определения этих функций:
(define add-curried
((curry action) +))
(define mul-curried
((curry action) *))
Они стали значительно короче. Мы просто набрали функцию action
, передав ей только один аргумент kind
и получив функцию curried, которая принимает остальные два аргумента.
Этот подход позволяет вам писать меньше кода с высоким уровнем ремонтопригодности.
Представьте себе, что функция action
будет немедленно переписана, чтобы принять еще 3 аргумента. Без currying вам придется переписать свои реализации add
и mul
:
(define (action kind a b c d e)
(kind a b c d e))
(define (add a b c d e)
(action + a b c d e))
(define (mul a b c d e)
(action * a b c d e))
Но карри спас вас от этой противной и подверженной ошибкам работы; вам не нужно вообще переписывать символ в функциях add-curried
и mul-curried
, потому что вызывающая функция предоставит необходимое количество аргументов, переданных в action
.
Ответ 2
Они могут сделать код более удобным для чтения. Рассмотрим следующие два фрагмента Haskell:
lengths :: [[a]] -> [Int]
lengths xs = map length xs
lengths' :: [[a]] -> [Int]
lengths' = map length
Зачем указывать имя переменной, которую вы не собираетесь использовать?
Выполненные функции также помогают в таких ситуациях:
doubleAndSum ys = map (\xs -> sum (map (*2) xs) ys
doubleAndSum' = map (sum . map (*2))
Удаление лишних переменных делает код более удобным для чтения, и нет необходимости в том, чтобы вы мысленно уяснили, что такое xs и что такое ys.
НТН.
Ответ 3
Вы можете видеть currying как специализацию. Выберите некоторые значения по умолчанию и оставьте пользователя (возможно, себя) со специальной, более выразительной функцией.
Ответ 4
Я думаю, что currying является традиционным способом обработки общих n-арных функций при условии, что единственные, которые вы можете определить, являются унарными.
Например, в лямбда-исчислении (из которого строятся языки функционального программирования) существуют только абстракции с одной переменной (что приводит к унарным функциям в FPL). Что касается лямбда-исчисления, я думаю, что легче доказать, что такое формализм, поскольку на самом деле вам не нужно обрабатывать случай n-арных функций (поскольку вы можете представить любую n-арную функцию с несколькими унарными из-за каррирования).
(Другие уже рассмотрели некоторые практические последствия этого решения, поэтому я остановлюсь здесь.)
Ответ 5
Используя all :: (a -> Bool) -> [a] -> Bool
с предикатом в карри.
all (`elem` [1,2,3]) [0,3,4,5]
Операторы инфикса Haskell могут быть расположены с обеих сторон, поэтому вы можете легко заархивировать иглу или сторону контейнера функции elem
(is-element-of).
Ответ 6
Мы не можем напрямую составлять функции, которые принимают несколько параметров. Поскольку состав функций является одним из ключевых понятий в функциональном программировании. Используя технику Currying, мы можем составлять функции, которые принимают несколько параметров.
Ответ 7
Таким образом, вам не нужно увеличивать шаблон с небольшим количеством лямбда.
Ответ 8
Очень легко создавать замыкания. Время от времени я использую SRFI-26. Это действительно мило.
Ответ 9
В самом себе каррирование - синтаксический сахар. Синтаксический сахар - это то, что вы хотите сделать легко. C, например, хочет сделать инструкции, которые являются "дешевыми" на ассемблере, например, инкрементными, легкими и поэтому у них есть синтаксический сахар для инкрементации, обозначение ++.
t = x + y
x = x + 1
заменяется на t = x ++ + y
Функциональные языки могут так же легко иметь такие вещи.
f(x,y,z) = abc
g(r,s)(z) = f(r,s,z).
h(r)(s)(z) = f(r,s,z)
но вместо этого все автоматическое. И это позволяет передавать g, привязанные определенным r0, s0 (т.е. конкретными значениями), которые передаются как одна переменная.
Возьмем, например, функцию сортировки perl, которая принимает
сортировать список
где sub - функция двух переменных, которая вычисляется как логическая и
list - это произвольный список.
Вы, естественно, хотели бы использовать операторы сравнения (< = > ) в Perl и иметь
sortordinal = sort (< = > )
где сортировка осуществляется по спискам. Чтобы сделать это, вы бы выбрали функцию карри.
А на самом деле
вид списка определяется именно таким образом в Perl.
Короче: каррирование - это сахар, чтобы сделать функции первого класса более естественными.
Ответ 10
Я хотел бы добавить пример в ответ @Francesco.
![enter image description here]()