【问题标题】:In Haskell, how do you restrict functions to only one constructor of a data type?在 Haskell 中,如何将函数限制为只有一个数据类型的构造函数?
【发布时间】:2016-05-18 16:10:18
【问题描述】:

我不知道如何措辞这个问题。假设我正在尝试传递 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") 等。
  • 制作大量不同的数据类型VideoTmpFile、PictureTmpFile等。
  • 创建一个TmpFile 类型类
  • 处处使用偏函数,但添加保护函数以抽象模式匹配

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

编辑:我今天学到了很多东西!我尝试了下面列出的两种方法(DataKinds 和幻像类型,它们具有可以用另一个扩展删除的虚拟值构造函数),它们都有效!然后我试着走得更远一点。除了常规的TmpFile a 之外,它们都允许您创建嵌套类型TmpFile (ListOf 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

(这似乎是一件奇怪的事情,但我的实际程序将是一种小型语言的解释器,它编译为 Shake 规则,每个变量对应一个 tmpfile,因此类型化的 tmpfile 列表将很有用)

这看起来对吗?我更喜欢DataKinds 的想法,所以如果我可以将它们作为值进行检查,或者事实证明这根本不需要,我会选择它。

【问题讨论】:

  • 这在一定程度上取决于您想要做什么-例如,对于我从您的问题中看到的内容,我确实会选择简单的(单独的)newtype 包装器,例如newtype VideoPath = VideoPath FilePath
  • 或采用 幻像类型 方法:data TempFile a = TempFile FilePath 和 data Videoformat(需要 GHC 扩展 EmptyDataDecls)然后 let videoFile = TempFile myPath :: TempFile Videoformat ...

标签: haskell types type-systems


【解决方案1】:

你是对的:使用-XDataKinds,TmpFile Video -&gt; FilePath 方法会起作用。事实上,我认为这可能是该扩展的一个很好的应用程序。

{-# LANGUAGE DataKinds #-}

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

videoPath :: TmpFile Video -> FilePath

你需要这个扩展来写TmpFile Video的原因是FileFormat的构造函数是从头开始的value-level(因此只存在于运行时),而TmpFile是type-级别/编译时间。

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

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

data TmpFile a = TmpFile a FilePath
  deriving Show

videoPath :: TmpFile Video -> FilePath

这种类型称为幻像类型。但实际上,它们有点像解决以前缺少适当类型级别值的问题,DataKinds 现在已经给了我们。因此,除非您需要与旧编译器兼容,否则请使用 DataKinds!

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

data TmpFile = TmpFile FileFormat FilePath
  deriving Show

videoPath :: TmpFile -> Maybe FilePath
videoPath (TmpFile Video p) = p
videoPath _ = Nothing

事实上,这种方法可能更合理,具体取决于您打算做什么。

【讨论】:

  • “幻象类型”通常是指由不使用的类型变量参数化的类型构造函数。
  • @DerekElkins:我认为它指的是 parameters,它实际上并没有在某些类型中使用。
  • 我同意,我会说“幻像类型变量”而不仅仅是“幻像类型”,但人们似乎不是这样使用它的。 Phantom type
  • 我想我可以删除我的答案,因为它基本上是你的减去数据类型的方法
【解决方案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. Video 和Picture 是通用概念,它们不仅仅绑定到您的临时文件,它们已经实现了一些标准接口。这意味着它们可以重复用于其他目的。

  2. Video 和 Picture 的定义中有一个明显的模式。


您在Video 和Picture 中看到的模式可以称为“细化类型”,并且是从the "refined" package 等中抽象出来的。所以你可能对此感兴趣。


至于你的其他选择:

  • 创建一个 TmpFile 类型类

  • 处处使用偏函数,但添加保护函数以抽象模式匹配

这对双方来说都是一个明确的“不”。不要培育类型类,让它们成为真正普遍的概念,这些概念背后有规律和可能的(类别)理论。该语言为您提供了丰富的其他抽象方法。也不要让部分函数爬到您的 API 中 - 社区中的共识是它是一种反模式。

【讨论】:

  • 好点,我想我会避免使用 DataKinds,因为它是另一种要记住的语言概念,而且我确实觉得它令人困惑(添加了刻度线,并且在模式匹配时省略了类型,即使它们显示在函数类型签名中)。不过,我也无法让模式匹配与细化类型一起使用,因为如果不先指定格式就无法获得实际值。例如我会尝试匹配listFiles (ListOf (fmt (TmpFile p))),如果不知道fmt,这是行不通的。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2013-08-14
  • 2020-09-18
  • 2012-05-01
  • 1970-01-01
  • 2020-04-30
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多