我可以在Haskell中打印多态函数的类型,就像我传递给它一个具体类型的实体一样吗?
这是 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
这意味着它(.)是“实例化的”(我不确定这是正确的术语;作为 C++ 程序员,我会这样称呼它)b === Char和c === Int,因此(.)被应用的的签名digitToInt如下
(Char -> Int) -> (a -> Char) -> a -> Int
我的问题是:有没有办法在屏幕上打印这个签名,给定(.),digitToInt以及我想将前者应用于后者的“信息”?
对于谁感兴趣,这个问题早先作为这个问题的副本被关闭了。
回答
有一个巧妙的小功能隐藏在一个角落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 下复制有问题的表达式。
回答
我认为@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。这可能会令人困惑,因为我们正在创建一个本地绑定来掩盖 的顶级定义(.),但它仍然命名相同的东西(使用改进的类型),并且这种对 lambda 的滥用允许我们(.) 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
回答
您可以使用TypeApplications扩展来做到这一点,它允许您明确指定要用于实例化类型参数的类型:
? :set -XTypeApplications
? :t (.) @Char @Int
(.) @Char @Int :: (Char -> Int) -> (a -> Char) -> a -> Int
请注意,参数必须按确切顺序排列。
对于具有“常规”类型签名的函数,例如 foo :: a -> b,顺序由类型参数首次出现在签名中的顺序定义。
对于使用ExplicitForalllike 的函数,foo :: forall b a. a -> b顺序由它在 中的任何内容定义forall。
如果你想找出型专门基于应用(.)到digitToChar(而不是仅仅知道哪些类型填写),我敢肯定你不能在GHCI,但我可以强烈推荐Haskell的IDE支持。
例如,这是它在 VSCode 中查找我的方式(这是扩展名):
- Looks like it just replaces the placeholders with specific types. The question asks: is there a way to type something like `:t (.) Data.Char.digitToInt` instead of `:t (.) @Int @Char`?
回答
其他答案需要使用人为限制类型定义的函数的帮助,例如asTypeOfHTNW 答案中的函数。这不是必需的,如下面的交互所示:
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
回答
这是 HTNW 答案的一个小变化。
假设我们有任何涉及多态标识符的可能很大的表达式 poly
.... poly ....
我们想知道多态类型在那个时候是如何实例化的。
这可以利用 GHC 的两个特性来完成:(asTypeOf如 HTNW 所述)和类型孔,如下所示:
.... (poly `asTypeOf` _) ....
在读取_孔时,GHC 将生成错误报告应输入的术语类型以代替该孔。由于我们使用了asTypeOf,这必须与poly我们在该上下文中需要的特定实例的类型相同。
这是 GHCi 中的一个示例:
> ((.) `asTypeOf` _) Data.Char.digitToInt
<interactive>:11:17: error:
* Found hole: _ :: (Char -> Int) -> (a -> Char) -> a -> Int