在Haskell中,如何将函数仅限制为数据类型的一个构造函数?

时间:2016-02-08 20:53:33

标签: haskell types type-systems

我不确定如何说出这个问题。假设我试图传递tmpfiles的路径,我想捕获有不同格式的tmpfile的想法,并且每个函数仅适用于其中一个。这有效:

data FileFormat
  = Spreadsheet
  | Picture
  | Video
  deriving Show

data TmpFile = TmpFile FileFormat FilePath
  deriving Show

videoPath :: TmpFile -> FilePath
videoPath (TmpFile Video p) = p
videoPath _ = error "only works on videos!"

但是必须有更好的方法来编写没有运行时错误的权利吗?我想到了两个选择,这个:

type TmpSpreadsheet = TmpFile Spreadsheet
type TmpPicture     = TmpFile Picture
type TmpVideo       = TmpFile Video

videoPath :: TmpVideo -> FilePath

或者这个:

data TmpFile a = TmpFile a FilePath
  deriving Show

videoPath :: TmpFile Video -> FilePath

但显然他们没有编译。这样做的正确方法是什么?其他一些想法,没有特别吸引人的:

  • 以格式换行TmpFile而不是相反,因此值为Video (TmpFile "test.avi")等。
  • 制作大量单独的数据类型VideoTmpFilePictureTmpFile等。
  • 制作TmpFile类型类
  • 在任何地方使用部分函数,​​但添加保护函数以抽象模式匹配

我还考虑过学习-XDataKinds扩展,但怀疑我错过了一些更简单的东西,没有它就可以完成。

编辑:我今天学到了很多东西!我尝试了下面概述的两种方法(DataKinds和幻像类型,它们具有可以用另一个扩展名删除的虚拟值构造函数),它们都可以工作!然后我试着再往前走一点。除常规TmpFile (ListOf a)外,它们都允许您创建嵌套类型TmpFile a,这很酷。但是我暂时决定使用普通的幻像类型(完整的值构造函数),因为你可以对它们进行模式匹配。例如,我很惊讶这实际上有效:

data Spreadsheet = Spreadsheet deriving Show
data Picture     = Picture     deriving Show
data Video       = Video       deriving Show
data ListOf a    = ListOf a    deriving Show

data TmpFile a = TmpFile a FilePath
  deriving Show

videoPath :: TmpFile Video -> FilePath
videoPath (TmpFile Video p) = p

-- read a file that contains a list of filenames of type a,
-- and return them as individual typed tmpfiles
listFiles :: TmpFile (ListOf a) -> IO [TmpFile a]
listFiles (TmpFile (ListOf fmt) path) = do
  txt <- readFile path
  let paths = map (TmpFile fmt) (lines txt)
  return paths

vidPath :: TmpFile Video
vidPath = TmpFile Video "video1.txt"

-- $ cat videos.txt
-- video1.avi
-- video2.avi
vidsList :: TmpFile (ListOf Video)
vidsList = TmpFile (ListOf Video) "videos.txt"

main :: IO [FilePath]
main = do
  paths <- listFiles vidsList  -- [TmpFile Video "video1.avi",TmpFile Video "video2.avi"]
  return $ map videoPath paths -- ["video1.avi","video2.avi"]

据我所知,与DataKinds的等效内容非常相似,但无法作为值访问fmt

{-# LANGUAGE DataKinds, KindSignatures #-}

data FileFormat
  = Spreadsheet
  | Picture
  | Video
  | ListOf FileFormat
  deriving Show

data TmpFile (a :: FileFormat) = TmpFile FilePath
  deriving Show

vidPath :: TmpFile Video
vidPath = TmpFile "video.avi"

vidsList :: TmpFile (ListOf Video)
vidsList = TmpFile "videos.txt"

videoPath :: TmpFile Video -> FilePath
videoPath (TmpFile p) = p

listFiles :: TmpFile (ListOf a) -> IO [TmpFile a]
listFiles (TmpFile path) = do
  txt <- readFile path
  let paths = map TmpFile (lines txt)
  return paths

main :: IO [FilePath]
main = do
  paths <- listFiles vidsList
  return $ map videoPath paths

(这似乎是一个奇怪的事情,但我的实际程序将成为一个小语言的解释器,使用对应于每个变量的tmpfile编译Shake规则,因此键入的tmpfiles列表将很有用)

这看起来是对的吗?我更喜欢DataKinds的想法,所以如果我能将它们视为价值观,或者如果事实证明它永远不需要,我会选择它。

2 个答案:

答案 0 :(得分:5)

您是对的:使用-XDataKindsTmpFile Video -> FilePath方法可行。事实上,我认为这可能是该扩展的一个很好的应用。

{-# LANGUAGE DataKinds #-}

data TmpFile (a :: FileFormat) = TmpFile FilePath
  deriving Show

videoPath :: TmpFile Video -> FilePath

您需要此扩展名来编写TmpFile Video的原因是FileFormat的构造函数是ab initio value-level (因此仅在运行时存在),而{{ 1}}是类型级/编译时。

当然还有另一种生成类型级实体的方法:定义类型

TmpFile

此类型称为幻像类型。但实际上,他们可以解决以前缺乏正确的类型级别值的问题,而DataKinds现在给了我们这些价值。因此,除非您需要与旧编译器兼容,否则请使用DataKinds!

另一种方法是在编译时强制执行文件类型,而只是明确表示函数是部分的。

data Spreadsheet = Spreadsheet
data Picture = Picture
data Video = Video

data TmpFile a = TmpFile a FilePath
  deriving Show

videoPath :: TmpFile Video -> FilePath

事实上,这种方法可能更合理,取决于你计划做什么。

答案 1 :(得分:2)

首先,我建议不要使用像“DataKinds”这样的奇特扩展,除非你绝对需要它们。原因非常实用和通用:用于解决问题的语言概念越多,推理代码的难度就越大。

此外,“DataKinds”并不是一个容易理解的概念。它是一个同时跨越两个宇宙的过渡概念:价值观和类型。就个人而言,我觉得它很有争议,只有在没有其他选择时才会应用它。

在您的情况下,您已经找到了两种更简单地解决问题的方法,没有“DataKinds”:

  
      
  • 以格式而不是相反的方式包装TmpFile,因此值为Video(TmpFile“test.avi”)等。

  •   
  • 制作大量独立的数据类型VideoTmpFile,PictureTmpFile等。

  •   

我特别喜欢包装类型的想法,因为它具有灵活性和可组合性。以下是我如何积累它:

newtype Video a =
  Video a
  deriving (Functor, Foldable, Traversable)

newtype Picture a =
  Picture a
  deriving (Functor, Foldable, Traversable)

videoPath :: Video FilePath -> FilePath

您可以注意到两件事:

  1. VideoPicture是一般概念,它们不仅仅与您的临时文件绑定,而且它们已经实现了一些标准接口。这意味着它们可以重复用于其他目的。

  2. VideoPicture的定义中有明显的模式。

  3. 您在VideoPicture中看到的模式可以称为“优化类型”,并在the "refined" package等中抽象出来。所以你可能会对此感兴趣。

    至于你的其他选择:

      
        
    • 制作TmpFile类型类

    •   
    • 在任何地方使用部分函数,​​但添加保护函数以抽象模式匹配

    •   

    两者都是明确的“否”。不要繁殖类型类,留下它们是真正的一般概念,它们有法律和可能的(类别)理论。该语言为您提供了丰富的其他抽象方法。另外,不要让部分函数爬到你的API中 - 在社区中已经达成共识,认为它是反模式。