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 的参数将是 Strings,并且结果必须是 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 acoerce 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 -> AnyisPrefixOf 本身有类型 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

如果我们直接应用 isPrefixOf2 那么我们正在处理 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 -> bCoercible a b 约束是否真的经得起审查是另一回事) .对于 coerce 可以“尝试”在哪些类型之间进行转换没有任何限制,只有它可以成功转换的类型,因此无法通过 coerce 得出统一链。如果有任何类型变量,您需要独立确定每一侧的类型。3


1 事实上,可能有多种有效的方法来实例化它,从而导致最终函数的不同行为。一个明显的例子是 newtype CaseInsensitiveChar = C Char,其中 Eq 实例使用 toLowerisPrefixOf :: [CaseInsensitiveChar] -> [CaseInsensitiveChar] -> Bool 可以 被强制转换为 [Char] -> [Char] -> Any,但与被强制转换的 isPrefixOf :: [Char] -> [Char] -> Bool.

具有不同的行为

2 或者更确切地说,将其传递给应用于字符串的 foldMap

3 我指的是 isPrefixOfcoerce 应用程序的“内部”,以及其他所有内容仅与 coerce 的结果交互,因此在“外部”。