20

这是一个具有 3 种类型的多态函数:

:t (.)
(.) :: (b -> c) -> (a -> b) -> a -> c

这里是一个非多态函数:

:t Data.Char.digitToInt
Data.Char.digitToInt :: Char -> Int

如果我们将前者应用于后者,我们会得到一个多态的 1 类型函数:

:t (.) Data.Char.digitToInt
(.) Data.Char.digitToInt :: (a -> Char) -> a -> Int

这意味着使用and(.)被“实例化”(我不确定这是正确的术语;作为 C++ 程序员,我会这样称呼它) ,因此应用的签名如下b === Charc === Int(.)digitToInt

(Char -> Int) -> (a -> Char) -> a -> Int

我的问题是:有没有办法让这个签名打印在屏幕上,给定(.)digitToInt以及我想将前者应用于后者的“信息”?

对于有兴趣的人,这个问题早些时候作为这个问题的副本关闭

4

5 回答 5

12

有一个巧妙的小功能隐藏在一个角落里Prelude

Prelude.asTypeOf :: a -> a -> a
asTypeOf x _ = x

它被记录为“强制其第一个参数与第二个参数具有相同的类型”。我们可以使用它来强制 的(.)第一个参数的类型:

-- (.) = \x -> (.) x = \x -> (.) $ x `asTypeOf` Data.Char.digitToInt
-- eta expansion followed by definition of asTypeOf
-- the RHS is just (.), but restricted to arguments with the same type as digitToInt
-- "what is the type of (.) when the first argument is (of the same type as) digitToInt?"
ghci> :t \x -> (.) $ x `asTypeOf` Data.Char.digitToInt
\x -> (.) $ x `asTypeOf` Data.Char.digitToInt
  :: (Char -> Int) -> (a -> Char) -> a -> Int

当然,这适用于您需要的任意数量的参数。

ghci> :t \x y -> (x `asTypeOf` Data.Char.digitToInt) . (y `asTypeOf` head)
\x y -> (x `asTypeOf` Data.Char.digitToInt) . (y `asTypeOf` head)
  :: (Char -> Int) -> ([Char] -> Char) -> [Char] -> Int

您可以在评论中认为这是@KABuhr 想法的一种变体——使用签名比其实现更严格的函数来指导类型推断——除非我们不必自己定义任何东西,代价是不能仅仅在 lambda 下复制有问题的表达式。

于 2020-12-12T02:09:36.090 回答
8

我认为@HTNW 的答案可能涵盖了它,但为了完整起见,以下是inContext解决方案的详细工作原理。

函数的类型签名:

inContext :: a -> (a -> b) -> a

这意味着,如果你有一个你想要输入的东西,以及一个使用它的“上下文”(可以表达为一个将它作为参数的 lambda),那么用类型说:

thing :: a1
context :: a2 -> b

您可以通过构造表达式来强制统一a1(的一般类型thing)和(上下文的约束):a2

thing `inContext` context

通常,统一的类型thing :: a会丢失,但是 的类型签名inContext意味着整个结果表达式的类型也将与所需的类型统一a,GHCi 会很高兴地告诉您该表达式的类型。

所以表达式:

(.) `inContext` \hole -> hole digitToInt

最终被分配(.)在指定上下文中的类型。你可以这样写,有点误导,如下:

(.) `inContext` \(.) -> (.) digitToInt

因为(.)是匿名 lambda 的参数名称hole。这可能会令人困惑,因为我们正在创建一个隐藏 的顶级定义的本地绑定(.),但它仍然命名相同的东西(使用精炼的类型),并且这种对 lambdas 的滥用允许我们(.) digitToInt逐字编写原始表达式,使用适当的样板。

inContext如果您只是向 GHCi 询问它的类型,那么它实际上是如何定义的,那么它inContext = undefined会起作用。但是,只要看一下类型签名,就很容易给出inContext一个有效的定义:

inContext :: a -> (a -> b) -> a
inContext a _ = a

事实证明,这只是 的定义const,所以inContext = const也有效。

您可以使用inContext一次键入多个内容,它们可以是表达式而不是名称。为了适应前者,您可以使用元组;为了使后者起作用,您在 lambas 中使用了更合理的参数名称。

因此,例如:

λ> :t (fromJust, fmap length) `inContext` \(a,b) -> a . b
(fromJust, fmap length) `inContext` \(a,b) -> a . b
  :: Foldable t => (Maybe Int -> Int, Maybe (t a) -> Maybe Int)

告诉您,在表达式fromJust . fmap length中,类型已专门用于:

fromJust :: Maybe Int -> Int
fmap length :: Foldable t => Maybe (t a) -> Maybe Int
于 2020-12-12T02:57:19.207 回答
7

您可以使用TypeApplications扩展来做到这一点,它允许您明确指定要用于实例化类型参数的类型:

λ :set -XTypeApplications                                 
λ :t (.) @Char @Int
(.) @Char @Int :: (Char -> Int) -> (a -> Char) -> a -> Int

请注意,参数的顺序必须准确。

对于具有“常规”类型签名(如)的函数foo :: a -> b,顺序由类型参数首次出现在签名中的顺序定义。

对于使用ExplicitForalllikefoo :: forall b a. a -> b的函数,顺序由它在 中的任何内容定义forall


如果您想根据应用(.)来确定具体的类型digitToChar(而不是仅仅知道要填充哪些类型),我很确定您不能在 GHCi 中,但我强烈推荐 Haskell IDE 支持。

例如,这是它在 VSCode 中的样子(这里是扩展名):

在此处输入图像描述

于 2020-12-11T20:35:55.033 回答
7

其他答案需要使用人为限制类型定义的函数的帮助,例如asTypeOf来自 HTNW 的答案中的函数。这不是必需的,如以下交互所示:

Prelude> let asAppliedTo f x = const f (f x)

Prelude> :t head `asAppliedTo` "x"
head `asAppliedTo` "x" :: [Char] -> Char

Prelude> :t (.) `asAppliedTo` Data.Char.digitToInt
(.) `asAppliedTo` Data.Char.digitToInt
  :: (Char -> Int) -> (a -> Char) -> a -> Int

这利用了lambda 绑定中隐含在定义中的多态性的缺乏asAppliedTo。在其主体中出现的两个 都f必须具有相同的类型,这就是其结果的类型。这里使用的函数const也有它的自然类型a -> b -> a

const x y = x
于 2020-12-12T23:32:01.227 回答
5

这是 HTNW 答案的一个小变化。

假设我们有任何可能很大的涉及多态标识符的表达式poly

 .... poly ....

我们想知道多态类型是如何被实例化的。

这可以利用 GHC 的两个功能来完成:(asTypeOf如 HTNW 所述)和typed hole,如下所示:

 .... (poly `asTypeOf` _) ....

读取_空洞后,GHC 将生成一个错误,报告应输入的术语类型以代替该空洞。由于我们使用asTypeOf了 ,这必须与poly我们在该上下文中需要的特定实例的类型相同。

这是 GHCi 中的一个示例:

> ((.) `asTypeOf` _) Data.Char.digitToInt
<interactive>:11:17: error:
    * Found hole: _ :: (Char -> Int) -> (a -> Char) -> a -> Int
于 2020-12-12T09:39:10.730 回答