来认识一下Ruby Next,这是Ruby的第一个转换程序,它允许您今天就使用最新的语言功能,包括最新的实验性功能-而不需要进行项目范围的版本升级。阅读这块宝石背后的故事,发现它的内部工作原理,看看它如何帮助我们将Ruby推向更美好的未来。
如今,Ruby的发展速度比以往任何时候都要快。该语言的最新次要版本2.7引入了新的语法功能,如编号参数和模式匹配。然而,我们都知道,在试生产中切换到最新的下一个主要Ruby版本,或者切换到你最喜欢的开源项目,并不是在公园里散步。作为一个软件生产网站的高级开发人员,您必须首先处理数千行软件遗留代码,这些代码在软件升级后可能会以不同的微妙方式中断。作为一名Geme作者,您必须支持该语言的旧版本以及流行的替代实现(JRuby、TruffleRuby等)-而且可能需要一段时间才能让他们了解语法变化,如果有的话。
在这篇文章中,我想为Ruby开发者介绍一个全新的工具Ruby Next,它旨在更好地解决这些问题,也可以帮助Ruby Core团队的其他成员对实验功能和建议进行全面评估。一路上,我们要触及以下几个话题:
那么,为什么每年圣诞节都不能更新到最新版本的Ruby呢?众所周知,Matz多年来一直在12月25日发布新的主要版本。除了这样一个事实,任何人都不应该在假期更新任何东西,这也与尊重你的开发同事无关。
带我去吧。我喜欢先把自己想成一个类库开发人员,然后再做一个类库应用程序开发人员。我坚定不移地想要从一切中创造出新的宝石,这也是我在《邪恶火星人》的同事们之间长期开玩笑的一个主题。我目前正在积极维护几十个宝石-其中有几个相当受欢迎。无论如何,这导致我们解决了第一个问题。
为什么?因为至少支持所有官方支持的Ruby版本-那些还没有达到生命周期终点(EOL)的版本-这是一个很好的做法。
根据最新的Ruby维护日历,在本次发布之前仍然非常活跃的Ruby版本分别为2.5、2.6和2.7.。即使您仍然可以使用较旧的版本,但强烈建议您尽快升级-如果发现新的安全漏洞,将不会针对EOL版本进行快速修复。
这意味着我必须至少再等两年才能开始使用我的宝石中所有的2.7个好东西,而不会强迫每个人都升级。
我甚至不确定两年后我还能不能写Ruby,我现在就想要这个该死的模式匹配!
假设我不关心用户,将发布一个新版本,REQUIRED_RUBY_VERSION=";~>;2.7";。会发生什么事?
我的宝石会让他们的观众损失很大的时间。根据网站最近发布的RubyGems.org Stats,请参阅Ruby版本的详细信息:
在这张图表上,我们甚至看不到2.7。请记住,这些数据在某种意义上并不太技术性,因为它不仅包含来自Ruby应用程序和应用程序开发人员的统计数据,而且还包含来自无关来源的统计数据,例如System Rubies(它们通常比最新发布的版本落后几个版本)。
更真实的关于全球Ruby社区现状的图景来自JetBrains和他们的年度调查。目前的数据是2019年的数据,但我认为这些数字并没有发生巨大的变化:
如你所见,2.3已经不再是最受欢迎的了。尽管如此,最新的版本(在调查结束时为2.6)只获得了第二枚铜牌。
至少在任何时候都有两三个最新版本的Ruby被积极使用,而最新的版本在他们中也是最不受欢迎的。
这项调查的另一个洞察力是:“30%的人表示他们不会转行”。
为何如此?也许吧,因为为了升级而升级似乎也不是太昂贵。如果它没坏就别修了,对吗?
什么才是升级的最正确动机?性能改进(每个下一个Ruby版本都有)?不太确定。新功能能鼓励开发人员更早切换吗?当然,如果是这样的话,它们就不会以很低的价格出现(比如,关键字参数被否决,以及版本2.7中的一些变化)。
综上所述,最新增加的语言在理论上可能非常有吸引力,但在实践中很难立即适用。
我决定找到一种更好的方法来彻底改变这一点,让每个人都能在真正的项目中品尝到现代Ruby,而不是依赖于他们目前的环境。
在我们接下来深入了解Ruby的技术概述之前,让我先分享一下我将Ruby现代化的个人故事。
那是2017年的初冬,刚刚有一棵红色的圣诞树下出现了Ruby 2.5。从这个版本中有一件事以一种有点有争议的方式引起了我的注意:内核#Year_Self方法。我对此有点怀疑。我想:“这是一个可以改变我编写Ruby方式的新功能吗?我对此表示怀疑。“。
不管怎样,我决定试一试,并开始在我之前工作的应用程序中使用它(幸运的是,我们试图尽快升级Ruby,也就是大约x.y.1版本)。我越是使用这种方法,我就越喜欢它。
最终,#Year_Self出现在我的宝石之一的一个代码库中。当然,一旦发生了-Ruby2.4的测试就失败了。解决它们的最简单的方法是用猴子修补内核模块,让安的老红宝石嘎嘎叫起来就像一个全新的红宝石一样。
作为宝石开发最佳实践的忠实追随者(甚至是一个特殊清单的作者之一),我知道修补猴子是最后的手段,也是图书馆绝对不能去的。本质上,其他人可以定义一个与之同名的猴子打补丁的方法,从而制造政治冲突。对于#YIELD_SELF来说,这不是一个不太可能的情况。但大约几个月后,#Then Alias已合并到Ruby干线🙂中。
模块初始自定义然后细化BasicObject Do,除非为空。RESPONSE_TO?(:YIELD_SELF)def Year_Self Year自身结束别名_Method:THEN,:YIELD_SELF END END。
改进可以被称为最令人兴奋的Ruby功能。简而言之,精炼只是一个词汇范围内的猴子补丁。尽管我不认为这个定义有助于理解这个功能是一种什么样的野兽。让我们来考虑一个更好的例子。
如果我们使用的是Ruby 2.6+,我们可以运行它们并看到以下正确的结果:
当然,我们可以用#YIELD_SELF替换#THEN,让一切都按预期进行。我们不要那样做。取而代之的是,我们使用上面定义的IyeldSelfThen细化。
让我们将精化代码放入Year_self_then.rb文件中,并仅在该文件中激活此精化:
#main.rb+Required_Relative";Year_Self_Then";Required_Relative";SUCC";Required_Relative";.#Suc.rb+Using YeldSelfThen+def Succ(V)v.Then(&;:to_i)。然后(&;:Succ)结束。
改进不仅对猴子修补很有用,而且对性能也很好。在The Sidekiq repo中查看此帖子中的三个示例。
还记得我们的精炼定义中最重要的“词汇范围”部分吗?我们刚刚在实际操作中看到了这一点:在的YeldSelfThen模块中通过.finine方法定义的扩展仅在我们添加了使用声明的sust.rb文件中才是“可见的”。其他用于程序的Ruby文件不会“看到”它;如果根本没有任何扩展名,它们就可以工作。
这意味着改进允许我们更好地控制猴子补丁,更好地将它们拴在绳子上。因此,改进是一个非常安全的猴子补丁。
尽管在Ruby2.0中引入了一些改进(首先是作为一个初步的实验特性,并且从2.1开始就稳定了),但是它们并没有得到太多的支持。这主要有几个原因:
早期有大量的边缘案例(例如,没有任何模块支持,没有任何寄送支持)。随着Ruby的每一次发布,情况都在变得更好,现在绝大多数Ruby特性(在MRI中)都能识别出改进。
对替代红宝石精细化的支持是滞后的。JRuby Core团队(特别是Charles Nutter)最近在改善软件状况方面做了大量工作,自从9.2.9.0的改进开始在JRuby中可用。
最新的改进(与模块#Prepreend不兼容)是由Jeremy Evans在几个月前进行的,并已发布到2.7版本。
今天我敢说,所有关于精细化的关键问题都是过去式的,“精细化是实验性的,不稳定的”不再是一个有效的论据。
想知道在每个次要的Ruby版本中都添加了哪些新功能吗?请查看Viktor Sepelev的Ruby Changes项目。
在Ruby 2.7之前,让旧的红宝石嘎嘎叫起来就像一个全新的红宝石一样简单,只需用所有新缺失的方法添加一个新的通用精细化即可。这就是Ruby Next的最初想法-一次改进来统治所有人:
#RUBY_NEXT_2018.rb模块RubyNext,除非为空。RESPONSE_TO?(:YILD_SELF)细化基本对象操作#.。结束结束,除非[]。RESPONSE_TO?(:差异)优化数组操作#.。结束结束,除非[]。RESPONSE_TO?(:TALL)细化可枚举DO#.。结束结束#.。end#.然后在代码中使用RubyNext。
幸运的是,我在2018年还没有发布这个项目。Ruby 2.7新增的新功能显示了最新改进方法的不足之处:我们不能改进语法。就在那时,我开始了对Ruby Next最令人兴奋的部分的工作,这是一款自动拼音器。
2019年,Ruby语法开始了更为活跃的进化。一大堆新功能已经合并到MASTER中(尽管并不是所有的都存活了下来):一个新的方法引用操作符(最终恢复),一个新的管道操作符(几乎立即恢复),模式匹配和编号的参数。
我一直看着这位新干线路过,心想:“能不能把这些好东西都带到我的项目和宝石上来,不是很好吗?这到底有没有可能呢?“。事实证明,的确如此。今天的Ruby Next就是这样诞生的。
除了提供一系列新的多边形填充-细化之外,Ruby Next还获得了另一个强大的功能-从Ruby到Ruby的转换器。
一般说来,“Transpiler”只是用来描述源码到源码编译器的专有词汇,也就是输入和输出格式基本相同的编译器。因此,Ruby Next代码转换程序将Ruby代码“编译”成另一个Ruby代码,而不会损失原有的功能。更准确地说,我们将最新/EDGE Ruby版本的源代码转换为与旧版本兼容的源代码:
不同Ruby实现的数量每年都在快速增长。现在我们有Mruby、JRuby、TruffleRuby、Opal、RubyMotion、朝鲜蓟、棱镜。代码转换程序可以帮助您更好地使用新功能,而无需等待这些实现添加支持。
Transpling在前端开发的世界里也非常流行,在那里我们有用于JavaScript的Babel和用于CSS的PostCSS等工具。
这些工具之所以存在,是因为浏览器不兼容,语言进化太快(更准确地说,是技术规范的快速演变)。你可能会大吃一惊,但我们在Ruby中也有同样的问题。我们有不同的“浏览器”(Ruby Runtime),正如我们已经提到的,我们的语言也在快速变化。当然,解决问题的规模不会像五年前的前端发展状况那样可怕,但最好还是做好充分的准备。
让我们快速概述一下我们的Ruby Next代码转换器是如何工作的。高级技术细节将在未来的帖子(或会议演讲)中紧随其后,所以今天我只介绍一些基础知识。
转换的天真方式可能是将代码作为文本加载,应用几个gsub!-s,然后将结果写入一个新的文件。不幸的是,即使在最简单的情况下,这也不起作用:例如,我们可以尝试通过应用Soure.gsub!(/\.:(\W+)/,';.method(:\1)';)来转换新的方法引用运算符(.:)。它工作得很好,除非您有一个完整的字符串或内部带有“.:”的注释。因此,我们需要一些上下文感知的东西-例如,一个更抽象的语法树。
让我跳过理论,直接转到实践中去:如何从一个完整的Ruby源代码生成一个JAST?
在Ruby生态系统中,我们有多个工具可以用来生成AST,仅举几例:Ripper、RubyVM::AbstractSyntaxTree和JParser。
这个例子是从我过去的一本餐桌书中借用的:向你学习一些二郎语,永远做得很好!
让我们来看看这些工具为以下示例代码生成的AST:
#beach.rb def海滩(*温度)箱温度:摄氏度|:C,(20.。45):有利于:开尔文|:K,(293.。318):科学有利:华氏|:F,(68.。113):对我们有利,否则:避开海滩终点。
Ripper是一个全新的Ruby内置工具(从1.9开始),它允许您从原始源代码生成符号表达式:
$ruby-r Ripper-e";pp Ripper.sexp(File.read(';beach.rb';))";[:Program,[[:def,[:@ident,";Beach";,[1,4]],[:paren,[:params,[:REST_PARAM,[:@ident,";温度";,[1,11]],],[:bodystmt,[[:case,[:var_ref,[:@ident,";温度";,[2,7],[:in,[:aryptn,nil,[[:inary,[:symbol_cripal,[:bol,[:@ident,";celcius";,[3,6],.。
尽管Ripper非常神秘,但一些Ruby黑客仍然在积极使用它。例如,Penelope Phippen正在它的基础上构建一个新的Ruby格式化程序,Kevin Deisz编写了一个名为Preval的Ruby代码运行时优化器,它实际上是一个使用Ripper S-EXP内部的非常特定的代码转换程序。
如您所见,最终的返回值是一个带有一些标识符的深度嵌套数组。Ripper的一个问题是,没有任何相关文档来说明是否存在可能的“节点”类型,并且在节点结构中也没有明显的模式。更重要的是,出于转换的目的,Ripper不能从更老的Ruby中解析出更新的Ruby的源代码。我们不能仅仅为了转移而强迫开发人员使用最新的(特别是最新的)Ruby。
RubyVM::AbstractSyntaxTree模块最近添加到了Ruby中(在2.6中)。它提供了更好的、面向对象的AST表示,但存在与Ripper相同的问题-它的版本特定:
$ruby-e";pp RubyVM::AbstractSyntaxTree.parse_file(';beach.rb';)";(范围@1:0-14:3正文:(定义@1:0-14:3 MID::海滩正文:(范围@1:0-14:3 tbl:[:温度]参数:.。正文:(CASE3@2:2-13:5(Lvar@2:7-2:18:温度)(IN@3:2-12:16(ARYPTN@3:5-3:28 const:nil pre:(List@3:5-3:28(OR@3:5-3:18:18:Celcius)(LIT@3:16-3:18:C)).。
$GEM安装解析器$ruby-parse./beach.rb(def:Beach(args(restarg:Temperature))(case-Match(lvar:Temperature)(In-Pattern(array-Pattern(Match-alt(sym:Celcius)(sym:c))(Begin(irange(Int 20)(Int 45)nil(sym:prositive)).
与前两个不同,Parser是一个独立于版本的工具:您可以从任何支持的版本中解析任何Ruby代码。它有一个精心设计的API,一些有用的内置功能(例如,源代码重写),并且已经被RuboCop这样一个非常流行的工具保护得无懈可击。
这些好处都是以高昂的价格实现的:它并不是100%兼容Ruby。这意味着您可以编写一个离奇但有效的Ruby代码,解析器无法正确识别它。以下是两个最著名的例子:
(dstr(BEGIN(dstr(str";A";)(BEGIN(发送nil:B)(str";\n";)(str";str\n";)(str";A";)(Begin(发送nil:B)。
问题出在heredocs(send nil:b)节点:解析器将两个heredocs标签中的#{.}视为插值,但事实并非如此。我希望大家不要用这些黑暗面的知识去破坏所有依赖解析器😈的程序库。
如你所见,没有一种工具是十全十美的。从头开始编写一个数据解析器,或者试图重新提取MRI使用的数据解析器,对于这个新的实验项目来说,工作量太大了。
我决定牺牲Ruby的怪异,以提高生产力,于是我选择了Parser。
选择解析器的另一个卖点是Unparser宝石的大量存在。顾名思义,它从解析器生成的AST生成一个完整的Ruby代码。
def transspile(源)ast=Parser::Ruby27。parse(Source)#执行所需的AST修改new_ast=Transform ast#返回新的源代码Unparser。Unparse(New_Ast)结束
每个重写者都只负责一个单独的功能。让我们先来看看方法引用运算符重写器(是的,这个建议已经恢复,但对于演示目的来说非常完美):
模块重写器类MethodReference<;基于_meth_ref(节点)接收器的定义,MID=*节点。子节点。已更新(#(meth-ref:send,#(const nil:C):m)[#Receiver,#->;:Method,#s(:sym,MID)#(Send]#(const nil:c):method)#(sym:m)End End。
重写并不总是那么简单。例如,模式匹配重写器包含800多行代码。
定义泳滩(*温度)__m_=温度情况下((__p_1__=(__M__)。RESPONSE_TO?(:解构)&;&;(__m_arr__=__m_。deconstruct)||true)&;&;((Array=__m_arr__)||内核。RAISE(TypeError,";#deconstruct必须返回Array";)&;&;((__p_2__=(2==__m_arr__。size))&;&;(:Celcius=__m_arr__[0])||(:c=__m_arr__[0]))&;&;(20.。45)=__m_arr_[1]):在(__p_1__&;&;(__p_2_&;&;(:Kelvin=__m_arr__[0])||(:k==__m_arr__[0]))&;&;((293.。318)=__m_arr_[1]):科学上有利的(__p_1_&;&;(__p_2__&;&;(:Fahrenheit=__m_arr__[0]))||(:f==__m_arr__[0]))&;&;((68.。113)=__m_arr_[1]):有利在我们其他:避免海滩末端。
等等,什么?这太让人受不了了!别担心,这不是您要阅读或编辑的代码,这也是Ruby运行时要解释的代码。而且这些机器很擅长理解这样的代码。
然而,只有一种情况是,我们希望被篡改的代码在结构上尽可能接近于原始代码。我所说的“结构上”是指布局或行号基本相同。
在上面的示例中,被篡改的代码(:Science_Benefit)的第7行与原始代码(在:Fahrenheit|:F,(68..113)中)有很大的不同。
这什么时候会成为一个真正的问题?在调试期间。调试器、控制台(如IRB、PRY)使用原始源代码信息,但运行时的行号会有很大不同。调试😈很开心!
为了解决这个问题,我们在Ruby Next 0.5.0中引入了一种全新的“重写”转换模式。它使用Parser重写功能,并应用更改就地更新源代码(顺便说一句,这与RuboCop自动更正的工作方式相同)。
Ruby Next默认使用“GENERATION”(AST-to-AST-to-Ruby)转换模式,因为它更快、更可预测。在任何情况下,实际的后端代码都非常相似。
通常会出现的一个问题是:当结果的表现与最原始、优雅的案例相比时,案例如何?准备好对上证综指的最终结果大吃一惊吧:
我正在致力于将这些优化移植回EMRI。有关更多信息,请查看此公关。
怎么会发现转换后的代码比原来的本机实现更快呢?我增加了一些优化来完善新的模式匹配算法,比如#Deconstruct值缓存。
我如何才能确定这些优化不会破坏系统兼容性?谢谢你又提了一个好问题。
为了确保转换后的代码(和后端口多填充)按预期工作,我使用RubySpec和Ruby自己的测试。这并不意味着被篡改的代码与被转换的MRI代码的行为100%相同,但至少它的行为方式与预期的完全相同。(老实说,我知道一些奇怪的边缘情况会破坏兼容性,但我不会告诉你🤫)。
现在,当我们了解了Ruby TRANSE的内部工作原理后,是时候回答最耐人寻味的问题了:如何将Ruby Next集成到库或应用程序开发中?
与前端开发人员不同,我们Rubyist通常不需要编写“构建”代码(除非您使用的是MR。
..