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

Jef*_*eff 10 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!"
Run Code Online (Sandbox Code Playgroud)

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

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

videoPath :: TmpVideo -> FilePath
Run Code Online (Sandbox Code Playgroud)

或这个:

data TmpFile a = TmpFile a FilePath
  deriving Show

videoPath :: TmpFile Video -> FilePath
Run Code Online (Sandbox Code Playgroud)

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

  • TmpFile行格式而不是相反,所以值Video (TmpFile "test.avi")等.
  • 使大量独立的数据类型VideoTmpFile,PictureTmpFile等等.
  • 制作一个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"]
Run Code Online (Sandbox Code Playgroud)

据我所知,等效与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
Run Code Online (Sandbox Code Playgroud)

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

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

lef*_*out 5

你是对的:有了-XDataKinds,这种TmpFile Video -> FilePath方法可行.事实上,我认为这可能是该扩展的一个很好的应用.

{-# LANGUAGE DataKinds #-}

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

videoPath :: TmpFile Video -> FilePath
Run Code Online (Sandbox Code Playgroud)

您需要此扩展写入的原因TmpFile Video是构造函数FileFormat是ab initio 值级别(因此仅存在于运行时),而TmpFile类型级别/编译时.

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

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

data TmpFile a = TmpFile a FilePath
  deriving Show

videoPath :: TmpFile Video -> FilePath
Run Code Online (Sandbox Code Playgroud)

这种类型称为幻像类型.但实际上,对于前者缺乏适当的类型级别值而言,他们是一个很好的解决方法,而DataKinds现在给了我们这些价值.因此,除非您需要与旧编译器兼容,否则请使用DataKinds!

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

data TmpFile = TmpFile FileFormat FilePath
  deriving Show

videoPath :: TmpFile -> Maybe FilePath
videoPath (TmpFile Video p) = p
videoPath _ = Nothing
Run Code Online (Sandbox Code Playgroud)

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

  • "幻像类型"通常是指由不使用的类型变量参数化的类型构造函数. (2认同)