存在类型和单子变换器

7

背景:我正在尝试创建一个错误单子,它还可以跟踪警告列表,类似于这样:

data Dangerous a = forall e w. (Error e, Show e, Show w) =>
    Dangerous (ErrorT e (State [w]) a)

Dangerous a 是一种操作,其结果为 (Either e a, [w]),其中 e 是可显示的错误,w 也是可显示的。

问题在于,我似乎无法真正运行这个东西,主要是因为我不太理解存在类型。观察:

runDangerous :: forall a e w. (Error e, Show e, Show w) =>
    Dangerous a -> (Either e a, [w])
runDangerous (Dangerous f) = runState (runErrorT f) []

这段代码无法编译,因为:

Could not deduce (w1 ~ w)
from the context (Error e, Show e, Show w)
...
`w1' is a rigidtype variable bound by
    a pattern with constructor
    Dangerous :: forall a e w.
                 (Error e, Show e, Show w) =>
                 ErrorT e (State [w]) a -> Dangerous a
...
`w' is a rigid type variable bound by
    the type signature for
    runDangerous :: (Error e, Show e, Show w) =>
                    Dangerous a -> (Either e a, [w])

我迷惑了,w1是什么?为什么我们不能推断出它是~w

2个回答

12
一般来说,在此处使用existential类型可能不是您想要的;在Dangerous a值中没有办法“观察”绑定到e和w的实际类型,因此您完全受限于Error和Show提供给您的操作。
换句话说,关于w唯一可以知道的是您可以将其转换为String,因此它可能只是一个String(忽略优先级以简化事情),而关于e唯一可以知道的是您可以将其转换为String,您可以将String转换为它,并且它有一个特殊的值(noMsg)。没有办法断言或检查这些类型是否与其他类型相同,因此一旦将它们放入Dangerous中,就无法恢复这些类型可能具有的任何特殊结构。
错误信息想表达的是,您的runDangerous的类型声称您可以将Dangerous转换为任何具有相关实例的e和w的(Either e a,[w])。这显然是不正确的:您只能将Dangerous转换为该类型的一个选择,即创建它的那个选择。 w1之所以出现,是因为您的Dangerous类型定义了一个类型变量w,因此runDangerous也是如此,所以GHC将其中一个重命名以避免名称冲突。
您需要给runDangerous提供的类型应如下所示:
runDangerous
  :: (forall e w. (Error e, Show e, Show w) => (Either e a, [w]) -> r)
  -> Dangerous a -> r

提供一个函数,该函数接受类型为(Either e a, [w])的值,并且对于任何给定的ew实例,只要它们已经存在,就能接受该值。此外,还提供了一个Dangerous a类型的参数,用于生成该函数的结果。这很难理解!

实现非常简单:

runDangerous f (Dangerous m) = f $ runState (runErrorT m) []
这只是对你的版本进行微小更改,如果这样适合你,那很好;但我怀疑存在类型并不是实现你想要达到目标的正确方式。请注意,你需要使用“{-# LANGUAGE RankNTypes #-}”来表示“runDangerous”的类型。或者,你可以为结果类型定义另一个存在类型。
data DangerousResult a = forall e w. (Error e, Show e, Show w) =>
   DangerousResult (Either e a, [w])

runDangerous :: Dangerous a -> DangerousResult a
runDangerous (Dangerous m) = DangerousResult $ runState (runErrorT m) []

你可以使用case来提取结果,但需要小心处理,否则GHC会抱怨你让ew逃脱了 - 这等价于尝试将一个不够多态的函数传递给runDangerous的另一种形式;即需要对ew有更多的限制,超出了runDangerous类型所保证的。


为什么不将类型定义为 Dangerous e w a 呢?如果我理解你想要实现的内容(这很可能不是),那么这里没有必要使用存在类型。 - ehird
我有几个模块,它们都会抛出自己的错误和警告,并且在顶层进行处理。顶层只需要打印它们,但是在选项模块中说Dangerous OptError OptWarning [Option],在模板文件中说Dangerous TemplateError TemplateWarning Template很烦人,因为它们都只是要被“show”出来。我试图删除很多样板代码并学习一些东西,这当然不是必要的。 - So8res
1
@So8res 是的,别试图使用存在类型。要么在类型构造函数中包含类型变量,要么选择具体类型。或者两者都有一些。看起来您真正想要的是将e作为类型变量放在类型构造函数中,并且您确实希望w只是一个字符串。 - Carl
@So8res:嗯,如果你只是要“显示”它们,你可以像Carl说的那样使用字符串。或者,你可以在模块内部使用这些类型,但是在导出接口时使用基于字符串的“Dangerous”类型,并在边界处进行转换。我只会为每个模块的“Dangerous”单子定义一个类型同义词。但即使它非常笨拙,这也是一个无关紧要的问题,因为存在性不能真正帮助你 :) - ehird
1
@So8res 特性应该在它们更好地表达你实际意图时使用。如果你想让一个类型以一种多态的方式成为可能恢复使用的类型,你需要给它一个类型变量。就我所知,这就是 e 的意思。如果你想要 String,请说 String。从任何有用的意义上讲,这就是 w。把不澄清你意图的随机特性塞进去显然是个坏方法,这是毫无争议的。 - Carl
显示剩余4条评论

