当我迫切需要在Haskell中使用对象风格的多态消息传递时,该怎么办



我有一个关于这个问题的后续问题。在面向对象的语言中,与多态类级常量等效的惯用Haskell是什么?


我正在尝试使用事件存储和Haskell进行事件源。我一直在努力找出保存和加载事件的逻辑。

事件存储基于事件流的概念;在面向对象的领域模型中,事件流和聚合之间通常存在1:1的关系。您可以将流组织为类别;通常,在域模型中,每个聚合类都有一个类别。以下是如何在C#中对其进行建模的示意图:

interface IEventStream<T> where T : Event
{
string Category { get; }
string StreamName { get; }
IEnumerable<T> Events { get; }
}
class PlayerEventStream : IEventStream<PlayerEvent>
{
public string Category { get { return "Player"; } }
public string StreamName { get; private set; }
public IEnumerable<PlayerEvent> Events { get; private set; }
public PlayerEventStream(int aggregateId)
{
StreamName = Category + "-" + aggregateId;
}
}
class GameEventStream : IEventStream<GameEvent>
{
public string Category { get { return "Game"; } }
public string StreamName { get; private set; }
public IEnumerable<GameEvent> Events { get; private set; }
public GameEventStream(int aggregateId)
{
StreamName = Category + "-" + aggregateId;
}
}
class EventStreamSaver
{
public void Save(IEventStream stream)
{
CreateStream(stream.StreamName);
AddToCategory(stream.StreamName, stream.Category);
SaveEvents(stream.StreamName, stream.Events);
}
}

此代码确保GameEvent永远不会被发送到播放器的事件流,反之亦然,并且确保所有类别都被正确分配。我为Category使用多态常量来帮助保护这个不变量,并使以后添加新的流类型变得容易。

这是我第一次尝试将这个结构翻译成Haskell:

data EventStream e = EventStream AggregateID [e]
streamName :: Event e => EventStream e -> String
streamName (EventStream aggregateID (e:events)) = (eventCategory e) ++ '-':(toString aggregateID)
class Event e where
eventCategory :: e -> String
-- and some other functions related to serialisation
instance Event PlayerEvent where
eventCategory _ = "Player"
instance Event GameEvent where
eventCategory _ = "Game"
saveEventStream :: Event e => EventStream e -> IO ()
saveEventStream stream@(EventStream id events) =
let name = streamName stream
category = eventCategory $ head events
in do
createStream name
addToCategory name category
saveEvents name events

这太难看了。类型系统要求eventCategory在其签名中的某个位置提及e,即使它在函数中的任何位置都没有使用。如果流中不包含事件(因为我试图将类别附加到事件类型),它也会失败。

我知道我正在尝试用Haskell编写C#——有没有更好的方法来实现这种类型的多态常量?


更新:根据请求,以下是我认为do块中(目前未实现)存根应该具有的类型签名:

type StreamName = String
type CategoryName = String
createStream :: StreamName -> IO ()
addToCategory :: StreamName -> CategoryName -> IO ()
saveEvents :: Event e => StreamName -> [e] -> IO ()

这些函数将负责与数据库通信——设置模式并串行化事件。

有些人建议存在类型,但除非我有严重误解,否则您希望将某些偶数流限制为某些类型。

首先,

data EventStream e = EventStream AggregateID [e]
streamName :: Event e => EventStream e -> String
streamName (EventStream aggregateID (e:events)) = (eventCategory e) ++ '-':(toString aggregateID)

应该看起来很奇怪。您在第一个偶数调用eventCategory,然后丢弃其余的,因此您假设所有事件的类别都是相同的。当然,eventCategory可以为事件的不同值返回不同的字符串。如果没有事件,则必须执行eventCategory undefined

一个想法是改变eventCategory:的类型

data Proxy p = Proxy 
class Event e where
eventCategory :: Proxy e -> String

现在,函数不可能为事件的不同值返回不同的字符串,因为它无法访问实际值。换句话说,eventCategory只取决于类型,而不是值。

