Как работает Data.MemoCombinators? - PullRequest
36 голосов
/ 12 февраля 2011

Я искал источник для Data.MemoCombinators , но я не могу понять, где его сердце.

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

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

Ответы [ 4 ]

58 голосов
/ 13 февраля 2011

Эта библиотека представляет собой простую комбинаторизацию хорошо известного метода запоминания.Давайте начнем с канонического примера:

fib = (map fib' [0..] !!)
    where
    fib' 0 = 0
    fib' 1 = 1
    fib' n = fib (n-1) + fib (n-2)

Я понимаю, что вы сказали, что вы знаете, как и почему это работает.Поэтому я сосредоточусь на комбинаторизации.

Мы по существу пытаемся уловить и обобщить идею (map f [0..] !!).Тип этой функции - (Int -> r) -> (Int -> r), что имеет смысл: она берет функцию из Int -> r и возвращает запомненную версию той же функции.Любая функция, которая семантически идентична и имеет этот тип, называется «памяткой для Int» (даже id, которая не запоминает).Мы обобщаем эту абстракцию:

type Memo a = forall r. (a -> r) -> (a -> r)

Таким образом, Memo a, памятник для a, берет функцию из a во что угодно и возвращает семантически идентичную функцию, которая была запомнена (илине).

Идея различных памятников заключается в том, чтобы найти способ перечислить домен со структурой данных, отобразить функцию над ними и затем проиндексировать структуру данных.Хороший пример - bool:

bool :: Memo Bool
bool f = table (f True, f False)
    where
    table (t,f) True = t
    table (t,f) False = f

Функции из Bool эквивалентны парам, за исключением того, что пара будет оценивать каждый компонент только один раз (как в случае каждого значения, которое находится за пределами лямбды),Таким образом, мы просто отображаем пару и обратно.Существенным моментом является то, что мы поднимаем оценку функции над лямбда-аргументом (здесь последний аргумент table), перечисляя домен.

Memoizing Maybe a - похожая история, за исключениемТеперь нам нужно знать, как запоминать a для случая Just.Таким образом, памятник для Maybe принимает в качестве аргумента памятку для a:

maybe :: Memo a -> Memo (Maybe a)
maybe ma f = table (f Nothing, ma (f . Just))
    where
    table (n,j) Nothing = n
    table (n,j) (Just x) = j x

Остальная часть библиотеки - всего лишь варианты этой темы.

Способ, которым она запоминает интегральнуюТипы используют более подходящую структуру, чем [0..].Он немного сложен, но в основном просто создает бесконечное дерево (представляющее числа в двоичном виде для выяснения структуры):

1
  10
    100
      1000
      1001
    101
      1010
      1011
  11
    110
      1100
      1101
    111
      1110
      1111

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

Как указывает sclv, библиотека Conal MemoTrie использует ту же базовую технику, но использует представление класса типов вместо представления комбинатора.Мы выпустили наши библиотеки в одно и то же время независимо (на самом деле, в течение пары часов!).Conal проще использовать в простых случаях (есть только одна функция, memo, и она будет определять структуру памятки для использования на основе типа), тогда как моя более гибкая, так как вы можете делать такие вещи:

boundedMemo :: Integer -> Memo Integer
boundedMemo bound f = \z -> if z < bound then memof z else f z
   where
   memof = integral f

, который запоминает только значения, меньшие заданной границы, необходимые для реализации одной из проблем проекта Эйлера.

Существуют и другие подходы, например, предоставление функции открытой точки привязки над монадой.:

memo :: MonadState ... m => ((Integer -> m r) -> (Integer -> m r)) -> m (Integer -> m r)

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

Это ответило на то, что вам было интересно?Если нет, то, возможно, четко изложите те моменты, которые вас смущают?

18 голосов
/ 13 февраля 2011

Сердце - это функция bits:

-- | Memoize an ordered type with a bits instance.
bits :: (Ord a, Bits a) => Memo a
bits f = IntTrie.apply (fmap f IntTrie.identity)

Это единственная функция (кроме тривиальной unit :: Memo ()), которая может дать вам значение Memo a. Он использует ту же идею, что и на этой странице о запоминании на Haskell. В разделе 2 показана простейшая стратегия запоминания с использованием списка, а в разделе 3 - то же самое, используя двоичное дерево натуральных чисел, подобное IntTree, используемому в мемокомбинаторах.

Основная идея состоит в том, чтобы использовать конструкцию типа (map fib [0 ..] !!) или в случае мемокомбинаторов - IntTrie.apply (fmap f IntTrie.identity). Здесь следует отметить соответствие между IntTie.apply и !!, а также между IntTrie.identity и [0..].

Следующим шагом является запоминание функций с другими типами аргументов. Это делается с помощью функции wrap, которая использует изоморфизм между типами a и b для построения Memo b из Memo a. Например:

Memo.integral f
=>
wrap fromInteger toInteger bits f
=>
bits (f . fromInteger) . toInteger
=>
IntTrie.apply (fmap (f . fromInteger) IntTrie.identity) . toInteger
~> (semantically equivalent)
(map (f . fromInteger) [0..] !!) . toInteger

Остальная часть исходного кода имеет дело с типами, такими как List, Maybe, Either и запоминанием нескольких аргументов.

7 голосов
/ 12 февраля 2011

Часть работы выполнена IntTrie: http://hackage.haskell.org/package/data-inttrie-0.0.4

Библиотека Люка - это разновидность библиотеки Конала MemoTrie, которую он описал здесь: http://conal.net/blog/posts/elegant-memoization-with-functional-memo-tries/

Некоторое дальнейшее расширение - общее представление о функциональном запоминании состоит в том, чтобы взять функцию из a -> b и отобразить ее в структуре данных, индексированной всеми возможными значениями a и содержащими значения b. Такая структура данных должна быть ленивой в двух отношениях - во-первых, она должна быть ленивой в тех значениях, которые она содержит. Во-вторых, он должен быть ленивым. Первый по умолчанию на нестрогом языке. Последнее достигается использованием обобщенных попыток.

Различные подходы memocombinators, memotrie и т. Д. - это всего лишь способы создания композиций из фрагментов попыток для отдельных типов структур данных, позволяющих просто создавать попытки для все более сложных структур.

0 голосов
/ 14 февраля 2011

@ luqui Одна вещь, которая мне не ясна: имеет ли это такое же рабочее поведение, что и следующее:

fib :: [Int]
fib = map fib' [0..]
    where fib' 0 = 0
             fib' 1 = 1
             fib' n = fib!!(n-1) + fib!!(n-2)

Выше следует запоминать fib на верхнем уровне, и, следовательно, если вы определите двафункции:

f n = fib!!n + fib!!(n+1)

Если мы затем вычислим f 5 , мы получим, что fib 5 не пересчитывается при вычислении fib 6 .Мне не ясно, ведут ли себя комбинаторы запоминания к такому же поведению (т. Е. Запоминание на высшем уровне вместо того, чтобы запрещать только пересчет "внутри" вычисления fib), и если да, то почему именно?

...