1

好的,我想我明白了我之前在摸索什么:

data Failure = forall e. (Error e, Show e) => Failure e

data Warning = forall w. (Show w) => Warning w

class (Monad m) => Errorable m where
    warn :: (Show w) => w -> m ()
    throw :: (Error e, Show e) => e -> m ()

instance Errorable Dangerous where
    warn w = Dangerous (Right (), [Warning w])
    throw e = Dangerous (Left $ Failure e, [])

instance Monad Dangerousdata DangerousT 也很有帮助。)

这使您可以拥有以下代码:

foo :: Dangerous Int
foo = do
    when (badThings) (warn $ BadThings with some context)
    when (worseThings) (throw $ BarError with other context)

data FooWarning = BadThings FilePath Int String
instance Show FooWarning where
...

然后在您的主模块中,您可以定义Show FailureError FailureShow Warning的自定义实例,并且有一种集中的方式来格式化您的错误消息,例如

instance Show Warning where show (Warning s) = "WARNING: " ++ show s
instance Show Failure where ...

let (result, warnings) = runDangerous function
in ...

在我看来,这是一种非常酷的处理错误和警告的方式。我有一个工作模块,类似于这样,现在我要把它完善并可能将其放在Hackage上。欢迎提出建议。


1
抱歉,但是 data Warning = forall w. (Show w) => Warning w 等同于 data Warning = Warning String;你最好让你的 warn 函数在警告值上调用 show。存在类型在这里真的不是你想要的。 - ehird
1
我很欣赏你尝试应用一个酷炫的类型系统特性,但它在这里唯一的作用就是让代码变得更加复杂,而没有增加任何功能。 - ehird
这里有更多的材料来帮助解释:[1](http://www.haskell.org/haskellwiki/FAQ#How_do_I_make_a_list_with_elements_of_different_types.3F),[2](http://lukepalmer.wordpress.com/2010/01/24/haskell-antipattern-existential-typeclass/)——我曾经看到过一个很好的解释,为什么不要使用像`data Foo = forall a. (Show a) => Foo a`这样的东西,但不幸的是现在找不到链接了。 - ehird
嘿,如果我没有经历过一个阶段,尝试将每个问题分解成类型类和该类型类的存在性,我就不会那么快地劝阻他人使用它们 :) 孤立实例很讨厌,当它们在同一代码库中时更讨厌... 你可能想考虑传递一个记录,其中包含格式化错误和警告的函数,作为创建任何重构的runDangerous的替代方案。 - ehird
1
是的,那个解决方案对我来说看起来不错 - 虽然如果我必须挑刺的话,那就是 Show 实例应该生成有效的 Haskell 代码,因此你在那里技术上想要自己的类型类 :) - ehird
显示剩余3条评论

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