2013-05-17 3 views
21

Рассматривая карты как представления конечных функций, отображение двух или более переменных может быть задано либо в кардианной, либо в неопознанной форме; то есть типы Map (a,b) c и Map a (Map b c) изоморфны или что-то близкое к нему.Haskell: `Карта (a, b) c` против` Карта a (Карта b c) `?

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

+2

Я думаю, что «Карта (a, b) c', скорее всего, будет намного больше (и, возможно, времени). Если есть способ (я не уверен, много не использовали карты), чтобы сбрасывать префиксный диапазон ключей, тогда вы все равно могли бы эффективно использовать что-то вроде карри-приложений с этим представлением. – DarkOtter

ответ

17

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

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

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

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

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

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

Так uncurried форма явно лучше вообще , но кэрри форма может быть лучше, если вы планируете делать «частичное применение» часто и выиграют от совместного Map b c ценностей.

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

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

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


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

foo x = go 
    where z = expensiveComputation x 
     go y = doStuff y z 
+1

+1, но re: первая маркерная точка, не получившая подмапка, требует наихудшего линейного времени в нерегулярной версии по сравнению с логарифмической версией в карри. Или ленивая оценка предотвращает это? –

+0

@larsmans: ленивая оценка не позволяет определить, что означает «наихудший случай». :] Вы платите только за дорогостоящее вычисление, если вы делаете что-то, что заставляет его, что часто является чем-то дорогостоящим. Тем не менее, я считаю, что вы правы, но для этого, вероятно, потребуются сознательно патологические данные и схемы доступа, чтобы увидеть, что в худшем случае на практике. –

+0

Я думал о том, чтобы получить «Map b c' out, за которым следует O (n) или более высокая последовательность обращений, но я не понимал, что в этом случае стоимость построения карты доминирует в фактических доступах. –

4

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

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

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

Но нижняя строка: бенчмарк это !! ;-)

+1

+1 Я думаю, что вы. Необработанная форма также может быть быстрее, если выполняется много запросов, которые уже не сбой для отсутствия * и * числа уникальных карриных ключей (a, b) намного больше числа уникальных a. – Ingo

+0

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

+0

(Однако вы будете вынуждены платить за дополнительную проверку, потому что GHC не будет достаточно умен, чтобы узнать, что стороны кортежа уже были вынуждены с помощью первого сравнения, и только внешние '(,)' будут вынуждены вставка в пустую «карту») –

3

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

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

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

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

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

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

Это трудно писать (если не невозможно) в выделанной версии, но легко в uncurried версии.