Есть ли где-нибудь библиотека мемоизации, основанная на идентификаторах объектов?

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

Я знаю несколько различных готовых библиотек мемоизации для Haskell:

  • Мемо-комбинаторы и пакеты памяток, которые используют красивый трюк, включающий ленивые бесконечные структуры данных, для достижения мемоизации чисто функциональным способом. (Насколько я понимаю, первое немного более гибкое, а второе проще в использовании в простых случаях: см. этот ТАК ответ для обсуждения.)
  • Пакет uglymemo, который внутренне использует unsafePerformIO, но по-прежнему предоставляет ссылочно прозрачный интерфейс. Внутреннее использование unsafePerformIO приводит к более высокой производительности, чем в двух предыдущих пакетах. (В готовом виде его реализация использует структуры данных поиска на основе сравнения, а не, возможно, несколько более эффективные хэш-функции; но я думаю, что если вы найдете и замените Cmp на Hashable и Data.Map на Data.HashMap и добавите подходящие imports, вы получить версию на основе хэша.)

Однако я не знаю ни одной библиотеки, которая ищет ответы на основе идентичности объекта, а не значения объекта. Это может быть важно, потому что иногда типы объектов, которые используются в качестве ключей к вашей мемо-таблице (то есть в качестве входных данных для мемоизируемой функции), могут быть большими - настолько большими, что необходимо полностью изучить объект, чтобы определить, действительно ли вы Я уже видел это раньше, это медленная операция. Медленно, а также ненужно, если вы будете снова и снова применять мемоизированную функцию к объекту, который хранится в заданном «месте в памяти» 1. (Это может произойти, например, если мы запомнили функцию, которая вызывается рекурсивно над некоторой большой структурой данных с большим количеством структурного разделения.) Если мы уже вычислили нашу мемоизированную функцию на этом конкретном объекте раньше, мы можем уже знаете ответ, даже не глядя на сам объект!

Реализация такой библиотеки мемоизации связана с несколькими тонкими проблемами, и для ее правильного выполнения требуется несколько специальных элементов поддержки со стороны языка. К счастью, GHC предоставляет все необходимые нам специальные функции, и есть статья Пейтон-Джонса, Марлоу и Эллиотта, в которой вас беспокоят большинство из этих проблем, объясняя, как создать надежную реализацию. Они не предоставляют всех подробностей, но приближаются.

Единственная деталь, о которой я могу понять, о которой, вероятно, следует беспокоиться, но о которой они не беспокоятся, - это безопасность потоков - их код, по-видимому, вообще не является потокобезопасным.

Мой вопрос: знает ли кто-нибудь о пакетной библиотеке, которая выполняет запоминания, описанные в статье Пейтон-Джонса, Марлоу и Эллиотта, заполняя все детали (и желательно также заполняя надлежащую безопасность потоков)?

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


ОБНОВЛЕНИЕ

Следуя предложению @luqui ниже, вот еще немного данных о конкретной проблеме, с которой я сталкиваюсь. Предположим, есть тип:

data Node = Node [Node] [Annotation]

Этот тип может использоваться для представления простого типа корневого DAG в памяти, где Node - это узлы DAG, корень - это просто выделенный Node, а каждый узел аннотирован некоторыми Annotation, внутренняя структура которых, я думаю, не должна нас беспокоить ( но если это имеет значение, просто спросите, и я буду более конкретным.) Если используется таким образом, обратите внимание, что вполне может быть значительное структурное разделение между Node в памяти - может быть экспоненциально больше путей, которые ведут от корня к узел, чем сами узлы. Мне предоставляется структура данных этой формы из внешней библиотеки, с которой я должен взаимодействовать; Я не могу изменить тип данных.

У меня есть функция

myTransform : Node -> Node

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

recursiveTransform : Node -> Node

чей вывод выглядит так же, как и структура данных, как если бы вы сделали:

recursiveTransform Node originalChildren annotations = 
   myTransform Node recursivelyTransformedChildren annotations
   where
     recursivelyTransformedChildren = map recursiveTransform originalChildren    

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

Я понимаю, что все было бы проще, если бы, скажем, Nodes были пронумерованы до того, как я их получил, или я мог бы иначе изменить определение Node. Я не могу (легко) сделать ни то, ни другое.


Меня также интересует общий вопрос о существовании библиотеки, реализующей упомянутые мною функциональные возможности, совершенно независимо от конкретной конкретной проблемы, с которой я сталкиваюсь прямо сейчас: я чувствую, что мне приходилось несколько раз обходить эту проблему, и было бы хорошо убить дракона раз и навсегда. Тот факт, что SPJ и др. Сочли целесообразным добавить в GHC не одну, а три функции для поддержки существования библиотек этой формы, предполагает, что эта функция действительно полезна и ее нельзя обойти в all дела. (НО мне все равно было бы очень интересно услышать об обходных путях, которые помогут и в этом конкретном случае: долгосрочная проблема не так актуальна, как проблема, с которой я сталкиваюсь прямо сейчас :-))

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


