当前位置:网站首页 > R语言数据分析 > 正文

chronyc sources -v ^?原因(chronyc sources显示的结果)



来源:

来源:

另一个常见的 thunk 泄漏源于在容器上映射函数时,并没有严格执行它们的组合函数。通常的修复方法是改用函数的严格版本,比如 或 ,或者完全使用一个严格版本的结构。在今天的文章中,我们将更仔细地看待这种情况。特别是,我想回答以下几个问题:

示例

我们的例子是一个非常简单的数据结构,即 spine-strict 链表:

 
  

我们首先使用 创建数据结构的一个实例,然后使用 对其所有元素进行映射。我们假设列表的结构在语义上并不重要(毕竟,对于用户来说,不透明数据结构中树的分布可能仅仅出于性能原因是没有兴趣的。实际上,每当调用 和 时,它们都会反转结构,以避免堆栈溢出)。这里的空间泄漏典型地展示了“非严格容器函数”问题,即像 这样的函数看起来无害,实际上会导致问题。

如果你仔细看这个实现,这并不奇怪,基于对 的粗略查看:当然会积累 thunk,因为它不严格于值,只对结构本身严格。让我们看看一些解决方法。

修复

Bang-pattern permute. 这个修复方法很诱人,特别是如果你在考虑我们上一个例子:

 
  

但这是错误的。为什么错呢?首先,我们实际上并没有改变这个函数的语义: 已经是严格的了!导致的 嵌入表达式太深;我们需要更早地调用 ,而不是 。还要记住,上次修复组合函数仅起作用是因为我们成功启用了 GHC 优化,它使元组变成非分配,从而完全避免了它们的分配。然而,在这里行不通,因为我们有一个 GHC 不知道能否摆脱的严格数据结构,所以所有分配总是会发生。

在每次迭代中强制求值结构。 这种方法虽然有效,但相当不优雅且效率低下。实质上,你每次都要遍历一遍,导致最终的运行时间是二次的,仅仅是为了确保所有东西都被评估了。就像是一个重锤,通常最好避免使用它。

使用 ssMap 的严格版本。 这是一个相当普通的反应,任何改过 函数为 版本的人都已经尝试过:

 
  

剩余的空间使用仅仅是严格的数据结构在内存中的存在。为了修复这个问题,我们必须进入并调整我们的内部表示,以引入这种严格性。这是第一个问题的答案:我们无法通过修改组合函数来修复这个空间泄漏,因为我们需要的额外严格性需要“附加”(使用)到数据结构本身的外部构造函数上:这是只有当你能够操作数据结构的内部结构时才能访问到的东西。

这样做的一个好处是,当你喜欢的容器库无法提供你需要的函数的严格版本时,这是相当令人恼火的。事实上,历史上容器包一直存在这个问题,尽管我最近已经提交了一个提案来帮助解决这个问题。

*使结构体的值严格。*这是将转换为其严格版本的“更好”方法,因为惰性模式将为您完成所有的序列化工作:

 
  

当然,如果你真的想要一个脊柱严格但值惰性的列表,这并不是最好的选择。然而,从灵活性的角度来看,完全严格的数据结构确实更加灵活。这是因为你总是可以通过增加额外的间接性来模拟值惰性的版本:

 
  

现在构造函数被强制执行,但其内部未必会。你不能利用延迟数据结构来完成这一技巧,因为你需要所有函数的合作,以便在所有情况下评估容器的内部。然而,这种方法有一个缺点,即额外的包装器在内存和指针间接方面确实会造成成本。

使结构体变得懒惰。有趣的是,如果我们添加了惰性,空间泄漏就消失了:

 
  

我们添加了一个来确保所有东西实际上都被评估了。事实上,空间使用显著改善了!

发生了什么?技巧在于,因为数据结构是惰性的,我们实际上并没有一次性创建 个 thunk;相反,我们只在任何给定时间创建表示列表头部和尾部的 thunk。两者远小于一百万,因此内存使用量相应减少。此外,因为在评估完元素后,不需要保留列表的元素,所以我们能够立即进行垃圾回收。

融合。 如果你移除我们类似列表的数据构造器包装器,并使用内置的列表数据类型,你会发现 GHC 能够将所有的映射合并为一个极快的非装箱操作:

 
  

这并不完全公平:我们可以用我们严格的代码做同样的技巧;然而,我们不能使用简单的 foldr/build 融合,因为它对于 foldl(带有累积参数的递归)是无效的。我们也不能将我们的函数转换为 foldr,否则在大输入时会有堆栈溢出的风险(尽管在树状数据结构中可以施加对其脊柱大小的对数界限,这可能是可以接受的)。对我来说,也不清楚脊柱严格性是否会为融合带来任何好处,尽管它在值严格性存在时肯定可以更好地运作。

来源:

你可以在这里查看:Sequent Calculus 交互式教程。这是我在这篇博客文章中提到的“三语系统”。你还可以从这个页面更加开放地使用这个系统。以下是简介:

这个交互式教程将教你如何使用序推理,这是一组简单的规则,你可以用来展示一阶逻辑中陈述的真实性。它面向那些具有一些计算机软件编写背景和基本布尔逻辑知识的人。

开发这个系统是一次非常迷人的探索,涉及用户界面和游戏设计。虽然过去已经构建了类似的系统,但我的目标有些不同:我希望它足够简单和易于访问,以至于任何对这个主题感兴趣的人都可以在一个小时内掌握它,并学到一些关于形式逻辑的有趣内容。我认为这个演示对于数学恐惧症来说可能不会特别成功,但也许对那些对这类事物有固有兴趣的人来说会更成功。我一定是在系统上烦了我的许多麻省理工学院的朋友,他们一直忍受着我对系统反复纠缠的评论。第一个版本看起来非常不同。我非常感谢我的测试用户们。

下一代在线教学系统(edX)引起了很多关注,而这个演示(因为实际上就是这样)旨在探索如何模糊教科书和视频游戏之间的界限。但它确实没有走得太远:它仍然太像一本教科书,早期练习的创造性空间不够。我觉得我没有把一个逐步层层叠加概念的视频游戏的正确感觉。另一方面,我确实做了一些让文本可以快速浏览的工作,并且有很多小细节我认为增强了体验。我很尴尬地承认,有一些功能由于技术上的麻烦而没有包含在内。

如果这个演示背后有一个重要的设计原则,那就是给一个人一张地图和让一个人在城市中闲逛几个小时之间是有区别的。但是,亲爱的读者,你可能没有几个小时,我可能也要求你太多的注意力。尽管如此,请原谅我的无礼,还是,请试一试吧。

来源:

你本可以发明… 的文章遵循一种特定的方案:

  1. 简单介绍一个易于理解的问题,
  2. 尝试解决问题,但卡在显而易见的方式上,
  3. 简单介绍一个易于理解的见解,
  4. 方法 ically 解决其余的细节,最终得出最终结果。

为什么以这种方式构建问题有助于解决?

  • 虽然步骤 4 中涉及的细节导致结构并不一定显而易见(因此给人一种概念难以理解的印象),但其见解非常易于理解,其余部分只是“苦工”。推导解决方案的方法解决方案本身更易于压缩,因此更容易学习。
  • 选择一个非常具体且易于理解的问题有助于我们在一个具体示例中打下基础,而结果结构可能过于一般化,难以获得良好的直觉。

很重要的是问题要容易理解,“解决细节”过程要简单。否则,展示会感觉刻意。当观众足够聪明,能够直接看到最终结果并在直觉层面理解时,这种方法也不合适。通常是因为他们已经看过示例。但对于我们其他人来说,这是一种非常有效的教学方法。

我将利用这个机会分析两个特定的例子。第一个是丹·皮波尼的经典文章,你本可以发明单子。以下是四个步骤:

  1. 假设我们想调试纯函数,类型签名为 ,使它们也能返回一个发生了什么的字符串消息,。
  2. 如果我们想组合这些函数,手动线程状态的显而易见解决方案确实很烦人。
  3. 我们可以将这种模式抽象为一个高阶函数。
  4. 我们可以在许多其他例子上做同样的配方,然后显示其泛化。泛化即是一个单子。

第二篇是我的文章,你本可以发明拉链:

  1. 我想做两件事:访问树中节点的父节点和子节点,并对树进行持久更新(例如,无需变异)。
  2. 如果我们做显而易见的事情,我们必须更新树中的所有节点。
  3. 我们只需要翻转一个指针,使其指向父节点。
  4. 我们可以创建一个新的数据结构来保存这些信息(说实话,有点丑陋),然后展示这个过程的泛化。泛化即是一个拉链。

所以下次当你试图解释看似复杂但实际简单的事物时,请试试这种方法!下次:你本可以发明分数级联

附录. 这是一种常见的学术论文结构,尽管很少有论文以此为题。然而,一个显著的区别是,通常“细节工作”并不明显,或者需要一些新颖的技术方法。有时,研究人员会发现一种真正重要的技术方法,并且这种方法会在学术界广泛传播,以至于对于任何在该领域工作的人来说都是显而易见的。在某些方面,这正是一个真正成功的论文的特征之一。

来源:

在本文中,我们讨论了 thunk 泄漏的特征,这种泄漏已经成为“推理空间使用难题”在 Haskell 中的象征。我将考虑几个此类泄漏的例子,并认为这些泄漏实际上是微不足道的修复。相反,困难在于当一个 thunk 泄漏被混淆与其他类型的泄漏时(我们将在后续文章中讨论)。

描述

我将以两种方式描述各种泄漏:首先我将使用我在 Haskell 堆系列中开发的隐喻给出一个非正式的具体描述,然后我将在最后给出更直接、临床的处理。如果你无法忍受一种形式的解释或另一种形式的解释,请随意跳过。

当太多包裹好的礼物(thunk)同时存在时,就会发生 thunk 泄漏。

创造 thunk 并不一定是一件坏事:事实上,大多数 Haskell 程序生成大量的 thunk。有时候堆上存在 thunk 是不可避免的。问题是当它们没有及时评估:就像懒惰的大学生房间里的袜子一样,它们开始堆积起来。

有一个明确的意义,即 thunk “堆积”起来,这可以通过观察幽灵关心的礼物来观察到。

每个幽灵都关心堆叠中的下一个礼物(这样格林奇就不能将它们带走),而我们(用户)关心的是堆叠最底部的礼物。因此,当我们打开那份礼物时,整个礼物链就会倾覆下来(假设没有其他引用指向堆积)。

Thunk 的链条可以是任何你想要的形状,虽然线性是通常情况。

解决问题的方式是什么?显然不是等到礼物堆积起来然后一次清理(就像我们的大学生可能会做的那样):伤害(大内存使用)已经造成了!

相反,我们应该更加渴望并在收到礼物时立即打开它们。

然而,这种策略可能会失败。如果打开礼物导致比起始状态更大的东西,或者如果我们可能不需要打开所有礼物,我们最好还是懒得去做。

此外,还有一个问题,即这些礼物最初是从哪里来的。也许我们最初对于得到这些礼物太急切了…

总之,Thunk 泄漏是指当一个 Haskell 程序积累大量的 thunk 时,如果评估的话,将会导致更小的内存使用。这要求这些 thunk 具有几个属性:

  1. 它们不得有外部引用(因为 thunk 被评估时,它们的结果可以被垃圾回收),
  2. 它们必须执行某种减少,而不是创建一个更大的数据结构,而
  3. 它们应该是必需的。

如果(1)失败,这些未求值表达式很可能是合法的,并且只会产生很小的开销(真正困难的是算法问题)。如果(2)失败,评估所有未求值表达式可能会加剧内存情况。如果(3)失败,您可能正在看到流失败,因为未求值表达式正在急切地创建但懒惰地评估(它们应该也是懒惰地创建)。

举例

我提炼了一些例子来帮助说明所讨论的现象,并直接提供源码级的所有可能修复泄漏的方法。我还将提供一些未泄漏但因为 GHC 足够聪明(为优化欢呼!)而没有泄漏的示例。可运行的代码可以在GitHub 仓库找到,我会尽量保持更新。

首先我们来看看来自简单迭代代码的经典空间泄漏:

 
  

显而易见的是谁在累积未求值表达式:是 。不那么明显的是,当您使用优化编译 GHC 时,此代码实际上并泄漏。为什么会这样?快速查看 Core 将告诉我们为什么:

 
  

请注意, 的类型(重命名为 )是 ,而不是 。由于这是一个原始类型,它是 非懒惰的:无法创建这种类型的未求值表达式。因此,GHC 通过根本不创建它们来避免未求值表达式。修复未优化的情况就像使 严格化一样简单,因为整数的加法是一个严格函数。

GHC 通常无法执行此类拆箱优化,因为这可能违反代码的语义。我们的下一段代码正是在研究这样的情况:

 
  

这个空间泄漏在有优化和无优化的情况下都会发生。它也会导致栈溢出。

GHC 无法通过优化此代码以使得元组的元素被急切地求值,而不改变函数 的语义。为什么会这样?我们考虑对 的另一种调用:。函数当前的语义要求结果是 (因为对 添加任何东西仍然是 ),这意味着在强制内部元组之前我们无法进行任何求值。如果我们只在需要的时候对元组进行弱标准形式的求值(如 调用所做的),或者如果我们只使用第一个结果,那么不应抛出任何异常。如果我们用 替换 并运行程序,这确实是情况。

好吧,这就够理论的了,我们如何修复这个错误呢?我可以直接给出一个答案,但如果我们考虑一系列可能的修复方法并分析它们对程序的影响,这可能会更有信息性。希望这将使空间泄漏不再像符文预测那样难以捉摸,而更加有方法论。

在 函数中为 添加一个严格模式。这个方法行不通:

 
  

我们 这个洞见在于我们并没有改变函数的语义: 仍然应该返回 ,因为 并不会“查看元组内部”。然而,添加这个叹号模式可能有助于构建其他解决方案,如果评估元组本身有其他副作用(如我们可能会说,那只鬼会为我们打开一些礼物)。

使元组在 tick 中不可反驳。这只是混乱的:

 
  

不可反驳模式增加了 惰性,而不是严格性,因此问题变得更糟并不令人惊讶(注意内存使用量现在达到了 80M,而不是 40M)。

使 tick 严格。注意 已经通过 立即被强制,所以无需在这里添加叹号模式。我们只是在 和 上添加叹号模式:

 
  