另一种可能性是遵循c#代码,即类别是流的属性,而不是事件:

{-# LANGUAGE MultiParamTypeClasses #-}
import Data.ByteString 
class Event e where 
deserialize :: ByteString -> e 
... other stuff
class Event e => EventStream t e where 
category :: t e -> String
aggregateId :: t e -> Int 
events :: t e -> [e] 
name :: t e -> String 
name s = category s ++ "-" ++ show (aggregateId s)

EventStream类型类与接口紧密对应。

注意name是如何在typeclass中的,但是您可以在不知道使用哪个实例的情况下编写它。您可以很容易地将其从类型类中移出,但实现可能会决定它将定义一个自定义名称,该名称将覆盖默认定义。

然后定义您的事件:

data PlayerEvent = ...
instance Event PlayerEvent where ...
data GameEvent = ...
instance Event GameEvent where ...

现在流类型:

data PlayerEventStream e = PES Int [e] 
instance EventStream PlayerEventStream PlayerEvent where 
category = const "Player" 
aggregateId (PES n _) = n
events (PES _ e) = e 
data GameEventStream e = GES Int [e] 
instance EventStream GameEventStream GameEvent where 
category = const "Game" 
aggregateId (GES n _) = n
events (GES _ e) = e 

请注意,事件类型是同构的,但仍然是不同的类型。GameEventStream中不能有PlayerEvents(或者更确切地说,可以,但包含PlayerEvents的GameEventStream没有EventStream实例)。你甚至可以加强这种关系:

class Event e => EventStream t e | t -> e where 

这意味着对于给定的流类型,可能只存在一个事件类型,因此定义两个这样的实例是一个类型错误:

instance EventStream PlayerEventStream PlayerEvent where 
instance EventStream PlayerEventStream GameEvent where 

保存功能微不足道:

saveEventStream :: EventStream t e => t e -> IO ()
saveEventStream s = do 
createStream (name s)
addToCategory (name s) (category s) 
saveEvents (name s) (events s) 

我不知道这是否是你真正想要的,但它似乎完成了与c代码相同的事情。

我想向您展示如何在不使用类型类的情况下实现这一点。我经常发现结果更简单。

首先,这里有一些关于您可能使用的类型的猜测。您提供了其中一些:

type CategoryName = String
type Name = String
type PlayerID = Int
type StreamName = String
data Move = Left | Right | Wait
data PlayerEvent = PlayerCreated Name | NameUpdated Name
data GameEvent = GameStarted PlayerID PlayerID | MoveMade PlayerID Move

记录类型通常是类的有用替代品。在该示例中,CCD_ 18将是CCD_ 19或CCD_。

data EventStream e = EventStream
{ category :: String
, name :: String
, events :: [e]
}

在C#中,您有覆盖Category属性的子类。在Haskell中,您可以为此目的使用智能构造函数。如果您的C#子类是重写方法,那么在Haskell中,您将在EventStream e类型中包含函数。通过只公开以下智能构造函数,可以防止程序中的其他模块创建无效的EventStream对象:

-- generalized smart constructor
mkEventStream :: CategoryName -> Int -> EventStream e
mkEventStream cat ident = EventStream cat (cat ++ " - " ++ show ident) []
playerEventStream :: Int -> EventStream PlayerEvent
playerEventStream = mkEventStream "Player"
gameEventStream :: Int -> EventStream GameEvent
gameEventStream = mkEventStream "Game"

最后,您定义了一些函数。以下是它们的写法:

createStream :: StreamName -> IO ()
createStream = undefined
addToCategory :: StreamName -> CategoryName -> IO ()
addToCategory = undefined
saveEvents :: EventStream e -> IO ()
saveEvents = undefined
saveEventStream :: EventStream e -> IO ()
saveEventStream stream = do
createStream name'
addToCategory name' (category stream)
saveEvents stream
where
name' = name stream

这更接近于您的C#示例所做的操作。流的名称在创建时是固定的,类别是每个流的一部分,而不是链接到每个元素的类型。

最新更新