使用Haskell以本机速度编程R

2020-11-04 01:10:25

大多数好的数据故事都是从一个有趣的问题开始的。如果平均请求延迟再降低100毫秒,我们可以预期用户参与度会增加多少呢?给出一份全国范围内新建和修复现有道路的所有投标清单,我们如何才能发现政府官员腐败的证据?给出一个常见搜索词的时间表,我们能否确定一场新的大流行正在酝酿之中?然而,我们通常知道我们有数据,但我们甚至不知道这些数据可能有助于回答什么问题,或者故事将如何展开。从在空闲的下午挠痒痒的数据科学家,到部署在数百台机器上的低延迟、高可用性的实时分析,故事通常涉及大量重写、曲折和构建大量代码和实用程序,以提高分析的精度、速度或规模。

R提供了一个很好的交互环境,用于查看当前的数据集,并找出可能潜藏在其中的答案。它提供了丰富的现成的库,可以用来拼凑出一个可以讲述的初始故事。但是R是一种特殊用途的脚本语言。它在支持一次性“按我的意思做”编程以验证假设的迭代方面的优势,当一个模型要被建立成工业规模、高性能和可维护的产品或服务时,很快就会成为一个障碍。到那时,更通用的语言(鼓励结构化、模块化编程,提供强大的正确性静态保证,并编译成本机代码以获得最大速度)变得更加合适。哈斯克尔就是这样一种语言。

请注意,Haskell是一种支持快速迭代的伟大语言,“在小的”探索性编程中也是如此,但到目前为止,它还缺乏R提供的从机器学习到可视化的过多高质量库,也许还缺少一些语法工具来快速和松散地发挥作用。今天,我们很自豪地宣布HaskellR项目的第一个公开版本,它包括一个库和两个交互环境,可以在同一源文件中或在同一提示符下无缝地对R和Haskell进行编程。

该项目的核心是inline-r(其设计后来启发了inline-c-他们共享一个合著者),它导出一些准引号来表达对R函数的调用,甚至是R语法中的任意R代码。内联R设计背后的原则是,

按照R预期的方式使用R库:使用R的语法和调用约定;

尽可能降低跨越语言边界的开销,以鼓励两种语言的代码细粒度交错;

让用户在抽象堆栈中弯下腰或跳到他喜欢的高度:一切都在用户的控制之下,以防他需要它。

我们将在下面和以后的帖子中更详细地讨论以上每一点。但首先,让我们先来品尝一下这些东西。如果还没有的话,你可以考虑将下面的设置作为你的首选交互shell:它重用了现有的项目,工作方式很像GHCi,如果你愿意,在一个孤立的沙盒里,除了你有开箱即用的内嵌图形和公式,就像Shae Erisson最先在Haskell世界中用ghclive和Manuel Chakravarty最近在OSX上用Haskell for Mac实现的那样,你可以考虑使用下面的设置作为你的首选交互shell:它重用了现有的项目,工作方式很像GHCi,如果你愿意的话,就像你有开箱即用的内嵌图形和公式一样。

一个名为H的基本REPL。这是一个围绕GHCi的薄包装器,使用所有正确的扩展和导入来初始化它,以命中加载运行;

这是一款全唱全跳的互动笔记本,由Jupyter(以前的IPython)和AndrewGibiansky的奇妙的IHaskell内核提供支持。

在这篇文章中,我们将主要讨论后者。多亏了堆栈,开始使用HaskellR非常简单,更重要的是,相对可靠。我们组装了一个码头集装箱,让您轻松上手。它包括预装的Jupyter和IHaskell.。要在其中构建HaskellR,请执行以下操作:

使用IHaskell,您可以将笔记、公式和代码保存在一个称为笔记本的地方。使用HaskellR的IHaskell插件,您可以在笔记本中使用广受好评且非常受欢迎的R可视化包,如ggplot2前置绘图。在笔记本电脑(又名操场)中工作很方便:它们是独立的单元,很容易通过电子邮件或网络与同事共享,您可以编辑早期的定义,同时保持后面的定义同步。

下面是一个使用R的数据分析工具处理在Haskell中生成的数据的简单示例。假设您有一群噪音很大的数据。我们将使用随机包来生成样例集:

导入Control.Monad导入系统.Random.MWC作为MWC导入系统.Random.MWC.Distributions main=do gen<;-MWC.create Xs<;-replicateM 500$Normal 103 Gen ys<;-replicateM 500$Normal 103 Gen...。

现在,我们可以使用R的标准库Plot()函数根据y坐标列表绘制x坐标列表:

更好的是:假设我们想要这些点的密度估计的某种可视化。我们可以使用R的2D核密度估计函数,开箱即用:

