我从TypeScript切换到ReScript

2021-01-20 20:30:42

这不是ReScript的福音,也不是与TypeScript的一对一比较。我喜欢TypeScript。我决定将一个小的TypeScript + React + Jest辅助项目重写为ReScript。

ReScript不是新的。在某种程度上,它和JavaScript本身一样古老。 ReScript是ReasonML(Facebook)和BuckleScript(Bloomberg)的更名,它们的两端都包装了OCaml。前者是OCaml语法的接口,而后者则确保将AST编译为JavaScript。 ReasonML由React的创建者Jordan Walke创建。 ReasonML仍作为ReScript的并行项目存在,但语法和任务略有不同。

ReScript不仅是品牌重塑,它还是一个ReasonML,使自己摆脱了OCaml生态系统的束缚。这样,它就放弃了对本机代码和OCaml库互操作的编译,但是获得了一种更自由的语法,该语法进一步类似于JavaScript,以拥抱其开发人员,渴望使用更好的工具。

我的第一次尝试是将ReScript安装在我的项目上,启动观察程序,将简单文件重命名为.res,并根据错误进行引导。我立即了解到,重构为ReScript不是“广度优先”,而是“深度优先”。简单地重命名文件扩展名将不起作用,因为编译器会在出现类型错误时完全停止。

在TypeScript中,可以将类型和接口逐渐分配给动态类型,同时将某些类型标记为未知或任意类型。深度优先意味着您从一个小功能或一个小React组件开始,并正确编写它。如果所有类型都是正确的-并且具有数学精度-您的代码将编译为JavaScript。

虽然TypeScript通常会转换为无法读取的代码,但最好的做法是在由ReScript自动生成的js文件上保留打开的标签。编译速度,代码的简洁性和可读性以及此类代码的性能都会给您带来惊喜。如果编译了ReScript代码,则表明其类型安全无害,因此可以消除所有噪音。

我看到的关于生成的JavaScript的可读性和性能的唯一例外是咖喱函数。默认情况下,ReScript中的所有函数都是经过咖喱处理的,其中一些函数会生成导入Currying库的代码。这种情况并不经常发生,可以禁用currying。

但是TypeScript呢?与JavaScript代码的互操作是微不足道的,但是从TypeScript(或Flow)导入和导出类型可能更加复杂,并且它创建了两个事实来源:一个用于ReScript类型,另一个用于TypeScript。

如下所述,GenType会从ReScript代码中自动生成类型化的tsx文件,您可以将其导入其他模块。这有助于导出ReScript类型,但无法导入TypeScript类型。类型转换的自动化缓解了两个事实来源的问题。

此外,生成的ts代码使用CommonJs require语法,该语法在使用本机ECMAScript模块支持时会中断。我还必须调整tsc,以免将自动生成的tsx转换为第四个(!)源文件:

.gen.tsx由GenType自动生成,该文件将导入已编译的JavaScript代码,然后使用适当的类型将其重新导出。还要添加到您的.gitignore中。

我首先重新编写了算法,因为它们没有任何可相互操作的第三方导入,并且导入语法起初对我来说是艰巨的。一些团队追求数据优先策略或UI优先策略(就像Facebook在2017年为Messenger.com所做的那样,重写了50%的代码库)。

ReScript是静态类型的功能编程语言家族的一部分,这意味着它没有编译。只是开个玩笑,这意味着它使用Hindley-Milner类型算法,该算法以100%的确定性推导类型,并且只要您的变量是不变的(以及其他一些语言设计选择),就可以在数学上证明它。另一方面,TypeScript会尽力为所有用法找到通用类型。

作为TypeScript用户,这可能会让您大吃一惊,但是以下ReScript函数是完全静态键入的:

ReScript可以肯定地知道a和b都是int,并且该函数返回int。这是因为+运算符仅对两个int起作用,并返回int。要连接两个字符串,请使用++,对于两个浮点数,请使用+.。要组合两种不同的类型,您需要将它们中的任何一个都转换。另外,没有分号。

