我可以在 Haskell 中打印多态函数的类型,就像我将具体类型的实体传递给它时那样吗?
Can I print in Haskell the type of a polymorphic function as it would become if I passed to it an entity of a concrete type?
这里有一个 3 种类型的多态函数:
:t (.)
(.) :: (b -> c) -> (a -> b) -> a -> c
这里是一个非多态函数:
:t Data.Char.digitToInt
Data.Char.digitToInt :: Char -> Int
如果我们将前者应用到后者,我们将得到一个多态的函数在一种类型中:
:t (.) Data.Char.digitToInt
(.) Data.Char.digitToInt :: (a -> Char) -> a -> Int
这意味着 (.)
被“实例化”(我不确定这是正确的术语;作为 C++ 程序员,我会这样称呼它) b === Char
和 c === Int
,因此应用于 digitToInt
的 (.)
的签名如下
(Char -> Int) -> (a -> Char) -> a -> Int
我的问题是:给定 (.)
、digitToInt
和我想将前者应用到后者的“信息”,有没有办法在屏幕上打印此签名?
对于感兴趣的人,此问题早先已作为 的副本关闭。
您可以使用 TypeApplications
扩展来做到这一点,它允许您明确指定要使用哪些类型来实例化类型参数:
λ :set -XTypeApplications
λ :t (.) @Char @Int
(.) @Char @Int :: (Char -> Int) -> (a -> Char) -> a -> Int
请注意参数的顺序必须准确无误。
对于具有“常规”类型签名(如 foo :: a -> b
)的函数,顺序由类型参数首次出现在签名中的顺序定义。
对于像 foo :: forall b a. a -> b
这样使用 ExplicitForall
的函数,顺序由 forall
.
中的任何内容定义
如果您想根据将 (.)
应用到 digitToChar
(而不是仅仅知道要填充哪些类型)来具体计算类型,我敢肯定您不能GHCi,但我强烈推荐 Haskell IDE 支持。
例如,这是我在 VSCode 中的样子(这里是 the extension):
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
您可以认为这是@K.A.Buhr在评论中想法的变体——使用签名比其实现更严格的函数来指导类型推断——除了我们不必定义任何东西我们自己,代价是无法在 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
因为 (.)
和 hole
一样是匿名 lambda 的参数名称。这可能会造成混淆,因为我们正在创建一个本地绑定,它隐藏了 (.)
的顶级定义,但它仍然命名相同的东西(具有改进的类型),并且这种对 lambda 的滥用允许我们编写原始表达式 (.) digitToInt
逐字记录,带有适当的样板。
如果您只是向 GHCi 询问其类型,那么 inContext
的定义方式实际上是无关紧要的,因此 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
这是 HTNW 回答的细微变化。
假设我们有任何涉及多态标识符的可能很大的表达式poly
.... poly ....
我们想知道此时多态类型是如何实例化的。
这可以利用 GHC 的两个特性来完成:asTypeOf
(如 HTNW 所述)和 typed holes,如下所示:
.... (poly `asTypeOf` _) ....
读取 _
漏洞后,GHC 将生成一个错误,报告应输入的术语类型以代替该漏洞。由于我们使用了 asTypeOf
,这必须与我们在该上下文中需要的 poly
的特定实例的类型相同。
这是 GHCi 中的示例:
> ((.) `asTypeOf` _) Data.Char.digitToInt
<interactive>:11:17: error:
* Found hole: _ :: (Char -> Int) -> (a -> Char) -> a -> Int
其他答案需要借助定义为人为限制类型的函数,例如 HTNW 的答案中的 asTypeOf
函数。这不是必需的,如以下交互所示:
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
这里有一个 3 种类型的多态函数:
:t (.)
(.) :: (b -> c) -> (a -> b) -> a -> c
这里是一个非多态函数:
:t Data.Char.digitToInt
Data.Char.digitToInt :: Char -> Int
如果我们将前者应用到后者,我们将得到一个多态的函数在一种类型中:
:t (.) Data.Char.digitToInt
(.) Data.Char.digitToInt :: (a -> Char) -> a -> Int
这意味着 (.)
被“实例化”(我不确定这是正确的术语;作为 C++ 程序员,我会这样称呼它) b === Char
和 c === Int
,因此应用于 digitToInt
的 (.)
的签名如下
(Char -> Int) -> (a -> Char) -> a -> Int
我的问题是:给定 (.)
、digitToInt
和我想将前者应用到后者的“信息”,有没有办法在屏幕上打印此签名?
对于感兴趣的人,此问题早先已作为
您可以使用 TypeApplications
扩展来做到这一点,它允许您明确指定要使用哪些类型来实例化类型参数:
λ :set -XTypeApplications
λ :t (.) @Char @Int
(.) @Char @Int :: (Char -> Int) -> (a -> Char) -> a -> Int
请注意参数的顺序必须准确无误。
对于具有“常规”类型签名(如 foo :: a -> b
)的函数,顺序由类型参数首次出现在签名中的顺序定义。
对于像 foo :: forall b a. a -> b
这样使用 ExplicitForall
的函数,顺序由 forall
.
如果您想根据将 (.)
应用到 digitToChar
(而不是仅仅知道要填充哪些类型)来具体计算类型,我敢肯定您不能GHCi,但我强烈推荐 Haskell IDE 支持。
例如,这是我在 VSCode 中的样子(这里是 the extension):
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
您可以认为这是@K.A.Buhr在评论中想法的变体——使用签名比其实现更严格的函数来指导类型推断——除了我们不必定义任何东西我们自己,代价是无法在 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
因为 (.)
和 hole
一样是匿名 lambda 的参数名称。这可能会造成混淆,因为我们正在创建一个本地绑定,它隐藏了 (.)
的顶级定义,但它仍然命名相同的东西(具有改进的类型),并且这种对 lambda 的滥用允许我们编写原始表达式 (.) digitToInt
逐字记录,带有适当的样板。
如果您只是向 GHCi 询问其类型,那么 inContext
的定义方式实际上是无关紧要的,因此 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
这是 HTNW 回答的细微变化。
假设我们有任何涉及多态标识符的可能很大的表达式poly
.... poly ....
我们想知道此时多态类型是如何实例化的。
这可以利用 GHC 的两个特性来完成:asTypeOf
(如 HTNW 所述)和 typed holes,如下所示:
.... (poly `asTypeOf` _) ....
读取 _
漏洞后,GHC 将生成一个错误,报告应输入的术语类型以代替该漏洞。由于我们使用了 asTypeOf
,这必须与我们在该上下文中需要的 poly
的特定实例的类型相同。
这是 GHCi 中的示例:
> ((.) `asTypeOf` _) Data.Char.digitToInt
<interactive>:11:17: error:
* Found hole: _ :: (Char -> Int) -> (a -> Char) -> a -> Int
其他答案需要借助定义为人为限制类型的函数,例如 HTNW 的答案中的 asTypeOf
函数。这不是必需的,如以下交互所示:
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