person circular-ruin    schedule 29.07.2011    source источник
comment
Вы знакомы с с использованием StableNames? ?   -  person C. A. McCann    schedule 29.07.2011
comment
@C. А. Макканн: Да, использование StableNames при реализации такой библиотеки подробно обсуждается в статье, на которую я ссылаюсь. (В самом деле, может быть, эта статья была первым появлением StableNames в печати? Хотя я могу ошибаться!)   -  person circular-ruin    schedule 29.07.2011
comment
Вполне возможно. Как правило, существует довольно прямое соответствие между функциями GHC и статьями, в которых SPJ указан в качестве автора. В основном хотел знать, потому что вы не упомянули StableName напрямую, хотя (я думаю) он предоставляет необходимый вам примитив равенства псевдо-указателя. Так что вам нужен как раз сам мемоайзер.   -  person C. A. McCann    schedule 30.07.2011
comment
@C. А. Макканн: Да, они даже в основном пишут мемоайзер по модулю хеш-таблицы в газете. (А хеш-таблицу легко найти в библиотеке.) Я бы просто (а) предпочел бы избежать ввода кода из статьи и (б) скорее избежать возможности совершения глупой ошибки, поскольку я делаю вещи безопасными для потоков. Я думал, что не могу быть первым, кому это понадобится :)   -  person circular-ruin    schedule 30.07.2011
comment
Ok. Спасибо за разъяснение. И нет, мне ничего не известно о существующих реализациях, извините! Впрочем, вполне может быть что-то, что может помочь в Hackage под менее чем очевидным именем.   -  person C. A. McCann    schedule 30.07.2011
comment
@C. А. Макканн: Нет проблем --- спасибо за вашу помощь!   -  person circular-ruin    schedule 30.07.2011
comment
кстати, uglymemo предполагает однопоточный доступ и поэтому уступает двум другим пакетам   -  person hvr    schedule 30.07.2011
comment
@hvr: Это правда? Я думал, что это остается правильным в многопоточных приложениях, с единственной оговоркой, что это остаточная вероятность того, что данное значение функции будет вычислено дважды в разных потоках. Причина, по которой я подумал, что это обсуждение в сообщении блога, на которое я ссылаюсь в вопросе (о словах «лучшая производительность»), которое, похоже, конкретно связано с безопасностью потоков. Есть ли особые обстоятельства, при которых это не является потокобезопасным?   -  person circular-ruin    schedule 30.07.2011
comment
@ round-ruin: Да, я на самом деле имею в виду поведение, при котором одно и то же вычисление может оцениваться несколько раз при использовании в высокопараллельном сценарии, что, ИМХО, в первую очередь побеждает цель мемоизации, особенно если задействованные вычисления занимают в течение значительного времени шанс снова запросить то же вычисление до того, как предыдущее вычисление завершится, становится немаловажным; Но можно (я это сделал) закодировать uglymemo таким образом, чтобы поставить в очередь запросы на вычисления, которые уже выполняются, без блокировки всего поискового словаря.   -  person hvr    schedule 30.07.2011
comment
@ round-ruin: см. мой ответ в stackoverflow.com/questions/5484847/ для примера того, как кодировать uglymemo с учетом одновременных обращений без дублирования вычислений / эффектов.   -  person hvr    schedule 30.07.2011
comment
@hvr: Спасибо, очень интересно! Ваш вопрос и ответы очень полезны: я пропустил их при поиске старых вопросов, потому что я искал только «памятку», а не «кеширование». Если мне придется самому писать код, я обязательно постараюсь последовать вашему примеру и сделать это полностью реентерабельным.   -  person circular-ruin    schedule 30.07.2011
comment
Связано: Безопасное для типа наблюдаемое совместное использование в Haskell, хотя он использует StableNames и, похоже, не обращается к безопасности потоков.   -  person hammar    schedule 30.07.2011
comment
@hammar --- Спасибо, очень интересно. Обязательно просмотрю газету. Думаю, я думаю, что невозможно делать то, что мне нужно, без StableNames, поэтому я не так беспокоюсь о том, что они будут использоваться, но отсутствие безопасности потоков не так хорошо для меня.   -  person circular-ruin    schedule 30.07.2011


Ответы (3)


Если вы только хотите запоминать на основе идентичности объекта, а не равенства, вы можете просто использовать существующие механизмы лени, встроенные в язык.

Например, если у вас есть такая структура данных

data Foo = Foo { ... }
expensive :: Foo -> Bar

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

data Foo = Foo { ..., memo :: Bar }

Чтобы упростить использование, добавьте умный конструктор, чтобы завязать узел.

makeFoo ... = let foo = Foo { ..., memo = expensive foo } in foo

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

