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

时间:2018-09-25 15:18:17

标签: haskell

因此,假设我具有以下数据类型:

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)

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

2 个答案:

答案 0 :(得分:2)

给出Haskell类型,例如:

data Foo = Bar Int | Baz String

没有直接的方式来记录表示类型Foo的值的子集的新类型,该类型是由Bar偶数和{{1 }}扩展名。

尤其是,当您打开DataKinds时,获得的DataKinds类型不是值Bar和{{1}的类型。 }。实际上,新的提升的Bar 1类型实际上与值Bar 2Bar没有任何关系,只是它们共享名称Bar 1。与显式定义没有什么不同:

Bar 2

此新类型Bar与类型data True = TrueThing 的值True无关,只是它们名称相同。

假设您要进行的尝试是找到一种类型安全的方法,该方法表示仅对用True构造的那些值过滤Bool的结果构造函数,这样您就不能“偶然地”让CommandRequest进入您的列表,您必须采取另一种方法。有几种可能性。

最简单的方法完全不需要任何特殊的类型级编程,就是将CreateWorkspaceIntroduceIdea表示为单独的类型:

CreateWorkspace

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

IntroduceIdea

请注意,我们已使用刻度将这些构造函数与基础组件类型中使用的构造函数区分开。一个简单的变体就是将公共字段(例如{-# 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]

几乎可以肯定,这是完成您想做的事情的正确方法。我的意思是,这正是 的代数数据类型。这就是Haskell程序应该看起来的样子。

“但是不,”我听到你说! “我想花无休止的时间来解决令人困惑的类型错误!我想爬从属类型的兔子洞。你知道,是出于'原因'。”我应该阻挡你吗?一个人可以站在大海上吗?

如果您真的想在类型级别执行此操作,则 still 仍想为两个构造函数定义单独的类型:

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}] 的列表。现在,在类型级别上工作的关键将是找到一种方法,以使其尽可能难以表示类型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扩展名将其提升为类型级别。 现在,它开始看起来像Haskell程序!

[CreateWorkspace]

这是怎么回事?好吧,我们引入了一种新的类型DataKinds(尾随{-# 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) 代表“构造函数”),它带有两个构造函数CommandRequestCC。这些构造函数的唯一目的是使用CreateWorkspace将它们提升为类型级别,以便将它们用作IntroduceIdea数据系列的类型级别标记。这是使用DataKinds的一种非常常见的方式,也许是最常见的 。实际上,您给出的类型为CommandRequest的示例就是完全这种用法。类型:

DataKinds

不携带有用的数据。其存在的全部目的是将构造函数ReadResult 'RegularStream StreamSlicedata StreamType = All | RegularStream 提升为类型级别的标记,以便可以使用AllRegularStream来命名两个不同的相关类型,就像ReadResult 'All StreamSliceReadResult 'RegularStream StreamSlice命名两个不同的相关类型。

这时,我们的两个构造函数有两种单独的类型,它们恰巧是通过标记数据族而不是通过类型类关联的。

CommandRequest 'CreateWorkspace

请注意,即使我们可以编写类型CommandRequest '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还是cr) 。在这一点上,我们可以返回到类型类的工作,或者可以引入代理或单例来维护存在类型中构造函数的值级表示,但是有一个更直接的解决方案。

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

IntroduceIdeaCR

但是我们可以轻松实现过滤功能:

cr

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

{-# 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)

这看起来很熟悉吗?应该的,因为这是@chi的解决方案。这是唯一真正有意义的类型级解决方案,可以提供您要执行的操作。

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

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

但这只是一个把戏,所以我不会太在意它。

并且,如果这一切似乎都需要很多工作,并且让您对最终的解决方案感到不满意,那么欢迎您进入类型级编程领域! (实际上,这很有趣,但是不要期望过高。)

答案 1 :(得分:1)

您可以使用列表推导仅过滤通过特定构造函数获得的那些值。请注意,列表的类型不会改变。

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 ]