自学haskell(自学考试是什么意思)

 网络   2022-10-30 05:10   35

【CSDN 编者按】编程语言之争是开垦者们热议的长久话题,正在分歧语言的挑选以及妄图确定上也都概念没有一。那么正在面对于大型项目时该若何挑选全部完结呢?本文的作家借课程项目之机,较为了Rust、Haskell、OCaml、C++、Python、Scala 等语言编写的编译器分裂,最终发明,这些语言正在代码量以及功能完结上几乎千差万别!

作家 |Tristan Hume

译者 |弯月,责编 | 郭芮

出品 | CSDN(ID:CSDNnews)

以下为译文:

我正在滑铁卢大学的最终一个学期选了CS444:编译原理这门课程,课程项目是编写一个编译器,将Java语言的子集编译成x86代码,三人结组,语言自在挑选。

这是个罕见的机缘,我也许正在异样的大型项现时较为分歧的完结,而且我的冤家们的水平也跟我很相近,因而我也许借这个机缘看看分歧的妄图以及语言挑选。我从这个项目中取得了没有少心得,即使这个较为并没有完善,但比那些仅靠集体概念来较为编程语言的人要许多了。

咱们的编译器是用Rust写成的,开始与另一个利用了Haskell的组施行了较为。我以为他们的编译器应该更简明,但理论的代码行数差没有多。与另一个利用了OCaml的团队的较为也失去了异样的了局。然后我与一个利用了C++的团队较为,了局如我预见的那样,因为有头文件,和空洞汇总类别以及模式匹配的支柱,导致他们的编译器大了30%。下一个是跟我一个冤家的Python完结施行的较为,他的代码量没有到咱们的一半,这要归功于元编程以及动静类别。另一个冤家的团队利用了Scala,完结的编译器代码量也小于咱们。最让我惊奇的较为便是与另一个异样利用Rust的团队的较为,他们的代码量是咱们的三倍,由于他们选择了分歧的妄图确定,这最终导致了异样的功能须要的代码量孕育了辽阔分裂!

本文中开始我会来注释一下此次较为的意思,先容各个项想法根底状况,然后再注释引发编译器巨细分裂的全体缘由。最终,我交涉一谈从各个较为中学到的货色。

较为的意思

你只怕会以为,代码行数(我同时较为了代码行数以及字节数)是个很糟了的怀抱,但我以为正在这个项目中这种怀抱也许给出很实用的信息。正在我可见,至多代码行数是各个分歧的团队正在统一个大型项目上处事时最可控的一个变数。

直到咱们的项目告竣以前,没有一切人(席卷我)分解我会统计代码行数,因而没有人熟行数怀抱上做行动,每集体都尽最大尽力来加紧、正确地告竣项目。每集体(除了后面我交涉到的利用Python的项目之外)都正在完结统一个法式,想法只要一个,便是正在异样的停止日期以前经过异样的主动化测试套件,因而也没有会有某个组试图束缚分歧的课题,大概束缚更难的课题的状况。每个组都正在这个项目上花了数月的时光,专家都正在渐渐地推广功能,进而经过已知以及未知的测试。这意味着代码干净易读,没有一切取巧的地点。

除了要经过的课程测试之外,代码没有会被用于一切其他用途,也没人会赏玩它,而且因为它只可编译Java语言的一个子集,因而它也没有一切其他用途。除了规范库之外也没有禁止利用一切库,以至连协助剖析的库都没有禁止(假设规范库中没有蕴含此功能的话)。这意味着也没有会呈现一切仅有全体团队利用的、弱小的编译器库来困扰较为。

正在最终的提交停止日期之后,会运行一次奇奥的测试(咱们看没有到该测试),也便是说,自身编写测试用例并测试代码,也许保险编译器的强健、正确,也也许处置界限状况。