这些看起来像是一个糟糕的图,但看看比例。1.2 千字节。一般来说,如果在你修改 Haskell 程序后,开始再次看到很多带状数据,说明你已经修复了泄漏。所以我们已经修复了它!

好吧,不完全是。未经优化的代码仍然有内存泄漏:

通过启用一个 GHC 优化,我们修复了内存泄漏,类似于修复原始内存泄漏的方式。再一次,Core 让这一点变得清楚:

 
  

GHC 已经将元组优化为一个无框返回,并内联了对 的调用,因此我们没有任何元组惰性求值浮动在四周。我们本可以手动进行这个优化,但让编译器为我们做更好(并保持代码整洁)。

严格化 tick 和 f。与第一个例子类比,现在 是严格的,如果我们将两个地方都严格化,未经优化的代码也会没问题。果然,没问题。

对于优化后的情况,这并没有太大帮助!(堆内存剖面基本上没有变化。)

使对偶严格。使用严格对偶而不是默认的惰性对偶,相当于在我们对元组进行模式匹配时插入叹号模式。因此,它相当于将 严格化,如果你这样做,在未经优化的情况下你仍然需要一些额外的工作才能让它正常工作。当你控制进入循环的数据结构时,这通常更有效,因为你不需要更改所有的数据声明。

深度 seq c. 如果对 c 的简单叹号模式不起作用,深度叹号模式会起作用:

 
  

或者,你可以使用深度 seq 包中的 。虽然这确实有效,但我个人认为最好还是使用严格的数据类型,如果你要随便地使用 ,那么最好保持所有东西始终被完全评估。

我还有另一个例子,但今天时间不够了!作为告别的话,注意到元组并不是唯一的提升类型:从记录到单数据构造器()再到可变引用,都具有额外的语义,可能带来额外的空间成本。但识别和修复这个特定问题非常容易:堆配置文件非常独特,修复方法简单且非侵入性,甚至可以使用指称语义来帮助分析代码!你所需的只是一点额外的知识。

附言. 对于图表轴线和颜色的变化很抱歉。尽量关注形状和标签。我仍在努力使用生成正确类型的堆配置文件,并需要更一致的缩放机制和更一致的着色方案。这些实验是在 GHC 6.12.3 上进行的。

来源:

Adam Belay(Dune 的知名人物)最近在思考为什么 Haskell 的 如此缓慢。“缓慢?”我想,“Haskell 的 不是应该很快吗?” 所以我研究了一下 的工作原理,看看能否解释清楚。

让我们考虑在 Control.Concurrent.MVar 中函数 的操作。此函数非常简单,它解包 以获取基础的 原始值,然后调用 primop :

 
  

Primops 导致在 中调用 ,这是魔术发生的地方。为简单起见,我们只考虑多线程情况。

第一步是锁定闭包

 
  

在 GHC 堆上的对象具有信息表头,指示它们是什么类型的对象,通过指向对象的相关信息表来实现。这些表头还用于同步:由于它们是字大小的,因此它们可以原子地与其他值交换。 实际上是信息表头上的自旋锁:

 
  

用于一些其他对象,即线程状态对象(,通过 )和线程消息,即异常(,)。

下一步是在 上应用 GC 写屏障

 
  

正如我之前写过的,由于 是可变对象,可以变异以指向第 0 代中的对象;因此,当发生变异时,必须通过可变列表将其添加到根集中。由于每个能力都有一个可变对象,这归结为一堆指针修改,并不需要任何同步。请注意,即使我们最终阻塞在其上,我们也需要将 添加到可变列表中,因为 是阻塞在其上的 线程(TSO)的保留者!(然而,我怀疑在某些情况下,我们可以不这样做。)

接下来,我们根据 是否满或空进行分割。如果 为空,我们需要阻塞线程,直到 为满

 
  

解码 C-- primop 代码时的一个有用提示是 及其关联部分是我们如何访问对象字段的。C-- 对 C 结构布局一无所知,因此这些“函数”实际上是由 生成的宏。阻塞线程包括三个步骤:

  1. 我们必须将线程添加到附加到 MVar 的阻塞队列中(这就是为什么在 MVar 上阻塞会改变 MVar 的原因!)这包括为链表节点进行堆分配以及变更旧链表尾部。
  2. 我们必须将线程标记为阻塞状态( 的修改)。
  3. 我们需要为线程设置一个栈帧,以便线程唤醒时执行正确的操作(即对 的调用)。这个调用还负责解锁闭包。虽然这里的机制非常复杂,但它并不是这篇博文的重点。

如果 MVar 是满的,则可以从 MVar 中取出值

 
  

但这还不是全部。如果有其他阻塞的 在 MVar 上(记住,当线程尝试放置一个已满的 MVar 时,它会阻塞直到 MVar 清空),那么我们应立即解除其中一个线程的阻塞状态,以便 MVar 始终保持满状态:

 
  

有一件有趣的事情与检查阻塞线程的代码有关,那就是对 indirectees()的检查。在什么情况下,队列对象会被间接替换为间接引用呢?事实证明,当我们从链表中 删除 一个项时会发生这种情况。这非常好,因为在单链表中,除非我们也有指向前一项的指针,否则我们没有简单的方法来删除项。采用这种方案,我们只需用一个间接引用覆盖当前项,以便在下次垃圾回收时进行清理。(顺便说一句,这就是为什么我们不能仅仅直接链起 TSO,而不需要额外的链表节点。[1])

当我们找到其他线程时,立即运行它们,这样 MVar 就永远不会变为空:

 
  

这里有一个细节: 实际上并没有运行线程,它只是查看线程的堆栈以确定它打算 执行 什么。一旦 MVar 被放置,我们就唤醒线程,这样它就可以继续它的工作了。

 
  

总结一下,当你执行 时,你需要付出以下成本:

  • 一个自旋锁,
  • 大约数十个内存操作(写障碍、队列操作),以及
  • 当 MVar 为空时,进行(小)堆分配和栈写入。

亚当和我对此有些困惑,然后意识到循环次数之所以如此之多的原因:我们的数字是关于 往返 的,即使在如此轻量级的同步(和缺乏系统调用)中,当所有事情都说完时,你仍然需要经过调度器,这会增加循环次数。


[1] 曾经并非如此,请参见:

 
   

来源:

OpenIntents 有一个很棒的应用程序叫做SensorSimulator,允许您向 Android 应用程序提供加速度计、方向和温度传感器数据。不幸的是,在较新的 Android 2.x 系列设备上表现不佳。特别是:

  • 展示给用户的模拟 API 与真实 API 不同。部分原因是原始代码中复制了 Sensor、SensorEvent 和 SensorEventHandler,以解决 Android 没有这些类的公共构造函数的问题,
  • 虽然文档声称“无论何时您未连接到模拟器,您将获得真实设备的传感器数据”,但事实并非如此:所有与真实传感器系统接口的代码都被注释掉了。因此,不仅 API 不兼容,而且在您希望变换测试时,必须从一种方式编辑代码到另一种方式。 (代码在处理实际未测试应用程序的边缘条件时表现也很糟糕。)

对于这种现状,我感到相当不满,决定进行修复。借助 Java 反射的力量(咳咳),我将表示方式切换为真实的 Android 对象(在模拟器未连接时有效地消除了所有开销)。幸运的是,Sensor 和 SensorEvent 是小巧的数据导向类,所以我认为我没有对内部表示造成太大影响,尽管随着未来版本的 Android SDK,代码可能会彻底崩溃。也许我应该建议上游开发人员将它们的构造函数设置为公共的。

您可以在这里获取代码:Github 上的 SensorSimulator。如果发现错误,请告知;我只在 Froyo(Android 2.2)上进行了测试。

来源:

一个小技巧供你参考:在生成了幻灯片堆栈并将其打印为 PDF 后,你可能想要用评论来注释幻灯片。这是一个好主意,有几个原因:

  • 如果你的幻灯片构造得内容较少,它们可能会被优化用于展示,但并不适合稍后阅读。(“嗯,这里是个图表,我真希望我知道演讲者在这一点上说了什么。”)
  • 编写与幻灯片配套的对话是一种无声地练习你的演示的方式!

但是你如何将幻灯片页面与你的注释交叉排列?利用和的强大功能,你可以完全自动地完成这一过程,甚至无需离开终端!以下是具体方法。

  1. 创建一个“annotations”文本文件(我们将其称为)。其中包含了与幻灯片配套的文字评论。首先写出解释你的第一张幻灯片的文字,然后插入一个换页(,你可以在 vim 中按(插入模式)或在 emacs 中按来实现)。接着写出第二张幻灯片的文字。如此反复。
  2. 现在,我们希望将此渲染为一个 PDF 文件,并与幻灯片堆栈具有相同的尺寸。找出你的幻灯片尺寸为多少像素,然后编辑你的文件,加入以下行:
     

    其中 ll 表示左下,ur 表示右上:这四个数字表示文字的边界框。这些数字的一个可能组合是:

     

    现在我们可以调用 enscript 来生成我们注释的一个尺寸合适的 PostScript 文件,使用(如果你愿意,可以选择不同的字体。)

  3. 将生成的 PostScript 文件转换为 PDF,使用。
  4. 现在,使用 pdftk,我们将分割我们的注释 PDF 和幻灯片 PDF 成为单独的页面,然后将它们合并成一个 PDF。我们可以使用来输出页面,并建议命名输出文件以便它们正确地交叉排列:
     

    然后我们将它们合并回来:

     

这是完整的脚本:

 
   

不要忘记在你的文件中定义,并愉快地进行注释吧!

来源:

,也称为“Nix 风格的本地构建”,是受 Nix 启发的一个新命令,随 cabal-install 1.24 一同发布。Nix 风格的本地构建结合了非沙盒化和沙盒化 Cabal 的优点:

  1. 像今天的沙盒化 Cabal 一样,我们以确定性和独立于任何全局状态的方式构建一组独立的本地包。 永远不会告诉你,它不能构建你的包,因为这将导致“危险的重新安装”。在特定状态的 Hackage 索引下,你的构建是完全可重复的。例如,你不再需要预先使用性能分析编译包;只需请求性能分析, 将自动重新构建所有依赖项以进行性能分析。
  2. 像今天的非沙盒化 Cabal 一样,外部包的构建全局缓存,因此一个包只需构建一次,然后可以在任何其他使用它的地方重复使用。不需要在每次创建新沙盒时不断重新构建依赖项:可以共享的依赖项会被共享。

Nix 风格的本地构建与 cabal-install 1.24 支持的所有 GHC 版本兼容,目前包括 GHC 7.0 及更高版本。此外,cabal-install 的发布周期与 GHC 不同,因此我们计划以比 GHC 每年一次的发布周期更快的速度推送 bug 修复和更新。

尽管此功能目前仅处于测试版(存在一些 bug,请参见“已知问题”,并且文档有点稀少),但我一直成功地使用 Nix 风格的本地构建来进行我的 Haskell 开发。难以形容我对这一新功能的热情:它“只是有效”,而且你不需要假设有一个版本固定的、经过认可的包分发来构建(例如 Stackage)。最终, 将简单地取代现有的 命令。

快速入门

