Как сделать в Haskell универсальную функцию memoize?

Я видел другой пост об этом, но есть ли чистый способ сделать это в Haskell?

Как вторую часть, можно ли это сделать, не делая функцию монадической?

Я с трудом могу писать Haskell :), но я так не думаю. Мемоизация включает в себя именно то статическое состояние, которое чистые функциональные языки не допускают, как я их понимаю. Конечно, я думаю, что использование монады сделало бы это выполнимым. Но ваша вторая часть указывает на то, что вы это уже знаете.

Mike G. 27.09.2008 01:08

@Mike: Я мог подумать то же самое, но на самом деле функциональные языки могут хорошо запоминать, как показывают ответы. Им просто нужно передать состояние через параметры функции.

LarsH 09.09.2010 15:38
Стоит ли изучать PHP в 2026-2027 годах?
Стоит ли изучать PHP в 2026-2027 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в...
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Приемы CSS-макетирования - floats и Flexbox
Приемы CSS-макетирования - floats и Flexbox
Здравствуйте, друзья-студенты! Готовы совершенствовать свои навыки веб-дизайна? Сегодня в нашем путешествии мы рассмотрим приемы CSS-верстки - в...
Тестирование функциональных ngrx-эффектов в Angular 16 с помощью Jest
В системе управления состояниями ngrx, совместимой с Angular 16, появились функциональные эффекты. Это здорово и делает код определенно легче для...
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
21
2
4 553
5
Перейти к ответу Данный вопрос помечен как решенный

Ответы 5

Если ваши аргументы будут натуральными числами, вы можете сделать это просто:

memo f = let values = map f [0..]
     in \n -> values !! n

Однако это не очень помогает при переполнении стека и не работает с рекурсивными вызовами. Вы можете увидеть более изящные решения на http://www.haskell.org/haskellwiki/Memoization.

Это полезно, но я все же чувствую, что может быть что-то более общее.

Jonathan Tran 03.10.2008 05:13

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

memoize :: Ord a => (a -> IO b) -> IO (a -> IO b)
memoize f =
  do r <- newIORef Map.empty
     return $ \x -> do m <- readIORef r
                       case Map.lookup x m of
                            Just y  -> return y
                            Nothing -> do y <- f x
                                          writeIORef r (Map.insert x y m)
                                          return y

Но это как-то неудовлетворительно. Кроме того, Data.Map ограничивает параметр, чтобы он был экземпляром Ord.

Конечно, невозможно избежать каких-либо ограничений, явных или неявных. Как бы вы запомнили, например, функцию типа (Integer -> Bool) -> Bool?

luqui 21.11.2008 15:23
Ответ принят как подходящий

Это во многом следует за http://www.haskell.org/haskellwiki/Memoization.

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

type Cacher a b = (a -> b) -> a -> b

positive_list_cacher :: Cacher Int b
positive_list_cacher f n = (map f [0..]) !! n

или же

integer_list_cacher :: Cacher Int b
integer_list_cacher f n = (map f (interleave [0..] [-1, -2, ..]) !!
    index n where
        index n | n < 0  = 2*abs(n) - 1
        index n | n >= 0 = 2 * n

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

f_with_memo :: (a -> b) -> a -> b
f_with_memo memoed base = base_answer
f_with_memo memoed arg  = calc (memoed (simpler arg))

Мемоизированная версия - это, конечно, то, что мы пытаемся определить.

Но мы можем начать с создания функции, которая кэширует свои входные данные:

Мы могли бы построить один уровень, передав функцию, которая создает структура, которая кэширует значения. За исключением того, что нам нужно создать версию f что уже есть передала кешированная функция.

Благодаря лени это не проблема:

memoize cacher f = cached where
         cached = cacher (f cached)

тогда все, что нам нужно, это использовать его:

exposed_f = memoize cacher_for_f f

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

И последнее предостережение: использование этого ленивого метода означает, что кеш никогда не сжимается, он только растет. Если вы вместо этого используете монаду ввода-вывода, вы можете справиться с этим, но разумное выполнение этой задачи зависит от модели использования.

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

Jonathan Tran 04.10.2008 02:04

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

wnoise 04.10.2008 02:22

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

wnoise 04.10.2008 02:24

Пакетные данные-мемокомбинаторы на hackage предоставляют множество многоразовых подпрограмм мемоизации. Основная идея:

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

Т.е. он может запоминать любую функцию из файла. Затем модуль предоставляет некоторые примитивы (например, unit :: Memo () и integral :: Memo Int) и комбинаторы для создания более сложных таблиц заметок (например, pair :: Memo a -> Memo b -> Memo (a,b) и list :: Memo a -> Memo [a]).

Вы можете изменить решение Джонатана с помощью unsafePerformIO, чтобы создать "чистую" мемоизирующую версию вашей функции.

import qualified Data.Map as Map
import Data.IORef
import System.IO.Unsafe

memoize :: Ord a => (a -> b) -> (a -> b)
memoize f = unsafePerformIO $ do 
    r <- newIORef Map.empty
    return $ \ x -> unsafePerformIO $ do 
        m <- readIORef r
        case Map.lookup x m of
            Just y  -> return y
            Nothing -> do 
                    let y = f x
                    writeIORef r (Map.insert x y m)
                    return y

Это будет работать с рекурсивными функциями:

fib :: Int -> Integer
fib 0 = 1
fib 1 = 1
fib n = fib_memo (n-1) + fib_memo (n-2)

fib_memo :: Int -> Integer
fib_memo = memoize fib

Хотя этот пример представляет собой функцию с одним целочисленным параметром, тип memoize говорит нам, что его можно использовать с любой функцией, которая принимает сопоставимый тип. Если у вас есть функция с более чем одним параметром, просто сгруппируйте их в кортеж перед применением memoize. F.i .:

f :: String -> [Int] -> Float
f ...

f_memo = curry (memoize (uncurry f))

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

Thomas Ahle 07.02.2012 05:35

unsafePerformIO не нужен. Тот же код можно выполнить в монаде ST.

uhbif19 03.04.2019 08:10

@ uhbif19 Это так? Я призываю вас написать его (с окончательным чистым (a -> b) без изменений)

luqui 10.04.2019 01:10

Другие вопросы по теме