4

设置

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

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

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

问题

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

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

想法

  1. 将所有的收藏品放在一个巨大的结构中,然后传递这个结构。
  2. 使用状态单子完成基本相同的事情
  3. 使用 IORef(或其更强大的表亲之一,如 MVar)并建立一个 IO 操作
  4. 使用功能响应式编程框架

1 和 2 似乎携带了很多包袱,特别是如果我设想最终将一些集合移动到数据库中。(该死的 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

但是类型检查器会让我中风。我如何构建这个商店,使checkoutorapplyUserCredit函数保持模块化和抽象?我不可能是唯一一个有这个问题的人,对吧?

4

2 回答 2

6

好的,让我们分解一下。

您有“更新”函数,其类型类似于A -> A各种特定类型A,这些类型可能源自部分应用程序,根据先前值指定某种类型的新值。每个这样的类型A都应该特定于该函数的作用,并且随着程序的开发应该很容易更改这些类型。

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

此外,您希望能够在不影响上述内容的情况下抽象地组合更新函数。

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

  • 在完全共享状态和每个功能所需的细节之间需要一个中间层,允许独立于其余部分投射和替换状态片段。

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

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

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

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

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

  • 使共享状态成为记录类型,其中包含类似STReffor each 的内容,并将字段选择器与STmonad 更新操作结合起来以提供接口层。

  • Make the shared state a collection of TChans, with separate threads to read/write them as appropriate to communicate asynchronously with an external data store.

Or any number of other variations.

于 2011-12-16T22:56:12.343 回答
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)]
于 2011-12-16T22:38:43.850 回答