通过仅返回一种数据构造函数而不是类型构造函数的类型来筛选列表



假设我有以下数据类型:

data CommandRequest  = CreateWorkspace {commandId :: UUID , workspaceId ::UUID }
| IntroduceIdea {commandId :: UUID , workspaceId ::UUID , ideaContent :: String} deriving (Show,Eq)

{-# LANGUAGE DataKinds #-}

我想实现以下功能(伪代码):

filter :: [CommandRequest] -> [CreateWorkspace] (promoting the data constructor to a type level)

你能帮我实现该功能吗?...谢谢!

给定一个Haskell类型,如下所示:

data Foo = Bar Int | Baz String

即使使用DataKinds扩展名,也没有直接的方法写下表示用Bar构造的Foo型值子集的新类型。

特别是,当您打开DataKinds时,您获得的Bar类型不是值的类型Bar 1Bar 2. 事实上,新的提升Bar类型实际上与值Bar 1Bar 2没有任何关系,除了它们共享名称Bar。 这与显式定义没有什么不同:

data True = TrueThing

这个新类型True与类型Bool的值True无关,只是它们恰好具有相同的名称。

假设您要做的是找到一种类型安全的方式来表示筛选结果,CommandRequest仅针对使用CreateWorkspace构造函数构造的那些值,以便您不能"意外"让IntroduceIdea潜入您的列表,您将不得不采取另一种方法。 有几种可能性。

最直接的方法(根本不需要任何特殊的类型级编程)是将CreateWorkspaceIntroduceIdea表示为单独的类型:

{-# LANGUAGE DuplicateRecordFields #-}
data CreateWorkspace = CreateWorkspace
{ commandId :: UUID
, workspaceId ::UUID
} deriving (Show)
data IntroduceIdea = IntroduceIdea
{ commandId :: UUID
, workspaceId ::UUID
, ideaContent :: String
} deriving (Show)

然后创建一个新的代数和类型来表示这些单独类型的不相交并集:

data CommandRequest
= CreateWorkspace' CreateWorkspace
| IntroduceIdea' IntroduceIdea
deriving (Show)

请注意,我们使用刻度将这些构造函数与基础组件类型中使用的构造函数区分开来。 一个简单的变体是将公共字段(如commandId,也许还有workSpaceId)移动到CommandRequest类型中。 这可能有意义,也可能没有意义,具体取决于您要完成的任务。

无论如何,这增加了一点语法绒毛,但定义起来很简单:

filterCreateWorkspace :: [CommandRequest] -> [CreateWorkspace]
filterCreateWorkspace crs = [ cw | CreateWorkspace' cw <- crs ]

以及一些额外的"构造函数":

createWorkspace :: UUID -> UUID -> CommandRequest
createWorkspace u1 u2 = CreateWorkspace' (CreateWorkspace u1 u2)
introduceIdea :: UUID -> UUID -> String -> CommandRequest
introduceIdea u1 u2 s = IntroduceIdea' (IntroduceIdea u1 u2 s)

创建和过滤[CommandRequest]列表并不难:

type UUID = Int
testdata1 :: [CommandRequest]
testdata1
= [ createWorkspace 1 2
, createWorkspace 3 4
, introduceIdea 5 6 "seven"
]
test1 = filterCreateWorkspace testdata1

给:

> test1
[CreateWorkspace {commandId = 1, workspaceId = 2}
,CreateWorkspace {commandId = 3, workspaceId = 4}]

这几乎可以肯定是做你想做的事情的正确方法。 我的意思是,这正是代数数据类型的用途。 这就是Haskell程序应该的样子。

"但是没有,"我听到你说! "我想花无尽的时间与令人困惑的类型错误作斗争! 我想爬下依赖型兔子洞。 你知道,出于'原因'。 我应该挡你的路吗? 一个人能对抗大海吗?

如果您确实想在类型级别执行此操作,您仍然希望为两个构造函数定义单独的类型:

data CreateWorkspace = CreateWorkspace
{ commandId :: UUID
, workspaceId ::UUID
} deriving (Show)
data IntroduceIdea = IntroduceIdea
{ commandId :: UUID
, workspaceId ::UUID
, ideaContent :: String
} deriving (Show)

和以前一样,这使得表示类型[CreateWorkspace]的列表变得容易。 现在,在类型级别工作的关键是找到一种方法,使表示类型[CommandRequest]列表尽可能困难。 一个标准的方法是引入一个CommandRequest类型类,其中包含我们两种类型的实例,以及一个存在类型来表示属于该类的任意类型:

{-# LANGUAGE DuplicateRecordFields #-}
{-# LANGUAGE ExistentialQuantification #-}
type UUID = Int   -- for the sake of examples
data CreateWorkspace = CreateWorkspace
{ commandId :: UUID
, workspaceId ::UUID
} deriving (Show)
data IntroduceIdea = IntroduceIdea
{ commandId :: UUID
, workspaceId ::UUID
, ideaContent :: String
} deriving (Show)
class CommandRequest a where
maybeCreateWorkspace :: a -> Maybe CreateWorkspace
instance CommandRequest CreateWorkspace where
maybeCreateWorkspace c = Just c
instance CommandRequest IntroduceIdea where
maybeCreateWorkspace _ = Nothing
data SomeCommandRequest = forall t . CommandRequest t => SomeCommandRequest t

现在我们可以定义:

import Data.Maybe
filterCreateWorkspace :: [SomeCommandRequest] -> [CreateWorkspace]
filterCreateWorkspace = catMaybes . map getCW
where getCW (SomeCommandRequest cr) = maybeCreateWorkspace cr

这工作正常,尽管语法仍然有点麻烦:

testdata2 :: [SomeCommandRequest]
testdata2 = [ SomeCommandRequest (CreateWorkspace 1 2)
, SomeCommandRequest (CreateWorkspace 3 4)
, SomeCommandRequest (IntroduceIdea 5 6 "seven")
]
test2 = print $ filterCreateWorkspace testdata2

测试给出:

> test2
[CreateWorkspace {commandId = 1, workspaceId = 2}
,CreateWorkspace {commandId = 3, workspaceId = 4}]

这个解决方案的尴尬之处在于我们需要一个类型类方法来标识CreateWorkspace类型。 如果我们想构造每个可能的构造函数的列表,我们需要为每个构造函数添加一个新的类型类方法,并且我们需要为每个实例的方法提供一个定义(尽管我们可以摆脱默认定义,它为除一个实例之外的所有实例返回Nothing,我猜)。 无论如何,这太疯狂了!

我们犯的错误是很难表示[CreateWorkspace]类型的列表,而不是荒谬的困难。 为了使它变得非常困难,我们仍然希望将两个构造函数表示为单独的类型,但我们将使它们成为数据系列的实例,该系列由构造函数名称键入,构造函数名称由DataKinds扩展提升到类型级别。现在这开始看起来像一个Haskell程序!

{-# LANGUAGE DataKinds #-}
{-# LANGUAGE DuplicateRecordFields #-}
{-# LANGUAGE TypeFamilies #-}
data CommandRequestC = CreateWorkspace | IntroduceIdea
data family CommandRequest (c :: CommandRequestC)
type UUID = Int   -- for the sake of examples
data instance CommandRequest CreateWorkspace
= CreateWorkspaceCR
{ commandId :: UUID
, workspaceId ::UUID
} deriving (Show)
data instance CommandRequest IntroduceIdea
= IntroduceIdeaCR
{ commandId :: UUID
, workspaceId ::UUID
, ideaContent :: String
} deriving (Show)

这是怎么回事? 好吧,我们引入了一种新的CommandRequestC类型(尾随C代表"构造函数"),其中包含两个构造函数CreateWorkspaceIntroduceIdea。 这些构造函数的唯一用途是使用DataKinds将它们提升到类型级别,以便将它们用作CommandRequest数据系列的类型级标记。 这是一种非常常见的使用DataKinds方式,也许是最常见的。 事实上,你给出的ReadResult 'RegularStream StreamSlice类型的例子正是这种用法。 类型:

data StreamType = All | RegularStream

没有携带有用的数据。 它存在的全部意义在于将构造函数提升到类型级标记AllRegularStream,以便ReadResult 'All StreamSliceReadResult 'RegularStream StreamSlice可用于命名两种不同的相关类型,就像CommandRequest 'CreateWorkspaceCommandRequest 'IntroduceIdea命名两种不同的相关类型一样。

此时,我们的两个构造函数有两个单独的类型,它们恰好通过标记的数据系列相关联,而不是通过类型类相关联。

testdata3 :: [CommandRequest 'CreateWorkspace]
testdata3 = [CreateWorkspaceCR 1 2, CreateWorkspaceCR 3 4]
testdata4 :: [CommandRequest 'IntroduceIdea]
testdata4 = [IntroduceIdeaCR 5 6 "seven"]

请注意,即使我们可以编写类型[CommandRequest c],将构造函数标记保留为未指定的类型变量c,我们仍然无法编写混合这些构造函数的列表:

testdata5bad :: [CommandRequest c]
testdata5bad = [CreateWorkspaceCR 1 2, CreateWorkspaceCR 3 4, 
IntroduceIdeaCR 5 6 "seven"]  -- **ERROR**

我们仍然需要我们的存在类型:

{-# LANGUAGE ExistentialQuantification #-}
data SomeCommandRequest = forall c . SomeCommandRequest (CommandRequest c)

和额外的存在主义语法:

testdata6 :: [SomeCommandRequest]
testdata6 = [ SomeCommandRequest (CreateWorkspaceCR 1 2)
, SomeCommandRequest (CreateWorkspaceCR 3 4)
, SomeCommandRequest (IntroduceIdeaCR 5 6 "seven")]

更糟糕的是,如果我们尝试编写一个过滤器函数,则不清楚如何实现它。 一个合理的第一次尝试是:

filterCreateWorkspace :: [SomeCommandRequest] -> [CommandRequest 'CreateWorkspace]
filterCreateWorkspace (SomeCommandRequest cr : rest)
= case cr of cw@(CreateWorkspaceCR _ _) -> cw : filterCreateWorkspace rest
_ -> filterCreateWorkspace rest

但这失败并显示有关无法匹配CreateWorkspace标记的错误。

问题在于数据族不够强大,无法让您推断您实际拥有的家族成员(即,crCreateWorkspaceCR还是IntroduceIdeaCR)。 在这一点上,我们可以回到使用类型类,或者引入代理或单例来维护存在类型中构造函数的值级别表示,但有一个更直接的解决方案。

GADT足够强大,可以推断cr的类型,我们可以将我们的数据家族重写为GADT。 不仅语法更简单:

{-# LANGUAGE DataKinds #-}
{-# LANGUAGE DuplicateRecordFields #-}
{-# LANGUAGE ExistentialQuantification #-}
{-# LANGUAGE GADTs #-}
{-# LANGUAGE StandaloneDeriving #-}
data CommandRequestC = CreateWorkspace | IntroduceIdea
type UUID = Int
data CommandRequest c where
CreateWorkspaceCR :: UUID -> UUID -> CommandRequest 'CreateWorkspace
IntroduceIdeaCR :: UUID -> UUID -> String -> CommandRequest 'IntroduceIdea
deriving instance Show (CommandRequest c)
data SomeCommandRequest = forall c . SomeCommandRequest (CommandRequest c)

但是我们可以毫不费力地实现我们的过滤功能:

filterCreateWorkspace :: [SomeCommandRequest] -> [CommandRequest 'CreateWorkspace]
filterCreateWorkspace crs
= [ cw | SomeCommandRequest cw@(CreateWorkspaceCR _ _) <- crs ]

定义一些有用的"构造函数":

createWorkspace :: UUID -> UUID -> SomeCommandRequest
createWorkspace u1 u2 = SomeCommandRequest (CreateWorkspaceCR u1 u2)
introduceIdea :: UUID -> UUID -> String -> SomeCommandRequest
introduceIdea u1 u2 s = SomeCommandRequest (IntroduceIdeaCR u1 u2 s)

并对其进行测试:

testdata7 :: [SomeCommandRequest]
testdata7 = [ createWorkspace 1 2
, createWorkspace 3 4
, introduceIdea 5 6 "seven"]
test7 = filterCreateWorkspace testdata7

这样:

> test4
[CreateWorkspaceCR 1 2,CreateWorkspaceCR 3 4]
>

这些看起来很熟悉吗? 它应该,因为它是@chi的解决方案。 它是唯一真正有意义的类型级解决方案,给出了你想要做的事情。

现在,通过几个类型别名和一些巧妙的重命名,从技术上讲,您可以获得所需的类型签名,如下所示:

{-# LANGUAGE DataKinds #-}
{-# LANGUAGE DuplicateRecordFields #-}
{-# LANGUAGE ExistentialQuantification #-}
{-# LANGUAGE GADTs #-}
{-# LANGUAGE StandaloneDeriving #-}
data CommandRequestC = CreateWorkspaceC | IntroduceIdeaC
type CreateWorkspace = ACommandRequest 'CreateWorkspaceC
type IntroduceIdea = ACommandRequest 'IntroduceIdeaC
type UUID = Int
data ACommandRequest c where
CreateWorkspaceCR :: UUID -> UUID -> CreateWorkspace
IntroduceIdeaCR :: UUID -> UUID -> String -> IntroduceIdea
deriving instance Show (ACommandRequest c)
data CommandRequest = forall c . CommandRequest (ACommandRequest c)
filterCreateWorkspace :: [CommandRequest] -> [CreateWorkspace]
filterCreateWorkspace crs
= [ cw | CommandRequest cw@(CreateWorkspaceCR _ _) <- crs ]
createWorkspace :: UUID -> UUID -> CommandRequest
createWorkspace u1 u2 = CommandRequest (CreateWorkspaceCR u1 u2)
introduceIdea :: UUID -> UUID -> String -> CommandRequest
introduceIdea u1 u2 s = CommandRequest (IntroduceIdeaCR u1 u2 s)
testdata8 :: [CommandRequest]
testdata8 = [ createWorkspace 1 2
, createWorkspace 3 4
, introduceIdea 5 6 "seven"]
test8 = filterCreateWorkspace testdata8

但这只是一个把戏,所以我不会太认真。

而且,如果所有这些看起来都是很多工作,并且让您对最终的解决方案感到不满意,那么欢迎来到类型级编程的世界! (实际上,这很有趣,但尽量不要期望太多。

可以使用列表推导式仅筛选通过特定构造函数获取的那些值。请注意,列表的类型不会更改。

filter :: [CommandRequest] -> [CommandRequest]
filter xs = [ x | x@(CreateWorkspace{}) <- xs ]

如果你想要更精确的类型,你需要更复杂的类型级机制,如GADT。

这是一种未经测试的方法。您需要启用一些扩展程序。

data CR = CW | II  -- to be promoted to "kinds"
-- A more precise, indexed type
data CommandRequestP (k :: CR) where
CreateWorkspace :: {commandId :: UUID, workspaceId ::UUID }
-> CommandRequestP 'CW
IntroduceIdea :: {commandId :: UUID, workspaceId ::UUID, ideaContent :: String}
-> CommandRequestP 'II
-- Existential wrapper, so that we can build lists
data CommandRequest where
C :: CommandRequestP k -> CommandRequest
filter :: [CommandRequest] -> [CommandRequestP 'CW]
filter xs = [ x | C (x@(CreateWorkspace{})) <- xs ]

最新更新