使用 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,我们不能自动导出 Eq
、Show
和 Read
。我们必须自己写实例。
此外,当我们不想打扰 DBType
类型索引时,我们需要辅助 SomeDBVal
包装器。
VArray
和 VRecord
包含 SomeDBVal
,因此我们无法在子组件上使用更精确的 get
版本。
虽然我不确定我是否理解这样做的动机。如果您的 DBVal
是从外部来源获得的,则它们到达时不太可能带有足够的信息“标记”以将它们识别为 VRecord
。所以看来你在任何情况下都需要进行模式匹配。
其他答案对我来说看起来很复杂。我认为它应该很简单:不要取 DBVal
,取你知道的构造函数的字段。所以:
get :: [DBField] -> Keyword -> DBVal
get fields = ...
类似地定义对记录的其他操作。然后,编译器将强制调用者 知道 他们的 DBVal
是 VRecord
—— 因为他们无法访问内部的 [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
我有这样的代数数据类型:
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,我们不能自动导出 Eq
、Show
和 Read
。我们必须自己写实例。
此外,当我们不想打扰 DBType
类型索引时,我们需要辅助 SomeDBVal
包装器。
VArray
和 VRecord
包含 SomeDBVal
,因此我们无法在子组件上使用更精确的 get
版本。
虽然我不确定我是否理解这样做的动机。如果您的 DBVal
是从外部来源获得的,则它们到达时不太可能带有足够的信息“标记”以将它们识别为 VRecord
。所以看来你在任何情况下都需要进行模式匹配。
其他答案对我来说看起来很复杂。我认为它应该很简单:不要取 DBVal
,取你知道的构造函数的字段。所以:
get :: [DBField] -> Keyword -> DBVal
get fields = ...
类似地定义对记录的其他操作。然后,编译器将强制调用者 知道 他们的 DBVal
是 VRecord
—— 因为他们无法访问内部的 [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