即使到场的每集体都是学生,但我议论的这些团队都是我以为很是优厚的法式员。每集体都至多有两年全职的练习体味,大普遍都正在高真个科技公司,一些公司以至还正在开垦编译器。多少乎每集体都有7-13年的编程体味,都十分热心正在网上赏玩课程之外的货色。

主动天生的代码没有统计正在内,但天生的语法文件以及代码被统计了。

所以我以为,就各个项目须要破费的精神,和假设是永恒项想法话须要破费几许精神去维护而言,代码量是一个很没有错的近似统计。我以为,细小的分裂也能反应出辽阔的课题,例如下面说过的用Haskell编写的编译器代码量没有到C++的一半。

Rust(较为基准)

我以及团队里的另一位成员往日不同写过1万多行的Rust代码,另一个成员正在某次编程马拉松项目上写过约莫500行Rust。咱们的编译器用wc -l统计的了局是6806行,个中席卷5900代码行(没有席卷空行以及解释),wc -c的了局为220kb。

我发明的一个课题是,这多少项怀抱的比率正在其他项目中也是如同的,只要一些细小的分裂(过会儿我会先容)。下文中提到代码行数时,我指的都是wc -l的了局,但上述结论说明,代码行数根据哪个法则施行统计本来是无所谓的(除非稀奇指出),你也许经过比率施行换算。

