Частичное применение функций и карри, как сделать лучший код вместо большого количества карт?
Я новичок в Haskell, и я пытаюсь понять это.
У меня возникла следующая проблема:
У меня есть функция, которая получает 5 параметров, скажем,
f x y w z a = x - y - w - z - a
И я хотел бы применить его, только меняя переменную x
от 1
в 10
в то время как y
, w
, z
а также a
всегда будет таким же. Реализация, которую я достиг, состояла в следующем, но я думаю, что должен быть лучший путь.
Допустим, я хотел бы использовать:
x from 1 to 10
y = 1
w = 2
z = 3
a = 4
В соответствии с этим мне удалось применить функцию следующим образом:
map ($ 4) $ map ($ 3) $ map ($ 2) $ map ($ 1) (map f [1..10])
Я думаю, что должен быть лучший способ применить много пропущенных параметров к частично примененным функциям без необходимости использовать слишком много карт.
6 ответов
Все предложения пока хороши. Вот еще один, который на первый взгляд может показаться немного странным, но во многих других ситуациях он оказывается весьма удобным.
Некоторые операторы формирования типа, такие как []
, который является оператором, который отображает тип элементов, например Int
к типу списков этих элементов, [Int]
иметь свойство быть Applicative
, Для списков это означает, что есть какой-то способ, обозначаемый оператором, <*>
произносится "применить", чтобы превратить списки функций и списки аргументов в списки результатов.
(<*>) :: [s -> t] -> [s] -> [t] -- one instance of the general type of <*>
а не обычное приложение, заданное пробелом или $
($) :: (s -> t) -> s -> t
В результате мы можем выполнять обычное функциональное программирование со списками вещей вместо вещей: мы иногда называем это "программирование в списке идиом". Единственный другой ингредиент состоит в том, что, чтобы справиться с ситуацией, когда некоторые из наших компонентов являются отдельными вещами, нам нужен дополнительный гаджет
pure :: x -> [x] -- again, one instance of the general scheme
который превращает вещь в список, чтобы быть совместимым с <*>
, То есть pure
перемещает обычное значение в аппликативную идиому.
Для списков, pure
просто делает список синглтона и <*>
производит результат каждого попарного применения одной из функций к одному из аргументов. Особенно
pure f <*> [1..10] :: [Int -> Int -> Int -> Int -> Int]
это список функций (так же, как map f [1..10]
) который можно использовать с <*>
снова. Остальные твои аргументы за f
не листовые, поэтому нужно pure
их.
pure f <*> [1..10] <*> pure 1 <*> pure 2 <*> pure 3 <*> pure 4
Для списков это дает
[f] <*> [1..10] <*> [1] <*> [2] <*> [3] <*> [4]
то есть список способов подать заявку из f, один из [1..10], 1, 2, 3 и 4.
Открытие pure f <*> s
это так часто, это сокращенно f <$> s
, так
f <$> [1..10] <*> [1] <*> [2] <*> [3] <*> [4]
это то, что обычно пишут. Если вы можете отфильтровать <$>
, pure
а также <*>
шум, это вроде как приложение, которое вы имели в виду. Дополнительная пунктуация необходима только потому, что Haskell не может определить разницу между вычислением по списку для набора функций или аргументов и вычислением не по списку того, что подразумевается как одно значение, но оказывается списком. Однако, по крайней мере, компоненты находятся в том порядке, в котором вы начали, так что вам будет легче видеть, что происходит.
Esoterica. (1) в моем (не очень) частном диалекте Хаскелла
(|f [1..10] (|1|) (|2|) (|3|) (|4|)|)
где каждая идиома скобка, (|f a1 a2 ... an|)
представляет применение чистой функции к нулю или более аргументам, которые живут в идиоме. Это просто способ написать
pure f <*> a1 <*> a2 ... <*> an
У Идриса есть идиоматические скобки, но Хаскелл их не добавил. Еще.
(2) В языках с алгебраическими эффектами идиома недетерминированных вычислений - это не то же самое (для проверки типов), что и типы данных списков, хотя вы можете легко конвертировать между ними. Программа становится
f (range 1 10) 2 3 4
где диапазон недетерминированно выбирает значение между заданными нижней и верхней границами. Таким образом, недетермизм рассматривается как локальный побочный эффект, а не структура данных, позволяющая выполнять операции при сбое и выборе. Вы можете заключить недетерминированные вычисления в обработчик, который придает значения этим операциям, и один такой обработчик может генерировать список всех решений. То есть дополнительная запись, объясняющая, что происходит, выдвигается к границе, а не проникает через весь интерьер, как те, <*>
а также pure
,
Управление границами вещей, а не их внутренностей, является одной из немногих хороших идей, которые наш вид смог реализовать. Но, по крайней мере, мы можем иметь это снова и снова. Вот почему мы занимаемся фермерством, а не охотой. Вот почему мы предпочитаем статическую проверку типов динамической проверке тегов. И так далее...
Другие показали, как вы можете это сделать, но я думаю, что полезно показать, как превратить вашу версию во что-то более приятное. Вы написали
map ($ 4) $ map ($ 3) $ map ($ 2) $ map ($ 1) (map f [1..10])
map
подчиняется двум основным законам:
map id = id
, То есть, если вы сопоставите функцию идентификации с любым списком, вы получите тот же список.- Для любого
f
а такжеg
,map f . map g = map (f . g)
, То есть сопоставление списка с одной функцией, а затем с другой - это то же самое, что сопоставление списка с композицией этих двух функций.
Второй map
право это то, что мы хотим применить здесь.
map ($ 4) $ map ($ 3) $ map ($ 2) $ map ($ 1) (map f [1..10])
=
map ($ 4) . map ($ 3) . map ($ 2) . map ($ 1) . map f $ [1..10]
=
map (($ 4) . ($ 3) . ($ 2) . ($ 1) . f) [1..10]
Что значит ($ a) . ($ b)
делать? \x -> ($ a) $ ($ b) x = \x -> ($ a) $ x b = \x -> x b a
, Как насчет ($ a) . ($ b) . ($ c)
? Это (\x -> x b a) . ($ c) = \y -> (\x -> x b a) $ ($ c) y = \y -> y c b a
, Шаблон теперь должен быть ясным: ($ a) . ($ b) ... ($ y) = \z -> z y x ... c b a
, Итак, в конечном итоге, мы получаем
map ((\z -> z 1 2 3 4) . f) [1..10]
=
map (\w -> (\z -> z 1 2 3 4) (f w)) [1..10]
=
map (\w -> f w 1 2 3 4) [1..10]
=
map (\x -> ($ 4) $ ($ 3) $ ($ 2) $ ($ 1) $ f x) [1..10]
В дополнение к тому, что говорят другие ответы, было бы неплохо изменить порядок параметров вашей функции, особенно x
обычно это параметр, который вы меняете следующим образом:
f y w z a x = x - y - w - z - a
Если вы сделаете так, чтобы x
параметр идет последним, вы можете просто написать
map (f 1 2 3 4) [1..10]
Конечно, это не сработает при любых обстоятельствах, но хорошо бы увидеть, какие параметры с большей вероятностью будут меняться в течение серии вызовов, и поместить их в конец списка аргументов, а параметры, которые, как правило, остаются неизменными в начале., Когда вы делаете это, карри и частичное применение, как правило, помогут вам больше, чем в противном случае.
Предполагая, что вы не возражаете против переменных, вы просто определяете новую функцию, которая принимает x
и звонки f
, Если у вас нет определения функции (вы можете использовать let
или же where
) вы можете использовать лямбду вместо.
f' x = f x 1 2 3 4
Или с лямбдой
\x -> f x 1 2 3 4
Карри не принесет вам никакой пользы, потому что аргумент, который вы хотите изменить (перечислить), не последний. Вместо этого попробуйте что-то вроде этого.
map (\x -> f x 1 2 3 4) [1..10]
Общее решение в этой ситуации - лямбда:
\x -> f x 1 2 3 4
однако, если вы видите, что вы делаете это очень часто, с одним и тем же аргументом, имеет смысл перевести этот аргумент в качестве последнего аргумента:
\x -> f 1 2 3 4 x
в этом случае карри применяется отлично, и вы можете просто заменить вышеприведенное выражение на
f 1 2 3 4
так что вы можете написать:
map (f 1 2 3 4) [1..10]