foldMap 回调中强制的意外行为
Unexpected behavior of coerce inside foldMap's callback
此代码编译:
import Data.List (isPrefixOf)
import Data.Monoid (Any(..))
import Data.Coerce
isRoot :: String -> Bool
isRoot path = getAny $ foldMap (coerce . isPrefixOf) ["src", "lib"] $ path
我正在使用 coerce
as a shortcut for wrapping the final result of isPrefixOf
in 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 `($)', namely
`foldMap (coerce isPrefixOf) ["src", "lib"] $ path'
但我的直觉是它也应该编译。毕竟,我们知道 isPrefixOf
的参数将是 String
s,并且结果必须是 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
中,无法从表达式 foo isPrefixOf
中推断出 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
转换为 return 值中的 Any
,但是参数呢?我们是否将 isPrefixOf
实例化为 [Char] -> [Char] -> Bool
然后强制转换为 [Char] -> [Char] -> Any
?或者我们是将 isPrefixOf
实例化为 [T] -> [T] -> Bool
(对于某些 T
)然后将 T
强制为 Char
以及将 Bool
强制为 Any
?我们需要知道 isPrefixOf
的实例化,然后才能说这是否有效。1
如果我们直接应用 isPrefixOf
2 那么我们正在处理 String
的事实将实例化 isPrefixOf
的类型变量到 Char
一切都会正常。但是你永远不会直接使用 isPrefixOf
;你使用 coerce isPrefixOf
。因此,您正在处理的那些字符串未连接到 isPrefixOf
类型中的 a
,它们连接到 coerce isPrefixOf
的结果类型。这并不能帮助我们在 isPrefixOf
之前实例化 coerce
的类型。 a
可以是 coerce
可以 将 翻译成 Char
的任何东西,它不会被强制 成为 Char
在此上下文中。还需要其他东西来告诉我们 a
必须是 Char
.
这种模棱两可正是 GHC 所抱怨的。这并不是说编译器不够聪明,没有注意到 isPrefixOf
的唯一可能选择是 [Char] -> [Char] -> Any
,而是您编写的代码实际上缺少编译器需要的一条信息(正确地) 推断。
coerce
完全破坏了“通过”它的类型推断,因为就类型推断而言 coerce :: a -> b
(Coercible a b
约束是否真的经得起审查是另一回事) .对于 coerce
可以“尝试”在哪些类型之间进行转换没有任何限制,只有它可以成功转换的类型,因此无法通过 coerce
得出统一链。如果有任何类型变量,您需要独立确定每一侧的类型。3
1 事实上,可能有多种有效的方法来实例化它,从而导致最终函数的不同行为。一个明显的例子是 newtype CaseInsensitiveChar = C Char
,其中 Eq
实例使用 toLower
; isPrefixOf :: [CaseInsensitiveChar] -> [CaseInsensitiveChar] -> Bool
可以 也 被强制转换为 [Char] -> [Char] -> Any
,但与被强制转换的 isPrefixOf :: [Char] -> [Char] -> Bool
.
具有不同的行为
2 或者更确切地说,将其传递给应用于字符串的 foldMap
。
3 我指的是 isPrefixOf
在 coerce
应用程序的“内部”,以及其他所有内容仅与 coerce
的结果交互,因此在“外部”。
此代码编译:
import Data.List (isPrefixOf)
import Data.Monoid (Any(..))
import Data.Coerce
isRoot :: String -> Bool
isRoot path = getAny $ foldMap (coerce . isPrefixOf) ["src", "lib"] $ path
我正在使用 coerce
as a shortcut for wrapping the final result of isPrefixOf
in 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 `($)', namely
`foldMap (coerce isPrefixOf) ["src", "lib"] $ path'
但我的直觉是它也应该编译。毕竟,我们知道 isPrefixOf
的参数将是 String
s,并且结果必须是 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
中,无法从表达式 foo isPrefixOf
中推断出 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
转换为 return 值中的 Any
,但是参数呢?我们是否将 isPrefixOf
实例化为 [Char] -> [Char] -> Bool
然后强制转换为 [Char] -> [Char] -> Any
?或者我们是将 isPrefixOf
实例化为 [T] -> [T] -> Bool
(对于某些 T
)然后将 T
强制为 Char
以及将 Bool
强制为 Any
?我们需要知道 isPrefixOf
的实例化,然后才能说这是否有效。1
如果我们直接应用 isPrefixOf
2 那么我们正在处理 String
的事实将实例化 isPrefixOf
的类型变量到 Char
一切都会正常。但是你永远不会直接使用 isPrefixOf
;你使用 coerce isPrefixOf
。因此,您正在处理的那些字符串未连接到 isPrefixOf
类型中的 a
,它们连接到 coerce isPrefixOf
的结果类型。这并不能帮助我们在 isPrefixOf
之前实例化 coerce
的类型。 a
可以是 coerce
可以 将 翻译成 Char
的任何东西,它不会被强制 成为 Char
在此上下文中。还需要其他东西来告诉我们 a
必须是 Char
.
这种模棱两可正是 GHC 所抱怨的。这并不是说编译器不够聪明,没有注意到 isPrefixOf
的唯一可能选择是 [Char] -> [Char] -> Any
,而是您编写的代码实际上缺少编译器需要的一条信息(正确地) 推断。
coerce
完全破坏了“通过”它的类型推断,因为就类型推断而言 coerce :: a -> b
(Coercible a b
约束是否真的经得起审查是另一回事) .对于 coerce
可以“尝试”在哪些类型之间进行转换没有任何限制,只有它可以成功转换的类型,因此无法通过 coerce
得出统一链。如果有任何类型变量,您需要独立确定每一侧的类型。3
1 事实上,可能有多种有效的方法来实例化它,从而导致最终函数的不同行为。一个明显的例子是 newtype CaseInsensitiveChar = C Char
,其中 Eq
实例使用 toLower
; isPrefixOf :: [CaseInsensitiveChar] -> [CaseInsensitiveChar] -> Bool
可以 也 被强制转换为 [Char] -> [Char] -> Any
,但与被强制转换的 isPrefixOf :: [Char] -> [Char] -> Bool
.
2 或者更确切地说,将其传递给应用于字符串的 foldMap
。
3 我指的是 isPrefixOf
在 coerce
应用程序的“内部”,以及其他所有内容仅与 coerce
的结果交互,因此在“外部”。