Nix 风格的本地构建“只是有效”:几乎不需要任何配置即可开始使用它。

  1. 下载并安装 cabal-install 1.24:
     

    确保新安装的 已添加到你的路径中。

  2. 要构建单个 Cabal 包,不需要运行 ,可以通过在这些命令前加上 来使用 Nix 风格的构建;例如 。 也受支持。(不幸的是,其他命令尚未支持,例如 (#2957) 或 (#2996)。)
  3. 要构建多个 Cabal 包,需要首先在某个根目录下创建 文件。例如,在 Cabal 存储库中,有一个根目录,其中每个包都有一个文件夹,例如,文件夹 和 。然后在 中,指定每个文件夹:
     

    然后,在包的目录中,您可以使用 来构建该包中的所有组件;或者,您可以指定要构建的目标列表,例如, 要求构建 测试套件和 可执行文件。组件可以从任何目录构建;您不必 cd 到包含要构建的包的目录中。此外,您可以按其来自的包合格目标,例如 具体要求从 Cabal 获取 组件。无需手动配置沙盒:添加 文件,它就可以工作了!

不像沙盒,无需 ;只需将包目录添加到您的 中。而且与传统的 不同,无需显式请求安装包; 将自动获取并构建依赖项。

还有一个方便的脚本,您可以用它来连接 到您的Travis 构建。

工作原理

Nix 风格的本地构建采用了以下两个重要的思想:

  1. 对于外部包(来自 Hackage),在编译之前,我们将会影响包编译的所有输入(标志,依赖选择等)进行哈希处理,生成一个标识符。就像在 Nix 中一样,这些哈希唯一地标识构建的结果;如果我们计算出此标识符,并发现我们已经构建了这个 ID,我们可以直接使用已经构建好的版本。这些包存储在全局的 中;您可以使用 列出所有全局可用的 Nix 包。
  2. 对于本地包,我们使用 标识符,例如 ,这是针对给定 的本地包。这些包存储在本地的 目录中;您可以使用 列出所有按项目分组的包。这种处理方式适用于任何依赖于本地包的远程包(例如,如果您嵌入某些依赖项,而其他依赖项依赖于它们)。

此外,Nix 的本地构建采用确定性依赖解决策略,通过独立于本地安装包进行依赖解决。一旦我们解决了要使用的版本,并确定了编译过程中将使用的所有标志,我们生成标识符,然后检查是否可以改进我们需要构建的包,使其成为已经在数据库中的包。

命令

基于 FLAGS 覆盖 。

构建一个或多个组件,自动构建任何本地和非本地依赖项(本地依赖项是指我们在开发过程中可以修改的现有源代码目录)。不具有本地包的传递依赖关系的非本地依赖项安装到 ,而所有其他依赖项安装到 。

从 中读取本地包的集合;如果不存在,则假定默认项目包括本地目录中的所有 Cabal 文件(即 ),以及每个子目录中的可选包(即 )。

本地 包的构建配置是通过以下来源读取标志来计算的(后续来源具有优先级):

  1. (通常由 生成)
  2. 命令行的

非本地包的配置只受这些来源中特定于包的标志的影响;全局选项不适用于构建。(例如,如果 ,则仅适用于本地的 inplace 包,而不适用于它们的远程依赖项。)

不从 中读取配置。

短语手册

这里是一个便捷的短语手册,说明如何使用 Nix 本地构建来执行现有的 Cabal 命令:

old-style new-style 目前不存在 目前不存在 不必要的(由 处理) 目前不存在(对于库, 应该足够;对于可执行文件,它们可以在 中找到)

cabal.project 文件

文件实际上支持多种选项,用于配置构建的详细信息。以下是一个简单的示例文件,展示了一些可能性:

 
   

运行 时,它会输出一个 文件,其中保存了从命令行输入的额外配置选项;如果想知道如何将命令行参数转换为 文件,请运行 并检查输出。

已知问题

作为技术预览,这段代码仍然有些粗糙。以下是一些可能遇到的更重要的问题:

  • 虽然依赖关系解析是确定性的,如果使用 更新你的 Hackage 索引,依赖关系解析也会改变。没有 ,所以你必须手动构建所需约束的集合。
  • new-build 的一个新功能是,当包没有变化时,它避免重新构建包,通过跟踪它们内容的哈希值。然而,这种依赖跟踪不是百分之百准确(具体来说,它依赖于你的 Cabal 文件准确地报告所有文件依赖项,就像 ,并且不知道搜索路径)。目前没有 UI 强制重新编译一个包;不过你可以通过删除适当的缓存文件相对容易地诱发重新编译:特别是对于名为 的包,删除文件 。
  • 在 Mac OS X 上,Haskell 平台,你可能会收到“警告:‘hackage.haskell.org’ 的包列表不存在。运行 ‘cabal update’ 下载它。”这是问题 #3392;查看链接的票证以获取解决方法。

如果你遇到其他 bug,请在Cabal 的问题跟踪器上告诉我们。

来源:

注意保留通知。作者回忆在高中学习物理的经历,并声称教学往往过于专注于具体公式,而忽视其背后的统一理论。

在小学时,你可能学过 D=RT(发音为“dirt”),即距离等于速率乘以时间。这基本上是谎言,但没关系,因为无论这个方程式与现实世界的联系多么脆弱,老师们都可以用它来介绍代数操作的概念,即仅凭 D=RT,你也可以找出 R,如果你知道 D 和 T,也可以找出 T,如果你知道 D 和 R。除非你异常聪明,否则你不会知道自己被欺骗了;你只是学会了解决他们给你的应用题。

快进到高中物理。谎言仍在传播,尽管服饰略有不同:“位置等于速度乘以时间”,他们说。但然后会说到关键的限定语:“这个方程适用于匀速运动。”然后你会被介绍到你的朋友均匀加速度,还会有另一个方程供你使用,到你结束为期一个月的运动单元时,你会有一大堆方程和变量需要跟踪。

CollegeBoard 的 AP 物理继续这一良好传统,正如他们的方程式表所述:

隐含的预期是学生知道每个方程式的含义,并且还有意外的效果,即训练学生在何时使用方程式以及哪些值对应于哪些变量。

我更喜欢这种表述:

有了这两个方程式,我可以运用微积分,深入探讨位置、速度和加速度之间的核心关系:其中一个仅仅是前一个的导数。这些方程是完全通用的(不仅适用于非匀速运动,还适用于任意维度的向量),简洁而优雅。但从计算的角度来看,它们并不立即有用。

是否有一个比另一个更有价值?它们各有优点:第一组更有可能帮助你计算苹果从建筑物上落下需要多长时间,忽略空气阻力。但第二组更有可能帮助你真正理解运动,而不仅仅是一系列代数操作。

直到我在麻省理工学院上了高级古典力学课程才学到这些。由于某种原因,流行的是停留在具体公式中,而不是教授其背后的理论。即使是 AP 物理也不例外:即使声称更具分析性的 AP 物理 C,也会在其公式表上填满前一组方程。

大多数学生在中学时期大部分时间都在学习如何进行代数运算。在修完物理课程之后,他们可能会进入完全不涉及物理的职业,所有那些演习的训练都将被浪费。他们应该得到更好的待遇,而不是被灌输更多的代数运算;他们应该知道古典力学的优雅和简洁。

附言。 对于阅读本博客的程序员来说,可以随意将自己的类比画到你的工艺上;我相信科学的其他领域在抽象主题上有很多话要说。对于阅读本博客的准 MIT 学生来说,你可能听说过 8.012 和 8.022 很难。这是真的;但同样真实的是,这一对课程已经吸引了许多本科生进入物理系。我对这一对课程的推荐无比推崇。

来源:

关于主要来源的重要性。

(前言资料。)这篇博客的大多数读者应该至少对适用函子有所了解:

 
   

这个接口非常方便日常编程(特别是,它使得漂亮的习语变得容易),但它遵守的法律非常糟糕:

 
   

所以,如果你(像我在二十四小时前一样)还没有看过它,你应该展示这个接口等价于 Applicative:

 
   

(顺便说一句,如果你还没有证明对于单子等价于,你也应该这样做。)这种表述的定律要好得多:

 
   

其中 。我稍微美化了一下,通过使用“等同于”来抑制 和 之间的差异,以及 和 之间的差异,对于严格的相等性,你需要一些额外的函数来将结果转换为正确的类型。看起来有一个一般模式,即具有良好法律表述的 API 并不方便编程,而良好编程的表述则没有良好的法律。C’est la vie…但至少它们是等价的!

有了这种表述,可以轻松地陈述交换适用函子遵循的法律:

 
   

原始论文使用效果的适用性编程非常值得一读。快去看看吧!这就是本次公共服务通知的结束。

来源:

作为刺客公会的财务,我经常不得不向 GMs 乞求和恳求,以获取他们各自游戏的海报,因为 UA Finboard 规定,要获得活动资金,必须提供海报。

对于 Lauren Gust 在 Arcadia 海报上的工作,我感到非常惊讶、惊叹和印象深刻。构图简单而有效,绝对增添了游戏的氛围。点击查看更大版本,并可以获取Lauren Gust 的公开的海报的完整尺寸 PDF。要了解海报背后的更多背景信息,请查看《Arcadia Rising 情景》。

来源:

我今天在排练中,作为第二管簧管演奏圣桑的管风琴交响曲,这已经是第 n 次了,突然想到:我已经听过并演奏了这首音乐足够多的次数,以至于知道整体流程和大部分管弦乐部分,不仅仅是我的部分。因此,当圣歌的呼叫为最后一乐章的管风琴的胜利入场让路,或者当速度开始变化,同时加快和减慢,在曲末时,这并不令人惊讶;几乎是不可避免的。不能有其他方式。

但我们本来可以另有他法;圣桑本可以决定移动第二乐章或引入另一个主题或任何其他多种变化。但他创作了这首曲子,唯独这首曲子,这就是被奉为美的东西。

这让我想起了我计算可计算性问题集上的第一个问题,它要求我展示宇宙的一个基本真理(好吧,在数学哲学家的界限内);不可协商的、不动的、普遍的。或者我写的程序,当然是一个创造性的过程,但通过需求和规范牢固地锚定在具体的领域。那些数学家和程序员需要多么创造性才能设计出优雅的证明和程序,然而他们离艺术家还有多远。

不合逻辑的推论。MIT/GNU Scheme 在你运行它时喜欢冒出大量额外的横幅垃圾,即使你实际上并不想使用交互式 REPL,只是运行一些 mit-scheme 代码。事实证明,mit-scheme 的维护者做出了以下决定:

过去,我(CPH)对于稳定版本的政策是在发布之前必须更新文档。实际上,这意味着近年来没有稳定版本。从这个版本开始,我们将不再将更新后的文档视为稳定版本的先决条件。

哎,什么?

无论如何,这里有一个名为的奇妙未记录选项,可以抑制入口消息。然而,在 7.7.90 版本(Ubuntu Karmic 的默认版本,别试图自己编译;你需要 mit-scheme 来编译 mit-scheme),它并不能抑制“Loading…”消息,所以你需要用以下小技巧来调用 load:

 
   

简而言之,有点令人失望。

来源:

我最近在 MIT Scheme 中对持久化数据结构进行了一些基准测试,作为我的 UROP 项目。我们对几个问题感兴趣:

  1. 对于什么样的关联大小,更高级的数据结构能够超越普通的关联列表?
  2. 持久性的代价是多少?也就是说,持久化数据结构与普通的哈希表相比慢了多少倍?
  3. 什么是最佳的持久化数据结构?

这些结果并非权威;我仍需仔细检查测试和代码的正确性。但它们已经具有一些有趣的含义,因此我想分享一下。

所有实现都使用进行键比较。

不出所料,assoc 击败了其他所有数据结构,因为它只需做一个简单的 cons 操作。然而,有些奇怪的高峰出现在固定的间隔,我不确定它们的起源,可能是垃圾收集器在起作用。

当然,你通过简单的关联操作支付廉价的更新,具有线性查找时间;这个故事同样适用于权衡树,它们具有快速插入但最慢的查找。

当键不存在时,Hamt 确实效率很高,甚至在 15 个元素左右也超过了哈希表。

可以在scheme-hamt repository找到运行基准测试的源代码、我们自己开发的实现以及图表。

来源:

寒假期间,我购买了一台Yaesu VX-8R,这是 Yaesu 最新的型号,也是VX-7R的后继者,受到 MIT 社区许多人的青睐。决定购买这台特定的收音机并不容易:购买(更便宜的)VX-7R 意味着我可以利用已经围绕这一特定型号形成的庞大知识库。但是我父亲愿意多出 50 美元购买更新版本,因此我决定尝试一下。

然而,这并不是本文的重点(对 VX-8R 的评论将等到我真正亲自使用它才能发表):这项练习真正令人困惑的部分是弄清楚购买哪些配件以获得麦克风耳机。如果听起来有些模糊,那是因为确实如此。Yaesu 的官方配件——强调蓝牙的——缺乏标准的有线耳机。经过一番调查,并与 Kelsey 进行了非常信息丰富的交谈后,这是我找到的情况。

首先,一些词汇。如果你曾经将耳机插入计算机,你一定熟悉TRS 连接器,也被称为音频或耳机插孔。这是外露的锥形插孔,并包含多个导体(黑色环分隔它们)。你所拥有的立体声扬声器的连接器可能有三个导体:一个接地,一个左声道,一个右声道。TRS 连接器在尺寸和导体数量上有各种各样的类型。对于无线电(以及更普遍地可以使用按键式(PTT)耳机的设备),我们对直径为 3.5 毫米(1/8 英寸)且具有四个导体的 TRS 连接器感兴趣:一个接地,一个音频,一个麦克风和一个 PTT。

DIN 连接器有多个由金属屏蔽保护的引脚。DIN 连接器的引脚对应导体,并且引脚分配因应用而异常见有三到八个引脚(超过四个导体 TRS 连接器非常罕见)。DIN 连接器具有标准尺寸(13.2 毫米),但引脚分配因应用而异。

现在,说到实际的无线电设备。我们先从 VX-7R 开始,因为 VX-8R 的插孔比 7R 的更强大。VX-7R 采用四导体 3.5mm TRS 连接器,但有一个小变化:它特别设计为防水,因此为了更紧密地连接,你需要一个带有插孔后螺丝的特殊 TRS 连接器。CT-91 就是这样一款 TRS 连接器,它将连接分成一个三导体 3.5mm 耳机 TRS 插头和一个三导体 2.5mm 麦克风 TRS 插头;这些显然是标准插孔,因此你可以找到各种耳机以及单独的耳塞和 PTT 麦克风。(注:我家里没有任何闲置的,也没机会去无线电店,所以这些只是听说。)

另一方面,VX-8R 支持 GPS,因此不能仅仅使用四导体:相反,它采用 8 引脚 DNS 插头,这在实质上是专有的。你可以连接 GPS 单元(CT-136 和 FGPS-2),但通常需要$30 的连接器 CT-131,将其转换为四导体 3.5mm TRS 插孔。这与 VX-7R 上的 TRS 插头相同,但没有防水螺旋部分。要拆分它,你可以使用 CT-91,但螺丝部分会显示出来,为了更紧密的连接,你将不得不购买他们建议的 CT-44。

我们找到了一个四导体耳机插头,但它不起作用;就像我尝试的杂乱的三导体立体声耳机一样,插入后能听到声音信号,但会导致 PTT 持续激活。目前的理论是立体声可能干扰了一些东西。

我想了解以下几点:

  1. VX-8R 有一个独立的立体声耳机插孔,所以我很好奇如果我将 PTT 麦克风插入四导体插孔会发生什么。如果两者奇迹般兼容,这意味着不需要额外的适配器。考虑到分线器建议使用 2.5mm 麦克风,而四导体插头是 3.5mm,这似乎不太可能。
  2. CT-131 和 CT-91 组成了一种看起来有点不靠谱的连接,我不确定在实际操作中这是否会成为问题,或者我是否需要用电子胶带把它们粘在一起。这里需要一些现场测试,我也很好奇购买或制作四导体到 2.5mm PTT 麦克风适配器有多难。
  3. 我需要找到剑桥/波士顿附近的商店,可以测试各种按键对讲麦克风。任何建议将不胜感激!

来源:

在构建一个支持在多个实现之间参数化代码(即 functors)的模块系统时,你会遇到一个重要的实现问题:如何编译这些参数化的代码?在现有的语言实现中,有三种主要的思路:

  1. 独立编译学派认为,你应该独立编译你的 functors,而不管它们的具体实现。这个学派更重视编译时间而非性能:一旦一个 functor 被构建,你可以自由地替换其参数的实现,而无需重新构建 functor,从而实现快速的编译时间。Pre-Flambda 的 OCaml 就是这种方式。缺点是,无法基于实现的知识对 functor 主体进行优化(除非你可能有一个即时编译器可用)。
  2. 使用时专门化学派说,你可以通过在已知实现的使用点内联 functors 来获得性能提升。如果 functor 主体不太大,你可以在不需要大幅度改变编译器架构的情况下透明地获得良好的性能效益。Post-FLambda OCaml 和 C++ 模板在Borland 模型中都是这样工作的。缺点是,代码必须在每个使用点重新优化,并且可能会存在大量的代码重复(这可以在链接时减少)。
  3. 专门化仓库学派认为,不断重新编译实例化是愚蠢的:相反,每个实例化的编译代码应该被缓存到某个全局位置;下次需要相同实例时,应该重用它。C++ 中的模板在 Cfront 模型和 Backpack 中都是这样工作的。

仓库视角听起来不错,直到你意识到这需要对编译器的工作方式进行重大的架构更改:大多数编译器尝试将中间结果写入某些共享缓存中,而添加对此的支持可能会非常复杂且容易出错。

Backpack 通过将实例化的缓存工作外包给包管理器来规避这个问题,后者确实知道如何缓存中间产品。这种折衷是,Backpack 并没有像某些人希望的那样完全整合到 Haskell 本身中(它极其不是第一类)。

来源:

在 PVP 中,如果向一个模块添加函数,则增加次要版本号;如果从一个模块中移除函数,则增加主要版本号。直观地说,这是因为添加函数是向后兼容的更改,而删除函数是破坏性的更改;更正式地说,如果新接口是旧接口的子类型,则只需要增加次要版本号。

Backpack 给混合添加了一个新的复杂性:签名。向签名添加/删除函数的 PVP 政策应该是什么?如果我们将具有必需签名的包解释为一个函数,理论告诉我们答案:签名是逆变的,因此添加必需函数是破坏性的(增加主要版本号),而删除必需函数是向后兼容的(增加次要版本号)。

然而,故事并没有结束。签名可以重复使用,即一个包可以定义一个签名,然后另一个包可以重用该签名:

 
   

在上面的例子中,我们将一个签名放在 sigs 单元中,p 通过对 sigs 声明依赖项来使用它。B 可以访问 sigs 中由 A 定义的所有声明。

但这里有一些非常奇怪的地方:如果 sigs 曾经删除了 x 的声明,p 将会中断(x 将不再在作用域内)。在这种情况下,上述的 PVP 规则是错误的:p 必须始终对 sigs 声明一个精确的版本边界,因为任何添加或删除都将是破坏性的更改。

所以我们处于这种奇怪的情况中:

  1. 如果我们包含一个依赖项和一个签名,但我们从未使用过该签名的任何声明,我们可以对依赖项指定一个宽松的版本边界,允许它从签名中删除声明(使签名更容易实现)。
  2. 然而,如果我们导入签名并使用其中的任何内容,我们必须指定一个精确的边界,因为现在删除操作将是破坏性的更改。

我认为不应该期望 Backpack 的最终用户能够自行正确地理解这一点,因此 GHC(在这个 提议的补丁集 中)试图通过向仅来自可能已被指定为宽松边界的包的声明附加此类警告来帮助用户。

 
   

更新。 在发布这篇文章后,我们最终删除了这个错误,因为它在与 PVP 兼容的情况下触发了。(详细信息:如果一个模块重新导出了一个来自签名的实体,那么来自该模块的实体使用将会触发错误,这是由于过时通知的工作方式。)

当然,GHC 对边界一无所知,所以我们使用的启发式方法是,如果一个包不暴露任何模块,则认为它是一个签名包,具有精确的边界。像这样的包只通过导入其签名才有用,所以我们从不对这种情况发出警告。我们保守地假设暴露模块的包可能受到 PVP 风格的边界约束,因此在这种情况下会发出警告,例如:

 
   

正如警告所示,可以通过在 p 中明确指定来修复这个错误,这样,即使 q 移除其要求,也不会导致代码破坏:

 
   

或者将签名放入自己的新库中(就像原始示例中的情况一样)。

这个解决方案并不完美,因为仍然有一些方法可以使你以 PVP 不兼容的方式依赖继承的签名。最明显的是与类型相关的情况。在下面的代码中,我们依赖于 q 的签名强制 T 类型等于 Bool 的事实:

 
   

原则上,q 可以放宽对 T 的要求,允许其实现为任何形式(而不仅仅是 Bool 的同义词),但这一变更将破坏 P 中对 x 的使用。不幸的是,在这种情况下并没有简单的方法来发出警告。

也许一个更有原则的方法是禁止来自非签名包的签名导入。然而,在我看来,这样做会使 Backpack 模型变得更加复杂,而这并没有很好的理由(毕竟,总有一天我们会用签名增强版本号,那将是辉煌的,对吧?)

总结一下。 如果你想重用来自签名包的签名,请在该包上指定一个精确的版本边界。如果你使用的组件是参数化的签名,不要导入和使用这些签名的声明;如果你这样做,GHC 会警告你。

来源:

这是由阮开曦撰写的客座文章。

背包 是 Haskell 的一个模块系统,最近在 GHC 8.2.1 中发布。作为一项新功能,我想知道人们如何使用它。因此,我每天在 Twitter 上搜索,前些天看到了 这条推文:

除了 String/Bytestring/Text 外,还有其他的示例吗?到目前为止我还没有看到任何其他示例;看起来背包只是用来赞美的字符串洞。

有一些 良好的回应,但我想给出另一个来自深度学习的用例。

在深度学习中,人们对在 张量 上进行计算感兴趣。张量可以具有不同的值类型:整数、浮点数、双精度等。此外,张量计算可以在 CPU 或 GPU 上进行。尽管有许多不同类型的张量,但每种类型的张量计算是相同的,即它们共享相同的接口。由于背包允许您针对可以有多个实现的单个接口进行编程,因此它是实现张量库的理想工具。

Torch 是一个广泛使用的用于深度学习的库,用 C 实现。亚当·帕什克有一篇关于 Torch 的好文章 文章。我们可以为 Torch 编写一些 Haskell 绑定,然后使用背包在浮点和整数张量之间切换实现。这是一个通过背包签名使用张量的程序:

 
   

我们有一个简单的主函数,从文件中读取两个一维张量,计算两者的点积,对第一个张量的所有条目求和,然后最后打印出这两个值的和。(这个程序是从亚当的文章中转录的,不同之处在于亚当的程序使用浮点张量,而我们保持张量类型抽象,因此使用背包可以同时处理浮点数和整数)。程序使用像点积这样的函数,在签名中定义。

这里是 的实现以及浮点张量的类型。使用 Haskell 的 FFI 调用 C 函数:

 
   

正如您所见,背包可以用于构建一个深度学习库,该库具有多个不同类型操作的实现。如果您为 Torch 中的所有函数编写绑定,您将拥有一个用于 Haskell 的深度学习库;通过背包,您可以轻松编写对张量类型和处理单元(CPU 或 GPU)不可知的模型。

你可以在 GitHub 上找到完整的示例代码。

来源:

自从夏天开始以来,我一直在缓慢地重新阅读一篇论文,这篇论文是 Erik Meijer、Maarten Fokkinga 和 Ross Paterson 的《香蕉、透镜、信封和铁丝网的函数式编程》。如果你想知道 {cata,ana,hylo,para}morphisms 是什么,这篇论文是必读的:第二节为所爱的链表提供了一个非常易读的形式化定义。

然而上次,当他们开始讨论代数数据类型时,我的眼睛有点发直,尽管我在 Haskell 中已经使用和定义了它们;部分原因是我感到自己淹没在三角形、圆形和波浪线的海洋中,当他们讨论基本组合子的定律时,我甚至可能会说:“这全都是数学!”

更仔细地阅读揭示了实际情况,所有这些代数运算符都可以用简单的 Haskell 语言书写出来,对于那些在 Haskell 中已经有一些时间的人来说,这可以提供更流畅(尽管更冗长)的阅读体验。因此,我呈现这份翻译指南。

类型运算符。 按照惯例,类型用 外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传 表示在左边,而 表示在右边。我们将其与函数运算符区分开来,尽管本文没有并且依赖于惯例来区分这两者。

 
   

(对于学究们来说,你需要在所有的 Bifunctors 后面加上 。)

函数运算符。 按照惯例,函数用 外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传 表示在左边,而 表示在右边(类型根据需要统一)。

 
   

现在,让我们来看看 abides law

被翻译成 Haskell 后,这一句是:

 
   

对我来说(至少是这样)更有意义:如果我想从 Either 中提取一个值,然后对其运行两个函数并返回结果的元组,我也可以立即将该值分成一个元组,并使用不同的函数从 either 中“两次”提取值。(尝试手动在 和 上运行该函数。)

来源:

在你的高级符号编程课上是一个晴朗的一天。你的老师刚开始讲授单子——虽然是在 Scheme 语言中——而你坐在教室后排,嘲笑你从 Haskell 语言知道的一些小知识片段。突然间,老师很认真地说,“爱德华似乎对单子知识很多。为什么不让他上来教给大家呢?”突然间,你站起来向从未使用过 Haskell 语言的人们解释类型,却完全无法向他们解释延续单子的工作原理。经过数次尝试之后,你终于部分重写了演示文稿,不再假设人们已经精通 Haskell 语言。你已经陷入了专家陷阱。

你是一个专家。你拥有深入的知识,积累了智慧和直觉,在你的领域内比其他人能更有效地工作。你可能有点自负;你可能会和其他专家激烈辩论。或者你可能非常谦逊和深思熟虑;你的专业知识与你的自我无关。

但是,除非你注意到你假设的先决知识,否则你在教授你的专业领域时会表现糟糕。你的专业知识阻碍了有效教学,因为专家假设了太多的先决知识

当我谈到先修知识时,我并不是指先决的“事实”——例如迭代算法解决线性方程,使用折叠函数反转列表,或者在我最喜欢的框架中如何操作 X。我指的是基础知识:抽象和高阶原语用于思考——比如线性代数,减少高阶运算符和框架的架构。一个回答“如何”,另一个回答“为什么”。

所有的工程和数学都在不断寻找正确的抽象来解决问题。当你将问题放在正确的表现形式中时,最显著的变化也许就是它变得更简洁、更易于在更高层次上操作。不足为奇,牛顿需要发明微积分来发展他的物理思想。今天我们构建的高级编程语言和系统,在纯汇编语言或硅中是无法想象的。

找到并理解正确的抽象概念就是启蒙:它使得困难变得简单,不可能的事情变得可能。曾经需要一页才能计算的内容现在可以简洁地用一句话描述。冗长系统的结构被编码成抽象,留下问题的关键部分。对程序也可以说类似的事情:在高级语言出现之前,汇编程序可以放在几页纸上,并且可以被单个程序员理解。必须如此。 现代软件发展已经远远超出这个阶段。

在这两种情况下,专家会看到这种新的表述,并立即理解。初学者,也许对这种编码有所了解但不熟练,现在必须重新探索基础(或者冒着使用简单但错误的前提的风险四处碰壁)。

你可能会说,“嗯,这不是专家的问题;他们只是没有先修条件!一旦他们掌握了基础,我将教他们这个主题。” 这是不能接受的。 确实,正规教育可以使他们熟悉抽象的基本原语和关系;它尤其有效地清除了错误的概念。但是,专家对于抽象的熟悉程度只有在你花时间“在战壕中”,使用和应用抽象到更大的问题时才会出现。

你可能会说,“我并不那么刻薄;我也会教授先修课程。” 你甚至可能期望能够向听众传授知识!不要自欺欺人。除了简单的主题(简单的解决方案已足以阐明的主题),如果你仅仅向他们讲授,他们不会理解。教学只是做事的路线图,是真正理解任何难题的唯一途径。

你应该说的是,“我只是初学者理解的工具之一。我寻求精确地照亮初学者没有想到的地方。” 实际上,有一个实现这一目标的简单方法:强迫初学者去教!他们将从一个非常有限和不清晰的概念模型开始:在理解的许多道路中,只有一条是他们知道的。他们将详细解释所有错误的细节以及你的隐含知识。他们会被问到问题,这些问题将迫使他们澄清对这条路的理解。最终,他们将对这条路的知识感到自信,如果他们继续学习,这条路将扩展到包括许多道路,不同的理解路径。初学者已经成为专家。但是,正如佛陀可能会说的那样,他们必须发现启蒙。老师只是向他们展示路径。

来源:

今天,我们讨论 Haskell 堆上的礼物如何命名,无论是通过顶层绑定、let 绑定还是参数。我们介绍了表达式现有等效交换,这突显了表达式在 Haskell 堆上也是延迟绑定。最后,我们解释了这种函数内的 let 绑定如何导致创建更多礼物,而不是常量应用形式(CAF),后者从执行的开始就存在于 Haskell 堆上。

当我们描绘 Haskell 堆上的礼物时,它们通常有名称。

我们一直对这些名称的来源有些保密。部分原因是因为大多数这些名称的来源很简单:它们只是 Haskell 程序中的顶层绑定:

 
   

我们还有一些名称作为函数参数绑定的绑定名称。我们在讨论函数时也已经讨论过这些内容。您将一个标签插入到机器中,该标签是幽灵知道 的“真实”位置的方式:

 
   

所以如果我写 ,幽灵就知道无论在哪里看到 ,它都应该寻找 。但这个解释还有些漏洞。如果我写 ,的标签是什么?

一种看待这个问题的方式是用不同的方式重写这个函数:,其中是一个新的变量:在表达式中没有其他地方出现过的变量。所以,只要我们理解了的作用,我们就理解了紧凑的的作用。我称之为表达式现有等效交换。

但 究竟是做什么呢?

有时,完全与顶层绑定的工作相同。这些是常量应用形式(CAF)。

因此,我们只需将变量提升到全局堆中,为其赋予一些唯一名称,然后它就像原始情况一样。我们甚至不需要在对函数的后续调用中重新评估它。再次强调,关键区别在于自由变量(请参见帖子底部的术语表):常量应用形式没有自由变量,而我们编写的大多数 绑定都有自由变量。

术语表。即使您从未学过λ演算,自由变量的定义也非常有用。一个表达式的自由变量是指通过查看表达式无法得知其值的变量。在表达式 中, 和 是自由变量。它们被称为自由变量,因为λ“捕获”它们: 中的 不是自由的,因为它是由λ 定义的。形式上:

 
   

如果我们有自由变量,事情就会变得有些棘手。所以这里有一个扩展漫画,解释了当你强制执行一个延迟绑定时会发生什么。

注意幽灵如何传递自由变量。当一个 thunk 保留未评估时,看重的是它的自由变量,因为这些是其他也保留未评估的 thunk。还值得重申的是,函数始终使用礼物的标签,而不是实际未打开的礼物本身。

规则非常简单,但交互可能会很复杂!

上次:Grinch 是如何窃取 Haskell 堆的

技术注释。 在编写严格的迷你语言时,一个常见的技巧是,在实现时认识到它实际上是 lambda 应用的语法糖:与相同。但这在惰性语言中不起作用:如果引用怎么办?在这种情况下,我们有一个递归的 let 绑定,通常需要使用特殊的构造,需要一些变异。但在惰性语言中很容易:进行绑定永远不会评估等式的右侧,所以我可以随意设置每个变量。我还选择以相反的方式进行演示,因为我希望人们始终考虑名称。CAFs 没有名称,但从所有目的来看,它们是会被共享的全局数据,因此给它们命名是有用的,特别是在尝试调试与 CAF 相关的空间泄漏时。

或许的更精确的翻译是,但我觉得那看起来有点奇怪。抱歉。

此作品根据知识共享署名-相同方式共享 3.0 未本地化许可协议许可。

来源:

比特币由中本聪设计,主要客户端由一群来自bitcoin.org的人开发。你在乎这些人是谁吗?理论上来说,你不应该在乎:他们所做的一切只是为开源协议开发一个开源客户端。任何人都可以开发自己的客户端(有些人已经这样做了),除了比特币网络中每个人的一致同意,没有人能改变协议。这是因为比特币网络被设计成去中心化的。

如果你相信比特币的长期可行性,你应该关心这些人是谁。虽然比特币本身是去中心化的,但从比特币到新货币的过渡不能是去中心化的。这一过渡的发生是由所有密码系统最终变得过时这一事实所保证的。谁将决定如何构建这种新货币?很可能是比特币的原始创造者,如果你在比特币中拥有重要的持有,你应该关心这些人是谁。

以下的文章将更仔细地阐述这一论点,如下:

  1. 包括加密哈希在内的密码系统必须在最终被替换的前提下使用。有人可能会争辩说,“如果比特币的加密学被激活成功教程,金融行业的其余部分也会陷入麻烦” — 我们解释了为什么这对比特币是不相关的。我们还看到,如果比特币成为一个严肃的货币,合理地预期它将存在足够长的时间来发生这种过时。
  2. 比特币社区流传着几种粗糙的过渡计划。我们描述了最常见的分散化和最常见的集中化变体,并解释了为什么分散化变体不能以非破坏性方式运行,同时吸引了经济学和具有类似属性的现有市场。
  3. 我们更加仔细地审视这些分散化和集中化转变的影响,并评估与比特币作为新兴货币面临的其他风险相比,转变的风险。我们建议,虽然比特币的转变并不是一个核心关注点,但天真的去中心化的观念是一个需要打破的神话。

我已将这篇文章分成几个部分,以便那些对特定论点感兴趣的读者随意跳跃阅读。

密码系统的时限炸弹

“所有加密系统最终会变得过时。” 与货币相比,加密哈希是一个相对较新的发明,只能追溯到 20 世纪 70 年代。MD5 是 1991 年发明的,仅花了大约十五年的时间就彻底被攻破了。对于计算机程序员来说,加密技术的变化是不可避免的,并且系统设计时考虑到了这一点。例如,考虑一下用于安全互联网交易(包括金融交易)的 SSL 证书。这些证书需要定期更新,随着新证书的颁发,可以增加它们的保护级别,使用更新的密码或更长的密钥长度。大多数当前使用的加密技术遵循这种模式:密码和密钥可以相对容易地替换。

然而,比特币是特殊的。它实现去中心化的方式是将所有相关的技术细节嵌入到协议中。其中包括哈希算法 SHA-256。在比特币中“改变”哈希算法是完全不可能的;任何变动都会构成协议的变动,因此会导致一个全新的货币。不要相信任何告诉你相反的人。论点“如果比特币的加密被激活成功教程,其他金融行业也会陷入困境”是无关紧要的,因为其他金融机构可以控制它们使用的密码,并且可以轻松地更改它们:比特币却不能。由于 SHA-1 的弱点可能会影响 SHA-2 系列(其中 SHA-256 是一员),因此 SHA-3 的竞赛已经在进行中。

欺诈交易是否会持续足够长时间以变得实际?也许不会(毕竟,该货币可能在到达此阶段之前被许多其他潜在问题杀死)。然而,如果它确实变得建立起来,你可以期待它会是一个顽强的小家伙。货币会长期存在。

去中心化和中心化货币的过渡

比特币社区已经意识到转变将是必要的事实,尽管普遍的感觉是“我们到那时再想办法”,也有一些模糊的提议被浮出水面。冒着制造草人的风险,我现在想呈现我对两种最广泛提出的计划的看法。首先是去中心化计划:

由于加密系统不会一夜之间崩溃,一旦对 SHA-256 的关注达到足够高的程度,我们将创建一个使用更强加密哈希的新版本比特币。然后,我们将让市场决定这两种货币之间的汇率,并允许人们从一种货币转移到另一种货币。

这是分散的,因为任何人都可以提出一种新的货币:市场将决定最终哪一种会胜出。它也不可能以非破坏性的方式运作,因为任何想要将旧比特币兑换为新比特币的人都必须找到愿意购买的买家,而在某些时候,超级通胀将确保没有愿意购买的买家。所有现有的比特币将变得毫无价值。

此时,我们将稍作停留,进入中国的月饼黑市场,这是一个非常引人入胜的“货币”,与即将过时的比特币有很多相似之处。这个市场的前提是,虽然给予现金贿赂是非法的,但是赠送月饼券是合法的。因此,想要贿赂某人的人可以简单地“赠送”给他们一个月饼券,然后将其在黑市上出售,转换回现金。

参与月饼黑市的人必须小心,因为一旦中秋节到来,所有这些券必须兑换成月饼或变得毫无价值。随着日期的临近,你会看到对越来越贬值的券进行越来越疯狂的烫手山芋游戏。输家?他们最终会拥有大量的月饼。当然,有一个关键的区别,那就是比特币游戏的输家最终一无所有。

这是一个过渡吗?是的。它会造成混乱吗?绝对是。这当然不是你希望用于日常交易的货币做的事情。当然,对一些行业来说,这可能是可以接受的风险,我们将在最后一节中进一步分析这一点。

这是集中计划:

一旦对哈希算法的担忧达到足够高的程度,我们将创建一个新的比特币协议。这个协议不仅包括一个新的哈希算法,还基于某个日期的旧比特币经济价值:在那一点上,所有新的交易在新的比特币方案中都是无效的,并且使用该快照来确定每个人拥有的比特币数量。

还有一种变体,涉及到在他们设法切换之前对哈希算法进行主动攻击的情况,其中包括将特定的区块链标记为已知的良好区块链,并清除疑似的欺诈交易。

这个计划真的是集中的吗?是的:有人需要设计新的协议,说服所有客户接受它,并在新经济到来时统一切换到新的协议。比特币经济的分裂将会极大地破坏,对任何主要参与者都不利。比特币协议的任何其他更改(到那时可能会有很多提议)都可能对比特币经济产生重大影响。

影响和风险

在这里,我们评估了一个问题,“我真的在乎吗?” 短期内,不在乎。比特币有许多,许多弱点将被检验。虽然我个人希望它会成功(它无疑是一个从未进行过的伟大实验),但我的评估是它的机会并不乐观。过度担心过渡并不是明智的时间利用方式。

然而,这并不意味着这不是一个重要的事实需要记住。比特币的未来取决于那些将设计其继任者的人。如果您在比特币上投入了大量资金,至少应该考虑谁拥有下一个王国的钥匙。更为紧迫的问题是比特币客户端的单一文化的影响(某人可能会推出一个更新,调整协议以达到不良目的)。使用比特币的人应尽快多样化其客户端。您应极度怀疑那些使他人能够将您的客户端从协议的一个版本翻转到另一个版本的更新。尽可能保持协议的不可变性,因为没有它,比特币根本不是去中心化的。

感谢 Nelson Elhage,Kevin Riggle,Shae Erisson 和 Russell O’Connor 阅读并评论本文草稿。

更新. 与主题无关的评论将会被严格审查。你已经被警告了。

更新二. 在 Hacker News 和 Reddit 的讨论中出现了一个可能的第三继任计划,即分散化的自启动货币。基本上,多种货币竞争入驻和采纳,但与仅通过汇率分开的两种完全独立的货币不同,这些货币在某种程度上被固定在旧比特币货币上(也许它们拒绝在某个日期之后的所有比特币交易,或者它们要求进行某种破坏性操作才能将旧比特币转换为新比特币——后者可能存在安全漏洞)。我没有分析过这种情况下的经济情况,我鼓励其他人接手。我的直觉是,它仍然会带来破坏性影响;也许会更多,因为这些货币的人为固定。

来源:

我刚在第 13 届算法与数据结构研讨会上介绍了责备树。责备树是一种功能性数据结构,通过融入关于结构任意部分“责备”的信息(类似于),支持高效的合并操作。这是一篇理论论文,因此常数因子并不理想,但渐近性能比现代版本控制系统中使用的传统合并算法要好得多。

这是与大卫·A·威尔逊、帕维尔·潘切哈和埃里克·D·德迈恩共同完成的工作。你可以查看论文,或查看幻灯片。 我还有一份稍早版本的演讲录像在YouTube (20 minutes),我用它来从外地合作者那里获取反馈,然后才真正做演讲。还要感谢大卫·马兹雷斯亲自对演示稿提出有用的评论。

来源:

本文摘自我发表在 glasgow-haskell-users 列表中的一篇帖子。

根据Control.Exception,异常(以及相关的异常)在“线程被阻塞在 MVar 上,但没有其他引用指向 MVar,因此它永远无法继续执行”时被抛出。描述实际上相当精确,但容易被误解。要完全理解此异常的工作原理,需要一些额外来自Control.Concurrent的文档,以及对 Haskell 的绿色线程与 GHC 垃圾收集机制的直觉感受。

这是一个试金石测试:您能预测这三个程序会做什么吗?

 
   

不要偷看。要获取提示,请查看forkIO的文档。


第一个程序没有输出,尽管表面上让两个分叉线程都得以调度、运行并发生死锁。实际上,会引发,而您没有看到的原因是安装了一个异常处理程序,使该异常静音,以及和。您可以使用及其相关方法安装自己的异常处理程序。

这个程序末尾有一组有趣的额外咒语,确保线程高概率调度,并且抛出异常。请注意,只有在“没有其他引用指向 MVar”时才会抛出该异常。由于 Haskell 是一种垃圾收集语言,它仅在垃圾回收发生时才会发现引用已消失,因此在看到这些错误之前,您需要确保发生其中一个垃圾回收。

这意味着的一个推论是,GHC 不会神奇地知道要将异常抛给哪个线程来“解开”程序的死锁状态:相反,它会将异常抛给所有死锁线程,包括(如果适用)主线程。这种行为在第二个程序中得到了展示,该程序由于主线程获取了异常的副本而以终止,尽管子线程的处理程序本来会解决死锁。尝试将最后一行替换为。这真的很滑稽。

最后一个程序考虑了 被“可达”是什么意思。由于它在死锁时是沉默的,这必须意味着 仍然可达;确实,我们的引用 防止了 从未变为死的,并且因此我们无限循环,即使 没有可能被填充。GHC 只知道如果没有引用指向它,线程就可以被视为垃圾(这会导致异常被抛出)。谁持有线程的引用?,因为线程 阻塞 在这个数据结构上,并将自身添加到该阻塞列表中。谁保持 存活?嗯,我们的闭包中包含一个对 的调用。所以线程保持存在。一般规则如下:如果一个线程被阻塞在一个可从非阻塞线程访问的 上,那么该线程就会一直存在。虽然有一些明显的情况(GHC 无法处理),其中 显然已经死了,即使还有引用指向它,但是在一般情况下找出这一点是不可判定的。(练习:编写一个程序来解决停机问题,如果 GHC 能够在一般情况下弄清楚这一点。)

总结起来,没有一点工作(顺便说一句,看起来会很有趣), 并不是一个显而易见的机制,用来给你的 Haskell 程序提供死锁保护。相反,你被邀请把它看作是一种垃圾收集那些本来会无休止地闲置下去的线程的方法:默认情况下,一个死锁的线程是沉默的(除了内存使用方面)。事实上,异常的出现是方便的,从操作的角度来看,但不应依赖这一点。

来源:

…因为我不再住在 245s 号的房间了。是的。 😃

这是一头牛。它们在卡姆河旁边啃草。

小测验。矩阵链乘法、最长公共子序列、最优二叉搜索树的构造、双峰欧几里德旅行推销员问题、编辑距离和维特比算法有什么共同之处?

来源:

在试图找出如何更深入地解释惰性与严格字节串而不会让我的读者半途而废时,我偶然发现了在命令式语言中缓冲流的标准实现与函数式语言中的迭代器之间存在的一个有趣的对比。

没有自重的输入/输出机制会缺少缓冲。通过将读取或写入操作分组以便作为单个单元执行,缓冲可以提高效率。在 C 中,一个简单的读取缓冲区可以像这样实现(当然,使用静态变量封装到数据结构中…并且对中的错误条件进行适当处理…):

 
    

导出的接口是,每次用户调用时提供一个转换为的单个,但在幕后仅在缓冲区耗尽时实际从输入中读取()。

对于大多数应用程序来说,这已经足够好了:底层行为的粗糙性被一个简单而明了的函数隐藏起来。此外,我们的函数并不过于简单:如果我们将所有标准输入读入一个巨大的缓冲区中,直到到来之前我们将无法做任何其他事情。在这里,我们可以在输入到来时做出反应。

在纯函数设置中,这样一组函数会是什么样子呢?一个明显的困难是被重复地突变,因为我们执行读取操作。在持久性的精神中,我们应该尽量避免在最初填充之后对缓冲区进行变异。使缓冲区持久化意味着我们在读取更多数据时也可以保存数据而不必复制它(你可以称之为零拷贝)。我们可以使用一些简单的方式将缓冲区链接在一起:比如说,一个链表。

链表?让我们查看惰性和严格字节字符串的定义(稍作编辑,以适应你,读者):

 
    

在 C 中,这些将是:

 
    

严格字节串只不过是一个精心设计的、内存管理的缓冲区:两个整数跟踪偏移量和长度。在持久性存在的情况下,选择偏移量是一个特别好的选择:从字符串中取子串不再需要复制:只需创建一个新的严格字节串,适当设置偏移量和长度,并使用相同的基指针。

那么什么是呢?嗯,它是一种显赫的懒惰严格字节串的惰性链表——只需将理解为,将理解为:惰性源自于对的第二个参数的非严格性(注意没有感叹号,这是一种严格性注释)。这种惰性是我们在结构中有联合和布尔值的原因:当调用时,此 API 将新的 scribble 到函数指针上(这与 GHC 的工作方式非常相似;少了一层或更多的间接层)。如果忽略惰性,这听起来有点像我们之前描述的缓冲区链表。

然而,有一个重要的区别。是纯的:我们无法调用原始的函数(一个系统调用,这使得它几乎是最 IO 的操作)。因此,当我们有一些纯计算(比如马尔可夫过程)可以生成无限量的文本时,懒惰字节串是合适的选择,但在缓冲输入方面则显得不足。

“没问题!”你可能会说,“只需将数据类型更改为持有而不是即可:

 
    

但是这种数据类型有些问题:没有人阻止多次调用。事实上,将 IO 操作放在值中没有任何意义:由于文件描述符的状态性质,每次都是相同的代码:。我们又回到基于句柄的 IO。

作为列表的想法是一个重要的直觉。关键的洞察力在于:谁说我们必须将 IO 操作的列表提供给用户?相反,倒转控制,使得用户不再调用迭代器:迭代器调用用户并将 IO 的结果返回给用户。用户反过来可以启动其他 IO 操作,或将迭代器组合在一起(我们还没有讨论过),以从一个迭代器流向另一个。

此时,我推荐参考 Oleg 的优秀注释幻灯片(PDF)进一步解释迭代器(不是开玩笑,幻灯片写得非常好),以及多种迭代器 教程。我希望对由 IO 操作生成的“缓冲区链表”进行重视,引起对迭代器本质的注意:这是一个在 IO 操作列表之上的抽象。

总结一下:

  • 使用严格的字节串作为构建更有趣的结构的原语,这些结构具有缓冲区(尽管避免重新实现惰性字节串或迭代器)。当数据量较小、全部可以一次性初始化或随机访问、切片和其他非线性访问模式很重要时,请使用它们。
  • 使用lazy bytestrings作为表示纯计算生成的无限数据流的机制。考虑在执行主要适合于惰性列表的操作(、、等)时使用它们。尽管模块上写着,避免在惰性 IO 时使用它们。
  • 使用iteratees表示可以逐步处理的来自 IO 源的数据:这通常意味着大型数据集。迭代器特别适合多层逐步处理:它们可以自动且安全地“融合”处理。

来源:

Git 对你的文件非常小心:除非你明确告诉它要进行破坏性操作,否则它将拒绝覆盖它不认识的文件,并显示如下错误:

未跟踪的工作树文件’foobar’将被合并覆盖。

在我的工作中,经常需要对Wizard上那些“维护不良”的工作副本执行合并操作,例如,在旧目录上解压了新版本的源代码树,却忘记添加新加入的文件。当 Wizard 尝试以正确方式自动升级它们到新版本时,这将导致各种未跟踪的工作树文件投诉,然后我们必须手动检查这些未跟踪的文件,并在它们正常后移除它们。

对此有一个简单的解决方法:虽然我们不想将所有未跟踪的文件添加到 Git 仓库中,但我们可以只添加那些可能会被覆盖的文件。Git 将停止对这些文件的投诉,并且我们仍然可以在历史记录中找到它们的记录:

 
    

以前,代码中的星号行为 。你能猜到这有什么错误吗?回想一下 和 之间的区别;如果涉及的文件是符号链接, 会跟随它,而 则不会。因此,如果将要被覆盖的文件是一个损坏的符号链接,旧版本的代码将不会将其删除。在许多情况下,你无法区分这些情况:如果文件符号链接指向的父目录存在,我可以通过符号链接创建一个文件,以及其他正常的“文件操作”。

然而,Git 非常清楚符号链接和普通文件之间的区别,并且如果它将会覆盖一个符号链接,它会相应地投诉。保留了这些好的老信息!

附言。 昨天是我在 Galois 工作的第一天!如此令人兴奋,以至于我没能整理思绪写一篇博客。敬请期待更多。

来源:

你还记得你的第一个计算机程序吗?当你完成编写它时,你做的第一件事是什么?你进行了最简单的可能测试:你运行了它。

随着程序规模的增加,可能测试的数量也在增加。值得考虑的是我们实际运行了哪些测试:想象一下儿童游戏“战舰”,其中海洋是所有可能程序执行的空间,战舰是你要寻找的 bug,每一颗发射的导弹就是你运行的一个测试(如果测试通过则为白色,失败则为红色)。你没有无限的导弹,所以你必须决定将它们发送到哪里。

关于“你的第一个计算机程序”的情况,答案似乎很明显:只有一种方式来运行程序,只有少数几种测试情况。

但这种幻想很快就会被真实软件的遭遇所打破。即使你的程序没有输入,硬件、操作系统、开发环境和其他环境因素也会立即增加测试空间。添加显式输入和不确定性到应用程序中,你会看到一个游泳池和海洋之间的差异。

我们如何决定测试什么?我们的策略是什么——我们在哪里发送更多导弹,哪里发送更少?不同的测试策略导致在所有可能执行的空间上的不同测试分布。即使我们在编写测试或在整个系统中运行集成测试时可能没有考虑测试用例的分布,不同的测试策略也会导致不同的覆盖范围。

例如,你可能决定不进行任何测试,并依赖用户向你报告 bug。结果是,你的应用程序在经常使用的区域具有较高的覆盖率,在很少使用的区域覆盖率较低。在某种意义上,当你有一个愿意容忍失败的大用户群体时,这是一种最优策略——尽管任何在不寻常情况下使用软件时遇到 bug 的人可能会持不同意见!

对于回归测试有不同的想法,即你为过去发生过的任何 bug 添加一个自动测试。与将覆盖面集中在经常使用的区域不同,回归测试套件最终会集中在应用程序的“棘手”区域,即过去发现大多数 bug 的区域。这种策略背后的假设是,历史上有 bug 的代码区域未来更可能有 bug。

你甚至可能对应用程序中的 bug 发生位置有一些先验假设;也许你认为应用程序中的边界情况最有可能出 bug。那么你可能会合理地把测试工作集中在这些区域上。

其他测试策略可能专注于测试的分布。当您关注最坏情况行为(例如安全漏洞)而不是平均情况行为(普通错误)时,这一点尤为重要。例如,模糊测试涉及随机在测试空间中泼洒,而不考虑使用频率等因素:结果是您在很少使用和没有发现许多错误的区域上获得了更多的分布。

然而,您可能会注意到,虽然模糊测试改变了测试的分布,但它并不提供任何保证。为了保证没有任何错误,您必须测试每一个输入,而在现代软件工程实践中,这是不可能的。实际上,有一种非常巧妙的技术叫做模型检查器,专门设计了各种技巧以加速进行这种详尽的测试。对于有限的状态空间来说,无论如何都是如此——还有更近期的研究项目(例如 Alloy),可以执行这种详尽的测试,但仅限于一定的深度。

模型检查器在某种意义上是“愚笨的”,因为它们并不真正理解程序试图做什么。我们可以采取的另一种方法是利用我们知道的程序工作方式的事实,以选择一些非常谨慎设计的测试输入,这些输入“泛化”以覆盖整个测试空间。(我们很快会更加精确地阐明这一点。)

然而,上面的图表有点误导:测试用例很少能够如此轻松地泛化。甚至可以说,将特定测试的行为泛化到程序行为的能力,正是区分好程序与坏程序的关键。坏程序充满了许多不同的情况,所有这些情况都必须单独测试才能确保。

说一个测试用例泛化是什么意思?我个人的信念是,被称为相互等效的测试输入空间块对应于程序的单个案例,是更大数学证明的一部分,可以独立进行论证。当您将一个复杂的程序分解为部分以解释其功能时,这些部分应该对应于程序的等效划分。

这种信念的推论是,好程序易于证明正确

这比“运行程序看看是否正常”要复杂得多。但我认为,无论软件工程师是否使用像模型检查器和定理证明器这样的学术工具,对于想要制作正确可靠软件的人来说,这种过渡是必要的。无论如何,最终你仍然需要编写测试用例。但如果你理解构建测试用例背后的分布理论,你将会更加有效。

附言. 类型检查与测试之间的关系经常被误解。我认为这张图表很好地总结了它们之间的关系:

类型可以消除某些 bug 区域,但对其他区域无效。依赖类型的理念是增加这些边界,直至覆盖所有空间,即使你只能管理测试空间的子集,其好处也是非常明显的。

这项工作根据知识共享署名-相同方式共享 3.0 未本地化版本许可协议进行许可。

来源:

我原本应该在今天发布关于 Hoopl 的另一篇文章,但当我写的一个示例程序触发了我认为是 Hoopl 的一个 bug 时(如果这不是一个 bug,那么我的关于 Hoopl 内部工作方式的心理模型严重错误,我也不应该写这个),所以今天的文章将是关于所谓的 Hoopl 遇到的 bug:使用错误变量导致的 bug。

如果我没记错,使用了错误的变量就是缺少撇号:

 
    

实际上,这种 bug 在函数式代码中经常发生。这里是最近我与 Simon Marlow 一起修复的 GHC 本地代码生成后端中的一个 bug:

 
    

几周前,当我在处理 abcBridge 时,由于类似以下原因导致无限循环:

 
    

如何防范这些错误?有多种选择:

将旧变量变异/遮蔽掉

对于任何命令式程序员来说,这是经典的解决方案:如果某个值不再使用,用新值覆盖它。因此,您会得到这样的代码:

 
    

在函数式编程语言中,您可以通过重新使用名称来创建新的绑定,这将遮蔽旧的绑定。但是这种做法有些不鼓励,因为可能会建议:

 
    

使用点无关风格消除变量

特定情况下,如果变量只在一个地方使用,在这种管道样式中通过一系列函数可以相对直接地消除它,将代码移至点无关风格(“点”在“点无关”中指的是变量名):

 
    

但是当中间值需要多次使用时,这种方式通常效果不佳。通常可以安排一种方法,但是“多次使用”通常是点无关风格变得难以理解的一个很好的指标。

视图模式

视图模式是一种相当巧妙的语言扩展,允许您避免编写类似这样的代码:

 
    

使用 ,您可以改写为:

 
    

因此避免了创建可能会意外使用的临时名称的需要,同时允许您使用名称。

打开警告

只需要几秒钟的凝视就能看出这段代码有什么问题:

 
    

是的,在函数体中从未被使用。 GHC 会提醒您这一点:

 
    

不幸的是,有人把它关闭了(眩光):

 
    

使用描述性的变量名和类型

Haskell 程序员倾向于使用像这样的短、数学风格的名称,当变量的作用域不是很大时。命令式编程者倾向于觉得这有些奇怪和难以维护。在 Haskell 中,这种风格能够被维护的原因在于静态类型系统:如果我写的函数是,其中,,我可以确定不会意外地在的位置使用:它会导致类型错误!如果所有关于变量内容的语义信息都包含在类型中,重复说明似乎没有多大意义。当然,不要在这个方向上走得太远是好的:当有两个变量都具有类型时,类型检查器将无法帮助你很多。在这种情况下,最好稍微多加一点描述。相反,如果你调整类型使得这两个变量再次具有不同的类型,错误的可能性再次消失。

来源:

今天偶尔会看到引用的一篇旧文章是重复一遍:“Cabal 不是一个包管理器”。很多批评不适用于 cabal-install 1.24 的新Nix 风格本地构建。让我们澄清一下。

事实:cabal new-build 不处理非 Haskell 依赖项

好的,这是自 Ivan 的文章以来没有改变的一点。与 Stack 不同,不会帮助你下载和安装 GHC,并且像 Stack 一样,它也不会下载和安装系统库或编译器工具链:这些都需要你自己来做。在这种情况下,你应该依赖于系统包管理器来启动 Cabal 和 GHC 的工作安装。

事实:Cabal 文件格式可以记录非 Haskell 的 pkg-config 依赖项。

自 2007 年起,Cabal 文件格式有一个字段,可用于指定对由pkg-config工具理解的库的依赖关系。它不会为您安装非 Haskell 依赖项,但它可以让您提前知道某个库是否不可用。

实际上,cabal-install 的依赖解决器了解字段,并会选择版本并设置标志,以便我们不会得到一个具有不可满足的 pkg-config 依赖的包。

事实:cabal new-build 可以升级包而不会破坏你的数据库

假设你正在开发一个依赖于几个依赖项的项目。你决定通过放宽项目配置中的版本约束来升级其中一个依赖项。做出这些改变后,只需运行重新构建相关的依赖项并开始使用它。就是这样!更好的是,如果你有一个使用旧依赖项的旧项目,它仍然能够正常工作,就像你希望的那样。

实际上并不像传统的升级那样工作。安装到全局存储的包是通过类似 Nix 的标识符唯一标识的,该标识符包含了影响构建的所有信息,包括构建所依赖的具体版本。因此,“升级”一个包实际上只是安装一个使用不同唯一标识符的包,这个包可以与旧版本共存。你永远不会因为输入而导致包数据库损坏。

目前没有删除软件包的机制,除非删除您的存储(),但值得注意的是,删除您的存储是完全安全的操作:如果存储不存在, 不会决定以不同方式构建您的软件包;存储仅仅是一个缓存,影响依赖解决过程。

事实:除了软件包作者外,Hackage 受托人还可以编辑已发布软件包的 Cabal 文件以修复错误。

如果上传的软件包带有错误的版本范围,并且随后的新版本破坏了这些范围,Hackage 受托人 可以介入,修改 Cabal 文件以根据新信息更新版本范围。这是一种比 Linux 发行版补丁更有限的干预形式,但其本质类似。

事实:如果可能的话,请使用您的系统包管理器。

很棒,但并非人人都适用。如果您只需在系统上安装一个可工作的二进制文件,并且不介意是否有最新的版本,您应该通过操作系统的包管理器下载和安装它。发行版软件包非常适合二进制文件;对于开发人员来说,它们的库通常太旧(尽管这通常是获得工作 OpenGL 安装的最简单方法)。 面向的是 Haskell 软件包的开发人员,他们需要构建并依赖于操作系统未分发的软件包。

我希望这篇文章能消除一些误解!

来源:

离开一边,扑克牌!虽然各种扑克牌手的概率已经被广泛理解和列出,但是中国的游戏Mahjong [1]拥有更为复杂的预期价值和概率结构。[2]这主要是由于可用的瓷砖种类更多(136 张瓷砖,而不是标准扑克牌组的 52 张),以及逐轮游戏玩法,这意味着虽然本质上是一种游戏的机会,但涉及到了相当多的策略。事实上,这个主题如此复杂,以至于我决定写我的博士论文。本博客文章是我论文的一个章节的精简版本,考虑到shanten的计算,我们将在下面定义。我将使用日本术语,因为我最喜欢的麻将变体是日本麻将;您可以查阅维基百科文章来翻译。

计算shanten

麻将的基本玩法包括将一张牌抓入手中的十三张牌,并且再弃掉另一张牌。目标是形成一个十四张牌的手牌(也就是在抓牌后,但弃牌前)这是一个获胜的配置。有许多不同的获胜配置,但大多数获胜配置都有一个类似的模式:十四张牌必须分为四个三张牌和一个对子。三张牌可以是相同的三张牌,或者是一个顺子中的三张牌(有三种“花色”可以用来形成顺子);对子则是两张相同的牌。以下是一个例子:

从数字上看,这手牌包括三张牌和对子 123 55 234 789 456。

有一个在麻将手牌中计算非常有用的有趣量——shanten 数字,即离胜利还有多少张牌。这可以用来给你提供一个最基本的启发式玩法:弃掉能让你更接近听牌的牌。最广为人知的shanten 计算器是天凤网站上的这个 [3];不幸的是,这个计算器的源代码不可用。还有另一个关于 StackOverflow 的问题,但“最佳”答案只提供了一个启发式方法,没有证明其正确性!我们能做得更好吗?

初级的话,向听数是在手牌排列的广度优先搜索。当找到一个获胜的手牌时,算法终止并指示搜索已达到的深度。这样的算法显然是正确的;不幸的是,对于 136 张牌,你必须遍历 ![外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传](https://img-home.csdnimg.cn/images/159.png?origin_url=https%3A%2F%2Fgithub.com%2FOpenDocCN%2Fgeekdoc-dl-zh%2Fraw%2Fmaster%2Fezyang%2Fimg%2F1085e81084d0a63d130f108b3be5cd74.png%20%22((136-13&pos_id=img-jdZuhQYF-92) imes 14)^n") 个手牌(新牌的选择乘以弃牌的选择),同时寻找一个相差 n 向听的获胜手牌。如果你差四张牌,你将不得不遍历超过六万亿个手牌。如果我们记忆化与手牌相关的向听数,可以减少这个数字;然而,所有可能手牌的总数大约是 外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传 或者 59 位。尽管我们可以将一手牌通过组合数系统嵌入到 64 位整数中,但结果表仍然太大,希望能够放入内存。

观察到每种花色的向听计算是对称的技巧;因此,我们可以在 1 到 9 号牌的更小空间上进行动态规划,然后在组装最终计算时重复使用这些结果。 外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传 仍然相当大,因此我们可以利用每种牌有四张副本的事实,一个等价的表示是一个 9 维向量,其中的数字是从零到四,有一个约束条件是这些数字的和为 13。即使没有约束条件,计数 外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传 也只有两百万,非常可行。每个条目一个字节,这是 2MB 的内存;比你的浏览器用来查看此网页的还少。(实际上,我们希望约束条件是总和小于或等于 13,因为并非所有的手都是单一花色,所以手中的牌数会少一些。)

解决单一花色的广度优先搜索如下进行:

  1. 初始化一个由牌配置(一个 0 到 4 的 9 维向量)索引的表 A。
  2. 初始化一个待办队列 Q,其中包含牌配置。
  3. 将表 A 中所有获胜配置的向听数初始化为零(可以通过枚举完成),并将这些配置记录在 Q 中。
  4. 当待办队列 Q 不为空时,弹出队列的前端元素,标记所有相邻未初始化节点的向听数为比该节点多一,然后将这些节点推入待办队列。

拥有这些信息后,我们可以汇总手牌的总实时。只需尝试所有三张牌和四种类型的牌(包括空牌),并查看请求形状的实时,并返回所有这些配置中的最小值。根据星和条,有外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传个组合,总计 140 个配置。计算每个配置的实时是一个常量时间操作,进入由每套计算生成的查找表。一个真正的实时计算器也必须适应那些不遵循此配置的罕见其他手牌,但这些获胜配置通常非常受限制,并且很容易(单独)计算实时。

带有一种实盘计算器,可以计算出许多其他的数量。浮法 是指可以减少您手上的实时的可能抽取的数量:人们努力追求高浮法,因为这意味着您有可能抽到一张可以接近于获胜的牌。给定一手牌,计算其浮法非常简单:只需查看所有相邻的手牌,并计算具有较低实时的手牌的数量。

进一步的扩展

假设您正在尝试设计一个可以玩麻将的人工智能。上述实时计算器是否能为您的手提供良好的评估指标?实际上不行:它有一个主要缺点,即它并未考虑到某些牌根本不可用(它们已被丢弃)。例如,如果所有四张“九条”牌都在桌上可见,那么实际上不存在包含九条的手牌配置。调整这种情况实际上非常困难,有两个原因:首先,我们不能再预先计算实时表,因为我们需要在运行时调整可达性指标;其次,各种类型的牌不再对称,因此我们需要做三倍的工作。但我们可以避免指数级的膨胀,因为各套之间没有交互作用。

实时和浮法指标的另一个缺点是它们不是“牌效率”的直接度量:也就是说,它们并不直接指导您在获胜之前最小化预期时间的丢牌策略。例如,假设您有 233 这样的牌,并且只需要再凑成一个三张牌就可以赢了。您有两种可能的丢牌方式:您可以丢弃一个 2 或者一个 3。在两种情况下,您的实时都是零,但是如果丢弃一个 2,您只能通过抽到一个 3 来赢,而如果丢弃一个 3,您可以通过抽到一个 1 或者一个 4 来赢。最大化效率需要考虑您手牌的生命周期浮法。

即便如此,完美的牌效率并不足以取得胜利:每个获胜的手牌都与一定的得分关联,因此在许多情况下,选择一个低概率但预期价值更高的手牌可能更合理。在这里,我们的分解方法完全失效了,因为虽然可以将获胜配置的空间划分,但是计分具有非局部效应,因此整手牌必须作为一个整体来考虑。在这种情况下,可以尝试蒙特卡罗方法,因为直接表征概率空间太困难。然而,在日本麻将的计分系统中,这种方法还面临另一个困难:计分系统是指数级的。因此,我们面临这样一种情况:大多数样本的得分较低,但极少数样本具有指数级的回报。在这种情况下,很难说随机抽样是否会真正给出一个好结果,除非抽取指数多的样本。(另一方面,由于这些手牌如此罕见,一个 AI 可能简单地忽略它们而表现得相当好。)

总之,麻将是一款迷人的游戏,其庞大的状态空间使得准确表征涉及的概率变得困难。在我的论文中,我尝试解决其中的一些问题;如果你对此感兴趣,请查看。


[1] 不,我不是在谈论那些麻将接龙的荒谬事情。

[2] 明确一点,我并不是说扑克策略简单——下注策略可能是游戏中最有趣的部分之一——我只是说从概率角度来看,基本游戏相对简单。

[3] 天凤是一个流行的日本在线麻将客户端。天凤计算器的输入格式是,其中之前的数字表示万子,表示筒子,表示索子,表示风牌(顺序依次为:东、南、西、北、白、发、中)。每个条目指示你可以丢弃哪张牌来接近听牌;下一个列表是连切牌的数目(及使手牌进一步完善的牌数)。

来源:

这是 c2hs 的六部分介绍之一。最终我们谈论的是 c2hs 的主要用途:从 Haskell 调用 C 函数。由于 c2hs 对 C 头文件有了解,因此可以自动生成 FFI 导入的工作。 钩子只是告诉 c2hs 生成 FFI 导入,而 钩子则生成另一个执行 marshalling 的 Haskell 函数。

Call. 调用的格式非常简单,因为像 和 一样,它意味着可以与其他 Haskell 代码交错使用。如果我想从 调用 函数,只需 即可;c2hs 将会生成正确签名的 FFI 导入,并将调用指令转换为 FFI 导入的名称。

当然, 不会回调到 Haskell,所以我们可以添加 :。如果你确信 C 函数没有副作用,可以添加 :。如果多次调用同一个函数并使用相同的 FFI 声明,它们的标志需要保持一致。

默认情况下, 将精确用于确定 FFI 导入的名称;如果它不是函数的有效 Haskell 标识符(即大写),或者 C 函数名称会与其他名称冲突,则需要指定 FFI 将导入为什么。常见的约定包括给函数添加前缀 ,或者使用 来进行 c2hs 的大写转换。 将转换为 (并带有适当的 FFI 声明)。

Fun. 因为 FFI 声明的签名都是 C 类型,而 Haskell 程序往往不使用这些类型,并且因为频繁进行 C 类型的转换,有一点自动化来帮助你处理 指令。与 不同,它是作为一个定义独立存在的,而不是嵌入在代码中的。请注意, 必须使用 ;例如 gtk2hs 就没有使用它,但很多人发现它很有用。

的开始与 类似:首先指定它是纯的和/或不安全的,指定 C 标识符,然后指定 Haskell 名称。由于大多数代码将引用 Haskell 名称,通常最好指定 来保持一致的命名约定。

在这里,我们需要指定 所需 Haskell 函数的最终类型,以及 如何从这些类型转换为 C 类型(即 marshalling 函数)。c2hs 教程在这个主题上有一些内容,所以我们将采取更多的示例导向的方法。

基本 C 类型。 整数、浮点数和布尔(通常在幕后是整数)基本的 C 类型非常普遍,如果没有指定,c2hs 将自动使用、和/函数进行编组。这些函数可以双向工作。这个指令:

 
     

生成:

 
     

您可以看到,会添加一堆(丑陋的)生成代码以在参数上运行编组函数,将其传递给 FFI,然后在结果上调用另一个编组函数。惯用的 Haskell 可能是这样的:

 
     

如果您想要为编组函数使用不同的名称,可以在参数类型之前指定它(“in”编组器),或者在结果之后指定它(“out”编组器),如下所示:

 
     

而您只需在生成的 Haskell 中替换相关函数调用。

String 参数。 字符串也在 c2hs 的心中占有特殊地位;处理以 null 结尾的字符串和需要显式长度信息的字符串都很容易。考虑这两个函数原型:

 
     

我们可以编写以下 c2hs 指令:

 
     

并且它们将自动使用和进行编组。

这里发生了几件有趣的事情。我们使用(Haskell 中的空类型)来表示空返回类型。此外,中的 String 参数有一个附加的和号;这意味着编组器应该产生一个参数元组,这些参数将作为两个单独的参数传递给函数。确实,的结果是,而 c2hs 使用略有不同的变体,它将转换为。(请注意,如果您需要更复杂的多参数排序,并不适合您。)

但也许最有趣的是附加到输入编组器上的,它有两个效果。首先,它表明输入编组函数是 IO 单子,例如,的类型是。但更重要的是,它指示了一个遵循括号资源模式“with”的函数。我们没有使用,因为如果我们不稍后释放,这可能导致内存泄漏!然后生成的代码是:

 
     

使用悬挂 lambda 保持布局一致。

编组结构参数。 尽管 c2hs 文档声称如果您在 C 中有以下情况,那么会有一个默认的编组器:

 
     

并在 Haskell 中:

 
     

因此,您应该能够写出:

 
     

其中,输入编组器是。不幸的是,我从未能让它起作用;此外,c2hs 认为是一个保留字,所以您需要重命名它才能使用它。

 
     

不透明指针参数。 当您不想在 Haskell 中对指针执行任何花哨的操作时,可以简单地指定指针是参数并使用作为编组器。在前面的例子中,也可以另外定义为:

 
     

一个约定是,如果只处理不透明指针,可以省略指针类型的名称中的 。

输出编组器的输入参数。 C 代码中的一个常见模式是使用指针参数允许函数返回多个结果。例如, 的签名如下:

 
     

指向一个指针,该指针将设置为我们解析的 字符串部分的结尾处的指针。如果我们不关心它,可以将 设置为 。

显然,我们不希望我们的 Haskell 函数这样做,并且我们有更简单的方法使用元组返回多个结果,所以 c2hs 有一个关于输入参数的输出编组器的概念。它还有一个“虚假”输入参数的概念,用户不必传递,以防我们的函数完全负责分配指向函数的指针的内存。

这是编写 的 钩子的第一个尝试:

 
     

我们避开了默认的字符串编组,因为否则 不会给我们非常有趣的信息。这个版本是原始内容的简单复制。

为了改进这一点,我们认为 是返回 的一种方式。因此,在函数运行后,我们应该 (解引用指针)并返回结果:

 
     

在 IO 中,所以它需要星号,但对于我们的编组器来说,它并不会导致任何复杂的括号使用。现在,这个函数的 Haskell 返回类型不是 ;它是 。

 
     

由于我们要覆盖指针的原始内容,强制用户向我们传递它并没有多大意义。我们可以在我们的输入编组器后缀上加,以表明它不是真正的 Haskell 参数,并改用代替:

 
     

请注意,我们去掉了 ;这是一种或另一种方式。现在我们有一个可用的函数:

 
     

或者,在习惯用法的 Haskell 中:

 
     

错误处理。 还有一个功能片段我们尚未讨论,即一个输出编组器上的 标志,导致 Haskell 忽略结果。单独使用时通常没有用,但与 (表示操作在 IO 中)结合使用时,可用于附加检查错误条件并在情况成立时抛出异常的函数。请记住, 的默认输出编组器是 ,忽略函数的输出结果。

来源:

I’m currently collecting non-stack-overflow space leaks, in preparation for a future post in the Haskell Heap series. If you have any interesting space leaks, especially if they’re due to laziness, send them my way.

Here’s what I have so far (unverified: some of these may not leak or may be stack overflows. I’ll be curating them soon).

 
     

来源:

Edward 在这里分享了一些关于剑桥的故事,并发布了许多照片。

显然,Alyssa B. Hacker (注)参加了剑桥麻省理工交换项目。


或许这是因为在 MIT 待得时间太长,这个校园以不太风景如画而闻名(我可能能用一只手数出真正漂亮的地方),但停下来欣赏这里的建筑和建筑物是真的很容易。看看!

确实,我并不住在这些地方;我只是在去剑桥市中心上课的路上经过它们。我的学院,费茨威廉,从外面看并不那么漂亮(省略图片),但内部绝对华丽:

啊,别介意等待收集的袋子里装满了碎纸。

一幅非常精美的费茨威廉学院地图(抱歉有些地方有反光):

如果你眯起眼睛,可以看到地图上标有The Grove的地方。关于学院的这部分有一个相当有趣的故事:它实际上曾属于达尔文家族(即查尔斯·达尔文),学院建造时围绕这片树丛而建,直到艾玛·达尔文去世,这所房子被合并为学院的一部分。


很容易走进一个房间看到一些名人。比如托尼·霍尔(Quicksort 和 Hoare logic 的发明者)。我告诉人们我来剑桥大学的一部分是为了品尝其理论风味,我并没有失望。我非常喜欢马塞洛·费奥雷关于指称语义的讲座(也许我会写几篇博客文章)。其他讲座有些是半斤八两的(但再怎么说,讲座什么时候不是呢?),但它们成功填补了我教育中我不知道我不知道的领域。来自我的逻辑和证明课程的斯科莱姆化部分让我想起了这篇博客文章,曾经抱怨没有人告诉你斯科莱姆常量是如何实现类型检查器的事实。嗯,显然,斯科莱姆化是逻辑世界中的一种经典技术,确实像你在类型系统中所期望的那样工作(毕竟,Curry-Howard 同构)。

此外,我们的逻辑和证明导师还给我们提供茶水。 他在三一学院的房间(我们进行辅导的地方)就是著名数学家拉马努金在他是三一学院研究员期间住的同一间房间。说起来,现在是罗素和怀特黑德的《数学原理》百年纪念。

到目前为止,我在实际做研究方面彻底失败了,但我很享受理论讨论并吸收所有这些基础知识。

说到这一点,计算机实验室是古典风格。我猜你很难说服现代建筑师建造老式的建筑了。你会在传统硬核学院内发现一些奇特的东西,比如高跷上的图书馆:

啊,现代化的进程。如果我没有选择历史和科学哲学,我可能会被困在西剑桥的现代建筑中,但幸运的是,在实验室和菲茨威廉学院之间有一条美妙的小自行车道(尼迪告诉过我,但我终于错过了,最后在丘吉尔学院的后面骑车下了草坪,然后沿着道路反方向行驶,因为有栅栏挡住了我的路):


在英国食物可能不怎么样,但你真的不能击败它的甜食:

唉,我已经禁止自己购买任何糖果,以免在回到学院之前神秘消失。早餐吃榛子酱也被证明是一个极其糟糕的主意,我已经放弃了这种做法,改为吃一个面包和两块水果。剑桥有一个每天开放的市场广场(不错!),但没有太多的本地产品(可以理解,但仍有点遗憾!)


实际上,我发现了位于高跷上的图书馆,因为我试图确定 ICUSU 活动的位置,而且与在麻省理工学院不同的是,我在剑桥时不随身携带我的笔记本电脑(哎呀!)最终我找到了位置:

如果你说你在剑桥是在踢足球,那意味着与在麻省理工学院有很大不同。尽管我参考了维基百科(consulted Wikipedia),它与剑桥对该术语的解释是一致的。哦,我提到了吗,狗也会踢足球!

踢球有点不同于帆船;特别是,你将踏板杆推向你想要去的方向(而舵柄则向你希望去的相反方向)。


这篇文章有点长了,也许我会留点到以后再说。

附言。 今天我注意到了柯里化和教堂编码之间(并不特别深刻的)关系:也就是说,如果你用数据柯里化销毁函数(,,等),你得到的函数与该数据类型的教堂编码具有相同的类型。

来源:

我最近完成了剑桥大学的一年留学项目。你可以在这里阅读我的原始动机和第一印象。

现在是考试前的星期日,天气格外好。大多数学生(也许除了纯历史学家外)都在忙于复习,没有时间利用这样的好天气。但我被推出了我的窝,这是由一堆笔记和过去的考试题组成的,为了与我们的历史导师 Mirjam 进行最后一次复习。我骑车沿着格兰治路,把我的自行车停在里德利大厅外,然后受到了一个惊喜的欢迎:Mirjam 选择在户外的一组公园长椅上进行复习,四周都是草地和树木,还带来了一个装有新鲜水果、小蛋糕和其他点心的柳条篮子,还有起泡酒和啤酒(或许对于一个本质上是复习的活动来说并不是最好的饮品,但我们还是享用了)。

历史与科学哲学是由两个部分组成的课程。它们最初相对独立:科学史始于伽利略和 16 世纪初期的黎明,而科学哲学则从分析因果关系的本质开始。但到了课程结束时,关于量化和量子力学的哲学问题已经深深嵌入这些科学的历史中,关于西方文明起源根源于希腊和古巴比伦数学的传统观念也在日益哲学化的分析中被探讨。这两个学科的混合效果非常好,修读这门课程是一次非常丰富的经历。(有时,我甚至希望我们被要求写更多的一篇文章一周——对于麻省理工的人文课程来说,这是相当罕见的。)

这也是一项艰苦的工作。特别是在历史课上,我的最初论文由于我对如何综合更广泛的材料而不仅仅是必读的调查文本的误解而起步艰难(我可能在完成所有必读内容时有些“疏忽”)。写 HPS(历史与科学哲学)论文要求我在周末抽出大段时间来完成;我很少因为分配的计算机科学作业而熬夜,但在 HPS 论文到期的那个晚上却不得不这样做。我总是尽力在讲座中集中注意力(有时比其他时候更成功——直到今天我仍然不太理解现代研究型大学系统的兴起)。

几位教授在我的记忆中格外突出。我永远不会忘记哈索克·张(上图)的入门讲座,在一个离题的插曲中,他解释说他最初在加州理工学院主修物理学,但在他的教授们因他问诸如“大爆炸之前发生了什么?”这样的问题而感到恼怒后,他转而学习哲学。这是他第一年教授物理科学哲学系列讲座(许多学生并没有过去的考试题来备考),但他继续进行了一系列极为扎实、引人入胜且信息丰富的讲座(我认为这些讲座也吸引了一些对因果关系和归纳的抽象问题感到厌倦的物理学家)。他甚至在最后一堂课后邀请我们到附近的酒吧喝一轮,我们在那里就最近的一个话题进行了热烈的辩论。

史蒂芬·约翰进行了一系列极富活力和引人深思的科学伦理学讲座。他的大部分讲座让我们更多地提出了问题而不是答案,但它们是一次引人入胜的旅程,涉及知情同意、预防原则和成本效益分析的领域。(知情同意是我新的个人最爱之一:它是一个很好的例子,显示了一个被奉为医疗实践的保守和开放同时存在的状态。)而且我将永远记得我们的导师理查德·詹宁斯(下图),他总是面带微笑,留着胡须,他的辅导并不真正是辅导,而更像是关于最近讲座中涉及的话题的交流。

在历史讲师中,我不得不向埃莉诺·罗宾致敬,她进行了最后四堂科学史讲座。我承认:当我第一次看到教学大纲时,我对学习古巴比伦不太感兴趣,但这些讲座最终成为了关于史学问题的讨论:历史学家们如何得知那些“枯燥的事实”,那些每个读过高中历史的人都非常熟悉的事实?在很多方面,我们知道的如此之多令人惊讶。这是你希望早些学到的教训之一,尽管你在脑海的后面知道,你早些时候可能并不完全欣赏它们。

西蒙·沙弗(上图)也是一个相当有个性的人物,他进行了我们的第一系列历史讲座。你可以从短暂的 BBC 系列 光芒四射 中体会到他的风格,尽管当你亲自听他讲课时,他可能会更加直率一些(“胖,易怒,聪明。绝对是一个榜样,”沙弗在提科·布拉赫身上说)。当然,还有我们的历史导师米尔雅姆,她一直鼓励我们改进我们的论文写作。


HPS 真的很神奇。(即使我也参加了第二部分的迎新活动,尽管我并不符合资格。)如果你是一个科学家,对哲学或历史有兴趣(我第一次涉足哲学是在 CTY 上修读“逻辑与推理”课程),我强烈推荐这个项目。读者可能已经注意到我在博客上尝试讨论这些问题的各种尝试——相比我讨论的更技术性的事物,这些问题更难写,但我认为它们也非常值得(有时,它们是我最受欢迎的作品之一——尽管回想起来,这篇文章现在看起来有些惠格历史主义)。这也很有趣:我设法写了一篇关于库恩式科学革命的论文,并将其放在 MIT Mystery Hunt 的背景下进行了构思。我不会否认:在短暂的一段时间里,我感觉自己像是一名文科学生。它使我在很多方面成为了一个更好的人。

来源:

克里斯托弗·德·萨(Christopher de Sa)和我一直在研究一种类别论方法来优化类似 MapReduce 的管道。实际上,我们开始并不涉及任何范畴论——最初我们只是试图在Delite 编译器执行的一些现有循环优化上施加一些结构,顺便发现了范畴论与循环优化之间丰富的关系。

一方面,我认为这种方法非常酷;但另一方面,该领域有许多先前的工作,很难弄清楚自己在研究景观中的位置。当我与约翰·米切尔讨论这个想法时,他对我说:“循环优化,难道你不能只用表查找来解决吗?”我们从现有工作中获得了很多灵感,特别是由伯德、梅尔滕斯、马尔科姆、迈耶等人在 90 年代初开创的程序计算文献。这篇博客文章的目的是讨论我们已经研究出的一些想法,并从你这位亲爱的读者那里获得一些反馈。

有几种思考我们试图做的事情的方式:

  • 我们希望实现一个基于计算的优化器,针对一个真实的项目(Delite),其中循环优化的应用可以对任务的性能产生 drastc 的影响(其他具有类似目标的系统包括Yicho,HYLO)。
  • 我们希望冒险涉足理论家通常不会涉足的领域。例如,有许多“无聊”的函子(如数组),它们具有重要的性能特性。虽然它们可能与适当定义的代数数据类型同构,但我们认为在计算优化器中,我们希望区分这些不同的表示方式。同样,许多不是自然变换本身的函数可以通过部分应用变成自然变换。例如,当被作为函数定义的一部分(结果函数可以应用于任何列表,而不仅限于原始的)时,就是一个自然变换。这种结果的自然变换丑陋有用
  • 对于股票优化器(例如 Haskell),一些计算优化可以通过使用 重写规则 支持。虽然重写规则是一个非常强大的机制,但它们只能描述“始终有效”的优化方式;例如对于森林化,我们总是希望尽可能消除中间数据结构。在我们希望优化的许多应用中,只有通过 增加 中间数据结构才能达到最佳性能:现在我们有一个可能的程序空间,而重写规则对于指定哪个程序最好是明显不足的。我们希望能够使用范畴论来解释带有结构的重写规则,并利用领域特定知识来选择最佳程序。

我想通过一个例子来阐述这些想法。这里有一些用 Delite 写的示例代码,用于计算(一维)k 均值聚类的一个迭代:

 
           

可以这样理解它:我们正在计算一个结果数组,其中包含每个簇的位置,最外层的块正在通过索引变量 循环遍历簇。要计算簇的位置,我们必须获取分配给簇 的所有点 (即 的条件),将它们加在一起,最后除以簇中点的数量来获取真实位置。

这段代码的主要问题在于,它在整个数据集上 numClusters 次进行迭代,而我们只想执行一次迭代。优化后的版本看起来是这样的:

 
           

换句话说,我们必须预先计算加权点和点数(请注意两个 hashreduce 可以和应该融合在一起)之后,才能为每个簇生成新的坐标:在这种情况下,生成 更多 中间数据结构是有利的。

现在让我们计算优化程序的方式。但是首先,我们必须定义一些函子:

  • 是大小由 指定的 数组(具体来说,我们将使用 来表示大小为 的数组,以及 来表示大小为 的数组)。这组函子也被称为 对角函子,适用于任意大小的乘积。我们还将依赖于 是可表示的事实,即 对于某些类型 (在这种情况下,它是索引集 )。
  • 是 的标准列表。它是函子 的初始代数。任何 都可以嵌入到 中;我们将隐式地进行这些转换(请注意反过来则不成立)。

还有一些函数,我们将在下面描述:

  • 见证了 和 之间同构的一个方向,因为 是可表示的。另一个方向是 ,它接受 和一个 ,并返回一个 。
  • 是在 上的初始代数唯一确定的函数。此外,假设我们有一个函数 ,通过取它们的笛卡尔积来组合两个代数,
  • 是一个自然变换,它接受 并基于某些函数将其分桶到 中,该函数将 中的元素分配到 的插槽中。这是一个自然变换的示例,在部分应用之前并不是自然变换:如果我们计算 ,那么我们可以创建一个永远不会查看 的自然变换;它只是“知道”每个 插槽在结果结构中应该去的位置。

现在让我们用更功能化的术语重新写循环:

 
           

(其中 只是一个函数,它除以它的参数,但检查除数不为零。)消除一些共同的子表达式并将两个折叠融合在一起,我们得到:

 
           

在这一点上,仍然完全不清楚我们可以执行任何重写: 对我们造成了问题。然而,因为 在进行相等性测试,我们可以以不同的方式重写它:

 
           

这里发生了什么?不直接筛选仅在群集 中的项目,我们可以将其视为在 上进行 分桶 ,然后索引出我们关心的单个桶。这种视角的转变对整体优化至关重要。

现在我们可以应用自然变换的基本规则。设 和 ,那么我们可以将 推到 的另一侧:

 
           

现在我们可以消除 和 :

 
           

最后,因为我们知道如何高效实现 (作为 ),我们分解 并加入折叠和桶:

 
           

我们已经实现了我们的完全优化程序。

所有这些都是正在进行的研究,有许多未解决的问题。尽管如此,我希望这篇文章能让你感受到我们推崇的方法。我对你的评论很感兴趣,无论是“太棒了!”还是“这在 20 年前就被 X 系统完成了。” 期待听到你的看法!

到此这篇chronyc sources -v ^?原因(chronyc sources显示的结果)的文章就介绍到这了,更多相关内容请继续浏览下面的相关推荐文章,希望大家都能在编程的领域有一番成就!

版权声明


相关文章:

  • scores(scores翻译)2025-08-29 11:54:07
  • yuv422和rgb哪个好(yuv420和rgb哪个好)2025-08-29 11:54:07
  • storey和story的区别(story和tales的区别)2025-08-29 11:54:07
  • crna是什么意思(naa/cr是啥)2025-08-29 11:54:07
  • aurochs怎么拆分音节(scholar音节拆分)2025-08-29 11:54:07
  • redhat9(Redhat9虚拟机安装)2025-08-29 11:54:07
  • 破解加密的word文档(破解加密的word文档怎么恢复)2025-08-29 11:54:07
  • docker-compose升级(docker如何升级)2025-08-29 11:54:07
  • driddle怎么读(dries怎么读)2025-08-29 11:54:07
  • yarn命令查看任务提交时间(yarn查看任务状态)2025-08-29 11:54:07
  • 全屏图片