Haskell время жизни запомненной функции, привязанной к экземпляру записи - PullRequest
2 голосов
/ 11 марта 2020

Прежде всего, я новичок в Haskell, поэтому будьте добры:)

Рассмотрим следующий пример:

{-# LANGUAGE RecordWildCards #-}

data Item = Item {itemPrice :: Float, itemQuantity :: Float} deriving (Show, Eq)
data Order = Order {orderItems :: [Item]} deriving (Show, Eq)

itemTotal :: Item -> Float
itemTotal Item{..} = itemPrice * itemQuantity

orderTotal :: Order -> Float
orderTotal = sum . map itemTotal . orderItems

Возможно ли запомнить функцию orderTotal таким образом, он выполняется только один раз для «экземпляра» записи Order, и, это сложная часть, запись в кеше, привязанная к этому экземпляру, удаляется, как только этот порядок собирается мусором? Другими словами, я не хочу иметь кеш, который постоянно растет.

Редактировать после комментариев:

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

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

Чтобы проиллюстрировать то, что я пытаюсь сказать, посмотрите на этот код (в Python):

def memoized(function):
    function_name = function.__name__

    def wrapped(self):
        try:
            result = self._cache[function_name]
        except KeyError:
            result = self._cache[function_name] = function(self)
        return result

    return property(wrapped)


class Item:
    def __init__(self, price, quantity):
        self._price = price
        self._quantity = quantity
        self._cache = {}

    @property
    def price(self):
        return self._price

    @property
    def quantity(self):
        return self._quantity

    @memoized
    def total(self):
        return self.price * self.quantity

Класс Item является неизменным (своего рода), поэтому мы знать, что каждое производное свойство может быть вычислено только один раз за экземпляр. Это именно то, что делает функция memoized. Кроме того, кэш находится внутри самого экземпляра (self._cache), поэтому он будет собирать мусор вместе с ним.

Что я ищу, так это добиться аналогичного результата в Haskell.

1 Ответ

4 голосов
/ 12 марта 2020

Относительно простой способ запоминания вычисления значения определенного типа состоит в том, чтобы перенести вычисленный результат в тип данных и использовать интеллектуальный конструктор. То есть запишите тип данных Order как:

data Order = Order
  { orderItems :: [Item]
  , orderTotal :: Float
  } deriving (Show, Eq)

Обратите внимание, что поле orderTotal заменяет вашу функцию с тем же именем. Затем создайте заказы, используя умный конструктор:

order :: [Item] -> Order
order itms = Order itms (sum . map itemTotal $ itms)

Из-за ленивых вычислений поле orderTotal будет вычислено только в первый раз, когда это необходимо, с последующим кэшированием значения. Когда Order является сборщиком мусора, очевидно, что orderTotal будет сборщиком мусора одновременно.

Некоторые люди упаковывают это в модуль и экспортируют только умный конструктор order вместо обычного конструктор Order, чтобы гарантировать, что заказ с несогласованным orderTotal никогда не может быть создан. Я беспокоюсь об этих людях. Как они справляются со своей повседневной жизнью, зная, что могут в любой момент обмануть себя? Во всяком случае, это доступный вариант для действительно параноика.

...