使用动态函数更新多个集合的元素

时间:2011-12-16 21:14:07

标签: haskell data-modeling ioref

设置

我有几个各种数据结构的集合,代表虚拟系统中模拟对象的状态。我还有许多函数可以转换(即基于原始对象和0个或更多参数创建对象的新副本)这些对象。

目标是允许用户选择一些对象来应用转换(在模拟规则内),将这些函数应用于这些对象,并通过用新的对象替换旧对象来更新集合。

我希望能够通过将较小的转换组合成较大的转换来构建这种类型的函数。然后评估这个组合函数。

问题

如何构建我的程序以使其成为可能?

我使用什么样的组合来建立像这样的交易?

  1. 将所有集合放入一个巨大的结构中并传递这个结构。
  2. 使用状态monad完成基本相同的事情
  3. 使用IORef(或其中一个更强大的表兄弟,如MVar)并建立IO动作
  4. 使用功能反应式编程框架
  5. 1和2似乎带来了很多行李,特别是如果我想最终将一些集合移动到数据库中。 (Darn IO Monad)

    3似乎运作良好,但开始看起来很像重新创建OOP。我也不确定在什么级别使用IORef。 (例如IORef (Collection Obj)Collection (IORef Obj)data Obj {field::IORef(Type)}

    4感觉功能最强大,但它似乎也创造了很多代码复杂性而在表现力方面没有太大的回报。


    示例

    我有一个网店前面。我维护了一系列产品,其中包括库存数量和价格(等等)。我还拥有一系列对商店有信用的用户。

    用户出现并选择购买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
    

    但是类型检查员会对我产生中风。如何构建此商店以使checkoutapplyUserCredit函数保持模块化和抽象化?我不可能是唯一一个有这个问题的人,对吗?

2 个答案:

答案 0 :(得分:6)

好的,让我们打破这个。

对于各种特定类型A -> A,您可以使用A等类型的“更新”函数,这些类型可以从部分应用程序派生,根据以前的值指定某种类型的新值。每个这样的类型A应该特定于该函数的功能,并且随着程序的发展,应该很容易更改这些类型。

您还有某种共享状态,可能包含任何上述更新功能使用的所有信息。此外,应该可以更改状态所包含的内容,而不会显着影响除直接作用于其之外的任何内容。

此外,您希望能够抽象地组合更新功能,而不会影响上述内容。

我们可以推断出简单设计的一些必要特征:

  • 在完全共享状态和每个函数所需的细节之间需要一个中间层,允许将状态的各个部分投射出来并替换为其余部分。

  • 根据定义,更新函数本身的类型与没有真正的共享结构不兼容,因此要构成它们,首先需要将每个函数与中间层部分组合在一起。这将为您提供对整个州的更新,然后可以以明显的方式组成。

  • 共享状态作为一个整体所需的唯一操作是与中间层接口,以及维护所做更改所需的任何操作。

这种分解允许每个整个层在很大程度上是模块化的;特别是,可以定义类型类来描述必要的功能,允许交换任何相关的实例。

特别是,这基本上统一了你的想法2和3.这里有一种固有的monadic上下文,建议的类型类接口允许多种方法,例如:

  • 使共享状态成为记录类型,将其存储在State monad中,并使用镜头提供接口层。

  • 使共享状态成为包含每个部分STRef之类的记录类型,并将字段选择器与ST monad更新操作组合以提供接口层。

    < / LI>
  • 使共享状态成为TChan的集合,并使用单独的线程来读取/写入它们,以便与外部数据存储异步通信。

或任何其他变体。

答案 1 :(得分:3)

您可以将状态存储在记录中,并使用镜头更新状态。这使您可以将各个状态更新组件编写为简单,集中的函数,这些函数可以组合以构建更复杂的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)]