Haskell: «Карта (a, b) c» по сравнению с «Карта a (Карта b c)»?

Думая о картах как о представлениях конечных функций, карта двух или более переменных может быть задана либо в каррированной, либо в некарриентной форме; то есть типы Map (a,b) c и Map a (Map b c) изоморфны или близки к этому.

Какие практические соображения существуют — эффективность и т. д. — для выбора между двумя представлениями?


person PLL    schedule 17.05.2013    source источник
comment
Я думаю, что Map (a, b) c, вероятно, будет гораздо более эффективным с точки зрения памяти (и, возможно, времени). Если есть способ (я не уверен, мало использовал карты) свернуть диапазон ключей префикса, то вы все равно могли бы эффективно выполнить что-то вроде каррированного приложения с этим представлением, я думаю.   -  person    schedule 17.05.2013


Ответы (3)


Экземпляр кортежей Ord использует лексикографический порядок, поэтому Map (a, b) c в любом случае сначала будет сортироваться по a, поэтому общий порядок будет таким же. Что касается практических соображений:

  • Поскольку Data.Map представляет собой двоичное дерево поиска, разбиение по ключу сравнимо с поиском, поэтому получение подкарты для заданного a в форме без карри не будет значительно дороже, чем в форме с карри.

  • Каррированная форма может давать в целом менее сбалансированное дерево по очевидной причине наличия нескольких деревьев вместо одного.

  • Каррированная форма будет иметь дополнительные накладные расходы для хранения вложенных карт.

  • Вложенные карты каррированной формы, представляющие «частичные приложения», могут использоваться совместно, если некоторые значения a дают одинаковый результат.

  • Точно так же «частичное применение» каррированной формы дает вам существующую внутреннюю карту, в то время как некаррированная форма должна создавать новую карту.

Таким образом, форма без каррирования явно лучше в целом, но форма с карри может быть лучше, если вы планируете часто выполнять "частичное применение" и выиграете от совместного использования Map b c значений.

Обратите внимание, что потребуется некоторая осторожность, чтобы гарантировать, что вы действительно выиграете от этого потенциального обмена; вам нужно будет явно определить любые общие внутренние карты и повторно использовать одно значение при построении полной карты.

Редактировать: Тихон Джелвис указывает в комментариях, что накладные расходы памяти на конструкторы кортежей, которые я не думал учитывать, вовсе не незначительны. Каррированная форма, безусловно, имеет некоторые накладные расходы, но эти накладные расходы пропорциональны количеству различных значений a. Накладные расходы конструктора кортежа в форме без карри, с другой стороны, пропорциональны общему количеству ключей.

Поэтому, если в среднем для любого заданного значения a есть три или более различных ключа, использующих его, вы, вероятно, сэкономите память, используя каррированную версию. Опасения по поводу несбалансированных деревьев, конечно, все еще актуальны. Чем больше я думаю об этом, тем больше подозреваю, что каррированная форма однозначно лучше, за исключением, возможно, случаев, когда ваши ключи очень разрежены и распределены неравномерно.


Обратите внимание, что поскольку арность определений имеет значение для GHC, такая же осторожность требуется при определении функций, если вы хотите, чтобы подвыражения были общими; это одна из причин, по которой вы иногда видите функции, определенные в таком стиле:

foo x = go
  where z = expensiveComputation x
        go y = doStuff y z
person C. A. McCann    schedule 17.05.2013
comment
+1, но в отношении первого пункта: не потребуется ли для получения подкарты наихудшего линейного времени в версии без каррирования по сравнению с логарифмическим в версии с карри? Или ленивая оценка предотвращает это? - person Fred Foo; 17.05.2013
comment
@larsmans: ленивая оценка не позволяет просто определить, что означает наихудший случай. :] Вы платите за дорогостоящие вычисления только в том случае, если делаете что-то, что их заставляет, что в любом случае часто является чем-то дорогим. Тем не менее, я считаю, что вы правы, но, вероятно, потребуются преднамеренно патологические данные и шаблоны доступа, чтобы увидеть этот наихудший случай на практике. - person C. A. McCann; 17.05.2013
comment
Я думал о том, чтобы получить Map b c, за которым следует O(n) или более последовательность обращений, но я не понимал, что в этом случае стоимость построения карты зависит от фактических обращений. - person Fred Foo; 17.05.2013
comment
Я не уверен, что каррированная форма обязательно займет больше памяти, чем обычная. Из [этой] (www.haskell.org/haskellwiki/GHC/Memory_Footprint) таблицы видно, что версия с карри будет иметь 6 дополнительных слов на уникальный ключ a, тогда как версия без карри будет иметь 3 дополнительных слова на пару a, b для хранения кортежа. . Если у вас не слишком много a, я думаю, что каррированная версия может быть более эффективной с точки зрения использования памяти. - person Tikhon Jelvis; 17.05.2013
comment
@larsmans: Для более простого примера рассмотрим временную сложность (++). Якобы длина первого аргумента должна быть O(N), но чтобы увидеть полную стоимость, необходимо пройти N элементов результата, что равно O(N) даже для полностью оцененного списка. С практической точки зрения часто имеет смысл амортизировать стоимость (++) по сравнению с внутренней стоимостью последовательных доступов, которые вызывают его, что дает чистую временную сложность O (1). - person C. A. McCann; 17.05.2013
comment
@TikhonJelvis: О, отличная мысль! Я обновил ответ, чтобы упомянуть об этом. - person C. A. McCann; 17.05.2013
comment
@larsmans Такой комментарий был бы неполным без упоминания первых нескольких глав Криса. Чисто функциональные структуры данных Окасаки - person J. Abrahamson; 17.05.2013
comment
@C.A.McCann Лень несколько отсутствует в ключевом отделе Map. Текущая форма позволяет вложенной карте быть более ленивой, чем в противном случае, как часть ключа в содержащей карте, это и хорошо, и плохо. Если вы накапливаете много изменений в содержащихся картах, не форсируя их, то вы можете утечь больше памяти в случае каррирования, но в форме без каррирования вы должны платить за ненужные кортежи и не можете запрашивать каррированные поддеревья почти так же эффективно. Я склонен к каррированию карты, особенно когда хочу иметь возможность использовать наличие внешней карты и вложенных пустых карт. - person Edward KMETT; 19.05.2013

