有一天我正在阅读关于Monad Challenge的评论(我强烈推荐给Haskell的任何初学者,就像我一样),最后我在this thread读到 ($) = id
.
我不知道如何吓唬人,但是许多编程语言的概念最好用小例子来说明,这些例子让人们说“哇” . 例如,令人惊讶的是,Prolog中的append()可以从连接结果“向后”运行,以产生可以连接以生成它的所有列表 . 或者Haskell(>> =)中的monadic绑定运算符可以用join和fmap或($)= id来定义 . ($)= id!? <在Raskell / Ghci尝试>我现在看到为什么它是真的,但仍然......哇!感谢那! (......)
然后我检查了base-4.10.0.0代码,寻找 ($)
和 id
的定义,但是在顶部我读到了这个:
NOTA BENE: Do NOT use ($) anywhere in this module! The type of ($) is
slightly magical (it can return unlifted types), and it is wired in.
But, it is also *defined* in this module, with a non-magical type.
GHC gets terribly confused (and *hangs*) if you try to use ($) in this
module, because it has different types in different scenarios.
This is not a problem in general, because the type ($), being wired in, is not
written out to the interface file, so importing files don't get confused.
The problem is only if ($) is used here. So don't!
他们的实现是:
-- | Identity function.
id :: a -> a
id x = x
-- | Application operator.
{-# INLINE ($) #-}
($) :: (a -> b) -> a -> b
f $ x = f x
我尝试在GHCi上逐个交换,我得到的只是类型错误(正如我所料) . 现在,我的问题比我开始时的问题多:
-
这是什么意思
($) = id
? -
在哪种情况下该陈述是真的?这是否意味着我可以使用一个而不是另一个?
-
在
base
中,($)
是"slightly magical (it can return unlifted types)"和"being wired in"是什么意思? -
那么"different types in different scenarios"怎么样?我认为,由于Haskell是一种强类型语言,因此一旦定义了类型签名,该签名将保留到时间结束 . 这不是真的吗?是否有人可以改变功能的类型?
2 回答
函数
($)
可以定义为即获取函数和参数并返回应用程序的结果 . 同样地,由于currying,我们可以将其解释为仅采用
f
并返回x
的函数 .在这里,我们可以注意到函数
\x -> f x
将任何输入x
映射到f x
- 这也是f
的作用!所以,我们可以简化定义现在,这与身份函数定义
id y = y
相同,所以我们不妨写一下这个等式始终存在,但有两个警告 .
第一个是
($)
具有比id
更受限制的类型,因为($) f = f
仅适用于函数f
,而不适用于任何值f
. 这意味着您可以将($)
替换为id
,但反之亦然 . 写作时一个可以使隐式类型参数显式,并为任何类型
a
和b
写另一个需要注意的是,在存在更高级别的函数时,
($)
在类型检查期间会收到一些特殊处理,而id
则不会 . 这是第3点打击 .通常,在诸如的多态函数中
类型变量
a
可以实例化为其他类型,但仅限于某些限制 . 例如,a
可以实例化为Int
,但不能实例化为另一个多态类型forall b . ...
. 这使类型系统保持预测性,这在类型推断期间有很大帮助 .通常,这不是日常编程中的问题 . 但是,某些功能使用rank-2类型,例如
runST
如果我们写
类型系统可以键入检查 . 但如果我们使用常见的习语
那么类型推理引擎必须采用
($)
的类型并选择
a ~ forall s. ST s x
这是多态的,因此被禁止 .由于这个习惯用法太常见,GHC开发人员决定在
($)
的输入中添加一个特殊情况来允许这个 . 由于这有点特别,如果您定义自己的($)
(可能在另一个名称下),并尝试键入checkrunST $ ...
,这将失败,因为它不使用特殊情况 .此外,
a
无法实例化为未装箱的类型,例如Int#
或未装箱的元组(# Int#, Int# #)
. 这些是GHC扩展,它允许编写传递"raw"整数的函数,而不需要通常的thunk包装器 . 这可以改变语义,例如使功能比...更严格他们是 . 除非您想从某些数字代码中获得更多性能,否则您可以忽略这些 .(我将这部分留在这里,但是lisyarus已经更精确地覆盖了它 . )
基本上,
($)
是id
,但具有更严格的类型 .id
可用于任何类型的参数,($)
而是采用函数参数 .注意
id :: c -> c
,所以($)
的类型确实是一种特殊情况 .在定义
($)
的库的代码中,GHC必须发挥一些技巧才能使特殊的输入规则适用于其他库和程序 . 为此,显然不需要在所述库中使用($)
. 除非您正在开发GHC或恰好定义($)
的base
模块,否则您可以忽略它 . 它是编译器内部的实现细节,而不是编译器和库的用户必须知道的内容 .Haskell确实是强类型的;该问题与特定于
($)
运算符的一些hackery有关 . 不幸的是,我不知道它是什么:希望有人会回答你的问题3(和问题4自动) .关于问题1,看看类型:
重命名
c = a -> b
并获得($) :: c -> c
,这意味着($)
的类型是id
类型的规范,因此至少类型允许我们使用id
来实现($)
.现在,看一下
($)
的定义:让我们重写一下:
并应用eta-reduction:
现在可以清楚地看到
($)
只是id
,具有更具体的类型(因此f
始终是一个函数) .请注意,它不会以另一种方式工作,因为
($)
的类型更具限制性 . 例如,您可以调用id 5
并获取5
,但($) 5
将不会进行类型检查:5
没有a -> b
形式的类型 .($)
的要点是它具有非常低的优先级并且允许避免在f
的参数周围使用括号,并且可以像