Haskell ADT中的类型安全命名字段

9

有一个非常常见的问题在 Haskell 中很容易遇到。下面是描述该问题的代码片段:

data JobDescription = JobOne { n :: Int }
                    | JobTwo
                    | JobThree { n :: Int }
  deriving (Show, Eq)

taskOneWorker :: JobDescription -> IO ()
taskOneWorker t = do
    putStrLn $ "n: " ++ (show $ n t)

main :: IO ()
main = do
  -- this runs ok:
  taskOneWorker (JobOne 10)

  -- this fails at runtime:
  -- taskOneWorker JobTwo

  -- this works, but we didn't want it to:
  -- taskOneWorker (JobThree 10)

我在这篇文章中描述了这个问题及其可能的解决方案:https://www.fpcomplete.com/user/k_bx/playing-with-datakinds 在这里,我想问一下,如何使用哪些工具以及什么文档来解决这个问题?

2
在我看来,这里唯一的问题是Haskell允许你在多字段ADT中声明字段访问器。我认为这是对记录语法的滥用,只要不这样做就没有问题。 - leftaroundabout
@leftaroundabout 当然可以。我生产代码的问题在于,字段访问器已经存在,并且在很多地方都被广泛使用。因此,我希望继续拥有字段访问器(以避免进行疯狂的重构),但仍然能够获得类型安全性。 - Konstantine Rybnikov
1
只需定义一个新函数 nOne :: JobDescription -> Int,该函数仅适用于 JobOne - Tom Ellis
1
@TomEllis 如果获取JobTwo,会抛出异常吗?呵呵。实际上,这就是问题所在,我的一些代码已经只能与JobOne一起使用,而现在得到了JobTwo并且“崩溃”了。 - Konstantine Rybnikov
哦,我以为你想要taskOneWorker JobTwo崩溃,所以我只是复制了这个行为。如果你不想它崩溃,你应该使用Prism - Tom Ellis
@TomEllis 当然,使用棱镜是一种获取完全访问器的选项,但考虑到你几乎总是针对一个具体任务使用一个大函数(从中多次提取字段),这样做就显得有些过度了,因为你需要一直获取可能存在的值。所以我会采用基于类型的方法(此外,我需要处理现有代码,这样做会更有帮助)。 - Konstantine Rybnikov
2个回答

2

我建议使用PrismPrism _JobOne 表示位于 JobOne 构造函数内部的 Int 值。在 taskOneWorker 中,我们使用^?查找它。如果值t确实是一个JobOne构造函数并且其参数在Just中返回,或者它不是一个JobOne构造函数,则返回Nothing

{-# LANGUAGE TemplateHaskell #-}

import Control.Lens

data JobDescription = JobOne { n :: Int }
                    | JobTwo
                    | JobThree { n :: Int }
  deriving (Show, Eq)
$(makePrisms ''JobDescription)

taskOneWorker :: JobDescription -> IO ()
taskOneWorker t = do
  case t ^? _JobOne of
    Just n  -> putStrLn $ "n: " ++ show n
    Nothing -> putStrLn "Not a JobOne"
               -- Or 'return ()' if you want to ignore these cases

main :: IO ()
main = do
  taskOneWorker (JobOne 10)
  taskOneWorker JobTwo
  taskOneWorker (JobThree 10)

-- *Main> main
-- n: 10
-- Not a JobOne
-- Not a JobOne

3
对我来说,这似乎是一个较差的选择。如果任务一工作者明确被认为只与JobOne(而不是JobTwo或JobThree)一起工作,那么这会导致任务一工作者具有更多的样板代码和较少的类型安全性,不如原帖中的解决方案。 - Konstantine Rybnikov

-1

是的,像你所说的那样使用镜头,并将“n”事物抽象成某种“JobData”,对我来说似乎不错(甚至可能很好)。


它确实可以让你轻松避免运行时崩溃(无需使用DataKinds),但在有意义的情况下通过类型限制参数仍然是很好的选择。 - Konstantine Rybnikov

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接