【问题标题】:Unexpected behavior of coerce inside foldMap's callbackfoldMap 回调中强制的意外行为
【发布时间】:2021-06-28 14:17:30
【问题描述】:

这段代码编译:

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 `($)', namely
    `foldMap (coerce isPrefixOf) ["src", "lib"] $ path'

但我的直觉是它也应该编译。毕竟,我们知道isPrefixOf 的参数将是Strings,并且结果必须是Any 类型。没有歧义。所以String -> String -> Bool 应该转换为String -> String -> Any。为什么它不起作用?

【问题讨论】:

  • 这与isRoot = or . sequence (map isPrefixOf ["src", "lib"]) 有什么不同吗?只是好奇。
  • @jpmarinier 是等价的。虽然代码有点人为,但我的问题的重点是coerce的问题。
  • MVCE: coerce id :: Int -> Int 产生同样的问题,但 coerce (id :: Int -> Int) :: Int -> Int 不会。所以与多态性有一些相互作用。
  • 我的猜测是coerce isPrefixOf,不像coerce . isPrefixOf,也会影响isPrefixOf的输入。如果我们有 newtype CaseInsensitive = CI Char 和自定义 Eq 实例,那么 coerce isPrefixOf 可能意味着 \x y -> coerce (isPrefixOf x y) 或 \x y -> coerce (isPrefixOf (map CI x) (map CI y)) 具有不同的语义。因此代码是模棱两可的(?)
  • @danidiaz - 谢谢。顺便说一句,您似乎通过制作“类型孔”获得了某种提示,即您将源代码中的coerce 替换为_coerce。似乎编译器被isPrefixOf 的多态性引入了歧途。不知道为什么。如果你使用(coerce (isPrefixOf :: String -> String -> Bool)),它会进行类型检查。同样,如果启用了TypeApplications GHC 扩展,使用(coerce (isPrefixOf @Char)) 也可以正常工作。

标签: haskell coerce


【解决方案1】:

这与强制无关。这只是一般的约束解决。考虑:

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

【讨论】:

    【解决方案2】:

    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
    

    【讨论】:

      【解决方案3】:

      我想我会指出一种有时很方便的解决方法。你基本上想要

      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
      

      【讨论】:

        【解决方案4】:

        这是我的想法。

        您有一个函数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 到Any?我们需要知道isPrefixOf 的实例化,然后才能判断这是否有效。1

        如果我们直接应用isPrefixOf2,那么我们处理String 的事实会将isPrefixOf 的类型变量实例化为Char,一切都会正常工作。但是你从不直接使用isPrefixOf;你使用coerce isPrefixOf。因此,您正在处理的那些字符串没有连接到isPrefixOf 类型中的a,它们连接到coerce isPrefixOf 的结果类型。这并不能帮助我们实例化 isPrefixOf before 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 “side”指的是isPrefixOf 在coerce 应用程序“内部”的方式,其他所有内容仅与coerce 的结果交互“外面”也是如此。

        【讨论】:

          猜你喜欢
          • 1970-01-01
          • 1970-01-01
          • 2017-01-10
          • 2013-04-05
          • 1970-01-01
          • 1970-01-01
          • 2021-04-19
          • 2016-03-05
          • 2014-06-20
          相关资源
          最近更新 更多