如果您像我一样,并且喜欢在原型制作过程中键入代码,则可以按照您的期望进行操作:

请注意,我没有指定任何模块导出,但是结果代码却指定了。这显示了默认情况下如何导出模块/文件中的所有内容。 JavaScript函数本身并不安全,因此将其导入JavaScript模块中并在其中使用将不会具有ReScript的所有优点。

要使用适当的类型信息与TypeScript进行互操作,您将使用第三方genType。将其添加为devDependency,并使用@genType注释要生成的模块导出(在以前的版本中,注释使用方括号括起来)。

这将导致以下TypeScript。请注意,生成的TypeScript如何导入生成的JavaScript MyModule.bs.js文件:

GenType使用正确的TypeScript类型生成生成的.bs.js文件的单行重新导出。在此示例中,您还会注意到两件事:

只有一种类型需要类型声明,即记录类型。类型声明将如下所示,并且不产生任何JavaScript代码:

类型必须以小写开头!如果我们在其前面加上@genType,则生成的TypeScript将如下所示:

如果您想破坏所有约定的小写字母类型,可以在转换时使用@ genType.as(" Student")重命名该类型。这将在上一个代码下面添加另一行代码:

它还包括一条tslint忽略行,我希望他们在弃用前者后尽快切换到eslint。

这些是记录类型,而不是ReScript对象(请不要滥用它们上的字符串类型)。键入foo.age之类的内容后,ReScript就会知道foo是Student类型的。如果还有另一个“年龄”字段记录,它将推断它是最后声明的记录。在这种情况下,您可能需要显式注释类型。

如果您不需要那么多的仪式,则可以使用对象类型并用字符串将其编入索引:student [" age"];那么您无需声明类型。

此外,您可以使用Student作为变量名,因此student.age是有效的表达式,TypeScript会发出类似这样的消息。变量(即绑定)和类型位于单独的命名空间中,因此,将类型为Student的学生写为student:学生。

记录类型具有类似于Java或C#的“名义类型”,而不是TypeScript的“结构类型”。这就是为什么接口在TypeScript中如此重要,并且比Types使用更多的原因。 TypeScript并不真正在乎“您是什么”,而是在乎“您的外观”。

例如,如果存在另一种类型,例如与学生具有相同领域的老师,则无法将学生分配到需要老师的地方:

//定义的第一个学生类型= {年龄:整数,名称:字符串} //定义的最后一个类型教师= {年龄:整数,名称:字符串} // t是一个老师,让t = {年龄:35,名称:&# 34; Ronen" } s:student = t //错误!

我们为您找到了一个错误! // ...类型:教师某个地方想要:学生失败:由于先前的错误,无法取得进展。 >>>完成编译(退出:1)

与TypeScript的tsc编译器不同,bsb不会轻易将其转译工作转换为可运行的JavaScript。它将以非零的退出代码停止,您必须修复该问题才能取得任何进展。

我最喜欢现代TypeScript(或将来的JavaScript)中的功能之一是可选功能。它们使使用空值类型的工作变得简单而简洁:

如果baz达到了那么远,它将是内容,或者是" default"。

ReScript中没有null或undefined。但是我们可以使用Variant选项处理可为空的值。但是,如何才能获得上述TypeScript代码的优雅呢?我试图回答这个问题,但目前还不能。没有足够的糖。

与其他功能语言一样,我们可以使用许多有趣的库函数。 Belt实用程序的一些功能是:

Belt.Option.Map将在可选值(如果存在)上执行一个函数,或者返回None。

让baz =切换foo {| Some({bar:Some({baz:baz})})=> baz |无=>没有 }

可选语法尚不完善。可选运算符对于TypeScript也是非常新的。

模式匹配的重要素质在于,如果您没有解决任何问题,无论嵌套的深度如何,编译器都会抱怨。在大多数情况下,这是最佳做法。

