У меня есть функция, которая берет параметр и приводит к результату. К сожалению, это берет, вполне очень хотят, чтобы функция привела к результату. Функция вызывается довольно часто с тем же входом, вот почему было бы удобно, если я мог бы кэшировать результаты. Что-то как
let cachedFunction = createCache slowFunction
in (cachedFunction 3.1) + (cachedFunction 4.2) + (cachedFunction 3.1)
Я изучал Данные. Массив и хотя массив ленив, я должен инициализировать его со списком пар (использующий listArray) - который непрактичен. Если 'ключ', например, 'Двойной' тип, я не могу инициализировать его вообще, и даже если я могу теоретически присвоить Целое число каждому возможному входу, у меня есть несколько десятков тысяч возможные исходные данные, и я только на самом деле использую небольшое количество. Я должен был бы инициализировать массив (или, предпочтительно хэш-таблица, поскольку только горстка результатов будет использоваться), использование функции вместо списка.
Обновление: Я читаю memoization статьи и насколько я понимаю это, MemoTrie мог проложить себе путь, я хочу. Возможно. Кто-то мог попытаться произвести 'cachedFunction'? Prefereably для медленной функции, которая берет 2 Двойных аргумента? Или, альтернативно, это берет один Международный аргумент в домене ~ [0.. 1 миллиард], который не съел бы всю память?
Ну, есть Data.HashTable
. Однако хеш-таблицы не очень хорошо работают с неизменяемыми данными и ссылочной прозрачностью, поэтому я не думаю, что они найдут много пользы.
Для небольшого количества значений, вероятно, будет достаточно быстро разместить их в дереве поиска (например, Data.Map
).Если вы можете смириться с некоторыми искажениями ваших Double
s, более надежным решением будет использование структуры, подобной дереву, такой как Data.IntMap
; у них время поиска пропорционально, в первую очередь, длине ключа и примерно одинаково по размеру коллекции. Если Int
слишком ограничивает, вы можете покопаться в Hackage, чтобы найти библиотеки trie, более гибкие по типу используемого ключа.
Что касается того, как кэшировать результаты, я думаю, то, что вы хотите, обычно называется «мемоизацией» . Если вы хотите вычислять и запоминать результаты по запросу, суть метода состоит в том, чтобы определить индексированную структуру данных, содержащую все возможные результаты таким образом, чтобы, когда вы запрашиваете конкретный результат, вынуждает только вычисления, необходимые для получения желаемого ответа. Общие примеры обычно включают индексацию в список, но тот же принцип должен применяться к любой нестрогой структуре данных. Как показывает опыт, значения, не относящиеся к функциям (включая бесконечные рекурсивные структуры данных), часто кэшируются средой выполнения, но не результаты функций, поэтому хитрость заключается в том, чтобы заключить все ваши вычисления в определение верхнего уровня, которое не зависят от любых аргументов.
Изменить: Пример MemoTrie Эй!
Это быстрое и грязное доказательство концепции; могут существовать лучшие подходы.
{-# LANGUAGE TypeFamilies #-}
{-# LANGUAGE TypeOperators #-}
import Data.MemoTrie
import Data.Binary
import Data.ByteString.Lazy hiding (map)
mangle :: Double -> [Int]
mangle = map fromIntegral . unpack . encode
unmangle :: [Int] -> Double
unmangle = decode . pack . map fromIntegral
instance HasTrie Double where
data Double :->: a = DoubleTrie ([Int] :->: a)
trie f = DoubleTrie $ trie $ f . unmangle
untrie (DoubleTrie t) = untrie t . mangle
slow x
| x < 1 = 1
| otherwise = slow (x / 2) + slow (x / 3)
memoSlow :: Double -> Integer
memoSlow = memo slow
Обратите внимание на расширения GHC, используемые пакетом MemoTrie; надеюсь, это не проблема. Загрузите его в GHCi и попробуйте вызвать slow
vs. memoSlow
с чем-то вроде (10 ^ 6) или (10 ^ 7), чтобы увидеть его в действии.
Обобщение этого на функции, принимающие несколько аргументов или еще чего-то, должно быть довольно простым. Для получения дополнительных сведений об использовании MemoTrie вы можете найти это сообщение в блоге его автора полезным.
Вы можете написать медленную функцию как функцию более высокого порядка, возвращающую саму функцию. Таким образом, вы можете выполнять всю предварительную обработку внутри медленной функции и той части, которая отличается в каждом вычислении в возвращаемой (надеюсь, быстрой) функции. Пример может выглядеть так: (код SML, но идея должна быть ясной)
fun computeComplicatedThing (x:float) (y:float) = (* ... some very complicated computation *)
fun computeComplicatedThingFast = computeComplicatedThing 3.14 (* provide x, do computation that needs only x *)
val result1 = computeComplicatedThingFast 2.71 (* provide y, do computation that needs x and y *)
val result2 = computeComplicatedThingFast 2.81
val result3 = computeComplicatedThingFast 2.91
Я добавлю собственное решение, которое тоже кажется довольно медленным. Первый параметр - это функция, которая возвращает Int32 - уникальный идентификатор параметра. Если вы хотите однозначно идентифицировать его разными способами (например, по 'id'), вам нужно изменить второй параметр в H.new на другую хеш-функцию. Я постараюсь узнать, как использовать Data.Составьте карту и проверьте, получу ли я более быстрые результаты.
import qualified Data.HashTable as H
import Data.Int
import System.IO.Unsafe
cache :: (a -> Int32) -> (a -> b) -> (a -> b)
cache ident f = unsafePerformIO $ createfunc
where
createfunc = do
storage <- H.new (==) id
return (doit storage)
doit storage = unsafePerformIO . comp
where
comp x = do
look <- H.lookup storage (ident x)
case look of
Just res -> return res
Nothing -> do
result <- return (f x)
H.insert storage (ident x) result
return result
У меня есть несколько десятков тысяч возможных входов, а на самом деле я использую только горстку. Мне бы потребовалось инициализировать массив ... с помощью функции вместо списка.
Я бы использовал listArray (start, end) (map func [start..end])
func
на самом деле не вызывалась выше. Haskell ленив и создает thunks, которые будут вычисляться, когда значение будет действительно необходимо. Я не знаю конкретно haskell, но как насчет сохранения существующих ответов в некотором хешированном виде? структура данных (можно было бы назвать словарем или хэш-картой)? Вы можете заключить свою медленную функцию в другую функцию, которая сначала проверяет карту и вызывает медленную функцию только в том случае, если она не нашла ответа.
Вы можете сделать это необычным, ограничив размер карты определенным размером и, когда он достигнет этого размера, выкинув наименее использовавшуюся запись. Для этого вам дополнительно потребуется сохранить карту сопоставлений ключей и отметок времени.
В системе времени выполнения GHC есть ряд инструментов, специально предназначенных для поддержки мемоизации.
К сожалению, мемоизация не является универсальной задачей, поэтому существует несколько различных подходов, которые нам необходимо поддерживать, чтобы справиться с различными потребностями пользователей.
Вы можете найти оригинальную запись 1999 года полезной, поскольку она включает в себя несколько реализаций в качестве примеров:
Расширение диспетчера хранилища: слабые указатели и стабильные имена в Haskell Саймона Пейтона Джонса, Саймона Марлоу и Конала Эллиотта
]