BarnOwl 的 Facebook 支持:ezyang 的博客
BarnOwl 的 Facebook 支持
这是专为 MIT 的人群准备的。今天早上,我满意地完成了我对 BarnOwl 的 Facebook 模块 的修改(我的满意度表现在对 Facebook API 调用的异步支持上,即不再随机冻结!)。但是,让它在 Linerva 上运行有点复杂,所以这里有个详细的步骤。
-
使用 MIT 网站上的说明 设置本地 CPAN 安装,使用
local::lib
。不要忘记将设置代码添加到.bashrc.mine
,而不是.bashrc
,然后进行源操作。不要忘记遵循先决条件:否则,CPAN 将会提示很多信息。 -
安装所有你需要的 CPAN 依赖项。对于 Facebook 模块,这意味着需要安装
Facebook::Graph
和AnyEvent::HTTP
。我建议使用notest
,因为Any::Moose
在 Linerva 上似乎会失败一个无害的测试。Facebook::Graph
失败了几个测试,但不用担心,因为我们将使用预打包版本。如果你想使用其他模块,你也需要在 CPAN 中安装它们。 -
克隆 BarnOwl 到本地目录 (
git clone git://github.com/ezyang/barnowl.git barnowl
),然后运行./autogen.sh
,configure
和make
。 -
使用
./barnowl
运行,然后输入命令:facebook-auth
并按照说明操作!
欢迎使用 Facebook!
附言. 我真的很惊讶,竟然没有一种流行的命令式语言有绿色线程和抢占式调度,允许你实际上编写看起来是阻塞的代码,尽管它在内部使用事件循环。也许这是因为在保证安全性的同时进行抢占是很难的……
已知的 bug. 读/写验证 bug 已修复。我们似乎在 BarnOwl 的事件循环实现中触发了一些 bug,这导致每天都会出现崩溃(这使得调试变得困难)。保持备份的 BarnOwl 实例是个好主意。
模块化编程的第一印象:ezyang 的博客
来源:
blog.ezyang.com/2011/08/first-impressions-of-module-programming/
在我在 Jane Street 的时间里,我做了大量涉及模块的编程工作。我涉及了函子、类型和模块约束、嵌套模块,甚至是一等公民模块(尽管只是次要的)。不幸的是,在《类型与编程语言高级主题》中关于模块的章节让我无法专注,所以我不能真正称自己在模块系统上是“有知识的”,但我认为我已经足够使用它们来对它们发表一些评论。 (所有关于惯例的评论都应该被视为 Jane Street 风格的指示。注:他们已经开源了部分他们的软件,如果你真的想看看我谈论的一些东西。)
好消息是它们基本上按照你的期望工作。事实上,它们非常巧妙。当你开始使用大量使用模块的代码库时,你会注意到的最基本的习惯用法是这样的:
module Sexp = struct
type t = ...
...
end
实际上,我曾经在 Henning Thielemann 的 Hackage 上看到过这种风格的地方,特别是data-accessor,我之前有过涵盖。与 Haskell 不同,在 OCaml 中,这种风格确实有意义,因为你从未像在 Haskell 术语中的未限定导入一样,你通常会将类型称为Sexp.t
。因此,抽象的基本单位可以被认为是一种类型——大多数简单的模块恰好是这样——但你可以辅助类型和操作该类型的函数。这是相当容易理解的,你可以将模块系统大多解析为一种便捷的命名空间机制。
然后事情变得有趣。
当你使用 Haskell 的类型类时,每个函数都会单独指定对参数的约束。OCaml 没有任何类型类,因此如果你想要这样做,你必须手动将字典传递给函数。你可以这样做,但这很烦人,OCaml 程序员更喜欢更大的东西。所以,你不是将字典传递给函数,而是将模块传递给函子,并一次性专门化所有“通用”函数。这更加强大,这种力量克服了在任何给定时间显式指定你使用的模块的烦恼。约束和嵌套模块从这个基本思想中自然而然地产生,当你实际尝试在实践中使用模块系统时。
对于我来说,关于模块系统最难理解的事情之一是类型推断和检查是如何在其上操作的。部分原因是类型类如何工作与之间存在的不匹配。当我有一个函数时:
f :: Monoid m => m -> Int -> m
m
是一个可以与任何特定类型统一的多态值。因此,如果我执行f 5 + 2
,如果为Int
定义了适当的 Monoid 实例(即使+
不是 Monoid 实例方法),那是完全合理的。
然而,如果我用模块做同样的技巧,我必须小心添加额外的类型约束来教编译器某些类型确实是相同的。这是一个额外的类型限制的例子,感觉应该被统一化消除,但实际上并没有:
module type SIG = sig
type t
val t_of_string : string -> t
end
module N : SIG = struct
type t = string
let t_of_string x = x
end
let () = print_endline (N.t_of_string "foo")
实际上,在您添加那个SIG
声明时,您必须指定t
和string
是相同的:
module N : SIG with type t = string = struct
有趣!(实际上,当您为大量类型指定约束时,而不仅仅是一个类型时,情况会变得更加恼人。)涉及到函子时,正确性也很棘手,在 OCaml 3.12 之前有一些错误,这意味着您必须采取一些丑陋的措施来确保您实际上可以编写您想要的类型约束(with type t = t
… 这些ts
是不同的…)
有时候,您确实会觉得在 OCaml 中真的很想要类型类。高度多态功能通常是关键因素:如果您有类似Sexpable
(可以转换为 S 表达式的类型),使用模块系统感觉非常像鸭子类型:如果它有一个sexp_of_t
函数,并且类型正确,它就是“sexpable”。天哪,我们基础库中大多数复杂的函子都是因为我们需要处理多参数类型类的道德等价物。
单子绑定当然是没有希望的。好吧,如果您的程序中只使用一个单子(然后您只需通过打开模块来专门化您的>>=
到该模块的实现)。但在大多数应用程序中,您通常在一个特定的单子中,如果您想快速切换到option
单子,您就没那么幸运了。或者您可以重新定义运算符为>>=~
,希望没有人刺伤您。:-)
VX-8R 的第一印象:ezyang 的博客
VX-8R 的第一印象
VX-8R 是我拥有的第一台业余无线电;我之前使用过 VX-7R,但我使用它的范围是有人递给我这台无线电说,“这是预先配置好了你需要的频率的无线电;这是静噪如何工作;这是如何调试常见问题;不要搞砸了。”这是我对 VX-8R 的印象。
-
尽管构造坚固,我还是要退回去更换保修。电池指示器有问题;在放电时它卡在 100% 电量,而在充电时是 0% 电量。根据 HRO 的代表,这是非常不寻常的。不得不送回无线电更换有点麻烦,但嘛,能怎么办呢。
-
Yaesu 试图尽量避免模式,但当它处于模式时,稍微难以确定哪个键执行什么功能。例如,在扫描时,按下 PTT 可以终止扫描,但 BAND 键和箭头键也有同样的作用。PTT 实际上是一个相对可靠的方法来退出 FOO 模式。
-
我喜欢扫描界面。按住 UP/DOWN 开始扫描,如果停留在错误的地方,使用旋钮微调它,当听到有趣的东西时按下 PTT。
-
VX-8R 最棒的地方之一是立体声耳机插孔,部分弥补了需要两个适配器才能获取分割扬声器和 PTT 麦克风套装的不便。我已经用 Yaesu 听了很多 FM 收音机(也许不是最有趣的用途,但总归有用!)立体插头位于一个相当可感知的深井内,所以你可能会发现较短的插头插入困难。
-
关于改装,尽管约一年前发布,看起来 VX-8R 仍然没有可用的软件修改软件。当前的硬件修改只开放了 MARS/CAP 发射频率。当前的硬件修改
关于麦克风困境还没有消息;我可能只是花点钱买个 Pryme 耳机(它们比我想象的要贵一点点)。
c2hs 的第一步:ezyang 的博客
来源:ezyang 博客
这是 关于 c2hs 的六部分教程系列中的第四部分。今天我们讨论 c2hs 中的简单事物,即类型、枚举、指针、导入和上下文指令。
Prior art. c2hs 支持的所有指令都在 “tutorial”页面 中简要描述(也许更准确地说是“参考手册”,而非教程)。此外,在 c2hs 的 研究论文 中,对大多数指令也有更为非正式的介绍。
Type. C 代码偶尔包含宏条件重新定义类型的情况,具体取决于某些构建条件(以下是真实代码):
#if defined(__ccdoc__)
typedef platform_dependent_type ABC_PTRUINT_T;
#elif defined(LIN64)
typedef unsigned long ABC_PTRUINT_T;
#elif defined(NT64)
typedef unsigned long long ABC_PTRUINT_T;
#elif defined(NT) || defined(LIN) || defined(WIN32)
typedef unsigned int ABC_PTRUINT_T;
#else
#error unknown platform
#endif /* defined(PLATFORM) */
如果你想要编写引用使用 ABC_PTRUINT_T
函数的 FFI 代码,你可能需要对 Haskell 中值的真实情况进行猜测或使用 C 预处理器重新实现条件。使用 c2hs,你可以通过 type
获取 typedef 的真实值:
type ABC_PTRUINT_T = {#type ABC_PTRUINT_T #}
考虑一个 64 位 Linux 系统的情况(__ccdoc__
未定义,LIN64
已定义),则结果是:
type ABC_PTRUINT_T = CLong
Enum. 枚举在编写良好的(即避免魔术数字)C 代码中经常出现:
enum Abc_VerbLevel
{
ABC_PROMPT = -2,
ABC_ERROR = -1,
ABC_WARNING = 0,
ABC_STANDARD = 1,
ABC_VERBOSE = 2
};
然而,在底层,这些实际上只是整数(ints),因此希望在 Haskell 代码中将枚举值传递给函数的代码必须:
-
创建一个新的数据类型来表示枚举,并
-
编写一个函数,将该数据类型映射到 C 整数,然后再次映射回来,以便创建
Enum
实例。
我们可以让 c2hs 为我们完成所有工作:
{#enum Abc_VerbLevel {underscoreToCase} deriving (Show, Eq) #}
变成了:
data Abc_VerbLevel = AbcPrompt | AbcError | AbcWarning | AbcStandard | AbcVerbose
deriving (Show, Eq)
instance Enum Abc_VerbLevel
fromEnum AbcPrompt = -2
-- ...
注意,由于 ABC_PROMPT
在 Haskell 中是一个非常难看的构造函数,我们使用如上述的 underscoreToCase
算法转换名称。您也可以明确列出这些重命名:
{#enum Abc_VerbLevel {AbcPrompt, AbcError, AbcWarning, AbcStandard, AbcVerbose} #}
或者更改数据类型的名称:
{#enum Abc_VerbLevel as AbcVerbLevel {underscoreToCase} #}
还有另外两种变换(可以与 underscoreToCase
结合使用:upcaseFirstLetter
和 downcaseFirstLetter
,尽管我不确定后者何时会导致有效的 Haskell 代码。
Pointer. 与指定在 Foreign.C.Types
中的 C 原语不同,Haskell 需要告知如何将指针类型(foo*
)映射到 Haskell 类型。考虑某些结构体:
struct foobar {
int foo;
int bar;
}
完全有可能在 Haskell 代码库中存在 data Foobar = Foobar Int Int
,在这种情况下,我们希望 Ptr Foobar
表示原始 C 代码中的 struct foobar*
。c2hs 无法直接推导出这些信息,因此我们向其提供这些信息:
{#pointer *foobar as FoobarPtr -> Foobar #}
这生成了以下代码:
type FoobarPtr = Ptr Foobar
但更重要的是,允许 c2hs 在为 FFI 绑定编写的签名中放置更具体的类型(我们将在本系列的下一篇文章中看到)。
一些主题的变种:
-
如果你想表示一个不会进行马歇尔处理的不透明指针,你可以选择空数据声明:
data Foobar {#pointer *foobar as FoobarPtr -> Foobar #}
或者你可以让 c2hs 使用新类型技巧生成代码:
{#pointer *foobar as FoobarPtr newtype #}
我更喜欢空数据声明,因为在这种情况下不需要包装和解包新类型:新类型将生成:
newtype FoobarPtr = FoobarPtr (Ptr FoobarPtr)
如果代码期望
Ptr a
,则需要将其解包。 -
如果你不喜欢
FoobarPtr
这个名称,而只想显式地说Ptr Foobar
,你可以告诉 c2hs 不要发出类型定义,使用nocode
:{#pointer *foobar -> Foobar nocode #}
-
如果没有指定 Haskell 名称映射,它将简单地使用 C 名称:
-- if it was struct Foobar... {#pointer *Foobar #}
-
如果你想引用 C 中已经是指针的 typedef,只需省略星号:
typedef struct Foobar* FoobarPtr {#pointer FoobarPtr #}
-
c2hs 也支持有限的声明指针为 foreign 或 stable,并相应地生成代码。我没有在这方面使用过,除了一个情况,发现指针的生成绑定不够灵活。效果可能有所不同。
导入. 包含多个头文件的 C 库可能会有一些头文件包含其他头文件以获取重要的类型定义。如果你组织你的 Haskell 模块类似地,你需要模仿这些包含:这可以通过 import 来实现。
{#import Foobar.Internal.Common #}
特别是,这会设置来自其他模块的 pointer
映射,并生成通常的 import
语句。
上下文(可选). 上下文有两个所谓的目的。第一个是指定文件中 FFI 声明应链接的库;然而,在 Cabal 中,这实际上没有任何作用——所以你仍然需要将库添加到 Extra-libraries
。第二个是通过为你引用的每个 C 标识符添加隐式前缀来节省击键次数,假设原始的 C 代码被命名空间为 gtk_
或类似的。我个人喜欢不需要将我的导入限定到更低级别的 API,并喜欢 C 前缀的视觉区分,所以我倾向于省略这一点。一些指令允许你在局部改变前缀,特别是 enum
。
下次. 使用 get 和 set 进行马歇尔处理。
五种高级的 Git 合并技巧:ezyang 的博客
五种高级的 Git 合并技巧
你是否曾经在 Git 中执行过合并,但结果并不如你所希望的那样?例如,你意外地将所有 UNIX 换行符转换为 DOS 换行符,现在整个文件都报告有冲突?也许你看到一个你并不想解决的冲突,想要以他们的版本解决?或者,冲突的文件是空的,你无法弄清楚发生了什么?
这里有一些高级技巧,你可以应用到冲突的合并中,使事情变得更容易一些。其中许多技巧利用了 Git 的底层命令;也就是说,直接与 Git 抽象层(索引、树、提交图)交互的内部命令。其他技巧则简单到只需改变一个配置开关。
-
使用
git config --global merge.conflictstyle diff3
来转换diff3
冲突。diff3
冲突风格在新的|||||||
标记和=======
标记之间添加了一个额外的部分,该部分显示了原始内容,你的修改在上面,他们(被合并的分支)的修改在下面。diff3
是重新建立你几个月前做出的更改背景的强大方式(要查看你的更改,比较中间部分和上部分;要查看他们的更改,比较中间部分和下部分),默认情况下应该开启这个选项,真的没有理由不这样做。 -
如果你曾经使用过 Subversion,你可能熟悉
FILE.mine
、FILE.r2
(你最初使用的原始文件)和FILE.r3
(最新版本检入的文件),以及运行svn resolve --accept theirs-full
或mine-full
的能力,这些命令表示“我不关心其他的更改,只使用这个文件的版本”。Git 提供了类似的功能,利用合并的父提交,尽管它们可能更为隐蔽。你可能已经熟悉了
git show
命令,它允许你查看提交以及在任何给定提交的树中查看任意的 blob。当你处于合并状态时,你可以使用特殊的:N:
语法,其中N
是一个数字,来自动选择其中一个合并父提交。1
选择共同的基础提交(较低的版本),2
选择你的版本(“mine”),3
选择他们的版本(较高的版本)。因此,git show :3:foobar.txt
会显示foobar.txt
的上游版本。要实际使用其中一种版本作为合并的解决方案,请使用
git checkout {--ours|--theirs} filename.txt
。 -
当你处于冲突状态时,
git diff
会提供所有发生冲突的详细信息,有时这些信息太多了。在这种情况下,你可以运行git ls-files -u
查看所有未合并的文件(这比git status
快得多,并且会省略所有已正确合并的文件)。你可能会注意到列表中存在多达三份文件的副本;这告诉你之前提到的“公共”,“我们的”和“他们的”副本的状态。如果 1(公共)丢失,这意味着该文件同时出现在我们的分支和他们的分支中。如果 2(我们的)丢失,这意味着我们删除了该文件,但它在上游有了变更。如果 3(他们的)丢失,这意味着我们做了一些更改,但上游删除了该文件。如果一个文件有冲突,但你无法弄清原因(因为没有冲突标记),这尤其有用。
-
有时生活会给你柠檬。许多人建议你制作柠檬汁。然而,如果 Git 给了你一个非常糟糕的冲突标记集,例如,你不小心颠倒了一个文件的换行样式,现在整个文件都发生了冲突,那就不要妥协:重新为该文件进行合并。你可以使用方便的
git merge-file
命令来做到这一点。这将运行一个三方文件合并,并接受三个参数:当前文件,公共文件和上游文件,并将合并写入当前文件(第一个参数)。使用git show
来转储你的文件,公共文件和上游文件,对这些文件进行必要的更改(例如运行dos2unix
),运行git merge-file mine common theirs
,然后将mine
复制到旧的有冲突的文件上。哇,即时得到新的冲突标记集。如果你在合并过程中较早发现了全局冲突,并且是你的错,回退合并可能更容易
git reset --hard
,修复错误,然后再尝试合并。然而,如果你已经在合并一个副本时取得了重大进展,重新合并一个单独的文件可能会拯救你的一命。 -
不要合并,应该变基!而不是运行
git pull
,运行git pull --rebase
。而不是运行git merge master
,运行git rebase master
。结果将会使你的历史记录更清晰,如果你想向上游提交补丁,你将不需要进行大规模的变基马拉松。
现在,继续前进,尽情合并吧!
五个保持可维护 Shell 脚本的技巧 : ezyang’s 博客
来源:
blog.ezyang.com/2010/03/five-tips-for-maintainable-shell-scripts/
五个保持可维护 Shell 脚本的技巧
当我十七岁时,我写了我的第一个 Shell 脚本。那是一个 Windows 批处理文件,小心地从网络上各种代码示例中摘录的片段。我已经体验过与 pear.bat
交互的 绝妙 乐趣,脚本编写并不是我喜欢的事情;“为什么不用一个真正的编程语言来写这个该死的东西!”(额外美味的是,“真正的编程语言”是 PHP。嘻。)
最终我转向了完全的 Unix 环境,随之开始广泛使用 bash。突然间,Shell 脚本变得更加有意义:你一直在日复一日地输入命令,不如把它们写成脚本!然而,Shell 脚本有个讨厌的小问题:它们是永远的;不管你喜不喜欢,它们已经成为维护代码的一部分。整个构建基础设施都建立在 Shell 脚本之上。它们像兔子一样繁殖;你必须小心这些小家伙。
这里有五个提示和技巧,当你将命令写入一个 Shell 脚本时,请记住,这些将使得长期维护变得更加愉快!
-
学会并喜欢使用
set
。几乎没有理由不使用-e
标志,这会导致如果任何命令返回非零退出码,你的脚本会报错,并且-x
可以通过在执行命令前打印出脚本正在执行的确切命令,帮你节省数小时的调试时间。启用这两个选项后,你在 Shell 脚本中得到了非常简单的“断言”:check_some_condition ! [ -s "$1" ]
尽管如此,如果可能的话,你应该编写错误消息来陪伴它们。
-
就因为你在终端时不定义子过程(或者你有吗?看看
alias
和朋友们)并使用C-r
进行反向命令历史搜索,这并不意味着在你的 Shell 脚本中重复命令是可以接受的。特别是,如果你有一组可能单独放入脚本中的命令,但又觉得单独建立文件有点奇怪,可以像这样将它们放在子过程中:subcommand() { do_something_with "$1" "$2" }
特别是,参数传递的行为与真实的 Shell 脚本完全一样,通常你可以把子命令当作它自己的脚本来处理;标准输入和输出的工作方式也符合你的预期。唯一的区别是
exit
会退出整个脚本,所以如果你想中断一个命令,应该使用return
代替。 -
Shell 脚本中的参数引用是一个奇怪而深奥的领域(虽然它不必如此;查看沃尔德曼关于 shell 引用的笔记)。简而言之,总是要用引号包裹将被插值的变量,除非你确实想要多个参数的语义。关于是否应该引用字面值,我的感觉参差不齐,最近我已经养成了不引用它们的恶习。
-
信不信由你,shell 脚本具有函数式编程的倾向。例如,
xargs
就是典型的“map”功能。然而,如果你将参数传递给的命令不接受多个参数,你可以使用这个技巧:pgrep bash | while read name; do echo "PID: $name" done
-
Shell 脚本在命令式编程时感觉非常自然,并且在控制流程时大多保持这种方式。然而,对于任何数据处理来说,它绝对是一个糟糕的语言(例如:sed 和 perl 管道),你应该避免在其中进行过多的数据处理。在更合理的语言中创建实用脚本可以有效地使你的 shell 脚本更加优雅。
在 coBurger King 中翻转箭头:ezyang 的博客
来源:
blog.ezyang.com/2010/07/flipping-arrows-in-coburger-king/
为工作中的 Haskell 程序员提供的范畴论速成课程。
在讨论对偶数据结构(最常见的是 co-monad)时经常出现的一个问题是:“co- 是什么意思?”范畴论的口气答案是:“因为你翻转了箭头。”这令人困惑,因为如果你看一看 monad 和 co-monad 类型类的一个变体:
class Monad m where
(>>=) :: m a -> (a -> m b) -> m b
return :: a -> m a
class Comonad w where
(=>>) :: w a -> (w a -> b) -> w b
extract :: w a -> a
这里有很多“箭头”,只有少数箭头被翻转(具体来说,是>>=
和=>>
函数的第二个参数内的箭头,以及 return/extract 中的箭头)。本文将准确解释“翻转箭头”的含义和使用“对偶范畴”,即使你对范畴论一窍不通也不例外。
符号. 本文中将会有几个图表。你可以把任何节点(又名对象)看作是 Haskell 类型,把任何实线箭头(又名态射)看作是连接这两种类型的 Haskell 函数。(不同的概念将用不同的颜色箭头来区分。)所以如果我有f :: Int -> Bool
,我会这样画出来:
Functors. Functor 类型类对于工作中的 Haskell 程序员来说并不陌生:
class Functor t where
fmap :: (a -> b) -> (t a -> t b)
虽然类型类似乎暗示了 Functor 实例的只有一个部分,即fmap
的实现,但还有另一个几乎微不足道的部分:t
现在是一个 kind 为* -> *
的类型函数:它接受一个类型(a
)并输出一个新的类型(无聊地命名为t a
)。因此,我们可以用这个图表示它:
箭头以不同的颜色标注是有充分理由的:它们指示完全不同的东西(并且碰巧出现在同一个图表中)。红色箭头表示一个具体的函数a -> b
(fmap
的第一个参数),而虚线蓝色箭头并不是声称存在一个函数a -> t a
:它只是指示 functor 如何从一个类型映射到另一个类型。它可能是一个没有合法值的类型!我们也可以假设该类型的一个函数的存在;在这种情况下,我们将有一个 pointed functor:
class Functor f => Pointed f where
pure :: a -> f a -- aka return
但是对于我们的目的来说,这样一个函数(或者说是吗?)在我们达到 monads 之前并不是很有趣。
你可能听说过 Functor 定律,这是所有 Functor 都应满足的一个等式。在这里,它以文本形式出现:
fmap (g . f) == fmap g . fmap f
并且以下是以图形方式表示:
你可以将这个图想象成一个巨大的if..then
语句:如果存在f
、g
和g . f
,那么fmap f
、fmap g
和fmap (g . f)
也存在(只需对它们应用fmap
!),并且它们恰好以相同的方式组合。
事实上,如果我们有f :: a -> b
和g :: b -> c
,则g . f
也必然存在,因此我们实际上不需要绘制箭头。这是函数组合的一个如此隐含的概念,所以我们会花一点时间问一下:为什么会这样?
原来当我画红色箭头的图表时,我在画数学家称为带有对象和箭头的范畴。最近几个图表都是在所谓的范畴 Hask 中绘制的,该范畴的对象是 Haskell 类型,箭头是 Haskell 函数。范畴的定义内置了箭头的组合和身份:
class Category (~>) where
(.) :: (b ~> c) -> (a ~> b) -> (a ~> c)
id :: a ~> a
(你可以在头脑中将~>
与->
替换为 Hask),并且有使箭头组合成为可结合的箭头的法则。最相关的是,当你谈论对偶范畴时,范畴箭头恰好是你翻转的箭头。
“太棒了!”你说,“这意味着我们完成了吗?”不幸的是,还没有。虽然余单子是对偶(或双重)范畴的单子,但它并不是范畴Hask.
(这不是你要找的范畴!)尽管如此,我们花了这么多时间在Hask
中舒适地绘制图表,如果不好好利用一下就太可惜了。因此,我们将看到 Hask 的对偶范畴的一个例子。
逆变函子。 你可能听说过fmap
被描述为将函数“提升”到函子上下文的函数:这个“函子上下文”实际上只是另一个范畴。(要真正数学地证明这一点,我们需要证明函子定律足以保留范畴定律。)对于普通函子来说,这个范畴就是 Hask(实际上是它的子范畴,因为只有类型t _
符合对象的条件)。对于逆变函子来说,这个范畴是 Hask^op。
在 Hask 中的任何函数f :: a -> b
都会成为逆变函子中的函数contramap f :: f b -> f a
:
class ContraFunctor t where
contramap :: (a -> b) -> t b -> t a
这里是对应的图表:
请注意,我们将图表分成了两部分:一部分在 Hask 中,另一部分在 Hask^op 中,注意从一个范畴到另一个范畴的函数箭头(红色)翻转,而函子箭头(蓝色)则没有翻转。t a
仍然是一个逆变函子值。
你可能会想,头疼不已地想知道:我们是否可以使用contramap
的任何实例?事实上,有一个非常简单的例子直接来自我们的图表:
newtype ContraF a b = ContraF (b -> a)
instance ContraFunctor (ContraF a) where
contramap g (ContraF f) = ContraF (f . g)
对于本文其余部分来说,理解这个实例并不太重要,但感兴趣的读者应该将其与普通函数的函子进行比较。除了新类型的包装和解包之外,只有一个变化。
自然变换。 我要提前给出结论:在余单子的情况下,你要找的箭头是自然变换。什么是自然变换?什么样的范畴以自然变换为箭头?在 Haskell 中,自然变换大致上是多态函数:它们是在函子上定义的映射。我们将用灰色表示它们,并且引入一些新的符号,因为我们将处理多个函子:下标表示类型:fmap_t
是fmap :: (a -> b) -> t a -> t b)
,而η_a
是η :: t a -> s a
。
让我们回顾一下围绕的三种箭头类型。红色箭头是函数,它们是 Hask 范畴中的态射。蓝色箭头指示了类型之间的函子映射;它们还作用于函数以生成更多函数(同样在 Hask 范畴中:这使它们成为自函子)。灰色箭头同样是函数,因此它们也可以被视为 Hask 范畴中的态射,但是在从一个函子到另一个函子的所有类型(对象)之间,灰色箭头的集合共同形成了自然变换(图表中描绘了自然变换的两个分量)。单个蓝色箭头不是函子;单个灰色箭头不是自然变换。相反,适当类型的集合才是函子和自然变换。
因为f
似乎在图表中杂乱无章,我们可以轻松地省略它:
Monad. 这是类型类,为了提醒你:
class Monad m where
(>>=) :: m a -> (a -> m b) -> m b
return :: a -> m a
你可能听说过一种定义Monad类型类的另一种方法:
class Functor m => Monad m where
join :: m (m a) -> m a
return :: a -> m a
其中:
m >>= f = join (fmap f m)
join m = m >>= id
join
更深入地扎根于范畴论(事实上,它定义了使 monad 成为 monoid 的臭名昭著的二元运算的自然变换),你应该确信join
或>>=
都能胜任。
假设我们对我们正在处理的 monad 一无所知,只知道它是一个 monad。我们可能会看到什么类型?
趣味的是,我这里将箭头标成了自然变换,而不是我们在 Hask 中为不显著函数所做的红色标记。但是,函子在哪里?m a
很简单:任何 Monad 也都是函子的有效实例。a
看起来像一个普通值,但也可以视为Identity a
,即a
在恒等函子中的形式:
newtype Identity a = Identity a
instance Functor Identity where
fmap f (Identity x) = Identity (f x)
而 Monad m => m (m a)
只是一个函子两层深:
fmap2 f m = fmap (fmap f) m
或者,以无参数风格:
fmap2 = fmap . fmap
(每个 fmap 将函数嵌入到更深的函子中。)我们可以精确地表示这些函子与类似以下内容组合的事实(抄袭自 sigfpe):
type (f :<*> g) x = f (g x)
在这种情况下 m :<*> m
是一个函子。
尽管这些图表直接源自 monad 的定义,但也有重要的 monad 定律,我们也可以为其绘制图表。我将只画带有 f
的 monad 恒等律:
return_a
表示return :: a -> m a
,而join_a
表示join :: m (m a) -> m a
。这里是其余的部分,去除了f
:
你可以将浅蓝色文字解释为“新鲜”—它是自然变换创建(或压缩)的新“层”。第一个图表示恒等律(传统上为return x >>= f == f x
和f >>= return == f
);第二个表示结合律(传统上为(m >>= f) >>= g == m >>= (\x -> f x >>= g)
)。这些图表等同于以下代码:
join . return == id == join . fmap return
join . join == join . fmap join
余单子。 单子属于自函子 Hask -> Hask
的范畴。自函子的范畴以自函子为对象,并(毫不奇怪地)以自然变换为箭头。因此,当我们制作余单子时,我们翻转自然变换。有两种:join 和 return。
这是类型类:
class Functor w => Comonad w where
cojoin :: w a -> w (w a)
coreturn :: w a -> a
它们分别已重命名为duplicate
和extract
。
我们还可以翻转自然变换箭头来得到我们的余单子法则:
extract . duplicate == id == duplicate . extract
duplicate . duplicate == fmap duplicate . duplicate
下一次。 尽管从联结和核返推导出<<=
是完全合理的,但一些读者可能会感到被愚弄,因为我实际上从未讨论过 Haskell 程序员经常处理的单子功能:我只是改变了定义,直到哪些箭头翻转为明显为止。因此,希望在未来某个时候,我能为 Kleisli 箭头绘制一些图表,并展示其含义:特别是为什么>=>
和<=<
被称为 Kleisli 组合。
致歉。 早晨三点,我竟然遗漏了所有正式定义和证明!对此我是个非常糟糕的数学家。希望在阅读完这篇文章后,你能去查阅每个主题的维基百科文章,并理解它们的描述!
附言。 你可能会对这篇关于在更简单环境中的对偶性的后续文章感兴趣。
食品相关的函数幽默:ezyang 的博客
食品相关的函数幽默
秋天即将来临,而随之而来的是一大群饥饿的新生们蜂拥而至麻省理工学院校园。我将举办三场食品活动… 全部都是函数式编程的双关语。嗨!
饺子复合主义
合成:结构的构建。消费:结构的消耗。复合:既是合成又是消费。这个活动?是饺子的复合。来学习基本的折叠,或者只是对食物进行新陈代谢还原。
我过去做过这个活动好几次,总是有趣的(尽管有点汗流浃背)。包饺子,事实证明,是一项非常可并行化的任务:你可以让多个人擀皮、包饺子,还有一些勇敢的厨师真正地煮或者煎它们。(其实,在中国,没有人再自己擀皮了,因为市售的饺子皮太好了。但在美国不是这样……)
炒菜锅组合器
计算机科学家熟悉这种组合方式,但食品科学家可能不太了解。在中国,炒菜锅组合器是一个严密保密的秘密,保证在最短时间内将蔬菜和肉类组合在一起。(也适合素食者。)
饺子在学期紧张的麻省理工学院学生来说有些不太实际;它们通常会被保留到学期初的特殊活动中,如果有的话。然而,炒菜是快捷、便宜、简单的选择,是任何大学生健康饮食的重要组成部分。我个人最喜欢的是西兰花和鸡肉(几乎不可能出错),但最近我也开始喜欢甜椒和西班牙香肠了(见下文)。这个活动运行起来的一个困难是确保有足够的电饭煲……米饭不够了可不好,因为重新煮一锅米饭需要很长时间!
多重烘烤主义
Roast(X) 其中 X = {西兰花,大蒜,猪肉,甜椒,西班牙香肠,胡萝卜,洋葱,芦笋,甜土豆}。
这是一个新的尝试。实际上,我只是想做烤西兰花和大蒜。真的,太好吃了。我以前从未烤过西班牙香肠,但菜单上的肉类似乎不够,所以我就加进去了。在波特兰时,我经常在农贸市场购买各种随机的蔬菜,回家后又得想办法如何烹饪它们。在许多情况下,烤制是一个相当不错的选择!我忘了在活动描述中加上甜菜根;也许我会去买一些…
从数据类型定义到代码:ezyang 的博客
这些问题有什么共同点:递归的相等性/排序检查,打印字符串表示,序列化/反序列化二进制协议,哈希,生成获取器/设置器?它们是具有强烈依赖于它们操作的数据结构的重复样板代码。由于程序员喜欢自动化事务,因此出现了各种关于如何做到这一点的思想流派:
-
让你的 IDE 为你生成这些样板代码。你右键单击上下文菜单,点击“生成
hashCode()
”,你的 IDE 就会为你进行必要的程序分析; -
创建一个自定义的元数据格式(通常是 XML),然后运行另一个程序,将这个描述转换为代码;
-
在你的语言中添加足够强大的宏/高阶功能,这样你就可以编写生成程序内实现的程序;
-
在你的语言中添加足够强大的反射功能,这样你就可以为这个功能编写一个完全通用的动态实现;
-
做一个编译器,并在抽象语法树上进行静态分析,以找出如何实现相关操作。
直到我遇到 camlp4 系统广泛使用的一个特定方面时,我才意识到第五个选项有多么普遍。虽然它自称为“宏系统”,但在 sexplib 和 bin-prot 中使用的宏并不是 C 传统的宏(这对于实现 3 是有好处的),而是 Lisp 传统的宏,包括访问 OCaml 的完整语法树和修改 OCaml 的语法的能力。然而,与大多数 Lisp 不同,camlp4 可以访问 数据类型定义 的抽象语法树(在非类型化语言中,这些通常是隐式的),它可以用来转换成代码。
我感兴趣的一个问题是,这种元编程是否能在语言的休闲用户中流行起来。如果我编写代码将数据结构转换为类似 Lisp 的版本,那么将这段代码概括为元编程代码,是否是一个逻辑上的下一步,还是一个仅由极限用户完成的非常大的飞跃?至少从用户的角度来看,camlp4 非常不显眼。事实上,一个月后我甚至没有意识到我在使用它!例如,使用 sexplib 就是一个简单的事情,只需写:
type t = bar | baz of int * int
with sexp
几乎像魔法一样,sexp_of_t
和 sexp_to_t
就会出现。
但是定义新的转换显然更加复杂。问题的一部分在于你操作的抽象语法树非常复杂,这是使语言编程友好的不可避免的副作用。我可以理论上使用求和和乘积定义我关心的所有类型,但是真实的 OCaml 程序使用带标签的构造函数、记录、匿名类型、匿名变体、可变字段等。因此,我必须为所有这些情况编写案例,如果我不是一个语言专家的话,这就很困难了。
解决这个问题的一个可能的方法是定义一个更简单的核心语言进行操作,这与 GHC Haskell 在代码生成之前编译到核心语言的方式类似。然后,您可以通过注解系统(即使您可以访问完整的 AST 时也是如此)提供额外的信息。如果这个想法基本上很简单,就不要强迫最终用户处理所有与创建良好的编程语言相关的附带复杂性。当然,除非他们愿意。
后记. 我绝对不擅长文献检索。与大多数想法一样,可以安全地假设其他人已经做过了。但我在这里找不到任何先前的研究成果。也许我需要一个比“用于元编程的中间语言”更好的搜索查询。
功能加密:ezyang’s 博客
功能加密
最近,Joe Zimmerman 向我分享了一种关于各种加密方案的新思路,称为功能加密。更深入地阐述了这一概念的是丹·博内等人在一篇非常易于理解的最新论文中。下面是摘录的摘要第一段:
我们通过给出概念及其安全性的精确定义,开始了对功能加密的正式研究。粗略地说,功能加密支持受限制的密钥,使密钥持有者能够学习加密数据的特定函数,但不会了解数据的其他信息。例如,给定一个加密程序,密钥可能使持有者能够学习在特定输入上程序的输出,而不会了解程序的其他任何信息。
值得注意的是,功能加密泛化了许多现有的加密方案,包括公钥加密、基于身份的加密和同态加密。不幸的是,在某些安全模型中,功能加密总体上存在一些不可能的结果(链接的论文对仿真模型有一个不可能的结果)。功能加密还没有维基百科页面;也许你可以写一个!
说来也奇怪, 我的一位数学博士朋友最近问我:“你认为 RSA 有效吗?” 我说:“不,但也许目前没有人知道如何破解它。” 然后我问他为什么这么问,他提到他正在上密码学课程,考虑到所有的假设,他很惊讶其中任何一个都能工作。我回答说:“是的,听起来大概是这样。”
函数产生 Haskell 堆:ezyang 的博客
来源:
blog.ezyang.com/2011/04/functions-produce-the-haskell-heap/
我们已经讨论过如何在 Haskell 堆中打开(评估)礼物(thunk):我们使用 IO。但是所有这些礼物都是从哪里来的呢?今天我们介绍的是所有这些礼物来自哪里,那就是 Ghost-o-matic 机器(一个 Haskell 程序中的函数)。
使用一个函数涉及三个步骤。
我们可以把这台机器看作是一个黑匣子,它接受礼物标签并产出礼物,但你可以想象其内部有无限多的相同幽灵和空的礼品盒:当你运行这台机器时,它会把一个幽灵的副本放入盒子中。
如果我们放入礼物中的幽灵是相同的,它们是否会表现得一样?是的,但有一个注意事项:幽灵的行为由脚本(原始源代码)决定,但在脚本内部有空洞,这些空洞由您插入到机器中的标签填充。
由于盒子里实际上什么也没有,我们可以通过困扰它的幽灵精确地描述一个礼物。
使用 Ghost-o-matic 的人经常遇到的问题是他们期望它像 Strict-o-matic(传统严格求值语言中的函数)一样工作。它们甚至不接受相同的输入:Strict-o-matic 接受未包装的、未幽灵化(未解除提升)的对象和礼品卡,并输出其他未幽灵化的礼物和礼品卡。
但是很容易忘记,因为严格函数应用和惰性函数应用的源代码语法非常相似。
这是一个必须非常强调的重点。事实上,为了强调这一点,我画了另外两幅图来重申 Ghost-o-matic 机器允许的输入和输出是什么。
Ghost-o-matic 机器只接受礼物的标签,而不是实际的礼物本身。这意味着 Ghost-o-matic 并不会打开任何礼物:毕竟,它只有标签,而没有实际的礼物。这与 Strict-o-matic 机器形成对比,后者接受实际礼物作为输入并打开它们:有人可能称这种机器为force
函数,类型为Thunk a -> a
。在 Haskell 中,并没有这样的东西。
Ghost-o-matic 总是会创建一个包装好的礼物。即使没有幽灵在礼物中(函数是常量),它也永远不会产生未包装的礼物。
我们先前说过在 Haskell 中没有force
函数。但是函数seq
似乎做了与强制求值 thunk 类似的事情。一个被seq
幽灵所困扰的礼物,在被打开时会导致另外两个礼物被打开(即使第一个是不必要的)。看起来第一个参数被强制执行;因此seq x x
可能是对命令式语言中force
的一个合理近似。但当我们打开一个被seq
幽灵所困扰的礼物时会发生什么呢?
虽然鬼魂最终会打开礼物而不是我们,但对于它来说已经为时已晚:在鬼魂打开礼物之后立即,我们将要打开它(它已经是)。关键观察是seq x x
鬼魂只在打开seq x x
礼物时打开x
礼物,并且在seq x x
打开后,我们必须通过间接方式去打开x
。seq 鬼魂的严格性被放入一个礼物中,直到需要x
时才打开,这一事实所击败。
一个有趣的观察是 Strict-o-matic 机器在运行时做一些事情。它可以打开礼物,发射导弹或执行其他副作用。
但是 Ghost-o-matic 机器完全是纯的。
为了避免混淆,Strict-o-matic 和 Ghost-o-matic 机器的用户可能会发现比较每台机器的礼物创建生命周期有用。
惰性 Ghost-o-matic 机器分为两个离散阶段:函数应用,实际上什么也不做,只是创建礼物,并且实际打开礼物。Strict-o-matic 在一个瞬间完成所有操作——尽管它可以输出一个礼物(这就是在严格语言中实现惰性时发生的事情)。但在严格语言中,你必须自己做所有事情。
Ghost-o-matic 被人类和鬼魂批准使用。
这确实意味着打开一个鬼魂礼物可能会产生更多的礼物。例如,如果礼物是给那些还没有在堆上存在的礼物的礼物卡。
对于一个脊柱严格的数据结构,它可以产生很多礼物。
哦,还有一件事:Ghost-o-matic 机器是给鬼魂和家人的绝佳礼物。它们也可以用礼物包装起来。毕竟,在 Haskell 中的一切都是礼物。
技术注释。通过优化,函数可能不一定在堆上分配。确保的唯一方法是查看程序生成的优化核心。事实上,传统严格的函数在 Haskell 中并不不存在:非装箱原语可以用来编写传统的命令式代码。这看起来可能有点吓人,但实际上和在 ML 中编写程序没什么不同。
我完全忽略了部分应用,这应该是以后帖子的主题,但我会注意到,从内部来看,GHC 确实尽其所能在应用时传递函数想要的所有参数;如果所有参数都可用,它将不会麻烦地创建部分应用(PAP)。但这些可以被认为是修改过的 Ghost-o-matics,其鬼魂已经具有一些(但不是全部)参数。天赋的 Ghost-o-matics(堆中的函数)也可以这样看待:但不是预先给鬼魂一些参数,而是给鬼魂其自由变量(闭包)。
下一篇文章:Grinch 是如何窃取 Haskell 堆的
本作品采用知识共享署名-相同方式共享 3.0 未本地化许可协议授权。
规范中的泛化和模糊性:ezyang’s 博客
来源:
blog.ezyang.com/2010/12/generalization-and-vagueness-in-specifications/
语义对规范的看法
普遍认为,过早泛化是不好的(架构宇航员),模糊的规范适合自上而下的工程,但不适合自下而上。我们能对此说得更具体一些吗?
语义是编程语言的形式规范。它们可能是最被深入研究的规范形式之一,因为计算机科学家喜欢调整他们使用的工具。他们也喜欢有很多语义可供选择:越多越好。我们有小步和大步操作语义;我们有公理语义和指称语义;我们有游戏语义、代数语义和并发语义。描述我们实际编写的程序是一项困难的工作,拥有尽可能多的不同解释是有帮助的。
根据我的经验,软件很少有多个规范,每个规范都被同等对待。重复使得在更多信息可用和需求变化时,难以演变规范(好像本来就不够难!)两个权威来源可能相互冲突。规范的一个版本可能要求系统的某一部分实施得非常精确,而另一个则保持开放(直到某种外部行为)。更常见的可能是单一的、权威的规范,然后是一系列信息参考,你在日常工作中可能真正会参考的。
当然,这种情况在编程语言语义世界中经常发生。关于冲突和不同的具体性问题,这里有两个例子来自指称语义(斯科特语义)和游戏语义。
太泛了吗? 这里,规范允许一些额外的行为(并行或在 PCF 中),这是无法以显而易见的方式实现的。这个问题困扰研究人员一段时间:如果规范太松散,你是添加规范建议的特性(PCF+por),还是尝试修改语义,使得这种额外行为被排除(逻辑关系)?泛化可能有好处,但通常以增加实现复杂性为代价。然而,在并行或的情况下,这种实现复杂性是一个线程化运行时系统,出于无关的原因也是有用的。
太模糊了吗? 在这里,规范未能捕捉到行为上的差异(seq 和 pseq 在语义上等同(Scott)),而这恰好在操作上是重要的(控制评估顺序)。游戏语义巧妙地解决了这个问题:我们可以区分x `pseq` y
和y `pseq` x
,因为在相应的对话中,前者的表达式首先询问 x 的值,后者首先询问 y 的值。然而,模糊的规范为编译器的优化提供了更多的自由度。
像“适合工作的正确语言”这样的口头禅一样,我怀疑在“适合工作的正确规范风格”方面也有类似的真理。但更甚的是,我主张从不同的视角审视同一领域会加深你对领域本身的理解。在使用语义学时,我们包含某些细节并排除其他细节:作为程序员,我们时常这样做——这对于处理任何复杂系统至关重要。在构建语义时,我们语义之间的差异提供了关于抽象边界和我们原始目标潜在不一致性的重要线索。
有一点需要注意,许多不同的计算思维范式存在一个明显的缺点:你必须学会它们全部!公理语义回忆起你可能记得的高中数学中的符号操作:机械而不是非常有趣。指称语义要求先解释一下,然后才能得到正确的直觉。游戏语义作为“对话”似乎相当直观(对我来说),但是有一些重要的细节最好通过某种形式来解决。当然,我们总是可以回到操作性的讨论,但这种方法在大型系统中不具可扩展性(“阅读源代码”)。
通用化 API:ezyang 博客
编辑. ddarius 指出,类型族的例子是反过来的,所以我把它们调整成了与函数依赖相同的方式。
类型函数可用于执行各种精妙的类型级计算,但也许最基本的用途是允许构建通用 API,而不仅仅依赖于模块导出的“大部分相同的函数”。你需要多少类型技巧取决于 API 的属性,也许最重要的是你的数据类型的属性。
假设我有一个单一数据类型上的单一函数:
defaultInt :: Int
而我想要通用化它。我可以通过创建一个类型类来轻松实现:
class Default a where
def :: a
对单个类型的抽象通常只需要普通的类型类。
假设我有一个在多个数据类型上的函数:
data IntSet
insert :: IntSet -> Int -> IntSet
lookup :: IntSet -> Int -> Bool
我们希望对IntSet
和Int
进行抽象化。由于我们所有的函数都提到了这两种类型,我们所需做的就是编写一个多参数类型类:
class Set c e where
insert :: c -> e -> c
lookup :: c -> e -> Bool
instance Set IntSet Int where ...
如果我们运气不好,一些函数可能不会使用所有的数据类型:
empty :: IntSet
在这种情况下,当我们尝试使用该函数时,GHC 会告诉我们它无法确定使用哪个实例:
No instance for (Set IntMap e)
arising from a use of `empty'
其中一件事要做的就是引入 IntSet
和 Int
之间的功能依赖。依赖意味着某些东西依赖于另一些东西,那么哪种类型依赖于什么?在这里我们没有太多选择:因为我们想要支持函数 empty
,其签名中并没有任何地方提到 Int
,因此依赖将从 IntSet
到 Int
,也就是说,给定一个集合(IntSet
),我可以告诉你它包含的是什么(一个 Int
)。
class Set c e | c -> e where
empty :: c
insert :: c -> e -> c
lookup :: c -> e -> Bool
注意,这仍然基本上是一个多参数类型类,我们只是给 GHC 一个小提示,告诉它如何选择正确的实例。如果需要,我们也可以引入反方向的功能依赖。出于教育目的,让我们假设我们的老板真的想要一个“null”元素,它总是集合的成员,并且在插入时不做任何事情:
class Set c e | c -> e, e -> c where
empty :: c
null :: e
insert :: c -> e -> c
lookup :: c -> e -> Bool
还要注意,每当我们添加功能依赖时,我们就排除了提供另一个实例的可能性。在最后一个类型类对于 Set
是非法的:
instance Set IntSet Int where ...
instance Set IntSet Int32 where ...
instance Set BetterIntSet Int where ...
这将报告“功能依赖冲突。”
功能依赖有时会因为与其他某些类型特性的交互而受到诟病。GHC 最近添加的等效功能是关联类型(也称为类型族或数据族)。
而不是告诉 GHC 如何自动从另一个类型中推断(通过依赖),我们创建一个显式的类型族(也称为类型函数),它提供了映射:
class Set c where
data Elem c :: *
empty :: c
null :: Elem c
insert :: c -> Elem c -> c
lookup :: c -> Elem c -> Bool
注意我们的类型类不再是多参数的:它有点像如果我们从 c -> e
引入了一个函数依赖。但是,它如何知道 null
的类型应该是什么?简单:它让你告诉它:
instance Set IntSet where
data Elem IntSet = IntContainer Int
empty = emptyIntSet
null = IntContainer 0
注意 data
的右侧不是一个类型:它是一个数据构造函数,然后是一个类型。数据构造函数将告诉 GHC 使用哪个 Elem
的实例。
在本文的原始版本中,我定义了相反方向的类型类:
class Key e where
data Set e :: *
empty :: Set e
null :: e
insert :: Set e -> e -> Set e
lookup :: Set e -> e -> Bool
我们的类型函数朝着另一个方向发展,我们可以根据正在使用的类型变体实现容器,这可能不是我们拥有的类型。这是数据族的一个主要用例,但与通用化 API 的问题不直接相关,所以我们暂时不考虑它。
IntContainer
看起来很像一个 newtype,并且实际上可以成为一个:
instance Set IntSet where
newtype Elem IntSet = IntContainer Int
如果你觉得包装和解包 newtype 很烦人,在某些情况下,你可以只使用类型同义词:
class Set c where
type Elem c :: *
instance Set IntSet where
type Elem IntSet = Int
然而,这样做会排除一些你可能想写的功能,例如自动专门化你的通用函数:
x :: Int
x = null
GHC 会报错:
Couldn't match expected type `Elem e'
against inferred type `[Int]'
NB: `Container' is a type function, and may not be injective
既然我也可以写成:
instance Set BetterIntSet where
type Elem BetterIntSet = Int
GHC 不知道要使用 null
的哪个 Set
实例:IntSet
还是 BetterIntSet
?你需要通过另一种方式将此信息传递给编译器,如果这完全在幕后进行,你就有点倒霉了。这与函数依赖有着明显的不同,如果你有一个非单射关系,它们会产生冲突。
另一种方法,如果你有幸定义你的数据类型,是在实例内部定义数据类型:
instance Set RecordMap where
data Elem RecordMap = Record { field1 :: Int, field2 :: Bool }
然而,请注意,新 Record
的类型不是 Record
;它是 Elem RecordMap
。你可能会发现类型同义词有用:
type Record = Elem RecordMap
与 newtype 方法相比,没有太大区别,只是避免了添加额外的包装和解包层。
在许多情况下,我们希望规定我们 API 中的数据类型具有某些类型类:
instance Ord Int where ...
强制执行这一点的一种低技术方式是将其添加到我们所有函数的类型签名中:
class Set c where
data Elem c :: *
empty :: c
null :: Ord (Elem c) => Elem c
insert :: Ord (Elem c) => c -> Elem c -> c
lookup :: Ord (Elem c) => c -> Elem c -> Bool
但更好的方法是只需在 Set
上添加一个类约束,使用灵活的上下文:
class Ord (Elem c) => Set c where
data Elem c :: *
empty :: c
null :: Elem c
insert :: c -> Elem c -> c
lookup :: c -> Elem c -> Bool
我们可以使函数和数据类型通用化。我们还可以使类型类通用化吗?
class ToBloomFilter a where
toBloomFilter :: a -> BloomFilter
假设我们决定允许多个 BloomFilter
的实现,但仍然希望为转换成任何你想要的布隆过滤器提供统一的 API。
不是直接,但我们可以伪造它:只需创建一个捕捉所有通用类型类,并将其参数化为真实类型类的参数:
class BloomFilter c where
data Elem c :: *
class BloomFilter c => ToBloomFilter c a where
toBloomFilter :: a -> c
稍微退后一步,比较函数依赖和类型族产生的类型签名:
insertFunDeps :: Set c e => c -> e -> c
insertTypeFamilies :: Set c => c -> Elem c -> c
emptyFunDeps :: Set c e => c
emptyTypeFamilies :: Set c => c
因此,类型族(type families)将实现细节隐藏在类型签名之后(你只使用你需要的关联类型,与Set c e => c
相反,其中e
是必需的但没有用于任何操作—如果你有 20 个关联数据类型,这更加明显)。然而,当你需要为你的关联数据引入新类型包装器(Elem
)时,它们可能会显得有些啰嗦。功能依赖(functional dependencies)非常适合自动推断其他类型,而无需重复自己。
(感谢 Edward Kmett 指出这一点。)
从这里开始要做什么呢?我们只是初步了解了类型级编程的表面,但是为了通用化 API,这基本上就是你需要知道的全部!找到你写过的在多个模块中重复的 API,每个模块提供不同的实现。找出哪些函数和数据类型是基本的。如果你有很多数据类型,就应用这里描述的技巧来确定你需要多少类型机制。然后,让你的 API 变得通用起来吧!
泛化可编程分号:ezyang 的博客
来源:
blog.ezyang.com/2012/10/generalizing-the-programmable-semicolon/
购买者注意:前方有半成品研究思路。
什么是单子(monad)?一个答案是,它是在非严格语言中排序操作的一种方式,一种表达“这应该在那之前执行”的方式。但另一个答案是,它是可编程分号,一种在进行计算时实现自定义副作用的方式。这些包括基本的效果,如状态、控制流和非确定性,以及更奇特的效果,比如labeled IO。即使你不需要单子来排序,这样的功能也是有用的!
让我们来个大逆转:对于按需调用语言来说,可编程分号会是什么样子呢?也就是说,我们能否在不对计算进行排序的情况下实现这种可扩展性呢?
乍一看,答案是否定的。大多数按值调用语言无法抵制副作用的诱惑,但在按需调用中,副作用是足够痛苦的,以至于 Haskell 设法避免了它们(在大多数情况下!)任何使用过带有 NOINLINE
修饰的 unsafePerformIO
的人都可以证明这一点:依赖于优化,效果可能会执行一次,或者执行多次!正如保罗·列维所说:“第三种评估方法,按需调用,对于实现目的是有用的。但它缺乏干净的指称语义——至少对于除了发散和不规则选择之外的效果来说是如此,它们的特殊属性被利用在[Hen80]中提供按需模型。”所以我们不考虑按需调用。保罗·列维并不是说对于纯按需调用,没有指称语义(这些语义与称为名字调用的语义完全一致),而是当你添加副作用时,事情变得复杂。
但是这里有一个攻击角度的提示:列维继续展示了如何在名字调用中讨论副作用,并且在这里指定指称语义毫无困难。直觉上来看,其原因在于,在名字调用中,所有对带有附加效果的延迟值的使用(例如 case-matches)都会导致效果显现。一些效果可能会被丢弃(因为它们的值从未被使用),但除此之外,效果的发生完全是确定性的。
嗯!
当然,我们可以轻松通过放弃记忆化来实现这一点,但这是一个难以接受的牺牲。因此,我们的新问题是:如何在保留共享的同时恢复具有影响力的按名字调用语义?
在Writer
单子的情况下,我们可以保留所有原始共享。过程非常简单:每个 thunk a
现在的类型是(w, a)
(对于某个固定的单子w
)。这个元组可以像原始的a
一样共享,但现在它还有一个嵌入的效果w
。每当a
被强制时,我们简单地将效果追加到结果 thunk 的w
中。下面是一个简单的解释器,实现了这一点:
{-# LANGUAGE GADTs #-}
import Control.Monad.Writer
data Expr a where
Abs :: (Expr a -> Expr b) -> Expr (a -> b)
App :: Expr (a -> b) -> Expr a -> Expr b
Int :: Int -> Expr Int
Add :: Expr Int -> Expr Int -> Expr Int
Print :: String -> Expr a -> Expr a
instance Show (Expr a) where
show (Abs _) = "Abs"
show (App _ _) = "App"
show (Int i) = show i
show (Add _ _) = "Add"
show (Print _ _) = "Print"
type M a = Writer String a
cbneed :: Expr a -> M (Expr a)
cbneed e@(Abs _) = return e
cbneed (App (Abs f) e) =
let ~(x,w) = run (cbneed e)
in cbneed (f (Print w x))
cbneed e@(Int _) = return e
cbneed (Add e1 e2) = do
Int e1' <- cbneed e1
Int e2' <- cbneed e2
return (Int (e1' + e2'))
cbneed (Print s e) = do
tell s
cbneed e
sample = App (Abs (\x -> Print "1" (Add x x))) (Add (Print "2" (Int 2)) (Int 3))
run = runWriter
尽管最终输出是"122"
(数字2
出现两次),但将2
添加到3
的实际加法只发生了一次(您可以通过添加适当的跟踪调用来验证)。对于Maybe
,您可以做类似的事情:通过稍微作弊,因为在Nothing
的情况下,我们没有x
的值,我们提供 bottom。我们永远不会被追究,因为我们总是在任何人获得值之前就进行了短路。
这里与应用函子有些相似之处,但我们要求更严格的条件:不仅计算的控制流需要固定,计算的值也必须固定!很明显,我们无法为大多数单子做到这一点。昨天在Twitter,我提出了以下签名和定律(回想起逆元),任何您想要对此过程执行的单子都必须实现这些:
extract :: Functor f => f a -> (a, f ())
s.t. m == let (x,y) = extract m in fmap (const x) y
但似乎只有Writer
具有适当的结构来正确执行这一点(既是单子又是余单子)。这很遗憾,因为我想要进行这种理论化的应用需要分配单元的能力。
然而,并非一无所获。即使无法完全共享,您仍可能实现部分共享:一种完全惰性和部分求值的混合体。不幸的是,这将需要对您的运行时进行重大和侵入性的更改(如果您想要将您的代码转换为 CPS,我不确定您将如何做到这一点),因此在这一点上我放下了这个问题,而是写了这篇博客文章。
GET /browser.exe:ezyang 的博客
Jon Howell 梦想着一个新的互联网。在这个新的互联网上,跨浏览器的兼容性检查成为了一个遥远的记忆,并且可以单方面地向浏览器添加新功能,而不必先说服整个世界进行升级。这种使这个互联网成为可能的想法如此疯狂,以至于它可能行得通。
如果一个网络请求不仅仅是下载一个网页,而是下载整个浏览器呢?
“这太愚蠢了”,你可能会说,“我绝不会从互联网上运行随机二进制文件!” 但你错了:豪威尔知道如何做到这一点,而且还知道如何以比你的浏览器经常接收和执行的 JavaScript 更安全的方式来执行。这个想法很简单:你正在执行的代码(无论是本地代码、字节码还是文本)并不重要,真正重要的是代码可以访问的系统 API,这决定了系统的安全性。
考虑今天的浏览器,这是安装在您计算机上的最复杂的软件之一。它提供了“HTTP、MIME、HTML、DOM、CSS、JavaScript、JPG、PNG、Java、Flash、Silverlight、SVG、Canvas 等”接口,几乎肯定都有漏洞。API 的丰富性是它们在安全性方面的致命弱点。现在再考虑一下,一个本地客户端需要暴露哪些 API,假设网站提供了浏览器和所有库。
答案非常简单:你只需要一个本地执行环境,一个最小化的持久状态接口,一个用于外部网络通信的接口,以及一个用于在屏幕上绘制像素的接口(如 VNC)。这就是全部:其他所有功能都可以作为网站提供的不受信任的本地代码来实现。这种接口足够小,我们有希望确保它没有漏洞。
从这种与原始互联网彻底不同的彻底离去中,你得到的是对应用程序栈的所有方面的精细控制。网站可以编写类似于本地应用程序的等价物(如应用商店),但无需按安装按钮。因为你控制了整个栈,你不再需要解决浏览器的错误或缺失功能的问题;只需选择一个适合你需求的引擎。如果你需要推送通知,不需要通过轮询循环来实现,只需正确地实现它。Web 标准仍然存在,但不再代表网站开发者与用户之间的合约(后者对底层技术一无所知);它们只是开发者与其他网络爬虫等之间的合约。
Jon Howell 和他的团队已经实现了这个系统的原型,你可以阅读更多关于实施这样一个系统所面临的(众多)技术困难。(我每次都要下载浏览器吗?如何实现 Facebook Like 按钮?浏览器历史怎么办?难道 Google Native Client 不已经做到了吗?这会不会很慢?)
作为开发者,我渴望这个新互联网。我再也不用编写 JavaScript 或担心浏览器兼容性了。我可以像管理服务器软件栈一样管理客户端软件栈,并在必要时使用现成组件。)作为客户端,我的感受更加矛盾。我不能再使用 Adblock 或 Greasemonkey(这需要将代码注入任意可执行文件),而且更难以使用网站的方式超出其所有者最初的预期。(在这个新世界秩序中,搜索引擎是否会以相同形式存在?)啊,勇敢的新世界,你有如此多的应用程序!
Getting a fix on fixpoints : ezyang’s blog
以前,我们已经 绘制了各种 Haskell 类型的哈斯图,从数据类型到函数类型,并查看了 可计算性和单调性之间的关系。事实上,所有可计算函数都是单调的,但并非所有单调函数都是可计算的。是否有某些函数描述涉及可计算性?是的:Scott 连续函数。在这篇文章中,我们将探讨定义连续性所需的数学机制。特别地,我们将研究最小上界、链、链完备偏序集(CPOs)和域。我们还将研究连续函数自然产生的不动点。
在我们之前的类型图中,我们让值以省略号一直延伸到无穷远。
正如几位评论者所指出的,这并不完全正确:所有的 Haskell 数据类型都有一个或多个顶值,即不小于任何其他值的值。(注意,这与大于或等于所有其他值的值不同:某些值是不可比较的,因为我们讨论的是偏序。)在 Nat 的情况下,有许多顶值:Z、S Z、S (S Z),等等是你可以得到的最明确的。然而,还有一个更多:fix S
,又名无穷大。
在这个值中没有潜伏的底部,但它似乎有点奇怪:如果我们去掉一个 S 构造子(减少自然数),我们又回到了 fix S
:显然,无限减一还是无限。
实际上,fix S
是链 ⊥, S ⊥, S (S ⊥)… 的最小上界。链只是一个值序列,其中 d_1 ≤ d_2 ≤ d_3 …;它们是我们绘制的图表中向上移动的线条。
自然数的链 0 ≤ 1 ≤ 2 ≤ … 尽管没有 0 ≤ 1 ≤ 2 ≤ … 的上界,但自然数有许多最小上界,因为每个元素 n 形成平凡链 n ≤ n ≤ n…
在偏序集中,链不一定有最小上界。考虑具有通常偏序关系的自然数。
链 0 ≤ 1 ≤ 2 ≤ … 没有上界,因为自然数集合不包含无穷大。我们必须转向 Ω,这是自然数和最小可能的无穷大,序数 ω。
这里链有一个最小上界。
尽管 0 ≤ 1 ≤ 2 ≤ … 没有 0 ≤ 1 ≤ 2 ≤ … 的上确界,自然数有许多最小上界,因为每个元素 n 形成平凡链 n ≤ n ≤ n…
这里是一些上界的图形表示。
如果一个链始终小于或等于另一个链,那么该链的上界小于或等于另一个链的上界。
双上界链的工作方式与您预期的方式相同;此外,我们可以对这条链进行对角线处理,以获取两个方向的上界。
所以,如果我们回想起先前绘制的任何图表,在任何地方有一个“…”,实际上我们可以在顶部放置一个上界,这归功于 Haskell 的惰性。以下是列表类型中具有最小上界的一个链:
正如我们前面看到的,对于所有偏序来说,这并不总是成立,因此我们为总是具有最小上界的偏序赋予了一个特殊的名称:链完备偏序或 CPO。
您可能还注意到在每个图表中,⊥位于底部。这也不一定适用于偏序。我们将称具有底部元素的 CPO 为域。
(术语域实际上在指示语义文献中被相当宽松地使用,许多时候具有超出此处给出的定义的额外属性。我从 Marcelo Fiore 的指示语义讲座中使用了这个最小定义,并且我相信这是域的 Scott 构思,尽管我尚未验证。)
因此,实际上我们一直在处理域,尽管我们一直忽略最小上界。我们将发现,一旦考虑了上界,我们将找到一个比单调性更强的条件,即可计算性。
考虑以下 Haskell 数据类型,它表示垂直自然数 Omega。
这是一个不可计算的单调函数。
为什么它不可计算?这要求我们对任意大的数和无穷大有不同的处理方式:在有限自然数和无穷大之间存在不连续性。从计算的角度来看,我们无法在有限时间内检查任何给定值是否实际上是无穷大:我们只能不断剥离 Ws,并希望我们不会达到底部。
我们可以如下形式地正式化:一个函数D -> D
,其中 D 是一个域,如果它是单调的并且保留最小上界,则称为连续。这并不是说所有上界都保持不变,而是说如果 e_1 ≤ e_2 ≤ e_3 …的上界是 lub(e),那么 f(e_1) ≤ f(e_2) ≤ f(e_3) …的上界是 f(lub(e))。符号化地:
图形化地表示:
现在是查看不动点的时候了!我们直接跳到要点:Tarski 的不动点定理声明,连续函数的最小不动点是序列⊥ ≤ f(⊥) ≤ f(f(⊥)) …的最小上界。
因为函数是连续的,它被迫保持这个最小上界,自动使其成为一个固定点。我们可以将这个序列看作是给我们提供固定点的越来越好的逼近值。事实上,在有限的定义域内,我们可以利用这个事实来机械地计算函数的精确固定点。
我们将首先查看的函数并没有非常有趣的固定点。
如果我们将底部传递给它,我们得到底部。
这里有一个稍微有趣的函数。
从定义上并不明显(尽管在哈斯图上看起来更明显),这个函数的固定点是什么。然而,通过重复在 ⊥ 上迭代 f,我们可以看到我们的值发生了什么变化:
最终我们会达到固定点!更重要的是,我们已经达到了最小的固定点:这个特定函数有另一个固定点,因为 f (C ()) = C ()。
为了完整起见,这里还有一个集合。
我们可以从这些图表中看到为什么塔斯基的固定点定理可能有效:我们逐渐向上移动定义域,直到我们停止向上移动,这就是固定点的定义,并且由于我们从底部开始,我们最终得到最小的固定点。
有几个问题需要回答。如果函数将值向下移动会怎样?那么我们可能会陷入无限循环。
然而,我们是安全的,因为任何这样的函数都会违反单调性:对于 e₁ ≤ e₂的循环将导致 f(e₁) ≥ f(e₂)。
我们的有限例子也是全序的:我们的图表没有分支。如果我们的函数将一个分支映射到另一个分支(这是完全合法的操作:考虑not
)会怎样?
幸运的是,要达到这样的循环,我们必须打破单调性:从一个分支跳到另一个分支意味着某种程度的严格性。这种情况的特例是,严格函数的固定点是底部。
固定点的典范示例是递归函数的“Hello world”:阶乘。与我们之前的例子不同,这里的定义域是无限的,因此需要无限次地应用 f 才能得到真正的阶乘。幸运的是,计算阶乘n!
只需要n
次应用。请记住,阶乘的固定点风格定义如下:
factorial = fix (\f n -> if n == 0 then 1 else n * f (n - 1))
下面是阶乘函数的定义域随着连续应用的增长方式:
鼓励读者验证这是否为真。下次,我们将不再看自然数的平面域,而是看垂直域的自然数,这将很好地将我们迄今为止涵盖的许多内容联系在一起。
GHC 和可变数组:一个脏小秘密:ezyang 的博客
来源:
blog.ezyang.com/2014/05/ghc-and-mutable-arrays-a-dirty-little-secret/
GHC 和可变数组:一个脏小秘密
Brandon Simmon 最近在 glasgow-haskell-users 邮件列表上发布了一个帖子,问了以下问题:
我一直在研究 一个问题,在这个库中,随着分配更多可变数组,GC 占主导地位(我想我验证了这个?),所有代码的速度与挂起的可变数组数量成正比地变慢。
…对此,我回复道:
在当前的 GC 设计中,指针数组的可变数组总是放置在可变列表上。未收集的代的可变代的列表总是被遍历;因此,指针数组的数量对于小 GC 产生了线性的开销。
如果你从传统的命令式语言转过来,你可能会发现这非常令人惊讶:如果你为系统中所有可变数组支付了 Java 中每个 GC 的线性开销… 嗯,你可能永远都不会使用 Java。但大多数 Haskell 用户似乎过得很好;主要因为 Haskell 鼓励不可变性,使得大多数情况下不需要大量的可变指针数组。
当然,当你确实需要时,这可能有点痛苦。我们有一个 GHC bug 跟踪这个问题,还有一些低 hanging fruit(一种变体的可变指针数组,写操作更昂贵,但只有在写入时才放入可变列表中),以及一些有前途的实现卡标记堆的方向,这是像 JVM 这样的 GC 策略所使用的策略。
更加元层次上,为不可变语言实现一个性能良好的分代垃圾收集器要比为可变语言实现一个更容易得多。这是我个人的假设,解释了为什么 Go 仍然没有一个分代收集器,以及为什么 GHC 在某些突变类别上表现如此糟糕。
后记。 标题是一个双关语,因为“DIRTY”用于描述自上次 GC 以来已写入的可变对象。这些对象是记忆集的一部分,必须在垃圾收集期间遍历,即使它们位于旧代中也是如此。
GHC migrating to Git : ezyang’s blog
来源:GHC 迁移到 Git
GHC migrating to Git
From cvs-ghc@haskell.org
:
Hi all,
We now plan to do the git switchover this Thursday, 31 March.
Thanks
Ian
我将会怀念 Darcs(darcs send
和“一切皆为补丁”确实运作良好的情况),但总体而言,看到 GHC 迁移到 Git 我感到非常满意。
ghc-shake:重新实现 ghc --make:ezyang 的博客
来源:
blog.ezyang.com/2016/01/ghc-shake-reimplementing-ghc-make/
ghc-shake:重新实现 ghc --make
ghc --make
是 GHC 中的一种有用模式,它会自动确定需要编译的模块,并为您编译它们。它不仅是构建 Haskell 项目的便捷方式,其单线程性能也很好,通过重用读取和反序列化外部接口文件的工作。然而,ghc --make
也存在一些缺点:
-
具有大模块图的项目在重新编译开始之前有相当长的延迟。这是因为
ghc --make
在实际进行任何工作之前会重新计算完整的模块图,解析每个源文件的头文件。如果您使用预处理器,情况会更糟(参见这里)。 -
这是一个单体构建系统,如果需要比 GHC 默认功能更复杂的东西,将其与其他构建系统集成起来会很困难。(例如,GHC 精心设计的构建系统知道如何在包边界之间并行构建,而 Cabal 不知道如何做。)
-
它无法提供有关构建性能的洞察,例如哪些模块需要很长时间构建,或者哪些“阻塞”模块很大。
ghc-shake 是使用 Shake 构建系统 重新实现的 ghc --make
。它可以作为 ghc
的替代品。ghc-shake 具有以下特性:
-
大大减少了重新编译的延迟。这是因为 Shake 不会通过解析每个文件的头文件来重新计算模块图;它会重用缓存的信息,仅重新解析已更改的源文件。
-
如果重新构建文件(并更新其时间戳),但构建输出未更改,我们就不会重新编译任何依赖于它的内容。这与
ghc --make
相比,后者必须在确定没有要做的工作之前运行每个下游模块的重新编译检查,有所不同。事实上,ghc-shake 从不运行重新编译测试,因为我们在 Shake 中本地实现了这种依赖结构。 -
使用
-ffrontend-opt=--profile
,你可以获得有关构建的详细分析信息,包括每个模块构建所花费的时间,以及更改一个模块的成本。 -
在单线程构建上与
ghc --make
一样快。与另一个使用 Shake 构建 Haskell 的构建系统 ghc-make 相比,ghc-make 并不使用 GHC API,并且必须使用(慢速的)ghc -M
来获取项目的初始依赖信息。 -
它是准确的。它正确处理许多边缘情况(如
-dynamic-too
),因为它是使用 GHC API 编写的,原则上可以与ghc --make
功能完全兼容。(当前情况不是这样,只是因为我还没有实现它们。)
也有一些缺点:
-
Shake 构建系统需要一个
.shake
目录来实际存储有关构建的元数据。这与ghc --make
相反,后者完全依赖于目录中构建产品的时间戳。 -
因为它直接使用了 GHC API 实现,所以只能与特定版本的 GHC(即即将发布的 GHC 8.0 版本)一起使用。
-
它需要一个修补过的 Shake 库版本,因为我们有一个基于 Shake 的(未导出的)文件表示的自定义模块构建规则。我已经在这里报告了。
-
仍然存在一些缺失的功能和 bug。我遇到的问题是(1)在某些情况下我们忘记了重新链接,以及(2)它不能用于构建分析代码。
如果你想今天就使用 ghc-shake
(不适合心脏虚弱的人),试试 git clone https://github.com/ezyang/ghc-shake
,然后按照 README
中的说明操作。但即使你不打算使用它,我认为 ghc-shake
的代码对任何想编写涉及 Haskell 代码的构建系统的人来说都有一些好的教训。其中最重要的架构决策之一是使 ghc-shake
中的规则不是围绕输出文件(例如 dist/build/Data/Foo.hi
,如 make
中那样)组织,而是围绕 Haskell 模块(例如 Data.Foo
)组织的。语义化的构建系统比强制将一切都放入“文件抽象”中要好得多(尽管 Shake 在我希望的模式下使用上并不完全支持)。还有一些其他有趣的经验教训… 但那应该是另一篇博客文章的主题!
这个项目的未来方向在哪里?在不太近的未来,我考虑做一些事情:
-
为了支持多个 GHC 版本,我们应该将 GHC 特定的代码分离出来成为一个单独的可执行文件,并通过 IPC 进行通信(向 Duncan Coutts 致敬)。这也将使我们能够支持独立进程的并行 GHC 构建,仍然可以重用读取接口文件。无论如何,
ghc-shake
可以作为 GHC 需要使构建系统更易于访问所需信息的蓝图。 -
我们可以考虑将这些代码移回 GHC。不幸的是,Shake 是一个太大的依赖项,无法实际让 GHC 依赖它,但可以考虑设计一些抽象接口(你好,Backpack!),用于表示类似 Shake 的构建系统,并让 GHC 提供
--make
的简单实现(但用户可以选择切换到 Shake)。 -
我们可以将这段代码扩展到
ghc --make
以了解如何构建整个 Cabal 项目(或更大的项目),比如ToolCabal,这是使用 Shake 重新实现的 Cabal。这将允许我们捕捉类似于 GHC 构建系统的模式,该系统可以并行构建所有引导包中的模块(而不必等待包完全构建完成)。
P.S. ghc-shake 不应与shaking-up-ghc混淆,后者是一个旨在用 Shake 基础构建系统替换 GHC 基于 Makefile 的构建系统的项目。
Gin and monotonic : ezyang’s blog
Gin, because you’ll need it by the time you’re done reading this.
上次我们看了数据类型值的部分顺序。有两件事情我想补充:一个是星下标底部如何扩展,一个是不使用星下标底部符号的列表图解。
这是三个星下标底部扩展的三重体现,形成了熟悉的 Hasse 图,通过包含关系排序的三个元素集的幂集:
下面是列表的部分顺序,在其全指数荣耀中(为了适应所有,灰色脊柱的部分顺序在向右增加时增加)。
现在,谈谈今天的主题,函数!到目前为止,我们只讨论了数据类型。在本篇文章中,我们将更仔细地研究函数具有的偏序关系。我们将介绍单调性的概念。并且会有很多图片。
让我们从一个简单的例子开始:从单元到单元的函数,() -> ()
。在你看图之前,你认为我们可以写多少不同的这种函数实现呢?
结果,一共有三种。一种无论我们传递什么都返回底部,一种是恒等函数(如果传递单元则返回单元,如果传递底部则返回底部),还有一种是const ()
,即无论传递什么都返回单元。注意这些不同函数与其参数的严格和惰性评估之间的直接对应关系。(你可以称底部函数为部分的,因为它对于任何参数都未定义,尽管没有直接编写此内容的方法,如果仅使用 undefined GHC 不会发出部分函数警告。)
在我提出的图中,我展示了关于偏序的三种等效思考方式。第一种只是λ演算中的术语:如果你更喜欢 Haskell 的表示法,你可以将λx.x 翻译为\x -> x
。第二种是将输入值映射到输出值,明确处理了底部(这种表示法有助于明确看到底部,但不太适合确定哪些值是合法的——即可计算的)。第三种仅仅是函数的定义域:你可以看到这些定义域逐渐变得越来越大,从空到整个输入类型。
在这一点上,一点正式性是有用的。我们可以定义一个函数的偏序如下:f ≤ g 当且仅当 dom(f)(f 的定义域,例如所有不会导致 f 返回底部的值)⊆ dom(g),对于所有 x ∈ dom(f),f(x) ≤ g(x)。你应该验证上面的图表是否一致(第二个条件非常容易,因为函数的唯一可能值是()
)。
一个敏锐的读者可能已经注意到,我忽略了一些可能的函数。特别是,第三个图表并不包含域的所有可能排列:只有底部的集合如何?事实证明,这样的函数是不可计算的(如果我们有一个函数 () -> ()
,如果其第一个参数是底部则返回 ()
,如果其第一个参数是 ()
则返回底部,那么如何解决停机问题)。我们稍后再回到这个问题。
由于 () -> ()
有三种可能的取值,一个问题是是否存在一个更简单的函数类型,其取值更少?如果我们接受空类型,也可以写为 ⊥,我们可以看到 a -> ⊥
只有一种可能的取值:⊥。
类型为 ⊥ -> a
的函数也具有一些有趣的属性:它们与类型 a
的普通值是同构的。
在没有公共子表达式消除的情况下,这可以是防止惰性计算结果共享的有效方式。然而,写 f undefined
是很麻烦的,因此人们可能会看到 () -> a
,它的语义并不完全相同,但类似。
到目前为止,我们只考虑了以 ⊥
或 ()
作为参数的函数,这些函数并不是很有趣。因此,我们可以考虑下一个可能最简单的函数:Bool -> ()
。尽管这种类型看起来很简单,实际上有五种不同的可能函数具有这种类型。
要看为什么可能是这种情况,我们可以看看函数对其三个可能参数的行为:
或者每个函数的定义域是什么:
尽管看起来域中元素可能有其他可能的排列,但这些偏序是完备的。再次强调,这是因为我们排除了不可计算的函数。接下来我们会看看这一点。
考虑下面的函数 halts
。如果传递给它的计算最终终止,则返回 True
,如果不终止,则返回 False
。正如我们通过 fix id
所见,我们可以将底部视为一个不终止的计算。我们可以通过绘制输入和输出类型的哈斯图,并绘制箭头将一个图表中的值映射到另一个图表中来绘制此图表。我还用灰色背景着色了不映射到底部的值。
众所周知,停机问题是不可计算的。那么这个看起来完全合理的图表有什么问题?
答案是我们的排序没有被函数保留。在第一个定义域中,⊥ ≤ ()
。然而,结果值却没有这种不等式:False ≰ True
。我们可以总结这种情况为单调性,即,当 x ≤ y 时,若 f(x) ≤ f(y),则 f 是单调的。
这里值得注意的两种退化情况:
-
在 f(⊥) = ⊥ 的情况下,即函数是严格的,你永远不必担心 ⊥ 不小于任何其他值,因为根据定义 ⊥ 小于所有值。从这个意义上说,使函数严格是“安全的做法”。
-
当 f(x) = c(即常数函数)对所有 x 都成立时,您同样是安全的,因为任何在原始域中存在的排序在新域中也是存在的,因为 c ≤ c。因此,常数函数是向 f(⊥)分配非底值的简单方法。这也清楚地表明单调性推论只是单向的。
更有趣(并且有些不明显)的是,我们可以编写计算函数,它们不是常量,但在传递⊥
时却提供了非⊥值!但在我们进入这种乐趣之前,让我们首先考虑一些可计算函数,并验证单调性是否保持。
最简单的所有函数是恒等函数:
它几乎什么都不做,但是您应该验证自己是否能理解这种表示法。
更不那么琐碎的是fst
函数,它返回一对中的第一个元素。
查看并验证函数保留了所有偏序关系:因为只有一个非底输出值,所以我们只需要验证灰色是否“位于”其他所有值之上。还要注意,我们的函数不关心对偶中的snd
值是否为底。
图表指出,fst
仅仅是一个未柯里化的const
,所以让我们接着看这个。
我们希望考虑const
的意义是a -> (b -> a)
,一个接受一个值并返回一个函数的函数。为了读者的利益,我们还绘制了导致这些函数的哈斯图。如果我们固定了a
或b
的类型,那么我们的偏序关系中将会有更多的函数,但在没有这些限制的情况下,通过参数性质,我们的函数能做的事情很少。
考虑到const
与seq
的对比是有用的,seq
是一种有点恶劣的函数,尽管它可以很好地使用我们的表示法来绘制。
这个函数之所以如此难缠,是因为它适用于任何类型(它将是一个完全合法且自动推导的类型类):它能够查看任何类型a
,并看到它是底部还是其构造函数之一。
让我们看看一些列表上的函数,它们与底部可能有非平凡的交互方式。null
有一个非常简单的对应关系,因为它真正询问的是“这是cons
构造函数还是null
构造函数?”
head
看起来有点有趣。
有多个灰色区域,但单调性从未被违反:尽管脊椎朝上无限扩展,每个叶子都包含偏序的最大值。
length
有类似的模式,但叶子的排列略有不同:
虽然head
只关心列表的第一个值不是底部,length
却关心cons
单元的cdr
是否为空。
我们还可以使用此符号来查看数据构造函数和新类型。
考虑下面的函数caseSplit
,它作用在一个具有唯一字段的未知数据类型上。
我们有非严格构造函数:
严格构造函数:
最后是新类型:
现在我们准备进行一个力作示例,研究从 Bool -> Bool
函数的偏序,并考虑布尔函数 ||
。为了刷新您的记忆,||
通常以这种方式实现:
从这张图表中不太明显的一点(我们希望很快能够明显),是这个运算符是从左到右的:True || ⊥ = True
,但 ⊥ || True = ⊥
(在命令式措辞中,它会短路)。我们将开发一个偏序,让我们能够解释这个左或及其表亲右或和更奇特的平行或之间的差异。
记住 ||
是柯里化的:它的类型是 Bool -> (Bool -> Bool)
。我们之前已经画出了 Bool
的偏序,那么 Bool -> Bool
的完全偏序是什么?一个非常有趣的结构!
我违反了我之前声明的约定,即更明确定义的类型位于其他类型之上,以展示这个偏序的对称性。我还缩写了 True 为 T,False 为 F。(作为补偿,我已经明确画出了所有的箭头。在未引起兴趣的未来图表中,我将省略它们。)
这些明确的 lambda 表达式有些模糊了每个函数的实际作用,因此这里是一个简写表示:
每个球或 bottom 的三重表示说明了函数对 True、False 和 bottom 的反应。
注意顶部/底部和左侧/右侧之间的轻微不对称性:如果我们的函数能够区分 True 和 False,那么就没有非严格可计算的函数。练习:画出哈斯图并说服自己这一事实。
从现在开始我们将使用这种简写表示法;如果你感到困惑,请参考原始图表。
首要任务(咳嗽)是重新绘制带有完全偏序的左或哈斯到哈斯图的图。
使用传递性验证,我们可以恢复简化的偏序的部分图。红色箭头表示原始布尔顺序中保留的排序。
百万美元的问题是:我们能写一个不同的映射来保持顺序(即单调吗)?正如你可能已经猜到的那样,答案是肯定的!作为一个练习,画出严格或的图表,它在其两个参数中都是严格的。
这是右或的图表:
注意一个非常有趣的事情发生了:bottom 不再映射到 bottom,但我们仍然成功地保留了顺序。这是因为目标域具有足够丰富的结构,可以让我们做到这一点!如果这对你来说有点神奇,请考虑我们如何在 Haskell 中编写一个右或:
rightOr x = \y -> if y then True else x
在我们查看 x 之前,我们先看 y;在我们的图中,如果 y 为 False,看起来 x 就被插入到结果中。
还有一件事情我们可以做(你现在可能已经想到了),使我们在面对 bottom 时能够给出最大能力的答案,平行或:
真的这是我们能走的最远:我们不能把我们的函数进一步推入定义链的底部,也不能移动我们的底部而不改变函数的严格语义。在 Haskell 中如何实现这一点也不明显:似乎我们真的需要能够模式匹配第一个参数,以决定是否返回const True
。但这个函数肯定是可计算的,因为单调性没有被违反。
这个名字极具暗示正确的策略:并行评估两个参数,并在任何一个返回 True 时返回 True。这种方式,哈斯图相当具有误导性:我们实际上从未返回三个不同的函数。然而,我真的不确定如何正确地说明这种并行方法。
这整个练习与卡诺图和电路中的亚稳态有很明显的并行。在电气工程中,你不仅要担心一条线是 1 还是 0,还要担心它是否从一个状态过渡到另一个状态。根据电路的构造方式,这种过渡可能导致危险,即使开始和结束状态相同(严格函数),或者无论第二行的操作如何都保持稳定(惰性函数)。我鼓励电气工程师评论一下在晶体管级别上严格或、左或、右或和并行或(我认为通常实现的方式)看起来像什么。这些类比让我觉得我花在学习电气工程上的时间并不浪费。😃
今天就到这里。下次,我们将扩展我们对函数的理解,并看一下连续性和不动点。(点击此处查看原文)
附言。 有一些本文的勘误。
Google Nexus 7 设置笔记:ezyang’s 博客
Google Nexus 7 设置笔记
我在寒假期间购买了一台 Google Nexus 7(仅 Wi-Fi 版)。我不太喜欢购买新设备:它们通常需要大量工作来按我的喜好设置。以下是一些笔记:
-
在 Linux 上越狱设备仍然有些麻烦。最终,最简单的方法可能是找一台 Windows 电脑并使用 Nexus Root Toolkit。这个工具有些不稳定;如果第一次检测失败,可以再试一次检测代码。
-
在 Linux 上进行文件传输真是痛苦。我已经通过 SSHDroid 使用 SCP 进行了工作;我还尝试了 DropBear SSH 服务器,但它们没有附带 scp 二进制文件,因此对文件传输目的来说几乎没有用。SSHDroid 并没有 out-of-the-box 解决:我需要应用 comment 14 来使真正的 scp 二进制文件在路径中被找到。默认情况下,这些应用程序配置为接受密码验证(甚至不是键盘交互式!)使用极其弱的默认密码:确保您禁用了这一功能。仍在寻找一个好的 rsync 实现。在 USB 方面,Ubuntu/Gnome/Nautilus 在 PTP 模式下本地识别 Nexus,但当我尝试复制文件时却挂起了。Ubuntu 12.10 对 MTP 支持较少,但是 go-mtpfs 在现代 libmtp 的支持下表现还算不错。Adam Glasgall 已经为 Quantal 打包了 libmtp,所以添加他的 PPA,然后 按照 go-mtpfs 的安装说明 进行安装。更新: 直接向可移动媒体传输文件也效果不错。
-
这款平板确实感觉像一部手机,因为两者都在 Android 平台上。但是没有 3G 意味着离线功能变得更加重要,而更大的屏幕使某些类型的应用程序使用起来更加愉快(更新: 我已经选择了 MX Player 作为我的视频播放器,因为它支持高级字幕 Alpha 和 MKV 文件。不幸的是,它不支持深色(例如 10 位)。)
-
微型 USB 到 USB OTG 电缆非常方便,特别是用于连接键盘或外部媒体。我敢说,它比外壳更为重要。请注意,微型 USB 端口无法为具有高功率需求的 USB 设备供电(例如旋转碟外置硬盘),因此您需要使用带电 USB 集线器连接它们。这会导致挂载时的一个症状是如果您尝试挂载功率不足的硬盘,目录列表会持续为空。它还可能发出点击声:这对硬盘可能不是好事。我使用 USB-OTG 进行挂载。
-
我试图将我的 Mendeley 论文数据库镜像到我的平板电脑上,但这相当困难。我一直在尝试使用 Referey,这是一个适用于安卓设备的 Mendeley 客户端,但它要求我以某种方式传播我的 Mendeley SQLite 数据库和所有的 PDF 文件。Dropbox 在这里看起来是一个很好的选择,但官方 Dropbox 客户端不支持保持整个文件夹同步(只支持收藏的文件)。如果你和我一样,不确定将阅读哪些论文,你必须使用其他方法,比如 Dropsync。(顺便说一句,如果你像我一样,有个聪明的主意,把 SQLite 数据库和 PDF 放在一起,这样它们就会在一个文件夹中同步,永远不要“整理”:Mendeley 会高兴地将你的 SQLite 数据库删除为“外来物”)。Mendeley 和 Dropbox 在各种方面似乎互动不良(区分大小写;此外,Mendeley 喜欢生成过长的文件名,而 Dropbox 则愚蠢而乐意接受它们)。
-
“打开窗口”按钮似乎没有正确尊重应用程序通过其自己的意愿关闭时的状态(即通过应用程序本身支持的退出按钮)。这有点恼人。
哦对了,祝你新年快乐。 😃
更新: 我的 Nexus 7 突然变砖了。幸运的是,一旦手机解锁,重新刷新镜像非常容易(并且我没有丢失数据,这通常会在首次解锁手机时发生)。我是在手机处于引导程序状态时(同时按住两个音量键并开机),通过 fastboot update image-nakasi-jop40d.zip
来完成的,然后按照这里的最后一组步骤来重新安装 SuperSu(即通过 fastboot 进入 ClockworkMod,然后通过 sideload 安装 SuperSu)。
Grad School, Oh My : ezyang’s blog
Grad School, Oh My
It still feels a little strange how this happened. Not a year ago, I was pretty sure I was going to do the Masters of Engineering program at MIT, to make up for a “missed year” that was spent abroad (four years at MIT plus one at Cambridge, not a bad deal.)
但是,通过与几位我非常尊敬的研究人员的交流、我父亲的唠叨以及在 MIT 期间实际上我将在哪里完成硕士论文的选择上的困惑,大约一个半月前,我决定参加今年秋季的研究生院入学循环。哎哟。感觉就像是再次经历本科入学一样(那并不是一个令人愉快的经历),尽管这一次,我需要写的是“研究声明”。
一个原因是我最近在博客上谈论 Mendeley,希望 Mendeley 能为我提供关于我感兴趣的论文类型的一些见解,并让我轻松地了解这些研究人员的机构隶属情况。(哎呀,还没有完全实现。)实际上,与 Simon Marlow 的一次对话更加富有成效:我目前正在积极调查 Ramsey(Tufts)、Morrisett(Harvard)、Harper(CMU)、Pierce/Weirich(UPenn)和 Mazieres(Stanford)。当然,我不禁在想,我是否错过了一些关于我在博客上经常讨论的主题的关键人物,所以如果你们有任何想法,请一定告诉我。
The process (well, what little of it I’ve started) has been quite bipolar. I frequently switch between thinking, “Oh, look at this grad student, he didn’t start having any publications till he started grad school—so I’m OK for not having any either” to “Wow, this person had multiple papers out while an undergraduate, solved multiple open problems with his thesis, and won an NSF fellowship—what am I supposed to do!” I’m still uncertain as to whether or not I’m cut out to do research—it’s certainly not for everyone. But I do know I greatly enjoyed the two times I worked at industrial research shops, and I do know that I love teaching, and I definitely know I do not want a conventional industry job. Grad school it is.
图形而非网格:缓存如何破坏年轻算法设计师及其修复方法:ezyang’s 博客
小标题:大规模多线程处理器让你的本科计算机科学教育再次变得相关。
快速排序。分而治之。搜索树。这些及其他算法构成经典本科算法课程的基础,展示算法设计的重要思想,以及性能模型是一个指令,一个时间单位。“一个指令,一个时间单位?多么古雅!”高速缓存无视算法研究人员和真实世界工程师知道,传统课程虽然不错,但却颇具误导性。仅仅看一些理论计算机是不够的:高性能算法的下一代需要与其运行的硬件保持协调。他们绝对正确。
上周五,John Feo 博士在 Galois Tech Talk 上发表了题为数据密集、不规则应用的要求和性能的演讲(幻灯片 1)。然而,Feo 还带来了另一个讲述更广泛的自适应超级计算软件中心的幻灯片(幻灯片 2)。最终的演示是关于大规模多线程处理器架构原则——特别是Cray XMT——以及在编写此类机器软件时遇到的实际工程问题的结合。由于我无法抵挡美好演示的诱惑,这些笔记的标题来自我与 Feo 在技术讨论后的一段对话;我并不是要贬低那些在传统处理器上进行研究的人,只是建议有另一种方法,Feo 认为这种方法应该得到更多关注。对于那些喜欢解谜题的人,本文末尾还将有一个“这是为什么会死锁?”的问题。
图表不是网格。 约翰·费奥开始区分科学问题和信息学问题。科学问题通常采用网格形式,是演化缓慢的系统,展示局部性原理,并仅涉及网格内部的最近邻通信。这类问题非常适合通过集群并行化解决:平面网格易于分割,最近邻通信意味着大部分计算将局限于包含分割的节点。局部性还意味着,在稍加注意的情况下,这些算法可以很好地与 CPU 缓存兼容:对于无关缓存的算法,这只是将问题分割直至适合板载的过程。
然而,数据信息学涉及的数据集却大不相同。考虑一下 Facebook 上的朋友关系图,或者互联网页面的相互链接,或者你国家的电力网络。这些都不是网格(即使是电网也不是):它们是图表。与量子色动力学模拟不同,这些图表是动态的,不断地被许多自主代理修改,这对传统处理器和并行化提出了一些独特的问题。
难以处理的图表。 有几种类型的图表特别难以运行算法。不幸的是,它们在现实世界的数据集中经常出现。
低直径(又称“小世界”)图是一种图表,其中任意两个节点之间的分离程度非常低。在这些图表上需要的工作量激增;任何查看节点邻居的算法很快就会发现自己不得不一次性操作整个图表。说再见内存局部性!紧密耦合还使得图表难以分割,而这是并行化图表计算的经典方法。
无标度图是一种图表,其中少数节点有大量的邻居,而大多数节点只有少量的邻居。这些图表也难以分割,并且导致高度不对称的工作负载:那些有大量邻居的少数节点往往会吸引大部分的工作。
图表的某些属性可能使计算更加困难。非平面图通常更难分割;动态图有并发的参与者插入和删除节点和边;加权图可能具有病理性的权重分布;最后,具有类型边的图阻止将图操作简化为稀疏矩阵操作。
这张来自 Feo 的幻灯片很好地总结了这些类型图表的即时效果。
多线程处理器:计算世界的加特林机枪。 加特林机枪是最早知名的快速射击枪之一。其他枪械简单增加射速,但很快发现,如果试图射击过快,枪管会过热。加特林机枪使用多管,每管独立射击速度较慢,但依次旋转时可以持续不断地发射子弹,同时允许未使用的枪管冷却。
空闲枪管冷却的时间类似于内存访问的延迟。由于内存访问开销大,传统处理器尝试“减少子弹使用”,通过处理器缓存来避免内存访问。然而,大规模多线程处理器采取不同的方法:而不是试图消除内存延迟,它通过上下文切换远离请求内存的线程来隐藏它,这样在切换回来时,访问已经完成并且数据可用。不需要无聊地等待数据;去做其他事情吧!在专用硬件上,PNNL 的研究人员已经能够使处理器利用率超过 90%;在非专用硬件上,性能目标要逊色一些,大约为 40%左右。
影响。 因为大规模多线程处理器隐藏了内存访问延迟,而不是试图消除它,传统的约束条件如内存局部性变得不重要。你不需要数据靠近计算,也不需要在处理器之间平衡工作(因为它们都进入共存的线程),也不需要像定时炸弹一样处理同步。你在本科计算机科学中学到的东西再次变得相关了!用 Feo 的话说:
-
自适应和动态方法都可以,
-
图算法和稀疏方法都可以,以及
-
递归,动态规划,分支和界限,数据流都可以!
因此,你的硬件将被定制用于类似图的计算。这包括一个巨大的全局地址空间来存放你的图,极其轻量级的同步形式如全/空位标志(Haskell 用户可能会认出它们与MVars非常相似;事实上,它们来自于数据流语言的同一血统)以及硬件支持线程迁移,以平衡工作负载。对于函数式语言来说,这是一种神圣的硬件圣杯!
Cray XMT 是约翰·Feo 及其研究伙伴一直在评估的一种特定架构。在处理具有较差引用局部性的算法时,它轻松击败传统处理器;然而,当你给传统处理器和具有良好引用局部性的算法时,它会慢一些。
最大权重匹配。有许多图问题——最短路径、节点间的介数中心性、最小/最大流、生成树、连通分量、图同构、着色、划分和等价性,仅举几例。Feo 选择详细介绍的是最大权重匹配。匹配是边的一个子集,使得任意两条边不相邻于同一个顶点;因此最大权重匹配是一种使所选边的权重最大化的匹配(也可以考虑其他成本函数,例如在无权重图中可能希望最大化边的数量)。
虽然存在一种多项式时间算法用于找到最大权重匹配,但是我们可以通过一种称为Hoepman 的算法的贪婪并行算法更快地得到近似答案。它类似于稳定婚姻(Gale-Shapely)算法;算法运行如下:每个节点请求与其最昂贵的本地顶点对配。如果两个节点相互请求,则它们被配对,并拒绝所有其他配对请求。如果一个节点被拒绝,则尝试下一个最高的顶点,依此类推。由于一个节点只会接受一个配对请求,配对中的边永远不会与同一个顶点相邻。
Hoepman 的算法依赖于一个能够为每个节点分配处理器的理论机器。这对传统的集群机器并不利,因此Halappanavar, Dobrian 和 Pothen提出了一个并行版本,将图分割成分区,每个分区分配给处理器,并使用队列来协调跨分区的通信。不幸的是,这种方法在某些情况下表现极差。Feo 对此现象进行了一些可视化:下面的图片展示了处理器核心的视觉图,绿色表示核心正在忙碌,白线表示处理器间的通信。尽管美国道路常规的平面图能很好地处理这个问题,但是由Erdős–Rényi 模型和无标度图(我们之前提到的“难以处理”的图类型之一)生成的图表现出了大量的处理器间通信爆炸。
然而,像 Cray XMT 这样的机器使得更接近实现 Hoepman 原始算法成为可能。为每个节点分配一个线程,并按描述的方式实现算法。
为了实现信号传递,我们可以使用完整/空位原语。每条边有两个完整/空位位,每个端点分别拥有其中一个。当一个节点尝试与一个顶点配对时,它将自己的位填充为 1,然后尝试读取另一个位。当该位为空时,节点的线程将阻塞。如果另一个位读取为 1,则节点已配对:将节点拥有的所有其他位填充为 0,然后终止。如果另一个位读取为 0,则尝试与下一个具有最高边的邻居。
这种方法并不完全奏效,因为在 Cray XMT 上存在实际约束。特别是对于大图,不可能同时运行每个线程;只有一部分节点可以同时运行。如果恰好每个节点都在等待另一个当前未运行的节点,所有节点都会阻塞,我们就会陷入死锁。特别是,Cray XMT 不会默认抢占一个被阻塞的线程,因为上下文切换的成本如此之高。(你可以打开抢占,这样死锁会消失,但运行时间会大大增加。虽然 Cray 每个周期进行线程级上下文切换,但实际上从处理器中驱逐线程是非常昂贵的。)
Feo 应用的简单修复方法是以下观察:只要我们安排在昂贵边附近的节点,总是会有工作要做:特别是,两个与最昂贵的未配对边相邻的节点总是能够配对。因此,按照它们最昂贵的顶点对节点进行排序,然后运行算法。这解决了大部分死锁问题。
结尾注释. 尽管高度多线程的架构很有前景,但硬件方面仍需大量工作(使这项技术在大宗硬件上可用,而不仅限于 Cray XMT),以及软件生态系统(构建新的编程 API 以利用这种架构)。更进一步,这个领域的问题如此多样化,以至于没有一台机器能真正攻击所有问题。
尽管如此,Feo 仍然持乐观态度:如果问题足够重要,机器会被建造起来的。
谜题. 即使进行了排序修改,在禁用抢占的 Cray XMT 上实现最大匹配仍然会在一些大图上发生死锁。什么样的图会导致死锁,以及解决这个问题的简单方法是什么?(根据 Feo 的说法,他花了三天时间调试这个死锁!而且,不,打开抢占不是答案。)解决方案将在星期五发布。
(可能会有答案在评论部分,所以如果你不想被剧透,请避开目光。)
更新。我已删除到 CACM 文章的链接;虽然我认为这对 Reddit 读者来说很及时,但它暗示 Varnish 的设计者是一个“被缓存局部性腐蚀的年轻算法设计师”,这完全是错误的。这种表达意在表达 Feo 对算法社区普遍对复杂的缓存感知/无感知算法的过分关注的一般不满,并非针对任何特定人物。
(此处故意留白)
瑞士的问候:ezyang 的博客
瑞士的问候
“说来也粗糙。”
没有预订也没有地方可去,希望是在“雾线”上方的少女峰地区找个地方睡觉
但这些计划被我发现 Wengen 没有青年旅舍所挫败了。啊,好吧。
还是相当美。
我没有一张照片,但在夜晚 Lauterbrunnen 的一个惊人的景象之一(我登记并问了业主:“有空床吗?”他们回答:“只有一个!”唯一可能的回答:“太棒了!”)是镶嵌在山上的城镇和火车几乎看起来像星星(由于它们的稀疏性,山被遮挡,形成彩色星系群)。
不合逻辑的结论。 一个问题给读者:“你有没有一个在寻找问题的解决方案?”
Groom:用于 Haskell 的人类可读的 Show:ezyang 的博客
来源:
blog.ezyang.com/2010/07/groom-human-readable-show-for-haskell/
Groom:用于 Haskell 的人类可读的 Show
在一个复杂的数据结构上敲击,我发现自己面对一堵巨大的语言困境之墙。
“天哪!”我惊叹道,“GHC 的神灵又一次用没有空白的派生 Show 实例咒骂了我!”我不满地自言自语,并开始匹配括号和方括号,扫描文本页以寻找可能告诉我正在寻找的数据的可辨识特征。
但是,我突然想到:“显示被指定为有效的 Haskell 表达式,不带空白。如果我解析它,然后漂亮地打印出生成的 AST 呢?”
几行代码后(借助Language.Haskell
的帮助)…
如何使用它。 在你的 shell 中:
cabal install groom
以及在你的程序中:
import Text.Groom
main = putStrLn . groom $ yourDataStructureHere
更新。 Gleb 提到了 ipprint,它基本上也是做同样的事情,但还有一个putStrLn . show
的函数,并且有一些调整后的默认设置,包括知道您终端的大小。
更新 2。 Don 向我提到了 pretty-show 这个由 Iavor S. Diatchki 开发的软件包,它也具有类似的功能,并配备了一个可让您离线美化输出的可执行文件!
Hacking git-rerere:ezyang 的博客
Git 的一个非常规工作流,Wizard广泛使用,是单个开发者需要在大量工作副本中执行合并。通常,维护者会从他关心的分支拉取,并将大量工作分配给那些有兴趣贡献补丁的人。然而,Wizard 正在使用 Git 为那些不了解并且不感兴趣学习 Git 的人提供服务,因此我们需要推送更新并为他们合并他们的软件。
在进行大量合并时遇到的问题是“重复解决相同的冲突”。至少对于经典案例来说,解决方法是使用git rerere
。此功能保存冲突的解决方案,然后如果再次遇到冲突,则自动应用这些解决方案。如果查看man git-rerere
,您可以了解到这些信息。
不幸的是,这个合并解决数据存储在每个.git
目录中,具体在rr-cache
子目录中,因此需要一些适度的聪明才能使其在多个仓库中正常工作。幸运的是,将所有rr-cache
目录符号链接到一个共同的目录的简单解决方案既有效又在最初合并时安全(写出解决方案时不是竞争安全,但我认为这种低竞争足以忽略不计)。
为什么这个解决方案是竞争安全的?初看rerere.c
中的代码,这似乎并非如此:如果发生两次合并以生成相同的合并冲突(这正是 git rerere 的用例),则以下代码将使用相同的hex
值执行:
ret = handle_file(path, sha1, NULL);
if (ret < 1)
continue;
hex = xstrdup(sha1_to_hex(sha1));
string_list_insert(path, rr)->util = hex;
if (mkdir(git_path("rr-cache/%s", hex), 0755))
continue;
handle_file(path, NULL, rerere_path(hex, "preimage"));
最后三行访问了(现在共享的)rr-cache
目录,并且handle_file
将尝试写出文件rr-cache/$HEX/preimage
的预影像内容;如果两个实例同时运行handle_file
,则此文件将被覆盖。
但事实证明,我们并不在乎;除非发生 SHA-1 碰撞,否则两个实例将写出相同的文件。handle_file
的签名是:
static int handle_file(const char *path,
unsigned char *sha1, const char *output)
第一个参数是从中读取冲突标记的路径,是必需的。sha1
和output
是可选的;如果output
不为 NULL,则其包含整个文件的内容,减去任何 diff3 冲突部分(由|||||||
和=======
分隔);如果sha1
不为 NULL,则写入其内容的 20 字节二进制摘要,这些内容output
本来会收到。于是,世界恢复了平衡。
附录
Anders 提出了一个有趣的问题,即两个进程是否将相同内容写入同一个文件是否真的是竞争安全的。事实上,有一个非常相似的情况涉及到两个进程将相同内容写入同一个文件,这是竞争条件的一个经典例子:
((echo "a"; sleep 1; echo "b") & (echo "a"; sleep 1; echo "b")) > test
在正常情况下,测试的内容是:
a
a
b
b
但是偶尔会出现其中一个进程输掉比赛的情况,你会得到:
a
a
b
因为写入和更新文件偏移量的非原子组合。
但是,这个例子与 Git 的情况的区别在于,在这个例子中只有一个文件描述符。然而,在 Git 的情况下,由于每个进程都独立调用 open
,所以有两个文件描述符。一个更类似的 shell 脚本可能是:
((echo "a"; sleep 1; echo "b") > test & (echo "a"; sleep 1; echo "b") > test)
其内容(据我所知)无疑是:
a
b
现在,POSIX 实际上并没有说明如果两个写入相同偏移量相同内容的情况会发生什么。然而,简单的测试似乎表明,Linux 和 ext3 能够更强地保证写入相同值不会导致随机损坏(注意,如果文件的内容不同,则可能会有任何组合,这是实际中的情况)。
Hails: 在不受信任的 Web 应用程序中保护数据隐私:ezyang’s 博客
来源:
blog.ezyang.com/2012/10/hails-protecting-data-privacy-in-untrusted-web-applications/
这篇文章是从 Deian Stefan 在 OSDI 2012 上为 Hails 发表的演讲改编而来。
它是一个广为人知的真理,任何网站(例如 Facebook)都渴望一个 Web 平台(例如 Facebook API)。Web 平台是很棒的,因为它们允许第三方开发者构建能够操作我们个人数据的应用程序。
但 Web 平台也是可怕的。毕竟,它们允许第三方开发者构建能够操作我们个人数据的应用程序。据我们所知,他们可能会将我们的电子邮件地址出售给垃圾邮件发送者或窥探我们的个人消息。随着第三方应用程序的普及,窃取个人数据几乎变得微不足道。即使我们假设所有开发者都抱着最好的意图,我们仍然必须担心那些不理解(或不关心)安全性的开发者。
当这些第三方应用程序存在于不受信任的服务器上时,我们无能为力:一旦信息泄露,第三方就可以随心所欲地做任何事情。为了减轻这种情况,像 Facebook 这样的平台采用了 CYA(“自我保护”)的方法:
Hails 项目的论点是我们可以做得更好。以下是如何实现的:
首先,第三方应用程序必须托管在一个受信任的运行时上,以便我们可以在软件中强制执行安全策略。至少,这意味着我们需要一种机制来运行不受信任的代码,并公开受信任的 API,例如数据库访问。Hails 使用Safe Haskell来实现和强制执行这样的 API。
接下来,我们需要一种方法在我们信任的运行时中指定安全策略。Hails 观察到大多数数据模型在相关对象中都内置了所有权信息。因此,一个策略可以被表示为一个对文档到可读人员标签集合和可写人员标签集合的函数。例如,“只有珍的朋友可以看她的邮箱地址”这个策略是一个函数,它接受一个代表用户的文档,并将文档的“朋友”字段作为有效读者的集合返回。我们称之为应用的 MP,因为它结合了模型和策略,并且我们提供了一个 DSL 来指定策略。策略往往非常简洁,更重要的是集中在一个地方,而不是散布在代码库中的多个条件语句中。
最后,即使在运行不受信任的代码时,我们也需要一种强制执行这些安全策略的方法。Hails 通过实现线程级动态信息流控制来实现这一点,利用 Haskell 的可编程分号来跟踪和执行信息流。如果第三方应用试图与 Bob 共享一些数据,但这些数据未标记为 Bob 可读取,运行时将引发异常。这种功能被称为LIO(标记输入输出),建立在 Safe Haskell 之上。
第三方应用运行在这三种机制之上,实现 Web 应用程序的视图和控制器(VC)组件。这些组件是完全不受信任的:即使它们存在安全漏洞或者是恶意的,运行时也会阻止它们泄露私人信息。您根本不需要考虑安全问题!这使得我们的系统甚至适合用于实现官方 VC。
我们开发的一个示例应用是GitStar,一个类似 GitHub 的 Git 项目托管网站。其主要区别在于,GitStar 的几乎所有功能都是通过第三方应用实现的,包括项目和用户管理、代码查看和 wiki。GitStar 仅仅为项目和用户提供了 MPs(模型策略)。其余组件都是不受信任的。
当前的 Web 平台让用户在功能和隐私之间做选择。Hails 让你两者兼得。Hails 已经成熟到可以在实际系统中使用;请访问www.gitstar.com/scs/hails
,或直接cabal install hails
查看。
Haskell:不够纯粹?:ezyang 的博客
Haskell:不够纯粹?
众所周知,unsafePerformIO
是一个邪恶的工具,通过它,不纯的效果可以进入本来纯洁的 Haskell 代码。但是 Haskell 的其余部分真的那么纯粹吗?这里有一些问题需要问:
-
maxBound :: Int
的值是多少? -
(\x y -> x / y == (3 / 7 :: Double))
,传入3
和7
作为参数时的值是多少? -
os :: String
的值来自System.Info
吗? -
foldr (+) 0 [1..100] :: Int
的值是多少?
对于这些问题的每一个答案都是模糊不清的——或者你可以说它们是明确定义的,但你需要一些额外的信息来确定实际结果。
-
Haskell 98 报告保证
Int
的值至少是-2²⁹
到2²⁹ - 1
。但是确切的值取决于你使用的 Haskell 实现(是否需要用于垃圾回收的位)以及你是在 32 位还是 64 位系统上。 -
根据浮点寄存器的过度精度是否用于计算除法,或者是否遵循 IEEE 标准,此等式可能成立也可能不成立。
-
程序运行的操作系统不同,此值将会改变。
-
程序在运行时分配的栈空间不同,可能会返回结果,也可能会栈溢出。
在某些方面,这些构造以有趣的方式破坏了引用透明性:虽然它们的值在程序的单次执行期间保证一致,但它们可能在我们程序的不同编译和运行时执行之间有所变化。
这个合理吗?如果不合理,我们应该怎么说这些 Haskell 程序的语义?
在#haskell
讨论了这个话题,我和一些参与者就此进行了热烈的讨论。我会尝试在这里总结一些观点。
-
数学学派认为所有这一切都非常不令人满意,他们的编程语言应该在所有编译和运行时执行中遵循一些精确的语义。人们应该使用任意大小的整数,如果需要模运算,要明确指定模数大小(
Int32
?Int64
?)。os
简直是该放在IO
罪恶箱中的一个悲剧。正如 tolkad 所说:“没有标准,你将迷失在未指定语义的海洋中。坚守规范的规则,否则你将被模糊性所吞噬。” 我们生活在的宇宙的局限性对数学家来说有些尴尬,但只要程序以一个漂亮的栈溢出崩溃,他们就愿意接受部分正确性的结果。一个有趣的子组是分布式系统学派,他们同样关心对计算环境所作的假设,但出于非常实际的原因。如果您的程序在异构机器上运行多个副本,则最好不要对传输中的指针大小做任何假设。 -
编译时学派认为数学方法在现实世界的编程中是不可行的:应该考虑编译编程。他们愿意在源代码程序中接受一些不确定性,但所有的歧义应该在程序编译后清除。如果他们感觉特别大胆,他们会根据编译时选项以多种含义编写程序。他们可以接受运行时确定的栈溢出,但对此也感到有些不舒服。这当然比
os
的情况要好,后者可能因运行时而异。数学家们用这样的例子取笑他们:“动态链接器或虚拟机怎么样,其中一些编译工作直到运行时才完成呢?” -
运行时学派说:“对执行间的引用透明度无所谓”,只关心程序运行期间的内部一致性。他们不仅可以接受栈溢出,还可以接受命令行参数设置全局(纯粹!)变量,因为这些在执行期间不会改变(也许他们认为
getArgs
的签名应该是[String]
而不是IO [String]
),或者不安全地读取外部数据文件的内容在程序启动时。他们在文档中写道:“这个整数在应用程序的一次执行到另一次执行之间不需要保持一致。”其他人都有些发抖,但大多数人在某个时候都会沉迷于这种罪恶的快感。
所以,你属于哪个学派呢?
附言. 由于 Rob Harper 最近发布了另一篇非常叛逆的博客文章,而且因为他的结尾言论与本文主题(纯度)有些关联,我觉得我忍不住要偷偷加上几句话。Rob Harper 说到:
那么为什么我们不默认这样做呢?因为这不是一个好主意。是的,起初听起来很美好,但后来你意识到这其实很可怕。一旦你进入 IO 单子,你就永远被困在那里,且被降为 Algol 风格的命令式编程。你不能轻易地在函数式和单子式风格之间转换,而不进行根本性的代码重构。而且你不可避免地需要使用 unsafePerformIO 来完成任何重要的工作。从实际角度来看,你失去了一个有用的概念——良性效应,这简直糟透了!
我认为 Harper 夸大了在 Haskell 中写函数式命令式程序的能力不足(从函数式到单子式的转换,在实践中确实很烦人,但相对来说是比较公式化的)。但这些实际上的关注确实影响了程序员的日常工作,正如我们在这里所看到的,纯度有各种各样的灰色阴影。在 Haskell 当前的情况上方和下方都有设计空间,但我认为认为纯度应该被完全放弃是错失了重点。
Haskell, The Hard Sell : ezyang 的博客
Haskell, The Hard Sell
上周我谈到了我们如何用等效的 Haskell 代码替换了一个小 C 程序。这里。尽管我很想说我们部署了代码,有很多欢呼和客户端缓存,但实际情况比这复杂得多。我们需要考虑一些非常好的问题:
-
在任何特定时间,有多少维护者知道这种语言? Scripts 项目由学生管理,并且具有异常高的人员流动率:任何给定的维护者只能保证在这里工作四到五年(如果他们留在城里可能会长一点,但除了一些显著的例外,大多数人在完成学生时代后就会离开)。这意味着在任何特定时点,我们都必须担心活跃贡献者的总知识是否足以涵盖系统的所有方面,而语言的熟练程度对于能够有效地管理组件至关重要(我们是学生,我们经常同时担任系统管理员和开发者的角色)。在企业环境中,这种情况不那么突出,但仍然起到作用:员工从一个组转移到另一个组,最终人们会离开或退休。我们目前有两位相当精通 Haskell 的维护者。这种方法的长期可持续性不确定,并且取决于我们能否吸引已经了解或有兴趣学习 Haskell 的潜在学生;在最坏的情况下,人们可能会打开代码,说“这到底是什么鬼”,然后用另一种语言重写它。
-
在任何特定时间,有多少维护者感觉在这种语言中能够轻松地进行编程? 虽然表面上类似于第一个观点,但实际上却大不相同;用不同的方式提出,这是“我能在这种语言中编写完整的程序”与“我能有效地对已写好的程序进行更改”的区别。在某种程度的流畅度上,程序员掌握了一项特殊的技能:能够查看任何 C/Fortran 衍生语言,并从周围的代码中获取他们需要的任何语法知识。这是学习语法和学习新编程范式的区别。我们可能不会同时是 Python/Perl/PHP/Java/Ruby/C 专家,但这些语言的经验相互促进,很多人都能在所有这些语言中拥有工作中的“黑客”知识。但 Haskell 是不同的:它的血统与 Lisp、Miranda 和 ML 相似,而命令式的知识无法转换。人们希望仍然能够理解任何给定的 Haskell 代码块做什么,但这仅限于只读能力。
-
还有谁在使用它? 对于团队的一名成员来说,从 Subversion 迁移到 Git 曾是一个非常难以推动的过程,但到目前为止,除了缺少进行迁移的正确基础设施外,他基本上已经被说服这是正确的前进方式。不过,这样做可以接受的一个重要原因是,他们能够列出一些他们经常使用的项目(Linux,我们的内核;AFS,我们的文件系统;Fedora,我们的发行版),这些项目也在使用 Git。但对于 Haskell 来说,我们无法这样说:Haskell 中“大”型的开源高可见应用程序是 Xmonad 和 Darcs,其中许多人从未使用过。作为一个学生团体,我们有更大的自由度来尝试新技术,但缺乏普及意味着更大的风险,而企业对这种风险过敏。
-
生态系统成熟吗? 在内部,我们对 Ruby 的维护者和打包者给予了很多批评,因为他们在向后兼容性方面的记录很糟糕(一次事件使我们无法全局更新我们的 Rails 实例,因为代码会在检测到版本不匹配时自动破坏网站)。在 Haskell 中也能看到一些相似的情况:static-cat 实际上不能在安装了默认软件包的 stock Fedora 11 服务器上构建,因为旧版本的 cgi 模块使用了异常的向后兼容包装器,因此与程序中其他异常处理代码不兼容。进一步的调查发现,cgi 模块实际上并没有在积极维护,并且 Fedora 的
cabal2spec
脚本存在问题。我个人也曾经有过这样的经历,从 Hackage 获得最新库的 Haskell 代码不再编译,因为 API 的漂移使得我的代码无法编译。Cabal install 拒绝一次性升级所有包。有许多解决方法。一个缓解因素是,一旦编译了 Haskell 程序,你就不必再担心包的组合问题了。解决方法包括重写我们的代码以前向和后向兼容,对我们的服务器做愚蠢的 Fedora 打包技巧以使 cgi 的两个版本同时存在,说服上游他们确实希望接受新版本,或者维护一个单独的系统范围 cabal 安装。但这并不理想,会让人产生疑问。
我非常幸运能在一个第一点真正重要的环境中工作。我们可以引入 Haskell 到代码库中,并期望长期维护吗?团队中总会有 C 语言的黑客(或者至少应该有;我们一些最重要的安全属性包含在对内核模块的补丁中),但团队中是否总会有 Haskell 的黑客?对于这个问题真的没有一个确切的答案。
我个人保持乐观态度。这是一次实验,你不会在这种环境下有更好的机会让事情发生。Haskell 代码的存在可能会吸引到项目的贡献者,这些贡献者可能最初并不是因为我们是社区的“免费共享网络托管提供者”而被吸引的。Haskell 似乎是唯一一个能够打破主流的语言(抱歉 Simon!),而且在没有一点风险的情况下,哪里会有创新呢?
Coq 程序员的 Haskell:ezyang 的博客
所以你可能听说过这个流行的新编程语言叫做 Haskell。Haskell 是什么?Haskell 是一种非依赖类型的编程语言,支持一般递归、类型推断和内置副作用。诚然,依赖类型被认为是现代、表现力强的类型系统的一个基本组成部分。然而,放弃依赖性可能会对软件工程的其他方面带来某些好处,本文将讨论 Haskell 为支持这些变化而做出的省略。
语法
在本文中,我们将指出 Coq 和 Haskell 之间的一些句法差异。首先,我们注意到在 Coq 中,类型用单冒号表示(false : Bool
);而在 Haskell 中,使用双冒号(False :: Bool
)。此外,Haskell 有一个句法限制,构造子必须大写,而变量必须小写。
类似于我的OCaml for Haskellers文章,代码片段将采用以下形式:
(* Coq *)
{- Haskell -}
宇宙/类型分类
宇宙是一种其元素为类型的类型。最初由 Per Martin-Löf 引入构造型理论。Coq 拥有无限的宇宙层次结构(例如,Type (* 0 *) : Type (* 1 *)
,Type (* 1 *) : Type (* 2 *)
等)。
因此,很容易将宇宙与 Haskell 类型的*
(发音为“star”)之间的类比,这种类型分类方式与 Coq 中的Type (* 0 *)
类似原始类型。此外,box类别也可以分类种类(* : BOX
),尽管这种类别严格来说是内部的,不能在源语言中书写。然而,这里的相似之处仅仅是表面的:把 Haskell 看作只有两个宇宙的语言是误导性的。这些差异可以总结如下:
-
在 Coq 中,宇宙纯粹作为一个尺寸机制使用,以防止创建过大的类型。在 Haskell 中,类型和种类兼具以强制阶段区分:如果
a
的种类是*
,那么x :: a
保证是一个运行时值;同样地,如果k
具有 box 类别,那么a :: k
保证是一个编译时值。这种结构是传统编程语言中的常见模式,尽管像Conor McBride这样的知识渊博的人认为,最终这是一个设计错误,因为不真正需要种类化系统来进行类型擦除。 -
在 Coq 中,宇宙是累积的:具有类型
Type (* 0 *)
的术语也具有类型Type (* 1 *)
。在 Haskell 中,类型和种类之间没有累积性:如果Nat
是一个类型(即具有类型*
),它不会自动成为一种。然而,在某些情况下,可以使用datatype promotion实现部分累积性,它构造了类型级别的构造函数的独立种级别副本,其中数据构造函数现在是类型级别的构造函数。提升还能够将类型构造函数提升为种构造函数。 -
在 Coq 中,所有级别的宇宙都使用共同的术语语言。在 Haskell 中,有三种不同的语言:用于处理基本术语(运行时值)的语言,用于处理类型级术语(例如类型和类型构造函数)的语言,以及用于处理种级术语的语言。在某些情况下,此语法是重载的,但在后续章节中,我们经常需要说明如何在种系统的每个级别上单独制定构造。
进一步说明:在 Coq 中,Type
是预测的;在 Haskell 中,*
是非预测的,遵循 System F 和 lambda 立方体中其他语言的传统,在这些风格的种系统中,这种类型的系统易于建模。
函数类型
在 Coq 中,给定两种类型A
和B
,我们可以构造类型A -> B
表示从 A 到 B 的函数(对于任何宇宙的 A 和 B)。与 Coq 类似,使用柯里化本地支持具有多个参数的函数。Haskell 支持类型(Int -> Int
)和种类(* -> *
,通常称为类型构造器)的函数类型,并通过并置应用(例如f x
)。(函数类型被 pi 类型所包含,但我们将此讨论推迟到以后。)然而,Haskell 对如何构造函数有一些限制,并在处理类型和种类时使用不同的语法:
对于表达式(类型为a -> b
,其中a, b :: *
),支持直接定义和 lambda。直接定义以等式风格书写:
Definition f x := x + x.
f x = x + x
而 lambda 使用反斜杠表示:
fun x => x + x
\x -> x + x
对于类型族(类型为k1 -> k2
,其中k1
和k2
是种类),不支持 lambda 语法。实际上,在类型级别不允许高阶行为;虽然我们可以直接定义适当种类的类型函数,但最终,这些函数必须完全应用,否则它们将被类型检查器拒绝。从实现的角度来看,省略类型 lambda 使得类型推断和检查变得更容易。
-
类型同义词:
Definition Endo A := A -> A.
type Endo a = a -> a
类型同义词在语义上等同于它们的扩展。正如在介绍中提到的,它们不能被部分应用。最初,它们旨在作为一种有限的语法机制,使类型签名更易读。
-
封闭类型(同义词)族:
Inductive fcode := | intcode : fcode | anycode : fcode. Definition interp (c : fcode) : Type := match c with | intcode -> bool | anycode -> char end.
type family F a where F Int = Bool F a = Char
尽管封闭类型家族看起来像是类型案例的添加(并且可能会违反参数性),但实际情况并非如此,因为封闭类型家族只能返回类型。事实上,封闭类型家族对应于 Coq 中的一个众所周知的设计模式,其中编写表示类型代码的归纳数据类型,然后具有解释函数,将代码解释为实际类型。正如我们之前所述,Haskell 没有直接的机制来定义类型上的函数,因此必须直接在类型家族功能中支持这种有用的模式。再次强调,封闭类型家族不能部分应用。
实际上,封闭类型家族的功能性比归纳代码更具表现力。特别是,封闭类型家族支持非线性模式匹配(
F a a = Int
),有时可以在没有 iota 缩减可用时减少术语,因为一些输入是未知的。其原因是封闭类型家族使用统一和约束求解进行“评估”,而不是像 Coq 中的代码那样进行普通术语缩减。事实上,在 Haskell 中进行的几乎所有“类型级计算”实际上只是约束求解。封闭类型家族尚未在 GHC 的发布版本中可用,但有一篇Haskell 维基页面详细描述了封闭类型家族。 -
开放类型(同义词)家族:
(* Not directly supported in Coq *)
type family F a type instance F Int = Char type instance F Char = Int
与封闭类型家族不同,开放类型家族在开放的宇宙中运行,在 Coq 中没有类似物。开放类型家族不支持非线性匹配,并且必须完全统一以减少。此外,在维持可决定类型推断的情况下,左侧和右侧的这类家族还有一些限制。GHC 手册的部分类型实例声明详细说明了这些限制。
封闭和类型级家族均可用于在数据构造函数的类型级别上实现计算,这些函数通过提升转换到了类型级别。不幸的是,任何此类算法必须实现两次:一次在表达级别,一次在类型级别。使用元编程可以减少一些必要的样板代码;例如,请参阅singletons库。
依赖函数类型(Π-类型)
Π-类型是一个函数类型,其目标类型可以根据应用函数的域中的元素而变化。在任何有意义的意义上,Haskell 都没有Π-类型。然而,如果您仅想单纯地使用Π-类型进行多态性,Haskell 确实支持。对于类型的多态性(例如具有类型forall a : k, a -> a
,其中k
是一种类型),Haskell 有一个技巧:
Definition id : forall (A : Type), A -> A := fun A => fun x => x.
id :: a -> a
id = \x -> x
特别是,在 Haskell 中,标准的表示法是省略类型 lambda(在表达级别)和量化(在类型级别)。可以使用显式的全称量化扩展来恢复类型级别的量化:
id :: forall a. a -> a
然而,没有办法直接显式地声明类型 lambda。当量化不在顶层时,Haskell 需要一个明确的类型签名,并在正确的位置放置量化。这需要排名-2(或排名-n,取决于嵌套)多态性扩展:
Definition f : (forall A, A -> A) -> bool := fun g => g bool true.
f :: (forall a. a -> a) -> Bool
f g = g True
类型级别的多态性也可以使用 kind polymorphism extension 支持。然而,对于种类变量,没有显式的 forall;你只需在种类签名中提到一种种类变量。
不能直接支持适当的依赖类型,但可以通过首先将数据类型从表达级别提升到类型级别来模拟它们。然后使用运行时数据结构称为单例来将运行时模式匹配的结果细化为类型信息。这种在 Haskell 中的编程模式并不标准,尽管最近有学术论文描述了如何使用它。其中特别好的一篇是 Hasochism: The Pleasure and Pain of Dependently Typed Haskell Program,由 Sam Lindley 和 Conor McBride 编写。
乘积类型
Coq 支持类型之间的笛卡尔乘积,以及一个称为空元的空乘类型。非常类似的构造也实现在 Haskell 标准库中:
(true, false) : bool * bool
(True, False) :: (Bool, Bool)
tt : unit
() :: ()
对偶可以通过模式匹配来解构:
match p with
| (x, y) => ...
end
case p of
(x, y) -> ...
有血性的类型理论家可能会对这种认同提出异议:特别是,Haskell 的默认对偶类型被认为是一个负类型,因为它对其值是惰性的。(更多内容请参阅polarity。)由于 Coq 的对偶类型是归纳定义的,即正的,更准确的认同应该是与严格对偶类型,定义为 data SPair a b = SPair !a !b
;即,在构造时,两个参数都被评估。这种区别在 Coq 中很难看到,因为正对偶和负对偶在逻辑上是等价的,而 Coq 并不区分它们。(作为一种总语言,它对评估策略的选择是漠不关心的。)此外,在进行代码提取时,将对偶类型提取为它们的惰性变体是相对常见的做法。
依赖对偶类型(Σ-类型)
依赖对偶类型是将乘积类型推广为依赖形式的一般化。与之前一样,Σ-类型不能直接表达,除非第一个分量是一个类型。在这种情况下,有一种利用数据类型的编码技巧,可以用来表达所谓的存在类型:
Definition p := exist bool not : { A : Type & A -> bool }
data Ex = forall a. Ex (a -> Bool)
p = Ex not
正如在多态性的情况下一样,依赖对的类型参数是隐式的。可以通过适当放置的类型注释来显式指定它。
递归
在 Coq 中,所有递归函数必须有一个结构上递减的参数,以确保所有函数都终止。在 Haskell 中,这个限制在表达级别上被解除了;结果是,表达级函数可能不会终止。在类型级别上,默认情况下,Haskell 强制执行类型级计算是可判定的。但是,可以使用UndecidableInstances
标志解除此限制。通常认为不可判定的实例不能用于违反类型安全性,因为非终止实例只会导致编译器无限循环,并且由于在 Haskell 中,类型不能(直接)引起运行时行为的改变。
归纳类型/递归类型
在 Coq 中,可以定义归纳数据类型。Haskell 有一个类似的机制来定义数据类型,但是有许多重要的区别,这导致许多人避免在 Haskell 数据类型中使用 归纳数据类型 这个术语(尽管对于 Haskeller 来说使用这个术语是相当普遍的)。
在两种语言中都可以轻松定义基本类型,例如布尔值(在所有情况下,我们将使用Haskell 数据类型扩展中的 GADT 语法,因为它更接近 Coq 的语法形式,且严格更强大):
Inductive bool : Type :=
| true : bool
| false : bool.
data Bool :: * where
True :: Bool
False :: Bool
两者也支持正在定义的类型的递归出现:
Inductive nat : Type :=
| z : nat
| s : nat -> nat.
data Nat :: * where
Z :: Nat
S :: Nat -> Nat
但是必须小心:我们在 Haskell 中对 Nat
的定义接受了一个额外的术语:无穷大(一个无限的后继链)。这类似于产品的情况,并且源于 Haskell 是惰性的这一事实。
Haskell 的数据类型支持参数,但这些参数只能是类型,而不能是值。(尽管,记住数据类型可以提升到类型级别)。因此,可以定义向量的标准类型族,假设适当的类型级 nat(通常情况下,显式的 forall 已被省略):
Inductive vec (A : Type) : nat -> Type :=
| vnil : vec A 0
| vcons : forall n, A -> vec A n -> vec A (S n)
data Vec :: Nat -> * -> * where
VNil :: Vec Z a
VCons :: a -> Vec n a -> Vec (S n) a
由于类型级λ不支持,但数据类型的部分应用是支持的(与类型族相反),因此必须谨慎选择类型中参数的顺序。(可以定义类型级的 flip,但不能部分应用它。)
Haskell 数据类型定义不具有 严格正性要求,因为我们不要求终止;因此,可以编写在 Coq 中不允许的奇怪的数据类型:
data Free f a where
Free :: f (Free f a) -> Free f a
Pure :: a -> Free f a
data Mu f where
Roll :: f (Mu f) -> Mu f
推断
Coq 支持请求通过统一引擎推断术语,可以通过在上下文中放置下划线或将参数指定为 implicit(在 Coq 中实现像 Haskell 中看到的省略多态函数的类型参数)。通常不可能期望在依赖类型语言中解决所有推断问题,Coq 的统一引擎(复数!)的内部工作被认为是黑魔法(别担心,受信任的内核将验证推断的参数是类型良好的)。
Haskell 如同 Haskell’98 规定的那样,在 Hindley-Milner 下享有主类型和完整类型推断。然而,为了恢复 Coq 所享有的许多高级特性,Haskell 添加了许多扩展,这些扩展不易适应于 Hindley-Milner,包括类型类约束、多参数类型类、GADTs 和类型族。当前的最新算法是一种名为 OutsideIn(X) 的算法。使用这些特性,没有完整性保证。然而,如果推断算法接受一个定义,那么该定义具有一个主类型,并且该类型就是算法找到的类型。
结论
这篇文章最初是在 OPLSS’13 开玩笑时开始的,我在那里发现自己向 Jason Gross 解释了 Haskell 类型系统的一些复杂方面。它的构建曾经中断了一段时间,但后来我意识到我可以按照同伦类型论书的第一章的模式来构建这篇文章。虽然我不确定这篇文档对学习 Haskell 有多大帮助,但我认为它提出了一种非常有趣的方式来组织 Haskell 更复杂的类型系统特性。合适的依赖类型更简单吗?当然是。但考虑到 Haskell 在大多数现有依赖类型语言之外的地方更进一步,这也值得思考。
后记
Bob Harper 在 Twitter 上抱怨,指出这篇文章在某些情况下提出了误导性的类比。我尝试修正了他的一些评论,但在某些情况下我无法推测出他评论的全部内容。我邀请读者看看是否能回答以下问题:
-
由于阶段区分,Haskell 的 类型族 实际上不是像 Coq、Nuprl 或 Agda 那样的类型族。为什么?
-
这篇文章对推导(类型推断)和语义(类型结构)之间的区别感到困惑。这种困惑出现在哪里?
-
对种类的量化不同于对类型的量化。为什么?
Haskell Implementor’s Workshop ’14 : ezyang 的博客
来源:
blog.ezyang.com/2014/09/haskell-implementors-workshop-14/
Haskell Implementor’s Workshop ’14
今年在 ICFP 上,我们对Haskell 实现者工作坊的参与人数非常多(有时甚至是站立空间不足)。我很荣幸能够介绍我在夏季关于 Backpack 的工作。
你可以获取幻灯片或者观看演示本身(感谢 ICFP 组织者今年在视频上的高效率!)这次讲座在某种程度上与我的博文A taste of Cabalized Backpack有交集,但有更多图片,并且我也强调了我们未来发展的长期方向(也许有点过头了)。
HiW 上有很多非常好的讲座。以下是我个人的一些亮点:
Haskell.org 委员会:ezyang 的博客
Haskell.org 委员会
试过访问 haskell.org 发现它宕机了吗?现在你有人可以抱怨了:haskell.org 委员会已经成立,而我显然是其中一员。8-)
我们将首先要做的事情之一是将 haskell.org 从目前托管在耶鲁大学的服务器(目前的托管效果不错,但周末服务器将会宕机,直到周一才有人去启动它)迁移到一些专用硬件上。我必须承认,我作为委员会的一员却没有任何经验(或直接经验)来帮助进行维护工作,对此我感到有些不好意思,希望这种情况会很快改变。
帮助我们进行“无需重新安装 Cabal”的 Beta 测试:ezyang 博客
来源:
blog.ezyang.com/2015/08/help-us-beta-test-no-reinstall-cabal/
帮助我们进行“无需重新安装 Cabal”的 Beta 测试
在今年夏天,Vishal Agrawal 正在进行一个 GSoC 项目,将 Cabal 移动到更类似 Nix 的包管理系统中。更简单地说,他正在努力确保您将不会再从 cabal-install 中遇到这类错误:
Resolving dependencies...
In order, the following would be installed:
directory-1.2.1.0 (reinstall) changes: time-1.4.2 -> 1.5
process-1.2.1.0 (reinstall)
extra-1.0 (new package)
cabal: The following packages are likely to be broken by the reinstalls:
process-1.2.0.0
hoogle-4.2.35
haskell98-2.0.0.3
ghc-7.8.3
Cabal-1.22.0.0
...
但是,这些补丁改变了 Cabal 和 cabal-install 中许多复杂的部分,因此在将其合并到 Cabal HEAD 之前,有意愿的小白鼠帮助我们消除一些错误将非常有帮助。作为奖励,您将能够运行“无需重新安装 Cabal”:Cabal 永远 不会告诉您无法安装包,因为需要一些重新安装。
以下是您可以提供帮助的方式:
-
确保你正在运行 GHC 7.10。早期版本的 GHC 存在一个严格的限制,不允许你针对不同的依赖多次重新安装同一个包。(实际上,如果你能测试旧版本的 GHC 7.8,这将非常有用,主要是为了确保我们在这方面没有引入任何退化。)
-
git clone https://github.com/ezyang/cabal.git
(在我的测试中,我已经在 Vishal 的版本基础上添加了一些额外的修正补丁),然后git checkout cabal-no-pks
。 -
在
Cabal
和cabal-install
目录中,运行cabal install
。 -
尝试在没有沙盒的情况下构建项目,看看会发生什么!(在我的测试中,我曾尝试同时安装多个版本的 Yesod。)
在测试之前不需要清除您的包数据库。如果您完全破坏了您的 Haskell 安装(可能性不大,但确实可能发生),您可以使用旧版的 cabal-install
清理掉您的 .ghc
和 .cabal
目录(不要忘记保存您的 .cabal/config
文件),然后重新引导安装。
请在此处报告问题,或者在 Cabal 跟踪器中的此 PR 中报告。或者下周在 ICFP 会议上与我面对面交流。 😃
高性能单子:ezyang 的博客
延续以其难以使用而闻名:它们是函数式编程世界中的“goto”。它们可以搞砸或者做出惊人的事情(毕竟,异常不过是一个结构良好的非本地 goto)。本文适合那些对延续有一定了解但怀疑它们能否用于日常编程任务的读者:我想展示延续如何让我们以一种相当系统的方式定义高性能单子,如逻辑单子。一个(可能)相关的帖子是所有单子之母。
> import Prelude hiding (Maybe(..), maybe)
我们将从一个热身开始:身份单子。
> data Id a = Id a
> instance Monad Id where
> Id x >>= f = f x
> return = Id
这个单子的延续传递风格(CPS)版本是您的标准Cont
单子,但没有定义callCC
。
> data IdCPS r a = IdCPS { runIdCPS :: (a -> r) -> r }
> instance Monad (IdCPS r) where
> IdCPS c >>= f =
> IdCPS (\k -> c (\a -> runIdCPS (f a) k))
> return x = IdCPS (\k -> k x)
虽然解释 CPS 不在本文的范围内,但我想指出这个翻译中的一些习语,我们将在一些更高级的单子中重复使用它们。
-
为了“提取”
c
的值,我们传递了一个 lambda(\a -> ...)
,其中a
是c
计算的结果。 -
只有一个成功的延续
k :: a -> r
,它总是最终被使用。在绑定的情况下,它被传递给runIdCPS
,在返回的情况下,它被直接调用。在后续的单子中,我们会有更多的延续漂浮。
顺着单子教程的步伐,下一步是看看那古老的 Maybe 数据类型及其相关的单子实例。
> data Maybe a = Nothing | Just a
> instance Monad Maybe where
> Just x >>= f = f x
> Nothing >>= f = Nothing
> return = Just
在实现此单子的 CPS 版本时,我们将需要两个延续:一个成功的延续(sk
)和一个失败的延续(fk
)。
> newtype MaybeCPS r a = MaybeCPS { runMaybeCPS :: (a -> r) -> r -> r }
> instance Monad (MaybeCPS r) where
> MaybeCPS c >>= f =
> MaybeCPS (\sk fk -> c (\a -> runMaybeCPS (f a) sk fk) fk)
> return x = MaybeCPS (\sk fk -> sk x)
将此单子与IdCPS
进行比较:你会注意到它们非常相似。实际上,如果我们从代码中消除所有关于fk
的提及,它们将是相同的!我们的单子实例大力支持成功。但是如果我们添加以下函数,情况就会改变:
> nothingCPS = MaybeCPS (\_ fk -> fk)
此函数忽略了成功的延续并调用失败的延续:你应该确信一旦调用失败的延续,它立即退出MaybeCPS
计算。(提示:看看我们运行MaybeCPS
延续的任何情况:我们为失败延续传递了什么?我们为成功延续传递了什么?)
为了更好地说明,我们还可以定义:
> justCPS x = MaybeCPS (\sk _ -> sk x)
其实这只是伪装的return
。
您可能还会注意到我们的MaybeCPS
新类型的签名与maybe
“析构”函数的签名非常相似,因此被称为它破坏了数据结构:
> maybe :: Maybe a -> (a -> r) -> r -> r
> maybe m sk fk =
> case m of
> Just a -> sk a
> Nothing -> fk
(为了教学目的,类型已重新排序。)我特意将“默认值”命名为fk
:它们是同一回事!
> monadicAddition mx my = do
> x <- mx
> y <- my
> return (x + y)
> maybeTest = maybe (monadicAddition (Just 2) Nothing) print (return ())
> maybeCPSTest = runMaybeCPS (monadicAddition (return 2) nothingCPS) print (return ())
这两段代码的最终结果相同。然而,maybeTest
在单子部分内部构造了一个 Maybe
数据结构,然后再次拆除它。runMaybeCPS
则完全跳过了这个过程:这就是 CPS 转换获得性能优势的地方:没有数据结构的构建和拆除。
现在,公平地说原始的 Maybe 单子,在许多情况下 GHC 会为您执行此转换。因为代数数据类型鼓励创建大量小数据结构,GHC 将尽最大努力确定何时创建数据结构,然后立即拆除它,从而优化掉这种浪费的行为。前进!
列表单子(也称为“流”单子)编码了非确定性。
> data List a = Nil | Cons a (List a)
> instance Monad List where
> Nil >>= _ = Nil
> Cons x xs >>= f = append (f x) (xs >>= f)
> return x = Cons x Nil
> append Nil ys = ys
> append (Cons x xs) ys = Cons x (append xs ys)
Nil
本质上等同于 Nothing
,因此我们的失败延续再次出现。但是,我们必须稍微不同地处理我们的成功延续:虽然我们可以简单地传递列表的第一个 Cons
的值给它,但这将阻止我们继续处理列表的其余部分。因此,我们需要向成功延续传递一个恢复延续 (rk
),以便在需要时继续其路径。
> newtype LogicCPS r a = LogicCPS { runLogicCPS :: (a -> r -> r) -> r -> r }
> instance Monad (LogicCPS r) where
> LogicCPS c >>= f =
> LogicCPS (\sk fk -> c (\a rk -> runLogicCPS (f a) sk rk) fk)
> return x = LogicCPS (\sk fk -> sk x fk)
请记住,return
生成的是单元素列表,因此没有更多的内容可以继续,我们将成功的延续 fk
作为恢复的延续。
旧的数据构造函数也可以进行 CPS 变换:nilCPS
看起来就像 nothingCPS
。consCPS
调用成功的延续,并且需要生成一个恢复的延续,这恰好可以通过它的第二个参数来方便地完成:
> nilCPS =
> LogicCPS (\_ fk -> fk)
> consCPS x (LogicCPS c) =
> LogicCPS (\sk fk -> sk x (c sk fk))
> appendCPS (LogicCPS cl) (LogicCPS cr) =
> LogicCPS (\sk fk -> cl sk (cr sk fk))
这些类型看起来应该非常熟悉。稍微调整一下这种类型(并将 b 重命名为 r):
foldr :: (a -> b -> b) -> b -> [a] -> b
我得到:
fold :: List a -> (a -> r -> r) -> r -> r
嘿,这是我的延续。所以我们所做的一切就是一个折叠操作,只是没有实际构造列表!
敏锐的读者可能也会注意到,列表的 CPS 表达式仅仅是列表的高阶 Church 编码。
在几个方面,CPS 转换后的列表单子赢得了巨大的优势:我们从不需要构造和拆除列表,并且连接两个列表只需 O(1)
时间。
最后一个例子:树叶单子(来自 Edward Kmett 的指示树幻灯片):
> data Leafy a = Leaf a | Fork (Leafy a) (Leafy a)
> instance Monad Leafy where
> Leaf a >>= f = f a
> Fork l r >>= f = Fork (l >>= f) (r >>= f)
> return a = Leaf a
事实证明,如果我们想要对这种数据类型进行折叠,我们可以重用 LogicCPS
:
> leafCPS x = return x
> forkCPS l r = appendCPS l r
要反向进行操作,如果我们结合到目前为止定义的所有关于逻辑的 CPS 操作,并将它们转换回数据类型,我们将得到一个可连接的列表:
> data Catenable a = Append (Catenable a) (Catenable a) | List (List a)
总结,我们已经表明,当我们构建一个大数据结构,只有在完成时才会被销毁时,我们最好将这两个过程合并,并且将我们的数据结构重新转换回代码。类似地,如果我们想对我们的数据结构执行“数据结构”-样的操作,实际上构建它可能更好:像tail
这样的 Church 编码因其效率低下而臭名昭著。我并未讨论编码某种状态的单子:在许多方面,它们与控制流单子是不同类别的(或许更准确地说“Cont 是所有控制流单子的鼻祖”)。
引用《星球大战》,下次当你发现自己陷入连续操作的混乱中时,使用数据结构!
附录. CPS(Continuation Passing Style)转换数据结构遍历与单子(monads)无关。你可以对任何东西进行这种操作。恰巧控制流单子的杀手级特性——非确定性,正好从这种转换中受益良多。
参考. 这个主题已经有大量现有的讨论。
我可能还错过了其他显而易见的一些内容。