以前的版本使用三角形运算符|>。区别在于将数据推送到何处:作为第一个参数(如箭头所示),或作为最后一个参数(如已弃用的三角形一样)。有关此的更多信息。

注意,对于单参数函数,我们不写单位,即()。这是一个常见的初学者的错误。在有多个参数的情况下,该值作为第一个参数传递,而其他参数以第二个参数开头。

这在函数式语言中尤其重要,因为我们失去了对象调用方法的某些优雅之处。

作为一个新手,我会尝试在任何可能的地方找到它的用法,这可能会导致错误的做法,那就是围绕它重写代码以打动我的同事。要在JavaScript库上使用它,您必须为其编写正确的绑定。这是我想在JavaScript中看到的一件事。以下是第一阶段的一些建议。

顺便说一句,如果您不使用Fira Code,那么您会错过很多管道的美感。

这让我非常沮丧。我喜欢在代码中使用现代的async和await语法,而ReScript尚未实现。我不得不重新考虑然后再解决,这使简单的代码看起来很复杂。

const getName = async(id:number):Promise< string> => {const user = await fetchUser(id);返回user.name; }

现在,将其视为Js.Promises模块中的函数,而不是将fetchUser(id)作为其最后一个参数的方法,您可以这样编写它:

键入为Js.Promise.t< string&gt ;,并具有箭头管道语法以提高可读性,以上函数可以写为:

Promise库仍然使用传递数据作为最后一个参数的旧约定,因此,为了使用较新的箭头管道,必须在适当的位置放置下划线。

ReScript团队承诺(并非双关语)使用自己的异步和等待功能来实现Promise API改造。

如果您仅使用ReScript编写,则无需理会导入或导出,这是在后台进行的。每个文件都是一个模块,其中的所有内容都将导出。如果只希望导出特定内容,则可以使用接口文件来导出。但是,要导入JavaScript模块,语法可能会变得复杂。

对于ReasonReact来说,这变得特别麻烦,因为我必须为每个React Component定义内联模块,然后将默认导出重新导出为“ make”函数,并注意诸如“ children”之类的命名参数。在这里,我从react-bootstrap导入了Container,并在ReasonReact中使用了它:

模块容器= {@ bs.module(" react-bootstrap / Container")@ react.component外部make:(〜children:React.element)=> React.element ="默认" } @ react.component让make =()=> <容器> ...

对于这种情况,我可以从redex获取绑定,并将其作为依赖项添加到package.json和bsconfig.json中。然后,我可以使用文件顶部的打开ReactBootstrap导入它。这类似于DefinitelyTyped,在其中您可以找到TypeScript的高质量类型定义。

但是对于这种情况,我遇到了一个错误,因为我需要的软件包没有更新到最新版本。我不得不将其分叉并手动将其更新为react-jsx版本3。

您无法从TypeScript导入类型并在ReScript中使用它,必须重新声明它。但是,您可以将创建的类型链接到原始TypeScript,以实现正确的互操作。这是Node.js的fs模块的示例:

请注意,我传递了要导入的元组,而不是参数列表。这会将我的类型dirent链接到fs.Dirent,并将生成以下TypeScript:

您可以声明整个类型,以防您需要使用其属性,也可以保持不变。

由于TypeScript-ReScript互操作的语法开销,我建议尽可能少地使用每种语言,在应用程序的不同区域中使用它。

ReasonML(现为ReScript)由React的创建者Jordan Walke创建。 Reason + React通过利用ReactJS编程模式的语言语法和功能进一步推动了React理念。

ReasonReact提供流畅的JS互操作,并使用内置的语言功能集成到ReactJS未解决的UI框架模式中,例如路由和数据管理。使用它们的感觉就像“只使用理性”。

如果要使用旧语法,只需将文件扩展名更改为.re而不是.res。