我写过另一篇对于妄图的文章(http://thume.ca/2019/04/18/writing-a-compiler-in-rust/),这个妄图经过了一切秘密以及奇奥的测试。它还席卷多少个极度的个性,这些个性咱们仅仅是出于趣味而开垦,并没有想着经过测试。这些个性精确占用了400行。咱们一共的单元测试以及测试用的代码约莫占了500行。

Haskell

Haskell团队由我的两个冤家组成,他们每集体精确写过多少千行Haskel,还赏玩过许多网上的Haskell实质,和许多其他一致的语言,如OCaml以及Lean。他们还有另一个我没有太纯熟的团队成员,但犹如是个很厉害的法式员,往日也用过Haskell。

他们编译器的wc -l了局是9750行,357kb,7777 SLOC(源代码行数)。这个团队的怀抱比率的差异也最大,他们的编译器中行数为1.4倍,SLOC为1.3倍,字节数为1.6倍。他们并没有完结一切极度功能,但经过了一切秘密以及奇奥的测试用例。

须要指出的主要的一点是,只要把测试用例统计正在内,对于这个团队才平正,由于他们的代码是最正确的,蕴含了1600行测试用例,并且拿获了好多少个团队未能拿获的界限状况,只没有过是课程供给的测试用例没有揭开到这些界限状况罢了。因而,假设二者都没有统计测试用例的话,他们的代码是8.1k行,咱们的是6.3k行,仅是咱们的1.3倍。

为了让怀抱更正当,我还统计了字节数,由于Haskell项目平衡每行要更长,而且没有许多只要停止括号的行,它的单行函数也没有会被rustfmt分化成多行。

正在与团队里的另一个冤家深切开采了代码巨细的课题后,咱们找到了以下缘由来注释代码巨细的分裂:

咱们选择了手写的词法分解器以及递归下降分解(recursive descent parsing),他们选择的是NFA到DFA的词法天生器,和一个LR分解器,然后再扫描一遍将剖析树变换成AST(抽象语法树,是更麻烦的代码示意大局)。这须要占用更多代码,占了2677行,比咱们的1705行约莫多了1k行。

他们利用的是更英俊的通用AST类别,能变换成分歧的类别参数,由于每次剖析都会推广更多信息。这须要更多的协助函数,所以导致了他们的AST代码比咱们的完结多了500行——咱们正在剖析并推广信息时利用的仅仅组织字面量,以及可改动的Option _ 字段。

他们约莫有400多行代码用于完结更高的抽象水准,进而用简单的函数式办法来完结代码天生以及配合,而咱们是直接改动字符串。

这些分裂再加上测试用例的分裂,就导致了代码行数的差异。理论上,咱们的文件正在中间剖析阶段(如常量折叠、影响域剖析等)的巨细跟他们的很是凑近。但照旧孕育了字节数上的区分,缘由是行的平衡长度,我预计缘由是他们须要更多的代码,正在每次剖析时誊写整体树,而咱们只须要拜候并改动便可。

我以为,思虑到Rust以及Haskell的妄图确定很是如同,都是表达性的,只要轻微的分裂,如Rust正在须要时恐怕很麻烦地改动变量等。另一点成心思的是,咱们挑选选择递归下降分解器以及手工编写词法分解器给咱们带来了回报。虽然这有点告急,由于教授并没有引荐这一点,我是自学来的,但我发明它很易于利用,是个正确的确定。

我以为,这个团队大概并没有开垦出Haskell的全数潜力。假设他们能更擅长利用Haskell,他们的代码应该行数更少。我置信,像Edward Kmeet之类的人也许利用更少的Haskell代码就能编写出异样的编译器,从这一点上来讲,我冤家的团队并没有利用太多超高等的抽象,而且他们也没有禁止利用更好的配合库,如lens等。不过,这样做的价值便是领会编译器的难度。团队的成员都是有体味的法式员,他们分解Haskell也许做很是英俊的办事,但依然确定没有这样做,由于他们以为,这样做破费的时光会逾越节流的时光,而且会让代码变得难以领会。正在我可见这确实是个正确的挑选,用“魔法”的办法利用Haskell编写编译器,会孕育“Haskell写编译器的门槛很是高,假设你没有思虑对付没有太领会Haskell的人的可维护性的话”的了局,而这种了局并没有是咱们想要的。

另一个乐趣的发明是,教授正在结束时说过,学生也许挑选一切恐怕正在学塾办事器上运行的语言,但同时针对于Haskell提出了忠告,说往昔利用Haskell的团队的分数的方差是最高的,由于许多挑选Haskell的团队都高估了他们的Haskell才略,导致他们的得分比挑选其他语言的团队低很多,也有另一全体Haskell团队像我冤家那样做得很是完善。

C++

接下来我与另一个正在团队中利用了C++的冤家施行了交谈。那个团队中我只认得这一集体,但因为滑铁卢大学中利用C++的课程很是集体,因而预计团队中的每集体都有C++体味。

他们的项目代码行数为8733,字节数为280kb,这些数字没有席卷测试代码,但席卷约莫500行的极度功能。与咱们没有含测试的代码(也蕴含500行的极度功能)比拟,他们的代码行数为1.4倍。他们经过了100%的秘密测试,但仅经过了90%的奇奥测试,很大概是由于它们没有完结项目要求的数组vtable,这个功能须要约莫50-100行代码完结。

我并没有深切开采代码分裂的缘由,我觉得最有大概的注释为:

他们利用了LR剖析器以及树誊写,而没有选择递归下降分解器;

C++空洞汇总类别以及模式匹配这两个很是常用的功能;

他们须要反复头文件中一切的函数出面,而Rust没有须要这样做。

咱们较为的另一件事是编译时光。正在我的笔记本上,咱们的编译器的调试版齐全编译须要9.7秒,调试版增量编译须要3.5秒。我的冤家并没有给出他们的C++编译器的构建时光(选择并行make),但说我供给的数字与他们的很是凑近,而且说他们把一些常用的小函数的出面放到了头文件中,以推广编译时光为价值来削减函数出面的反复(也正是因为这个缘由,我没有方法较为单纯的头文件代码行数)。

Python

我的一名冤家利害常优厚的法式员,她挑选利用Python独立告竣项目。她还比其他团队多完结了好多少个极度功能,席卷带有存放器分配的SSA马上示意,还有其他优化。另一方面,因为她是独立告竣的,而且完结了许多极度的功能,所以她正在代码质量上只破费了最小控制的履历,比如一切正确都会抛出一致的极度(因而调试时须要施行栈跟踪),而没有是像咱们一律每种正确都给出一定的正确类别以及正确信息。

她的编译器只要4581行,并且经过了一切秘密测试以及奇奥测试。她完结的功能比一切其他团队都多很多,但很难决定那些功能占了几许行代码,由于许多极度功能与每集体都正在做的功能都不异,例如常量折叠、代码天生等,但功能却更弱小。极度的功能预计至多占用了1000~2000行,因而我很置信她的代码的表达性要比咱们至多高两倍。

自学haskell(自学考试是什么意思)

形成这种分裂的最大缘由很大概是动静类别。咱们的ast.rs中类别定义就占了500行,编译器的其他全体还有更多的类别定义。咱们还经过类别系统做了各类类别限制。比如,咱们须要根底办法,才华正在分解代码历程中向AST中推广信息供以来利用,而Python中只须要给AST结点推广新的域便可。

弱小的元编程也是形成分裂的缘由之一。比如,即使她用的是LR分解器而没有是递归下降分解器,但她的项目代码量更小,由于她没有须要施行树誊写的历程,而是正在LR语法中参加了Python代码片段来构建AST,而天生器也许直接运用eval变为Python函数。咱们没有选择LR分解器的全体缘由是,没有利用树誊写来构建AST须要大度的代码(天生的Rust文件或历程式的宏)将语法绑定到Rust代码片段上。

元编程以及动静类别的弱小之处的另一个例子是,咱们有个名为visit.rs的文件有400行,里面大全体是反复性的榜样代码,仅为了完结正在各类AST组织上的拜候。正在Python中只须要一个约莫10行的函数便可递归地拜候AST结点的各个域(经过__dict__属性)。

算作Rust以及静态类别语言的癖好者,我须要指出,类别系统很是有助于避免bug以及进步机能。弱小的元编程同时会让代码更难领会,不过,这个较为了局照旧让我很是惊奇,我没想到代码的分裂能犹如此之大。假设分裂真的导致须要写两倍的代码,那我照旧以为Rust的支出是值得的,但两倍的分裂确实弗成无视,我以来会思虑正在独立告竣某项处事中的一次性代码时利用Ruby或Python。

Rust(另一个组)

最终一个较为,也是最成心思的,便是我以及另一个冤家的较为。他们组还有另一个成员(我没有认得),利用的也是Rust。我的冤家有许多Rust体味,也到场过Rust编译器,也读过许多材料。但我没有领会他的组员若何。

他们的项目有17,211行代码,没有算解释的话有15000行,没有席卷测试代码以及天生的代码公有637kb。他们没有完结一切极度功能,仅经过了4/10个奇奥测试,和90%的秘密测试,由于他们没有时光正在停止日期以前完结项目要求中的高等全体。异样的语言,代码量却是咱们的三倍,但功能却更少!

这个了局很是让我惊诧,与之比拟,以前的较为都黯然无光了。因而咱们较为了wc -l中的每个文件巨细,和提防反省各个功能是何如完结的。

犹如咱们做出的妄图确定全面没有一律。比如,他们的前端(词法、剖析、AST构建)席卷7597行,而咱们的只要2164行。他们利用的是基于DFA的词法分解器以及LALR(1)语法分解器,但其他选择了一致规划的组并没有写如许之多的代码。提防反省他们的代码后,我发明了许多分歧的妄图确定:

他们选择了有齐全类别的剖析树,而没有是规范的、基于字符串的同态剖析树。所以须要更多类别定义,和剖析历程中须要更多的变换代码,大概须要更繁复的剖析天生器。

他们正在验证正确性时,利用了TryFrom正在剖析树类别以及AST类别之间互相变换,这导致了大度的10~20行的impl代码块。咱们利用了前往Result类别的函数来完结异样的功能,极度代码量更小,也没有必对于组织适度推广类别,进而参数的重用更轻易。咱们的全体代码仅有一行match,对付他们则须要10行的impl语句。

咱们的类别须要更少的复制粘贴。比如,他们树立了零丁的is_abstract、is_native以及is_static域,由此导致的制约使得检修的代码须要被复制粘贴两次,一次正在没有前往了局的方式中,另一次正在前往了局的方式中,二者只要细小的改动。对付咱们来讲,void仅仅一个寻常的类别,咱们想出了一个方式,根据mode以及visibility分类,进而正在类别的层次上保险这些制约,制约的正确由match语句的default case天生,也许直接变化成mode以及visibility所需的modifier。

我没有检察他们代码中的分解历程,但这个历程也一律大。我跟我的冤家聊了聊,犹如他们的完结跟咱们的拜候者根底架构全面没有一律。我猜其他一些小的妄图分裂也导致了代码量的区分。

拜候者模式让咱们的分解历程只须要存眷它们须要存眷的AST,而没有用去匹配整体AST组织,进而节流了大度代码。

他们的代码天生全体是3594行,咱们的只要1560行。我看了他们的代码,犹如一切的分裂都正在于他们选择了一种中间数据组织来天生汇编指令,而咱们只利用了根底的字符串直接输出汇编代码。他们的做法须要为一切的指令以及操作数定义类别以及输出函数,这也意味着,构建汇编指令须要糜费更多的代码,而咱们的只须要利用一致于mov ecx, [edx]的指令,而他们须要一条辽阔得被rustfmt宰割成6行的语句,个中天生指令时,操作数利用了许多中间类别,还触及了多达6层的嵌套括号。咱们的输出全体也仅仅一个花样化语句,而他们须要为每条指令零丁组织。

我的团队曾经思虑过利用这种级其余抽象。假设能直接输出文本大局的汇编,大概直接输出呆板码,那就会麻烦许多,但这并没有是课程的要求。异样的货色也许利用X86Writer加上一致于push(reg: Register)之类的方式很简捷地告竣,代码量更少,效用更高。咱们思虑过的另一个角度是,抽象只怕能让调试以及测试更简捷,但咱们意识到,直接检察天生的文本汇编,大概会更轻易赏玩以及测试。但咱们预计到(昭彰是正确的),那样做会导致大度的极度代码,而且并没有能给咱们带来一切理论的优点,因而咱们没有做。

也许跟C++那个组利用的中间示意大局做个较为。他们将中间示意大局算作极度功能来完结,占用了约莫500行代码。他们选择的数据组织很是简捷(用于简捷的类别定义以及代码天生),它选择的操作与Java要求的很凑近。也便是说,他们的IR比天生的汇编更小(所以须要的组织代码更少),由于许多语言的操作(如挪用、逼迫类别变换等)须要大度的汇编指令。高层示意也使他们得以正在IR上做一些简捷的优化。C++团队想出了一个很是好的妄图,因而他们能用更少的代码告竣更多的功能。

总的来看,3倍的代码量犹如全面由分歧的妄图确定导致,每个妄图确定的分歧都导致了或大或小的代码量推广。他们完结了大度咱们没有做的抽象,推广了许多代码,反而咱们完结的一些能削减代码的抽象他们却没有做。

这个了局让我很是惊奇。我分解妄图确定很主要,但我没想到会导致如许大的分裂。思虑到我只考察了我以为很厉害的法式员的状况下,这个了局更让我战栗。正在一切的较为中,这个较为让我学到的货色至多。

我以为有帮忙的是,我正在选这门课以前读了许多对于何如编写编译器的货色,因而我也许自创他人的好的妄图,发明AST拜候者、递归下降分解等正在课程中没有教过的方式真得很好用。

我细密思虑的一件事便是抽象的价值。抽象也许让代码正在他日更轻易扩充,大概能避让一定类别的正确,但须要细密思虑,由于它大概会导致三倍的代码量,推广领会以及重构的处事量,也让大概呈现bug的位置推广了三倍,导致测试以及后续开垦的时光更少。咱们的课程跟可靠状况没有一律的是,咱们很领会地分解咱们须要完结甚么,而且咱们永久没有须要回偏激来维护代码,因而全面对消了抽象带来的优点。不过,假设你想让我扩充编译器,推广随便新功能,而我也许挑选从哪个编译器上结束处事,那我一定会挑选咱们自身的代码(即使没有是出于纯熟的缘由)。由于咱们的代码没有仅代码量更少,更轻易领会,而且我还也许正在分解须要扩充后想出一个更好的抽象方式(就像C++团队的IR那样)。

我还坚硬了分类法的抽象,即使我的想法仅仅根据现在的须要(如拜候者模式)来节略代码,和根据现在的须要推广抽象罢了,但它还能供给可扩充性、可调试性以及正确性等。

Scala

我还跟一个上学期用Scala的冤家议论过,他们的项目跟咱们的全面一律。他们的编译器蕴含4141行,160kb(没有算测试)。他们经过了8/10个奇奥测试以及100%的众人测试,没有完结一切极度功能。因而与咱们的5906行代码比拟,他们的代码只要0.7倍。

他们的代码更少的缘由之一便是他们选择了分歧的语法分解办法。这门课程禁止你利用LR表天生器器械,这个团队就利用了,而我以前提到的一切团队都没有利用。利用这个器械后,他们就没有须要自身完结LR表天生器。他们还从Java语法网站上找到了一段150行的Python剧本,该剧本从Java语法网站的页面上收集语法并变换成了天生器械的输入,进而他们没有必自身写LR语法。他们照旧要用Scala构建立,但他们整体分解阶段只用了1073行,而咱们用了1443行,大全体选择LR分解的其他团队的代码量都比咱们的递归下降分解更多。

他们的编译器的另外全体比咱们的更小,但没有分明的妄图区分,即使我没有深切赏玩代码。我以为缘由应该是Scala以及Rust语言之间的示意区分。Scala以及Rust拥有一致的函数式编程功能,如模式匹配,这对付编译器很实用,但Scala的受办理的内存能节流下一些代码。Scala还比Rust有更多的语法糖。

OCaml

因为咱们团队一切人都正在Jane Street练习,因而咱们思虑过的另一门语言是OCaml。咱们最终确定用Rust,但很想分解OCaml会何如。因而我与另一个也正在Jane Street练习的人谈了谈,他们的编译器便是用OCaml做的。

他们的编译器是10914行,377kb,席卷一小全体测试代码,没有极度功能,经过了9/10的奇奥测试以及一切的秘密测试。

与其他组一致,代码量的分裂是因为他们选择了LR分解器天生器以及树誊写,词法分解选择了正则表达式- NFA- DFA变换管线。他们的前端(词法分解+语法分解+AST构建)蕴含5548行,咱们的只要2164行,字节比率一致。他们对付语法分解器也用了expect tests,咱们也利用了一致的测试,但将预期的输出放到了代码之外,因而他们的分解器测试占了约莫600行,而咱们的只要200行。

他们的编译器的另外全体是5366行(个中461行是仅有类别定义的接口文件),而咱们的是4642行,假设思虑接口定义则只要1.15倍分裂,没有思虑接口定义,二者则多少乎是异样巨细。因而,除了语法分解器的妄图没有一律之外,Rust以及OCaml的表达性很如同,除了OCaml须要一些Rust没有须要的接口定义罢了。

归纳

总的来讲,我对付较为了局很是中意。

我从此次较为中学到了许多,也发明了许多令我惊奇的地点。我以为大伙来讲,妄图确定形成的作用要远宏大于语言的挑选,而正在完结分歧的妄图时,语言也是主要的,由于语言供给了完结妄图的器械。

原文:http://thume.ca/2019/04/29/comparing-compilers-in-rust-haskell-c-and-python/

本文为 CSDN 翻译,转载请讲授起因根源。

本文地址:http://yz.ziyouea.com/p/49068.html
版权声明:本站文章来自网络,如有违规侵权请联系我们下架。