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
。这个单元几乎是一个