ReasonReact比ReactJS更严格,主要是在类型的使用上(例如,字符串需要与JSX中的React.string()一起使用。除此之外,React.useState返回一个正确的元组而不是数组,就像以前那样)最后,React Components通过make函数呈现,并以@ react.component开头(我也为TypeScript生成添加了@genType):

如果我们不希望GenType用于TypeScript生成,我们只需导入Demo.bs。

为了用ReScript编写测试,从而直接测试代码,可以使用bs-jest,它提供了到Jest的ReScript绑定。如果您愿意,也可以使用稍微不太成熟的bs-mocha。您也可以在不进行额外配置的情况下测试生成的JavaScript或TypeScript文件。

由于ReScript位于JavaScript生态系统中,因此为ReScript创建专门的测试工具几乎没有意义,其方向似乎是为JavaScript测试工具开发绑定。

使用bs-jest,您必须命名,而不能使用有效的模块名称(例如foo_spec.res)来命名文件foo.spec.res。 Jest将在默认情况下在lib / js中的已编译文件夹中运行。同样,断言不会立即执行,而是由函数返回并在套件末尾运行。这是测试的一种实用方式。因此,每个测试只能编写一个断言,这是最佳做法。

ReScript开发人员在确定VSCode插件的优先级方面做得很好,效果很好。在运行ReScript的观察程序后,您会看到类型错误用红色下划线标出,并在悬停时带有描述性气泡。您还将获得类型提示,格式设置并跳转到定​​义。还对Vim(纯Vim和Coc语言服务器)和Sublime提供了官方支持。

在编码生涯中,有几次我不得不与小型社区合作,而我一直很喜欢它。我使用Solidity开发了智能合约,使用功能语言Q开发了一些数据库查询,并使用BrightScript开发了Roku通道。您最终会在Slack / Discord / Gitter打开的情况下工作,并与其他几个经历类似问题的人员一起进行编码。您甚至不必费心检查StackOverflow寻找答案。

由于您不想在聊天室中显得笨拙,因此这迫使您阅读和重新阅读官方文档和示例。此外,您是一个由真正的人维护的社区的一部分,在这里,您总是可以贡献出一些有趣的东西,甚至可以决定它的发展。

当然,并非所有社区都一样。我个人发现ReasonML / ReScript社区很受欢迎。 ReScript有一个官方论坛,您可以在其中进行异步通信,并可以搜索永久的纸质记录。核心团队由少数具有公开Twitter帐户的开发人员组成,并且有一个官方博客。但是,我发现社区在一个非正式的ReScript会议室中的ReasonML Discord服务器中闲逛。

最后,ReasonConf的YouTube频道和Redex提供了ReasonTown,“有关ReasonML语言及其完善社区的播客”,以查找库的绑定。

切换并不容易。考虑到它在第一个问题上的致命威胁,对现有应用程序的重构甚至更加困难。这肯定会阻碍其采用。诸如TypeScript,SCSS或CoffeeScript之类的流行编译器很容易获得采用。只需复制粘贴代码(或重命名文件)即可,操作就完成了。

这是不同的。与其他静态类型的功能语言一样,ReScript旨在从根本上改变代码的处理方式。我相信将来我们会看到功能编程的更多采用,最终将成为某些行业的默认功能。这是由于对类型的数学方法,对程序正确性的形式验证以及给定的不变性:较少的移动部分和思维导图。

我们已经处于在生态系统中采用“功能样式”以及通过JavaScript进行地图,过滤,归约功能的第一步。 ReScript代表了ML系列中一种功能正常的语言的下一个混合阶段,该语言已编译为行业标准的JavaScript。

函数式编程的核心是认真对待自己。这是数学上的,形式上的,并且不符合骇客行为。它渴望处理真理,而不是过程。用JavaScript编写“功能样式”只会激发人们的胃口,因为该语言会使人的良好意愿降低,而不是提高。 ReScript虽然令人沮丧,但它可能是生态系统中更加文明的未来的精确工具。