Haskell &引用;“嵌入/继承”;一个“数据”构造函数在另一个构造函数中?
考虑以下片段:Haskell &引用;“嵌入/继承”;一个“数据”构造函数在另一个构造函数中?,haskell,Haskell,考虑以下片段: data File = NoFile | FileInfo { path :: FilePath, modTime :: Data.Time.Clock.UTCTime } | FileFull { path :: FilePath, modTime :: Data.Time.Clock.UTCTime, content :: String } deri
data File
= NoFile
| FileInfo {
path :: FilePath,
modTime :: Data.Time.Clock.UTCTime
}
| FileFull {
path :: FilePath,
modTime :: Data.Time.Clock.UTCTime,
content :: String
}
deriving Eq
这种重复有点“疣”,尽管在这种一次性的情况下并不特别痛苦。为了进一步提高我对Haskell丰富类型系统的理解,除了简单地为两个重复字段创建一个单独的数据
记录类型(然后用新的数据
类型的单个字段替换它们)之外,重构其他的“干净”/“惯用”方法可能更可取或者将FileFull
记录符号替换为类似|FileFull File String
的内容,这也不是很干净(因为这里只需要FileInfo
,而不是NoFile
)
(这两种“幼稚”的方法都有点侵入性/恼人,因为它们必须在代码库的其余部分手动安装许多模块。)
我考虑的一件事是这样的参数化:
data File a
= NoFile
| FileMaybeWithContent {
path :: FilePath,
modTime :: Data.Time.Clock.UTCTime
content :: a
}
deriving Eq
那么对于那些“只是信息,没有加载”的上下文,a
将是()
,否则字符串
。看起来太笼统了,我们要么要字符串
要么什么都不要,这会导致我们找到也许
,再次去掉参数
当然我们以前也有过:content
当然可以用或者String
来完成,然后“重构所有编译错误”并“完成”。这可能是当前的趋势,但了解Haskell和许多时髦的GHC扩展。。谁知道我到底错过了什么诡计/公理/法则,对吧?!请参阅,“just meta data info”值和“file content with meta info”值之间的不同名称的“semantic insta differentior”在代码库的其余部分都能很好地发挥作用
(是的,我也许应该删除NoFile
并在整个过程中使用Maybe File
s,但是……我不确定是否真的有这样做的充分理由,以及一个完全不同的问题……)以下所有内容都是等效/同构的,我想你已经发现了:
data F = U | X A B | Y A B C
data F = U | X AB | Y AB C
data AB = AB A B
data F = U | X A B (Maybe C)
因此,车棚的颜色实际上取决于上下文(例如,您是否在其他地方使用了AB
)和您自己的审美偏好
它可能会澄清一些事情,并帮助你理解你在做什么,从而对你的行为有所了解
我们将之类的类型称为“总和类型”或(,)
之类的类型称为“产品类型”,它们受到与您熟悉的类似分解的相同类型的转换
f = 1 + (a * b) + (a * b * c)
= 1 + ((a * b) * ( 1 + c))
正如其他人所指出的那样,NoFile
构造函数可能不是必需的,但是如果需要,您可以保留它。如果您觉得您的代码更具可读性和/或更易于理解,那么我建议保留它
现在,组合其他两个构造函数的诀窍是隐藏content
字段。通过参数化文件
,您走上了正确的道路,但这还不够,因为我们可以使用文件Foo
,文件栏
,等等。幸运的是,GHC有一些巧妙的方法来帮助我们
我将在这里写出代码,然后解释它是如何工作的
{-# LANGUAGE TypeFamilies #-}
{-# LANGUAGE DataKinds #-}
import Data.Void
data Desc = Info | Full
type family Content (a :: Desc) where
Content Full = String
Content _ = Void
data File a = File
{ path :: FilePath
, modTime :: UTCTime
, content :: Content a
}
这里发生了一些事情
首先,请注意,在文件
记录中,内容
字段现在的类型是内容a
,而不仅仅是a
<代码>内容
是一个类型族,在我看来,它是一个令人困惑的类型级函数名称。也就是说,编译器根据a
是什么以及我们如何定义Content
用其他类型替换Content a
我们将Content Full
定义为String
,这样当我们有一个值f1::File Full
时,它的内容字段将有一个String
值。另一方面,f2::File Info
将有一个类型为Void
的content
字段,该字段没有值
酷吧?但是,是什么阻止我们现在拥有文件Foo
这就是datacateges
拯救的地方。它将数据类型Desc
提升为种类(Haskell中的类型),将类型构造函数Info
和Full
提升为种类Desc
,而不仅仅是类型Desc
的值
请注意,在内容的声明中,我已经注释了a
。它看起来像一个类型注释,但a
已经是一个类型。这是一种注释。它迫使a
成为某种类型的Desc
,唯一的Desc
类型是Info
和Full
到现在为止,你可能完全相信这是多么棒,但我应该警告你,没有免费的午餐。特别是,这是一个编译时结构。您的单个文件
类型将变成两种不同的类型。这可能导致其他相关逻辑(文件
记录的生产者和消费者)变得复杂。如果您的用例没有将文件信息
记录与文件完整
记录混合在一起,那么这就是解决方法。另一方面,如果您想创建一个文件
记录列表,这可以是两种类型的混合,那么您最好只创建内容
字段可能是字符串
另一件事是,既然content
字段没有Void
的值,那么如何准确地创建File Info
?嗯,从技术上讲,使用未定义的或错误“这永远不会发生”
,应该是可以的,因为(从道德上)不可能有Void->a
类型的函数,但是如果这让你感到不安(而且可能应该),那么就用()
替换Void
。这个单元几乎是一个