foldMap 回调中的意外强制行为

问题描述

代码编译:

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 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 转换为 Any,但是参数呢?我们是否将 isPrefixOf 实例化为 [Char] -> [Char] -> Bool,然后强制为 [Char] -> [Char] -> Any?或者我们是否将 isPrefixOf 实例化为 [T] -> [T] -> Bool(对于某些 T),然后将 T 强制为 Char 以及将 Bool 强制为 {{1 }}?我们需要知道 Any 的实例化,然后才能判断它是否有效。1

如果我们直接应用 isPrefixOf2 那么我们正在处理 isPrefixOf 的事实会将 String 的类型变量实例化为 {{1}一切都会好起来的。但是你从不直接使用 isPrefixOf;你使用Char。因此,您正在处理的那些字符串未连接到 isPrefixOf 类型中的 coerce isPrefixOf,而是连接到结果类型 a。这无助于我们在 isPrefixOf 之前实例化 coerce isPrefixOf 的类型。 isPrefixOf 可以是 coerce 可以翻译a的任何东西,它不会被强制成为 coerce这个上下文。还需要一些其他信息来告诉我们 Char 必须是 Char

这种歧义正是 GHC 所抱怨的。并不是编译器不够聪明,无法注意到 a 的唯一可能选择是 Char,而是您编写的代码实际上缺少编译器需要的一条信息(正确) 推断。

isPrefixOf 完全破坏了“通过”它的类型推断,因为就类型推断而言,[Char] -> [Char] -> Anycoerce 约束是否真的经得起审查是另一回事)。 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] -> BoolfoldMap 应用程序的“内部”的方式,而其他一切只与结果交互isPrefixOf 等在“外面”。