“嵌入/继承”另一个`data`构造函数?

时间:2017-02-02 17:40:22

标签: haskell

考虑以下片段:

data File
    = NoFile
    | FileInfo {
        path :: FilePath,
        modTime :: Data.Time.Clock.UTCTime
    }
    | FileFull {
        path :: FilePath,
        modTime :: Data.Time.Clock.UTCTime,
        content :: String
    }
    deriving Eq

这种复制有点像“疣”,尽管在这种一次性的例子中并不是特别痛苦。为了进一步提高我对Haskell丰富类型系统的理解,可能首选“干净”/“惯用”方法来重构其他而不是 只需创建一个单独的{{ 1}} 2个重复字段的记录类型(然后将其替换为新data类型的单个字段)data记录符号替换为{{1}这也不是很干净(因为这里只需要FileFull,而不是| FileFull File String)?

(这两种“天真”的方法对于必须在这里的其余代码库中手动修复许多模块而言会有点干扰/烦恼。)

我考虑的一件事就是参数化:

FileInfo

然后对于那些“只是信息,未加载”的上下文NoFile将为data File a = NoFile | FileMaybeWithContent { path :: FilePath, modTime :: Data.Time.Clock.UTCTime content :: a } deriving Eq ,否则为a。无论如何,似乎太笼统,我们要么()或者没有,要么引导我们String,再次使用String参数。

当然我们以前去过那里:Maybe当然可以用a完成,然后“重构任何编译错误”和“完成”。那可能是一天的顺序,但是知道Haskell和许多时髦的GHC扩展.. 谁知道 什么异乎寻常的理论技巧/公理/法律我是失踪了,对吧?!请参阅,“只是元数据信息”值和“具有元信息的文件内容”值之间的不同命名的“语义insta-differentialator”在代码库的其余部分中确实能够很好地理解。 / p>

(是的,我可能应该删除content并在整个过程中使用Maybe String,但是......不确定是否确实存在实体理由完全是一个不同的问题..)

2 个答案:

答案 0 :(得分:4)

以下所有内容都是等效/同构的,我认为您已经发现:

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?)以及你自己的审美偏好。

它可能会澄清事情并帮助您了解您正在做些什么来理解the algebra of algebraic data types

我们称Either类型为“和类型”类型和(,)类型“类型”等类型,它们受制于您熟悉的相同类型的转换,例如分解

f = 1 +            (a * b) + (a * b * c) 
  = 1 + ((a * b) * (  1    +          c))

答案 1 :(得分:3)

正如其他人所说,NoFile构造函数可能不是必需的,但如果需要,可以保留它。如果您觉得您的代码更易读和/或更好理解,那么我说保留它。

现在结合其他两个构造函数的技巧是隐藏content字段。通过参数化File,您走在了正确的轨道上,但仅此一点是不够的,因此我们可以File FooFile Bar等。幸运的是,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
  }

这里有一些事情发生。

首先请注意,在File记录中,content字段现在的类型为Content a,而不仅仅是aContent是一个类型系列,(在我看来)是类型级函数的混淆名称。也就是说,编译器根据Content a是什么以及我们如何定义a来替换其他类型的Content

我们将Content Full定义为String,因此当我们有值f1 :: File Full时,其内容字段将具有String值。另一方面,f2 :: File Info将有content字段,类型为Void,没有值。

酷吧?但是什么阻止我们现在拥有File Foo

这就是DataKinds来救援的地方。它将数据类型Desc“提升”为一种类型(Haskell中的类型类型),并将类型构造函数InfoFull“提升为类型Desc而不是类型Desc仅仅是Content类型的值。

请注意a我已注释a的声明。它看起来像一个类型注释,但a已经是一个类型。这是一种注释。它强制Desc成为善意的Desc,而InfoFull只有FileFile

到现在为止你可能已经完全卖掉了,但是我应该警告你没有免费的午餐。特别是,这是一个 compile -time构造。您的单File Info类型会变成两种不同的类型。这可能导致其他相关逻辑(File Full记录的生产者和消费者)变得复杂。如果您的用例不会将File条记录与content条记录混合在一起,那么这就是您的选择。另一方面,如果你想做一些像Maybe String个记录的列表,这些记录可以是两种类型的混合,那么你最好只是制作File Info字段的类型{ {1}}。

另一件事是,您如何制作Void,因为content字段没有undefined的价值?好吧,从技术上来说,使用error "this should never happen"Void -> a应该没问题,因为(道德上)不可能有Void类型的函数,但如果这让你感到不安(可能应该),然后用()替换>>>first = ['hello', 'hey', 'hi', 'hey'] >>>second = ['hey', 'hi', 'hello'] 。单位几乎没用,也不需要底部的“值”。