问题描述
此代码编译:
import Data.List (isPrefixOf)
import Data.Monoid (Any(..))
import Data.Coerce
isRoot :: String -> Bool
isRoot path = getAny $ foldMap (coerce . isPrefixOf) ["src","lib"] $ path
我使用 coerce
作为将 isPrefixOf
的最终结果包装在 Any
中的快捷方式。
这段类似的代码无法编译(注意缺少 .
):
isRoot :: String -> Bool
isRoot path = getAny $ foldMap (coerce isPrefixOf) ["src","lib"] $ path
错误是:
* Couldn't match representation of type `a0' with that of `Char'
arising from a use of `coerce'
* In the first argument of `foldMap',namely `(coerce isPrefixOf)'
In the first argument of `($)',namely
`foldMap (coerce isPrefixOf) ["src","lib"]'
In the second argument of `($)',"lib"] $ path'
但我的直觉是它也应该编译。毕竟,我们知道 isPrefixOf
的参数将是 String
,并且结果必须是 Any
类型。没有歧义。所以 String -> String -> Bool
应该转换为 String -> String -> Any
。为什么它不起作用?
解决方法
这与强制没有任何关系。这只是一般的约束求解。考虑:
class Foo a b
instance Foo (String -> Bool) (String -> Any)
instance Foo (String -> String -> Bool) (String -> String -> Any)
foo :: Foo a b => a -> b
foo = undefined
bar :: String -> String -> Any
bar = foo . isPrefixOf
baz :: String -> String -> Any
baz = foo isPrefixOf
bar
的定义工作正常; baz
的定义失败。
在bar
中,isPrefixOf
的类型可以直接推断为String -> String -> Bool
,只需统一bar
的第一个参数的类型(即String
) 第一个参数类型为 isPrefixOf
。
在 baz
中,无法从表达式 isPrefixOf
推断出 foo isPrefixOf
的类型。函数 foo
可以对 isPrefix
的类型执行任何操作以获得结果类型 String -> String -> Any
。
请记住,约束并不会真正影响类型统一。统一就好像约束不存在一样,当统一完成时,需要约束。
回到你最初的例子,以下是一个完全有效的强制转换,所以歧义是真实的:
{-# LANGUAGE TypeApplications #-}
import Data.Char
import Data.List (isPrefixOf)
import Data.Monoid (Any(..))
import Data.Coerce
newtype CaselessChar = CaselessChar Char
instance Eq CaselessChar where CaselessChar x == CaselessChar y = toUpper x == toUpper y
isRoot :: String -> Bool
isRoot path = getAny $ foldMap (coerce (isPrefixOf @CaselessChar)) ["src","lib"] $ path
,
isPrefix
推断出类型 [a] -> [a] -> Bool
(带有约束 Eq a
)coerce isPrefix
的预期类型存在 [Char] -> [Char] -> Any
,因此您最终得到了约束Coercible a Char
,但实际上并没有将 a
限制为 Char
。事实上,它可以是围绕 Char
的任何新类型,它可能具有不同的 Eq
实例。
newtype CChar = CChar Char
instance Eq CChar where
_ == _ = True
bad :: String -> Bool
bad path = getAny $ foldMap (coerce (isPrefixOf :: [CChar] -> [CChar] -> Bool)) ["src","lib"] $ path
,
我想我会指出一个有时方便的解决方法。你基本上想要
isRoot :: String -> Bool
isRoot path = getAny $ foldMap (Any . isPrefixOf) ["src","lib"] $ path
但想要强制 isPrefixOf
而不是用它组合一个函数。在这种情况下,确实没有意义,但是如果您有一些未知的传递函数而不是 isPrefixOf
,这有时对性能很重要。如果您不想为 coerce
提供完整的类型签名或使用类型应用程序,一种选择是将组合运算符替换为强制运算符。
import Data.Profunctor.Unsafe ((#.))
isRoot :: String -> Bool
isRoot path = getAny $ foldMap (Any #. isPrefixOf) ["src","lib"] $ path
Profunctor
的 ->
实例定义了 (#.)
之类的东西
-- (#.) :: Coercible b c => q b c -> (a -> b) -> a -> c
_ #. f = coerce f
,
这是我的想法。
您有一个函数 coerce isPrefixOf
,并且通过上下文该函数被限制为具有类型 String -> String -> Any
。 isPrefixOf
本身具有类型 Eq a => [a] -> [a] -> Bool
。
显然 coerce
需要将返回值中的 Bool
转换为 Any
,但是参数呢?我们是否将 isPrefixOf
实例化为 [Char] -> [Char] -> Bool
,然后强制为 [Char] -> [Char] -> Any
?或者我们是否将 isPrefixOf
实例化为 [T] -> [T] -> Bool
(对于某些 T
),然后将 T
强制为 Char
以及将 Bool
强制为 {{1 }}?我们需要知道 Any
的实例化,然后才能判断它是否有效。1
如果我们直接应用 isPrefixOf
2 那么我们正在处理 isPrefixOf
的事实会将 String
的类型变量实例化为 {{1}一切都会好起来的。但是你从不直接使用 isPrefixOf
;你使用Char
。因此,您正在处理的那些字符串未连接到 isPrefixOf
类型中的 coerce isPrefixOf
,而是连接到结果类型 a
。这无助于我们在 isPrefixOf
之前实例化 coerce isPrefixOf
的类型。 isPrefixOf
可以是 coerce
可以翻译为a
的任何东西,它不会被强制成为 coerce
这个上下文。还需要一些其他信息来告诉我们 Char
必须是 Char
。
这种歧义正是 GHC 所抱怨的。并不是编译器不够聪明,无法注意到 a
的唯一可能选择是 Char
,而是您编写的代码实际上缺少编译器需要的一条信息(正确) 推断。
isPrefixOf
完全破坏了“通过”它的类型推断,因为就类型推断而言,[Char] -> [Char] -> Any
(coerce
约束是否真的经得起审查是另一回事)。 coerce :: a -> b
可以在哪些类型之间“尝试”转换没有限制,只有它可以成功转换什么类型,因此无法通过 Coercible a b
绘制统一链。如果有任何类型变量,您需要独立确定每一侧的类型。3
1 实际上,可能有多种有效的方法来实例化它,从而导致最终函数的不同行为。一个明显的例子是 coerce
,其中 coerce
实例使用 newtype CaseInsensitiveChar = C Char
; Eq
也可以也被强制转换为 toLower
,但其行为与被强制转换的 isPrefixOf :: [CaseInsensitiveChar] -> [CaseInsensitiveChar] -> Bool
不同。
2 或者更确切地说,将其传递给应用于字符串的 [Char] -> [Char] -> Any
。
3 在“侧面”我指的是 isPrefixOf :: [Char] -> [Char] -> Bool
在 foldMap
应用程序的“内部”的方式,而其他一切只与结果交互isPrefixOf
等在“外面”。