如何为单一类型专门化(重载)函数的行为?

How to specialize (overload) behavior of a function for a single type?

我正在研究 Real World Haskell,作为练习的一部分,我意识到我想要一个类似于 show :: Show(a) => a -> String 的函数,但它不会影响字符串(而不是转义引号)。在伪代码中,我想要的是:

show' :: String -> String
show' = id

show':: Show(a) => a -> String
show' = show

显然这不起作用(ghc 抱怨多个类型签名和多个声明)。看来我应该改用类型类。当我尝试编写此代码时,编译器一直建议我添加更多语言扩展:

{-# LANGUAGE FlexibleInstances, UndecidableInstances, IncoherentInstances #-}

class Show' a where
  show' :: a -> String

instance Show' String where
  show' = id

instance (Show a) => Show' a where
  show' = show

-- x == "abc"
x = show' "abc"
y = show' 9

起初这似乎有效:x == "abc"y == "9" 正如预期的那样。但是当我尝试在另一个多态函数中使用它时,编译器似乎总是将它解析为通用实现:

use :: (Show a) => a -> String
use x = show' x

-- z == "\"abc\""
z = use "abc"

所以我在这里做错了,我想知道是否有一种方法可以在没有一堆语言扩展的情况下做到这一点(其中一些看起来不像我应该鲁莽使用的东西)。如何使用现有的 show 函数定义此 show'

use 唯一知道其参数类型的是它是 Show 的一个实例。由于这是它所知道的全部,它 必须 使用只需要 Show.

的“通用”实例

你真正想要的是告诉它它可以是 Show' 的任何实例(而不是 Show 的任何实例)。一个小改动解决了这个问题:

use :: (Show' a) => a -> String

旁白:如果我没记错的话,IncoherentInstances 偶尔会引起一些问题。我认为这里没问题,但如果您要在启用该扩展名的同一文件中创建更多实例,请记住这一点。

UndecidableInstances 可能会导致类型检查器(以及整个编译器)进入无限循环,如果类型 class 的实例之间存在依赖关系循环。此处并非如此,但值得一提。

回答你的另一个问题:不使用这些语言扩展是不可能做这件事的。这个问题是否有更多背景信息(您遗漏的细节等)?也许有更好的方法来解决更大的问题。

来自 OP 的评论:

[I] was surprised that such a simple function to describe was so difficult to implement.

这根本不是一个简单的函数,因为它有点破坏了基于类型类的多态性的通常保证。当我们看到一个实例时

instance Show' a => Show' [a] where ...

我们通常假定此实例适用于所有列表类型,无一例外。类型 a 的通用量化实际上意味着 forall a.

重叠/不连贯的实例打破了这个假设,并允许像你这样的“特殊情况”,正如大卫在他的回答中指出的那样。这是一种可能的解决方案,但请注意,通过打破上述假设,重叠/不连贯的实例会使实例解析变得相当脆弱,有时会意外地导致错误的实例。

作为替代方案,您可以考虑利用 Typeable 类型类,我认为它的争议较小:

import Data.Typeable

show' :: (Show a, Typeable a) => a -> String
show' x = case cast x of
   Just s  -> s           -- cast succeeded, it is a string
   Nothing -> show x      -- case failed, it is not a string

请注意,Typeable 的存在表明,在类型中,此函数是使用临时多态性定义的,允许我们检查 a 是否是某某类型,这在参数多态性下通常是不可能的。

请注意,这不需要 IncoherentInstances 也不需要 OverlappingInstances