Обновление элементов нескольких коллекций с помощью динамических функций - PullRequest
4 голосов
/ 17 декабря 2011

Настройка

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

Цель состоит в том, чтобы позволить пользователю выбрать некоторый объект для применения преобразований (в рамках правил моделирования), применить эти функции к этим объектам и обновить коллекции, заменив старые объекты новыми.

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

Вопросы

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

Какой комбинатор я использую для создания транзакции, подобной этой?

Идеи

  1. Соберите все коллекции в одну огромную структуру и обведите эту структуру.
  2. Используйте государственную монаду, чтобы выполнить в основном то же самое
  3. Используйте IORef (или одного из его более могущественных кузенов, таких как MVar) и создайте действие IO
  4. Использование функционально-реактивного каркаса программирования

1 и 2 выглядят так, как будто они несут много багажа, особенно если я предполагаю в конечном итоге перенести некоторые коллекции в базу данных. (Darn IO Monad)

3, кажется, работает хорошо, но начинает выглядеть как воссоздание ООП. Я также не уверен, на каком уровне использовать IORef. (например, IORef (Collection Obj) или Collection (IORef Obj) или data Obj {field::IORef(Type)})

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


* 1 042 * Пример

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

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

Это означает, что я получаю следующее:

checkout :: Cart -> ProductsCol -> UserCol -> (ProductsCol, UserCol)

Но тогда жизнь усложняется, и мне нужно иметь дело с налогами:

checkout :: Cart -> ProductsCol -> UserCol -> TaxCol 
            -> (ProductsCol, UserCol, TaxCol)

И тогда мне нужно обязательно добавить заказ в очередь доставки:

checkout :: Cart 
         -> ProductsCol 
         -> UserCol 
         -> TaxCol
         -> ShipList
         -> (ProductsCol, UserCol, TaxCol, ShipList)

И так далее ...

Я хотел бы написать что-то вроде

checkout = updateStockAmount <*> applyUserCredit <*> payTaxes <*> shipProducts
applyUserCredit = debitUser <*> creditBalanceSheet

но проверка типов могла бы привести меня в замешательство. Как мне структурировать хранилище таким образом, чтобы функции checkout или applyUserCredit оставались модульными и абстрактными? Я не могу быть единственным, у кого есть эта проблема, верно?

Ответы [ 2 ]

6 голосов
/ 17 декабря 2011

Хорошо, давайте разберемся с этим.

У вас есть функции «обновления» с типами, такими как A -> A для различных конкретных типов A, которые могут быть получены из частичного применения, которые определяют новое значение некоторого типа в терминах предыдущего значения. Каждый такой тип A должен быть определенным для того, что делает эта функция, и должно быть легко изменить эти типы по мере развития программы.

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

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

Мы можем вывести несколько необходимых особенностей простого дизайна:

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

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

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

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

В частности, это, по сути, объединяет ваши идеи 2 и 3. Здесь есть некоторый врожденный монадический контекст, и предлагаемый интерфейс класса типов допускает несколько подходов, таких как:

  • Сделайте общее состояние типом записи, сохраните его в монаде State и используйте линзы для обеспечения уровня интерфейса.

  • Сделайте совместно используемое состояние типом записи, содержащим что-то вроде STRef для каждой части, и объедините селекторы полей с ST действиями обновления монады, чтобы обеспечить уровень интерфейса.

  • Сделать общее состояние коллекцией из TChan с отдельными потоками для чтения / записи, в зависимости от ситуации, для асинхронной связи с внешним хранилищем данных.

или любое количество других вариантов.

3 голосов
/ 17 декабря 2011

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

{-# LANGUAGE TemplateHaskell #-}
import Data.Lens.Template
import Data.Lens.Common
import Data.List (foldl')
import Data.Map ((!), Map, adjust, fromList)

type User = String
type Item = String
type Money = Int -- money in pennies

type Prices = Map Item Money
type Cart = (User, [(Item,Int)])
type ProductsCol = Map Item Int
type UserCol = Map User Money

data StoreState = Store { _stock :: ProductsCol
                        , _users :: UserCol
                        , msrp   :: Prices }
                  deriving Show
makeLens ''StoreState

updateProducts :: Cart -> ProductsCol -> ProductsCol
updateProducts (_,c) = flip (foldl' destock) c
  where destock p' (item,count) = adjust (subtract count) item p'

updateUsers :: Cart -> Prices -> UserCol -> UserCol
updateUsers (name,c) p = adjust (subtract (sum prices)) name
  where prices = map (\(itemName, itemCount) -> (p ! itemName) * itemCount) c


checkout :: Cart -> StoreState -> StoreState
checkout c s = (users ^%= updateUsers c (msrp s)) 
             . (stock ^%= updateProducts c) 
             $ s

test = checkout cart store
  where cart = ("Bob", [("Apples", 2), ("Bananas", 6)])
        store = Store initialStock initialUsers prices
        initialStock = fromList 
                       [("Apples", 20), ("Bananas", 10), ("Lambdas", 1000)]
        initialUsers = fromList [("Bob", 20000), ("Mary", 40000)]
        prices = fromList [("Apples", 100), ("Bananas", 50), ("Lambdas", 0)]
...