Типизированный FP: аргументы кортежа и изменяемые аргументы

В статически типизированных языках функционального программирования, таких как 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)", что позволяет с одинаковой легкостью каррировать оба варианта.

Я предполагаю, что этот вопрос, должно быть, возник, когда были разработаны языки, подобные тем четырем, которые я упомянул. Так что кто-нибудь знает какую-либо причину или исследование, указывающее, почему все четыре из этих языков решили не «унифицировать» эти два паттерна типов?

Стоит ли изучать PHP в 2026-2027 годах?
Стоит ли изучать PHP в 2026-2027 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в...
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Приемы CSS-макетирования - floats и Flexbox
Приемы CSS-макетирования - floats и Flexbox
Здравствуйте, друзья-студенты! Готовы совершенствовать свои навыки веб-дизайна? Сегодня в нашем путешествии мы рассмотрим приемы CSS-верстки - в...
Тестирование функциональных ngrx-эффектов в Angular 16 с помощью Jest
В системе управления состояниями ngrx, совместимой с Angular 16, появились функциональные эффекты. Это здорово и делает код определенно легче для...
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
7
0
816
5

Ответы 5

Возможно, потому, что полезно иметь кортеж как отдельный тип и хорошо хранить разные типы отдельно?

По крайней мере, в 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. Думаю, подобное возможно и на других языках. Какие большие преимущества принесет автоматическое каррирование каждой функции, определенной в кортеже? (Поскольку это то, о чем вы, кажется, спрашиваете.)

Спасибо, curry и uncurry были интересны, и я думаю, они показывают, что это должно быть возможно в принципе. Причина, по которой я спрашиваю, отчасти стилистика, а отчасти то, что каррирование - одно из главных рекламируемых преимуществ этих языков. Мне эта дифференциация кажется очень искусственной.

harms 02.01.2009 04:04

По крайней мере, одна из причин, по которой не следует объединять каррированные и несложные функциональные типы, заключается в том, что было бы очень запутанно, если бы кортежи использовались в качестве возвращаемых значений (удобный способ для этих типизированных языков возвращать несколько значений). Как в системе смешанных типов функция может оставаться хорошо компонуемой? Будет ли 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, а именно, если последняя является полиморфной функцией, если она будет считаться полностью примененной или просто карри?

harms 02.01.2009 04:17

Хотя это можно было бы обойти, добавив в систему типов правило, согласно которому должна использоваться «максимально возможная» привязка.

harms 02.01.2009 04:18

Но как разрешить «максимально возможные» привязки в свете частичного применения?

namin 02.01.2009 04:22

Я дам отдельный ответ с некоторыми примерами кода, тогда вы можете указать на любые вопиющие ошибки в моих рассуждениях, если хотите :-)

harms 02.01.2009 04:37

В этих языках кортежи отсутствуют просто для использования в качестве параметров функции. Это действительно удобный способ представления структурированных данных, например, двухмерной точки (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

Спасибо. Я знаю это. Меня больше интересовало другое направление, не отказ от кортежей, а использование параметра кортежа и возможность частичного применения функции.

harms 02.01.2009 19:27

Расширяя комментарии под хорошим ответом Намина:

Итак, предположим, что функция имеет тип '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.

Alexey Romanov 02.01.2009 14:48

Есть Лиспы, которые также выполняют частичное применение. Clojure имеет #(+ (% 3)), а Scheme - SRFI 26: (cut + <> 3).

Alexey Romanov 02.01.2009 14:51

Спасибо за справочную информацию, Норман, и за статью, которую я с интересом прочту. И спасибо Алексею за информацию на других языках.

harms 02.01.2009 15:19

Другие вопросы по теме