在Haskell中创建泛型类型类

时间:2017-06-15 14:21:19

标签: haskell generics cqrs

作为一个将我的大脑包裹在Haskell类型和类型类中的练习,我试图实现一个简单的DDD / CQRS样式解决方案。我在Lev Gorodinski's F# Simple CQRS实施后直接对其进行建模。

我实施了一个非常简单的Vehicle聚合,这或多或少是Lev InventoryItem聚合

的直接端口
module Vehicle where

data State = State { isActive :: Bool } deriving (Show)
zero = State { isActive = True }

type Mileage = Int 

data Command = 
    Create String Mileage
    | UpdateMileage Mileage
    | Deactivate
    deriving (Show)    

data Event =
    Created String
    | MileageUpdated Mileage 
    | Deactivated
    deriving (Show)

-- Define transitions from a command to one or more events
execute :: State -> Command -> [Event] 
execute state (Create name mileage)   = [Created name, MileageUpdated mileage]
execute state (UpdateMileage mileage) = [MileageUpdated mileage]
execute state Deactivate              = [Deactivated]    

-- Apply an event against the current state to get the new state
apply :: State -> Event -> State
apply state (Created _)        = state
apply state (MileageUpdated _) = state
apply state Deactivated        = state { isActive = False }

我试图弄清楚的部分是如何为域聚合创建更高级别的抽象,因为所有聚合都将由相同的组件部分组成。在Lev的例子中,他定义了一个类型Aggregate<'TState, 'TCommand, 'TEvent>,它允许他定义一个通用命令处理程序,它可以对付域聚合的任何实例。在Haskell中,这感觉就像我应该使用Typeclasses。当然,因为我不知道我在做什么,这可能是对它们用途的完全误解。

我的想法是Aggregate类型类将定义executeapply命令的接口,以及某种类型需要关联类型来表示其状态,命令,和事件。从那里我可以定义一些通用命令处理程序,它能够对聚合的任何实例执行命令。例如

class Aggregate a where =
    execute :: state -> command -> [event]
    apply   :: state -> event   -> state

其中statecommandevent是表示给定Aggregate实例的状态,命令和事件的类型变量。当然,这不起作用。

我是否正在尝试正确使用类型类?如果是这样,我应该如何定义类,使Aggregate的实例必须具有相应的State,Command和Event类型?

如果这是错误的方法,我应该如何定义Aggregate以创建更高级别的抽象?

更新

按照@ jberryman的建议,我使用MPTC来定义我的Aggregate,这允许我创建我正在寻找的通用命令处理程序:

{-# LANGUAGE MultiParamTypeClasses #-}
{-# LANGUAGE FunctionalDependencies #-}

module Aggregate where

class Aggregate state command event
    | state -> command event, command -> state event, event -> state command where
        execute :: state -> command -> [event]
        apply   :: state -> event   -> state
        zero    :: state

makeHandler (load, commit) = 
    (\ (id, expectedVersion) command -> 
        do events <- load id
           let state = foldl apply zero events
           let newEvents = execute state command
           commit (id, expectedVersion) newEvents)

这导致我的Vehicle模块中的以下实例声明

instance Aggregate State Command Event where
    execute = Vehicle.execute
    apply   = Vehicle.apply
    zero    = Vehicle.zero

然后是一个示例脚本将它们捆绑在一起:

import Aggregate
import Vehicle

-- Mock out IO to a domain repository (EventStore)
load :: Int -> IO [Event]
load id = do return [Created "Honda", MileageUpdated 15000]
commit (id, expectedVersion) events = putStrLn (show events)

-- Create the handler provide a command to it
handler = makeHandler (load, commit)
handler (1,1) Deactivate

2 个答案:

答案 0 :(得分:4)

如果statecommandevent是特定实例所特有的三元组,则可以MultiParameterTypeClassesFunctionalDependencies一样使用

class Aggregate state command event | state -> command event, command -> state event, event -> state command  where
    execute :: state -> command -> [event]
    apply   :: state -> event   -> state

|读取后的fundeps:&#34;其中命令和事件由状态唯一确定,状态和事件由命令唯一确定,并且......&#34;如果我们可以推断实例头中的任何一种类型,这允许解析实例。你可以对类型系列做同样的事情。

但是在定义类型类之前你应该问自己三个问题:

  • 我是否打算在这个类中编写多态的有用代码?
  • 我希望用户能够定义我自己没有想过的实例吗?
  • 有没有法律?

如果答案对所有这些都不是,那么你可能不应该定义一个类型类。很难回答这是否属于这种情况。

但它可能是例如你真正想要的是:

data Aggregate state command where 
   Aggregate :: (state -> command -> [event]) -> (state -> event   -> state) -> Aggregate state command

这是一个隐藏event的GADT。

答案 1 :(得分:4)

我不熟悉Lev Gorodinski的F#示例,因此不能保证指向正确的方向,但我看一下它,看起来它的Aggregate类型的一个用途是到{{3}}。如果我们忽略这些函数所暗示的所有不纯操作,核心操作似乎是:

  • 折叠事件以获取当前状态
  • 执行针对该州的命令

因此,受@ jberryman的回答启发,您可以定义类似的类型类:

class Aggregate state command event | event -> state command where
  eventsToState :: [event] -> state
  execute :: state -> command -> [event]

为了编译它,您需要FunctionalDependencies语言扩展名。

现在,您可以基于该类型类编写处理函数的核心作为泛型函数:

handle :: Aggregate state command event => command -> [event] -> [event]
handle command = flip execute command . eventsToState

您现在可以为OP中定义的类型定义Aggregate的实例:

instance Aggregate State Command Event where
  eventsToState = foldl folder zero
    where
      folder state (Created _)        = state
      folder state (MileageUpdated _) = state
      folder state Deactivated        = state { isActive = False }
  execute _ (Create name mileage)   = [Created name, MileageUpdated mileage]
  execute _ (UpdateMileage mileage) = [MileageUpdated mileage]
  execute _ Deactivate              = [Deactivated]

在GHCI中尝试:

*Vehicle> handle (Create "BMW 520i" 250000) [] :: [Event]
[Created "BMW 520i",MileageUpdated 250000]
*Vehicle> handle (UpdateMileage 256000) it
[MileageUpdated 256000]
*Vehicle> handle Deactivate it
[Deactivated]

我没有在Lev Gorodinski的F#示例中进一步了解,所以我不确定这是否足够,但我希望这是一个开始。