person hammar    schedule 29.07.2011
comment
Похоже, что этот подход не может помочь, если кто-то не хочет и не может изменить тип, который будет использоваться в качестве ключа (= вход для функции, которую вы хотите запомнить). Для меня это не так. Я что-то упускаю? - person circular-ruin; 30.07.2011
comment
Хотя если действительно есть в этом случае, это более эффективно, экономя вам поиск карты на каждую оценку. - person circular-ruin; 30.07.2011
comment
Вы можете сохранить мемоизированное значение в типе оболочки, хотя вам придется поднять существующие вычисления (по крайней мере, те, которые должны использовать мемоизированное значение), чтобы вместо этого работать с обернутым типом, то есть чем-то вроде data MemoFoo = MemoFoo Foo Bar с makeFoo ... = let foo = Foo { ... } in MemoFoo foo (expensive foo). - person hammar; 30.07.2011
comment
Если я пытаюсь написать функцию f :: Foo -> Bar и (как вы предлагаете) создаю тип оболочки MemoFoo, который содержит два поля: поле, содержащее Foo, и поле memo, содержащее Bar, тогда я все еще не понимаю следующее: что делает f (получив Foo), чтобы найти связанный с ним MemoFoo объект (если он есть)? - person circular-ruin; 30.07.2011
comment
Вы не можете. Вот что я имел в виду под лифтингом. f необходимо заменить на f :: MemoFoo -> Bar. Однако вам придется изменить тип данных, если вам нужна рекурсивная мемоизация. - person hammar; 30.07.2011
comment
OK. Я думаю, увы, что такой подход не поможет в том, что мне нужно. В конце концов, мне нужно создать функцию типа Foo->Bar. - person circular-ruin; 30.07.2011
comment
@ circle-ruin, может быть, вы могли бы подробнее рассказать о своей цели и своих ограничениях, чтобы мы помогли вам решить эту проблему. Может быть, есть подход, о котором вы не думали ... - person luqui; 30.07.2011

Кажется, что stable-memo будет именно тем, что вам нужно (хотя я не уверен если он может обрабатывать несколько потоков):

В то время как большинство комбинаторов memoize основывается на равенстве, stable-memo делает это на основе того, был ли передан в функцию тот же самый аргумент ранее (то есть это тот же аргумент в памяти).

  • stable-memo оценивает только ключи WHNF.

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

  • stable-memo не сохраняет ключи, которые он видел до сих пор, что позволяет им собирать мусор, если они больше не будут использоваться. Устанавливаются финализаторы, чтобы удалить соответствующие записи из таблицы заметок, если это произойдет.

  • Data.StableMemo.Weak предоставляет альтернативный набор комбинаторов, которые также не сохраняют результаты функции, а повторно используют результаты только в том случае, если они еще не были обработаны сборщиком мусора.

  • Для аргумента функции нет ограничения класса типа.

stable-memo не будет работать с аргументами, которые имеют одинаковое значение, но не являются одним и тем же объектом кучи. Это исключает множество кандидатов на мемоизацию, например, наиболее распространенный пример, наивную реализацию Фибоначчи, предметной областью которой является машинный Ints; его все еще можно заставить работать в некоторых доменах, например, в lazy naturals.

person Petr    schedule 11.05.2013

Экметт только что загрузил библиотеку, которая обрабатывает это и многое другое (произведено в HacPhi): http://hackage.haskell.org/package/intern. Он уверяет меня, что это потокобезопасный.

Изменить: На самом деле, строго говоря, я понимаю, что это совсем другое. Но я думаю, вы можете использовать это в своих целях. На самом деле это скорее интернированная библиотека типа stringtable-atom, которая работает с произвольными структурами данных (включая рекурсивные). Он внутренне использует WeakPtrs для обслуживания таблицы. Однако он использует Ints для индексации значений, чтобы избежать проверок структурного равенства, что означает упаковку их в тип данных, когда то, что вы хотите, очевидно, на самом деле StableNames. Итак, я понимаю, что это отвечает на связанный вопрос, но требует изменения вашего типа данных, чего вы хотите избежать ...

person sclv    schedule 31.07.2011
comment
Превосходно! Очень интересно :). Я просто смотрю на это сейчас. Я, наверное, немного скучен, но мне непонятно, как использовать это в качестве библиотеки мемоизации. (Кажется, он сразу же отвечает на мой другой вопрос об интернировании строк!) Если я хочу написать memoize:: (a->b)->(a->b), как мне это сделать? (Я также сбит с толку, потому что, похоже, он не использует StableNames, что, по моему мнению, было необходимо ...) В любом случае, извините за глупость! - person circular-ruin; 31.07.2011
comment
Смотрите мою правку выше - я только что понял, почему это не соответствует вашему запросу. - person sclv; 31.07.2011
comment
Тем не менее, отличный взлом! На самом деле у меня есть еще один вопрос о SO, где я спрашиваю об интернировании строк и т. Д., В котором возникла проблема отсутствия надлежащей поточно-безопасной библиотеки интернирования: так что, возможно, вы хотели бы опубликовать это как ответ там, чтобы я мог проголосовать за него: ). - person circular-ruin; 01.08.2011