为什么我不能使用类型 `Show a => [Something -> a]`?

Why can't I use the type `Show a => [Something -> a]`?

我有一个记录类型说

data Rec {
    recNumber :: Int
  , recName :: String
  -- more fields of various types
}

我想为 Rec 编写一个 toString 函数:

recToString :: Rec -> String
recToString r = intercalate "\t" $ map ($ r) fields
  where fields = [show . recNumber, show . recName]

这行得通。 fields 的类型为 [Rec -> String]。但是我很懒,我更喜欢写作

recToString r = intercalate "\t" $ map (\f -> show $ f r) fields
  where fields = [recNumber, recName]

但这不起作用。直觉上我会说 fields 的类型是 Show a => [Rec -> a],这应该没问题。但是Haskell不允许。

我想了解这里发生了什么。如果我说在第一种情况下我得到一个函数列表使得 show 的 2 个实例实际上不是同一个函数,但是 Haskell 能够在编译时确定哪个是哪个(哪个这就是它没问题的原因)。

[show . recNumber, show . recName]
   ^-- This is show in instance Show Number
                     ^-- This is show in instance Show String

而在第二种情况下,我在代码中只有一个 show 的字面用途,并且必须引用多个实例,而不是在编译时确定?

map (\f -> show $ f r) fields
            ^-- Must be both instances at the same time 

谁能帮我理解一下?还有允许这样做的变通方法或类型系统扩展吗?

Haskell 中列表的所有元素必须具有相同类型,因此包含一个 Int 和一个 String 的列表根本不存在。可以在 GHC 中使用存在类型来解决这个问题,但你可能不应该这样做(这种使用存在类型被广泛认为是一种反模式,而且它往往不会表现得非常好)。另一种选择是从列表切换到元组,并使用 lens 包中的一些奇怪的东西来映射两个部分。它甚至可能有效。

类型签名并没有表达您的想法。

这似乎是一个普遍的误解。考虑函数

foo :: Show a => Rec -> a

人们经常认为这意味着“foo 可以 return 它想要的任何类型,只要该类型支持 Show”。 没有。

实际上的意思是foo必须能够return任何可能的类型,因为调用者 可以选择 return 类型。

稍加思考就会发现 foo 实际上是不存在的。无法将 Rec 变成 任何可能存在的类型 。做不到。

人们经常尝试做类似 Show a => [a] 的事情来表示 "a list of mixed types but they all have Show"。那显然是行不通的;这种类型实际上意味着列表元素可以是任何类型,但它们仍然必须相同。

您尝试做的事情似乎很合理。不幸的是,我认为你的第一个例子已经尽可能接近了。您可以 尝试使用元组和透镜来解决这个问题。您可以 尝试改用模板Haskell。但除非你有很多领域,否则可能不值得付出努力。

您实际想要的类型不是:

Show a => [Rec -> a]

任何带有未绑定类型变量的类型声明都有一个隐式 forall。以上相当于:

forall a. Show a => [Rec -> a]

这不是您想要的,因为 a 必须专门针对整个列表的单一类型。 (由调用者选择 any 一种类型,正如 MathematicalOrchid 指出的那样。)因为您希望列表中每个元素的 a 能够以不同方式实例化...你真正要找的是一种存在主义类型。

[exists a. Show a => Rec -> a]

您想要一种 Haskell 不太支持的子类型形式。 GHC 完全不支持上述语法。您可以使用新类型来完成此操作:

{-# LANGUAGE ExistentialQuantification #-}
newtype Showy = forall a. Show a => Showy a

fields :: [Rec -> Showy]
fields = [Showy . recNumber, Showy . recName]

但不幸的是,这与直接转换为字符串一样乏味,不是吗?


我认为镜头无法解决 Haskell 类型系统的这一特殊弱点:

recToString :: Rec -> String
recToString r = intercalate "\t" $ toListOf (each . to fieldShown) fields
  where fields = (recNumber, recName)
        fieldShown f = show (f r)

-- error: Couldn't match type Int with [Char]

假设字段 do 具有相同的类型:

fields = [recNumber, recNumber]

然后开始工作,Haskell 计算出在编译时使用哪个 show 函数实例;它不必动态查找。

如果您每次手动写出 show,就像在您的原始示例中一样,那么 Haskell 可以在编译时为每次调用 show 确定正确的实例。

至于存在...这取决于实现,但据推测,编译器无法确定静态使用哪个实例,因此将改用动态查找。

我想推荐一些非常简单的东西:

recToString r = intercalate "\t" [s recNumber, s recName]
  where s f = show (f r)