В статически типизированных языках функционального программирования, таких как Standard ML, F#, OCaml и Haskell, функция обычно записывается с параметрами, отделенными друг от друга и от имени функции просто пробелами:
let add a b =
a + b
Типом здесь является "int -> (int -> int)", то есть функция, которая принимает int и возвращает функцию, которую принимает в свою очередь и int, и которая, наконец, возвращает int. Таким образом становится возможным каррирование.
Также можно определить аналогичную функцию, которая принимает кортеж в качестве аргумента:
let add(a, b) =
a + b
В этом случае тип становится "(int * int) -> int".
С точки зрения языкового дизайна, есть ли причина, по которой нельзя просто идентифицировать эти два паттерна типов в алгебре типов? Другими словами, так что "(a * b) -> c" сводится к "a -> (b -> c)", что позволяет с одинаковой легкостью каррировать оба варианта.
Я предполагаю, что этот вопрос, должно быть, возник, когда были разработаны языки, подобные тем четырем, которые я упомянул. Так что кто-нибудь знает какую-либо причину или исследование, указывающее, почему все четыре из этих языков решили не «унифицировать» эти два паттерна типов?





Возможно, потому, что полезно иметь кортеж как отдельный тип и хорошо хранить разные типы отдельно?
По крайней мере, в Haskell довольно легко перейти от одной формы к другой:
Prelude> let add1 a b = a+b
Prelude> let add2 (a,b) = a+b
Prelude> :t (uncurry add1)
(uncurry add1) :: (Num a) => (a, a) -> a
Prelude> :t (curry add2)
(curry add2) :: (Num a) => a -> a -> a
поэтому uncurry add1 такой же, как add2, а curry add2 такой же, как add1. Думаю, подобное возможно и на других языках. Какие большие преимущества принесет автоматическое каррирование каждой функции, определенной в кортеже? (Поскольку это то, о чем вы, кажется, спрашиваете.)
По крайней мере, одна из причин, по которой не следует объединять каррированные и несложные функциональные типы, заключается в том, что было бы очень запутанно, если бы кортежи использовались в качестве возвращаемых значений (удобный способ для этих типизированных языков возвращать несколько значений). Как в системе смешанных типов функция может оставаться хорошо компонуемой? Будет ли a -> (a * a) также преобразовываться в a -> a -> a? Если да, то являются ли (a * a) -> a и a -> (a * a) одним и тем же типом? Если нет, как бы вы скомпоновали a -> (a * a) с (a * a) -> a?
Вы предлагаете интересное решение вопроса о композиции. Однако я не нахожу это удовлетворительным, потому что он плохо сочетается с частичным приложением, что действительно является ключевым удобством каррированных функций. Позвольте мне попытаться проиллюстрировать проблему на Haskell:
apply f a b = f a b
vecSum (a1,a2) (b1,b2) = (a1+b1,a2+b2)
Теперь, возможно, ваше решение могло бы понять map (vecSum (1,1)) [(0,1)], но как насчет эквивалентного map (apply vecSum (1,1)) [(0,1)]? Становится сложно! Означает ли ваша самая полная распаковка, что (1,1) распаковывается с помощью аргументов apply a и b? Это не цель ... и в любом случае рассуждения усложняются.
Короче говоря, я думаю, что было бы очень сложно объединить каррированные и несложные функции при (1) сохранении семантики кода, действующего в старой системе, и (2) обеспечении разумной интуиции и алгоритма для объединенной системы. Но это интересная проблема.
Спасибо, хорошее замечание. Я думаю, что ответ должен быть отрицательным, и, да, я вижу проблему с тем, как будет работать композиция a -> (aа) с (аa) -> a, а именно, если последняя является полиморфной функцией, если она будет считаться полностью примененной или просто карри?
Хотя это можно было бы обойти, добавив в систему типов правило, согласно которому должна использоваться «максимально возможная» привязка.
Но как разрешить «максимально возможные» привязки в свете частичного применения?
Я дам отдельный ответ с некоторыми примерами кода, тогда вы можете указать на любые вопиющие ошибки в моих рассуждениях, если хотите :-)
В этих языках кортежи отсутствуют просто для использования в качестве параметров функции. Это действительно удобный способ представления структурированных данных, например, двухмерной точки (int * int), элемента списка ('a * 'a list) или узла дерева ('a * 'a tree * 'a tree). Помните также, что структуры - это просто синтаксический сахар для кортежей. Т.е.,
type info =
{ name : string;
age : int;
address : string; }
- удобный способ работы с кортежем (string * int * string).
Нет никакой причины, по которой вам нужны кортежи в языке программирования с помощью фундаментальный (вы можете создавать кортежи в лямбда-исчислении, так же, как вы можете создавать логические и целые числа - действительно, используя каррирование *), но они удобны и полезны.
* Например,
tuple a b = λf.f a b
fst x = x (λa.λb.a)
snd x = x (λa.λb.b)
curry f = λa.λb.f (λg.g a b)
uncurry f = λx.x f
Спасибо. Я знаю это. Меня больше интересовало другое направление, не отказ от кортежей, а использование параметра кортежа и возможность частичного применения функции.
Расширяя комментарии под хорошим ответом Намина:
Итак, предположим, что функция имеет тип 'a -> ('a * 'a):
let gimme_tuple(a : int) =
(a*2, a*3)
Затем предположим функцию типа ('a * 'a) -> 'b:
let add(a : int, b) =
a + b
Тогда композиция (предполагая слияние, которое я предлагаю), насколько я могу судить, не вызовет никаких проблем:
let foo = add(gimme_tuple(5))
// foo gets value 5*2 + 5*3 = 25
Но тогда вы можете представить себе полиморфную функцию, которая заменяет add в последнем фрагменте кода, например небольшую функцию, которая просто берет кортеж из двух элементов и составляет список из двух элементов:
let gimme_list(a, b) =
[a, b]
Это будет тип ('a * 'a) -> ('a list). Композиция сейчас была бы проблематичной. Рассмотреть возможность:
let bar = gimme_list(gimme_tuple(5))
Будет ли bar теперь иметь значение [10, 15] : int list или bar будет функцией типа (int * int) -> ((int * int) list), которая в конечном итоге вернет список, головой которого будет кортеж (10, 15)? Чтобы это сработало, в комментарии к ответу Намина я указал, что в системе типов потребуется дополнительное правило, согласно которому привязка фактических к формальным параметрам должна быть «максимально возможной», то есть система должна пытаться выполнить неполную привязку. сначала и попробуйте частичную привязку только в том случае, если полная привязка недостижима. В нашем примере это будет означать, что мы получаем значение [10, 15], потому что в этом случае возможно полное связывание.
Является ли такое понятие «максимально возможного» бессмысленным по своей сути? Я не знаю, но не могу сразу понять причину, по которой это могло быть.
Одна проблема, конечно, заключается в том, что если вы используете хочу вторую интерпретацию последнего фрагмента кода, вам нужно будет пройти через дополнительный обруч (обычно анонимную функцию), чтобы получить это.
Я думаю, что сегодня консенсус состоит в том, чтобы обрабатывать несколько аргументов путем каррирования (форма a -> b -> c) и зарезервировать кортежи на тот случай, когда вам действительно нужны значения типа кортежа (в списках и т. д.). Этот консенсус соблюдается всеми статически типизированными функциональными языками, начиная со Standard ML, который (чисто по соглашению) использует кортежи для функций, которые принимают несколько аргументов.
Почему это так? Стандартный ML - самый старый из этих языков, и когда люди впервые писали компиляторы ML, не было известно, как эффективно обрабатывать каррированные аргументы. (В основе проблемы лежит тот факт, что каррированная функция любоймог частично применяется каким-то другим кодом, который вы еще не видели, и вы должны компилировать его с учетом этой возможности.) Поскольку стандартный ML был разработан, компиляторы имеют улучшено, и вы можете прочитать о состоянии дел в отличная статья Саймона Марлоу и Саймона Пейтона Джонса.
LISP, который является самым старым функциональным языком из всех, имеет конкретный синтаксис, который неприемлем для каррирования и частичного применения очень сильно. Хмф.
Вот два функциональных языка, которые предпочитают форму кортежа: Nemerle и Scala. Оба из них должны быть интегрированы с объектно-ориентированными языками (как и F#) и хотят, чтобы их библиотеки можно было использовать из CLR / JVM соответственно. Оба также допускают частичное применение с синтаксисом наподобие _ + 3.
Есть Лиспы, которые также выполняют частичное применение. Clojure имеет #(+ (% 3)), а Scheme - SRFI 26: (cut + <> 3).
Спасибо за справочную информацию, Норман, и за статью, которую я с интересом прочту. И спасибо Алексею за информацию на других языках.
Спасибо,
curryиuncurryбыли интересны, и я думаю, они показывают, что это должно быть возможно в принципе. Причина, по которой я спрашиваю, отчасти стилистика, а отчасти то, что каррирование - одно из главных рекламируемых преимуществ этих языков. Мне эта дифференциация кажется очень искусственной.