Кортежи ленивы в обоих элементах, поэтому версия с кортежами добавляет немного лени. Хорошо это или плохо, сильно зависит от вашего использования. (В частности, сравнения могут принудительно использовать элементы кортежа, но только при наличии большого количества повторяющихся значений a.)

Кроме того, я думаю, это будет зависеть от того, сколько у вас дубликатов. Если a почти всегда отличается от b, у вас будет много маленьких деревьев, поэтому версия кортежа может быть лучше. С другой стороны, если верно обратное, версия без кортежей может сэкономить вам немного времени (не нужно постоянно пересравнивать a после того, как вы нашли подходящее поддерево и ищете b).

Мне вспоминаются попытки и то, как они однажды хранят общие префиксы. Версия без кортежа выглядит примерно так. Trie может быть более эффективным, чем BST, если есть много общих префиксов, и менее эффективным, если их нет.

Но главное: оцените это!! ;-)

person MathematicalOrchid    schedule 17.05.2013
comment
+1 Я думаю как ты. Некаррированная форма также может быть быстрее, если выполняется много поисков, которые уже завершились неудачно из-за отсутствующего a и количество уникальных каррированных ключей (a,b) намного больше, чем количество уникальных a. - person Ingo; 17.05.2013
comment
На самом деле это не будет лениво, так как оно будет вынуждено сравнением ключей, как только вы поместите его в дерево, и в целом комбинаторы Map (несколько излишне) строги в ключе независимо от этого. - person Edward KMETT; 19.05.2013
comment
(Однако вы будете вынуждены заплатить за дополнительную проверку, потому что GHC не будет достаточно умен, чтобы знать, что стороны кортежа уже были форсированы первым сравнением, и только внешний (,) будет принудительно вставлен в пустой Map. ) - person Edward KMETT; 19.05.2013

Помимо аспектов эффективности, у этого вопроса есть и прагматическая сторона: что вы хотите делать с этой структурой?

Вы, например, хотите иметь возможность хранить пустую карту для заданного значения типа a? Если это так, то версия без карри может быть более практичной!

Вот простой пример: допустим, мы хотим хранить String-значные свойства людей — скажем, значение некоторых полей на странице профиля stackoverflow этого человека.

type Person = String
type Property = String

uncurriedMap :: Map Person (Map Property String)
uncurriedMap = fromList [
                   ("yatima2975", fromList [("location","Utrecht"),("age","37")]),
                   ("PLL", fromList []) ]
curriedMap :: Map (Person,Property) String
curriedMap = fromList [
                 (("yatima2975","location"), "Utrecht"),
                 (("yatima2975","age"), "37") ]

В каррированной версии нет хорошего способа записать тот факт, что пользователь "PLL" известен системе, но не ввел никакой информации. Пара человек/свойство ("PLL",undefined) вызовет сбои во время выполнения, поскольку Map является строгим в ключах.

Вы можете изменить тип curriedMap на Map (Person,Property) (Maybe String) и сохранить там Nothing, и это вполне может быть лучшим решением в этом случае; но там, где есть неизвестное/переменное количество свойств (например, в зависимости от типа человека), это также столкнется с трудностями.

Итак, я думаю, это также зависит от того, нужна ли вам такая функция запроса:

data QueryResult = PersonUnknown | PropertyUnknownForPerson | Value String
query :: Person -> Property -> Map (Person, Property) String -> QueryResult

Это сложно (если вообще возможно) написать в версии с карри, но легко в версии без карри.

person yatima2975    schedule 21.05.2013