使用 Haskell ADT 成员的构造函数作为类型

Using Haskell ADT members' constructors as types

我有这样的代数数据类型:

type Keyword = Text

data DBField = (:=) Keyword DBVal
  deriving (Show, Read, Eq)

data DBVal = VNull
           | VInt Int64
           | VDouble Double
           | VBool Bool
           | VString Text
           | VUTCTime UTCTime
           | VArray [DBVal]
           | VObjId ObjectId
           | VUUID UUID
           | VRecord [DBField]
  deriving (Eq, Show, Read)

因为我的 VRecord 有很多操作,我需要一种方法来定义像这样的函数:

get :: VRecord -> Keyword -> Maybe DBVal
get r k = ...

但由于 VRecord 是一个数据构造函数,我必须这样定义我的函数:

get :: DBVal -> Keyword -> Maybe DBVal
get r@(VRecord _) k = ...
get _ _ = ...

这既降低了可读性(主要是在文档中),也迫使我处理其他 DBVal 类型的情况。那么处理这种情况的最佳方法是什么?

这可以用 Liquid Haskell:

module DB where

import Data.Text
import Data.Int
import Data.Time

type Keyword = Text

data DBField = (:=) Keyword DBVal
  deriving (Show, Read, Eq)

data DBVal = VNull
           | VInt Int64
           | VDouble Double
           | VBool Bool
           | VString Text
           | VUTCTime UTCTime
           | VArray [DBVal]
           | VRecord [DBField]
  deriving (Eq, Show, Read)

{-@ measure isVRecord @-}
isVRecord (VRecord _) = True
isVRecord _ = False

{-@ type VRecord = {v:DBVal | isVRecord v} @-}

{-@ get :: VRecord -> Keyword -> Maybe DBVal @-}
get :: DBVal -> Keyword -> Maybe DBVal
get (VRecord xs) k = undefined

test :: Maybe DBVal
test = get VNull (pack "test") -- error

在这里试试:http://goto.ucsd.edu:8090/index.html#?demo=permalink%2F1629647897_38731.hs

也许我们可以使用 DataKinds 提升一个辅助和类型,然后将 DBVal 变成一个由辅助类型索引的 GADT。一个简化的例子:

{-# LANGUAGE DataKinds #-}
{-# LANGUAGE GADTs #-}
{-# LANGUAGE KindSignatures #-}
data DBType = TNull
            | TInt
            | TDouble
            | TBool
            | TString
            | TArray 
            | TRecord 
  deriving (Eq, Show, Read)

data DBVal (t :: DBType) where
  VNull :: DBVal TNull
  VInt :: Int -> DBVal TInt
  VDouble :: Double -> DBVal TDouble
  VBool :: Bool -> DBVal TBool
  VString :: String -> DBVal TString
  VArray :: [SomeDBVal] -> DBVal TArray
  VRecord :: [DBField] -> DBVal TRecord

data SomeDBVal where
    SomeDBVal :: DBVal t -> SomeDBVal

现在我们可以写一个像这样的函数

get :: DBVal TRecord -> Keyword -> Maybe SomeDBVal
get r@(VRecord _) k = ...

无需考虑其他分支。详尽检查器知道唯一可能的分支是 VRecord 并且不会抱怨。

因为 DBVal 现在是一个 GADT,我们不能自动导出 EqShowRead。我们必须自己写实例。

此外,当我们不想打扰 DBType 类型索引时,我们需要辅助 SomeDBVal 包装器。

VArrayVRecord 包含 SomeDBVal,因此我们无法在子组件上使用更精确的 get 版本。


虽然我不确定我是否理解这样做的动机。如果您的 DBVal 是从外部来源获得的,则它们到达时不太可能带有足够的信息“标记”以将它们识别为 VRecord。所以看来你在任何情况下都需要进行模式匹配。

其他答案对我来说看起来很复杂。我认为它应该很简单:不要取 DBVal,取你知道的构造函数的字段。所以:

get :: [DBField] -> Keyword -> DBVal
get fields = ...

类似地定义对记录的其他操作。然后,编译器将强制调用者 知道 他们的 DBValVRecord —— 因为他们无法访问内部的 [DBField] 除非他们(或他们的祖先)使用模式匹配来动态检查属性。

如果你真的需要,你可以定义一个提升操作来一劳永逸地做这个模式匹配,但我可能不会;在几乎所有情况下,我都会代替调用者进行按摩,因为对于如何处理非记录的值,他们总是会有自己的想法。但是,如果你真的想要这样的电梯,它可能看起来像

onRecord :: a -> ([DBField] -> a) -> (DBVal -> a)
onRecord def f val = case val of
    VRecord fields -> f vields
    _ -> def

然后你有,例如,

onRecord (error "not a record") get :: DBVal -> Keyword -> DBVal