请注意,在上面的代码中,一些代码显示为查询引号块。这是一个语法工具,用来告诉Haskell编译器,块内的任何代码都应该理解为使用R的语法,而不是像通常在这些块之外那样使用Haskell的语法。我们实现了一种机制,让R解释器的嵌入式实例为我们解析代码,这样我们就不必自己摸索R的完整表面语法。

按照惯例,_hs后缀变量并不引用环境中的绑定,而是引用Haskell环境中的绑定。在技术术语中,这些变量实际上是反引号(我们使用约定而不是额外的语法,这样我们就可以按原样重用R的股票解析器,而不是实现我们自己的)。反引用是R和Haskell之间进行数据通信的基本机制。在常见情况下,我们可以完全不用编组就可以做到这一点,所以如果您愿意,您可以在紧凑的循环中重复取消语言边界。

HaskellR背后的核心思想是语言互操作应该是零成本的,或者接近零成本。您应该没有理由犹豫使用一点R来完成这项工作,在Haskell中过度实现相同的事情,因为您担心将大量数据发送到Smer Remote R解释器实例的性能或成本。我们相信,让外来调用和本地调用一样快,是让同时使用CRAN和黑客包功能的体验编程无缝进行的关键。

为此,我们决定嵌入R解释器实例,也就是说将解释器的C代码与Haskell程序的Haskell代码链接在同一个二进制中。这样,我们就可以在相同的进程地址空间中与R解释器进行通信。为了提高速度,许多R函数实际上都是用C语言编写的,并编译成本机代码。其中一些原语可以从Haskell调用,其成本与任何其他外部函数调用的成本一样低。

但这并不是表演故事的结束。跨语言编程中一个典型的令人烦恼的问题是,一种语言坚持数据的一种表示形式,而另一种语言则想要它自己的表示形式。因此,数据通常必须不断地从一种表示形式编组到另一种表示形式。在HaskellR中,我们通过以下方式解决了这个问题:从头到尾都使用R的表示。这是R函数期望的形式,因此它们在被调用时可以直接对该数据进行计算。问题是,R的数据表示对于Haskell来说是陌生的,因此您失去了Haskell强大的语言工具,这些工具可以处理任何本机代数数据类型,比如模式匹配。或者你有吗?…。

两全其美(零编组和模式匹配)的诀窍是定义所谓的视图函数,该函数为您提供作为外来数据的代数数据类型的本机视图。下面是一个精心设计的玩具示例,其中我们在Haskell中定义阶乘函数,但定义在R个整数之上:

FACT::SEXP s';R.Int->;R s(SEXP s';R.Int)Fact(Hexp->;Int[0])=R.cast sing<;$>;[r|1L|]Fact [email protected](Hexp->;Int_)=R.cast sing<;$>;[r|n_hs*act_hs(n_hs-1L)|]。

Hexp是一个视图函数,它将原生R数据(所有内容在R内部都是SEXP)映射到Haskell原生GADT。多亏了类型注释(在以后的帖子中会有更多),我们静态地知道R数据只能是某种整数向量,所以我们对此进行模式匹配,检查它是否是单例零向量,然后递归。

我们现在不是回到编组了吗?是也不是!我们将这些视图函数精心设计为非递归的。非递归函数可以内联。因此,当您只使用视图函数在紧接着之后的结果上进行模式匹配时,就像上面的情况一样,GHC足够聪明,能够识别视图函数正在构造的数据类型值只是为了稍后对其进行变形,所以它简化了分配!是的,这是某种编组,但它是免费的智能编组:在运行时没有任何痕迹。

关于HaskellR的设计还有很多要讨论的内容,但这篇文章已经很长了,所以我们将保留一些主题留到下一次。

同时,还有更多的工作要做,所以请随时参与并为该项目做出贡献。最后,HaskellR提供的是一种以原生速度编写Haskell程序将R原语拼凑在一起的方法,而不是通过半结构化R脚本。但是我们还没有完全消除调用这些R原语的开销。接下来的步骤:

直接访问用C编写的原语,而不会因为R的动态绑定而在运行时招致查找成本。目前,这涉及到比完全合理的更深入地研究R的内部,但我们正在努力在R的未来恢复中使这一点变得更容易。

更好的Windows支持:目前对Windows的支持主要是试验性的,安装说明还需要整理。如有任何帮助,我将不胜感激。

Alexander Vershilov贡献了一个新的准引号实现,它应该会大大加快大型准引号的加速编译时间。

如果您对这项工作感兴趣,并想了解任何重要的更新,请务必注册HaskellR邮件列表。

特别感谢Dave Balaban的帮助和支持,以及HaskellR和以前版本的早期用户。