写点什么

如果 ReasonML 没有 JavaScript 的那些怪癖,你该不该试试它?

  • 2019-12-27
  • 本文字数:5425 字

    阅读完需:约 18 分钟

如果 ReasonML 没有 JavaScript 的那些怪癖,你该不该试试它?

TypeScript 和 ReasonML 都声称自己为 Web 开发人员提供了可编译为 JavaScript 的静态类型语言,那么它们之间的差别是什么?ReasonML 能带来 TypeScript 想要做到的一切(甚至更多),但是前者没有那些 JavaScript 怪癖。这样的话,你是否应该试试它呢?


TypeScript 是 JavaScript 的超集,这既是它的最佳特性,也是它最大的缺陷。虽说与 JavaScript 的相似性给了人熟悉的感觉,但这意味着我们所喜爱和反感的所有 JavaScript 怪癖都在 TypeScript 里重现了。TS 只不过是在 JavaScript 之上添加了类型,然后就差不多完事了。


ReasonML 提供的是一种完全不同但让人感觉很熟悉的语言。这是否意味着 JavaScript /TypeScript 开发人员会很难学习这个新语言?我们就来看看吧。

声明一个变量

让我们从变量声明开始:


ReasonMLlet a = "Hi";TypeScriptconst a = "Hi"
复制代码


在 ReasonML 中,我们使用 let 关键字声明一个变量。没有 const,默认情况下 let 是不可变的。


在这种情况下,两种语言都可以推断出 a 的类型。

函数

TypeScript


let sum = (a: number, b: number) => a + b
复制代码


ReasonML


let sum = (a,b) => a + b;
复制代码


尽管我没有手动编写任何类型,但这个函数的参数还是类型化了。为什么我们用不着在 ReasonML 中编写类型呢?因为强大的类型系统可以进行出色的类型推断。这意味着编译器可以在不需要你帮助的情况下推断出类型。ReasonML 中的 (+) 运算符仅适用于整数——a 和 b 只能是这种类型,因此我们不必编写它们。但如果需要,你随时都可以编写类型:


ReasonML


let sum = (a: int, b: int) => a + b;
复制代码

接口,记录

TypeScriptinterface Product {  name: string  id: number}
复制代码


ReasonML 中最接近接口(Interface)的是记录(Record,https://reasonml.github.io/docs/en/record)。


ReasonML


type product = {  name: string,  id: int,};
复制代码


记录就像 TypeScript 对象一样,但前者是不可变的,固定的,并且类型更严格。下面我们在某些函数中使用定义的结构:


ReasonML


let formatName = product => "Name: "++product.name;
复制代码


TypeScript


const formatName = (product: Product) => "Name: " + product.name
复制代码


同样,我们不需要注释类型!在这个函数中我们有一个参数 product,其属性 name 为字符串类型。ReasonML 编译器可以根据使用情况猜测该变量的类型。因为只有 product 这个类型具有字符串类型的 name 属性,编译器会自动推断出它的类型。

更新记录

let updateName = (product, name) => { ...product, name };
复制代码


const updateName = (product: Product, name: string) => ({ ...product, name })
复制代码


ReasonML 支持展开运算符,并像 TypeScript 一样对名称和值做类型双关。


看看 ReasonML 生成了什么样的 JavaScript 吧,这也很有趣:


function updateName(product, name) {  return [name, product[1]]}
复制代码


ReasonML 中的记录表示为数组。如果人类可以像编译器一样记住每种类型的每个属性的索引,则生成的代码看起来就会像人类编写的那样。

Reducer 示例

我认为这就是 ReasonML 真正闪耀的地方。我们来比较一下相同的 Reducer 实现:


在 TypeScript 中(遵循这份指南:https://redux.js.org/recipes/usage-with-typescript)


interface State {  movies: string[]}
const defaultState: State = { movies: [],}
export const ADD_MOVIE = "ADD_MOVIE"export const REMOVE_MOVIE = "REMOVE_MOVIE"export const RESET = "RESET"
interface AddMovieAction { type: typeof ADD_MOVIE payload: string}
interface RemoveMovieAction { type: typeof REMOVE_MOVIE payload: string}
interface ResetAction { type: typeof RESET}
type ActionTypes = AddMovieAction | RemoveMovieAction | ResetAction
export function addMovie(movie: string): ActionTypes { return { type: ADD_MOVIE, payload: movie, }}
export function removeMovie(movie: string): ActionTypes { return { type: REMOVE_MOVIE, payload: movie, }}
export function reset(): ActionTypes { return { type: RESET, }}
const reducer = (state: State, action: Action) => { switch (action.type) { case ADD_MOVIE: return { movies: [movie, ...state.movies] } case REMOVE_MOVIE: return { movies: state.movie.filter(m => m !== movie) } case RESET: return defaultState default: return state }}
复制代码


没什么特别的,我们声明了状态界面、默认状态、动作、动作创建者以及最后的 Reducer。


在 ReasonML 中也是一样:


type state = {  movies: list(string)};
type action = | AddMovie(string) | RemoveMovie(string) | Reset
let defaultState = { movies: [] }
let reducer = (state) => fun | AddMovie(movie) => { movies: [movie, ...state.movies] } | RemoveMovie(movie) => { movies: state.movies |> List.filter(m => m !== movie) } | Reset => defaultState;
/* No need for additional functions! */let someAction = AddMovie("The End of Evangelion")
复制代码


对,就这些。


让我们看看这里发生了什么。


首先,有一个状态类型声明。


之后是动作 Variant(https://blog.dubenko.dev/typescript-vs-reason/#https://reasonml.github.io/docs/en/variant)类型:


type action =  | AddMovie(string)  | RemoveMovie(string)  | Reset
复制代码


这意味着具有类型 action 的任何变量都可以具有以下值之一:Reset、带有一些字符串值的 AddMovie 和带有一些字符串值的 RemoveMovie。


ReasonML 中的 Variant 是一项非常强大的功能,可让我们以非常简洁的方式定义可以包含值 A 或 B 的类型。是的,TypeScript 有联合类型,但它没有深入集成到语言中,因为 TypeScript 的类型是给 JavaScript 打的补丁;而 Variant 是 ReasonML 语言的重要组成部分,并且与模式匹配等其他语言功能紧密相连。


说到模式匹配,我们来看一下 Reducer。


let reducer = (state) => fun  | AddMovie(movie) => { movies: [movie, ...state.movies] }  | RemoveMovie(movie) => { movies: state.movies |> List.filter(m => m !== movie) }  | Reset => defaultState;
复制代码


我们在这里看到的是一个函数,该函数接受状态作为第一个参数,然后将第二个参数与可能的值匹配。


我们也可以这样写这个函数:


let reducer = (state, action) => {  switch(action) {  | AddMovie(movie) => { movies: [movie, ...state.movies] }  | RemoveMovie(movie) => { movies: state.movies |> List.filter(m => m !== movie) }  | Reset => defaultState;  }}
复制代码


由于匹配参数是 ReasonML 中的常见模式,因此这类函数以较短的格式编写,如前面的代码片段中所示。{ movies: [movie, …state.movies] }这部分看起来与 TypeScript 中的一样,但是这里发生的事情并不相同!在 ReasonML 中,[1,2,3] 不是数组,而是不可变的列表。可以想象它是在语言本身内置的 Immutable.js。在这一部分中我们利用了一个事实,即 Append 操作在 ReasonML 列表中的时间是恒定的!如果你之前用的是 JavaScript 或 TypeScript,你可能会随手写下这种代码,无需太多顾虑,并且可以免费获得性能提升。


现在,让我们看看向 Reducer 添加新动作的操作。在 TypeScript 中是怎么做的呢?首先你要在类型定义中添加一个新动作,然后以动作创建者的形式编写一些样板,当然不要忘了在 Reducer 中实际处理这种情况,一般这一步都容易被忽略。


在 ReasonML 中,第一步是完全相同的,但是后面就都不一样了。在将新动作添加到类型定义后单击保存时,编译器会带你在代码库中慢慢挪动,处理对应的情况。


你会看到这样的警告:


Warning 8: this pattern-matching is not exhaustive.Here is an example of a case that is not matched:Sort
复制代码


这是很好的开发体验。它会指出你要处理新动作的确切位置,并且还会准确告诉你缺少哪种情况。

Null、undefined vs Option

在 TypeScript 中我们需要承受 JavaScript 留下来的负担,也就是用 null 和 undefined 来表示几乎一模一样的事物——毫无意义。


在 ReasonML 中没有这种东西,只有 Option 类型。


ReasonML


type option('a) =  | Some('a)  | None;
复制代码


这是一个大家很熟悉的 Variant 类型。但是它也有一个类型参数’a。这很像其他语言中的泛型,比如说 Option。


来对比更多的代码看看:


interface User {  phone?: number}
interface Form { user?: User}
function getPhone(form: Form): number | undefined { if (form.user === undefined) { return undefined } if (form.user.phone === undefined) { return undefined } return form.user.phone}
复制代码


访问可以为空的属性是最简单的情况之一,闭着眼也能写出来。在 TypeScript 中,我们可以启用严格的 null 检查然后手动检查 undefined 的值来纠正它。


open Belt.Option;
type user = { phone: option(int)};
type form = { user: option(user)};
let getPhone = form => form.user->flatMap(u => u.phone);
复制代码


在 ReasonML 中,我们可以使用内置的 option 类型和 Belt 标准库中的辅助函数,然后就能以标准化方式处理可能为空的值。

带标签的参数

我觉得所有人都会认为带标签的参数功能真是太棒了。可能每个人都必须在某个时候查询函数参数的顺序或含义。不幸的是,TypeScript 中没有带标签的参数。


TypeScript


function makeShadow(x: number, y: number, spread: number, color: string) {  return 0}const shadow = makeShadow(10, 10, 5, "black") /* meh */
复制代码


在 ReasonML 中,你在参数名称前加一个~ 字符,然后它就被标记了。


ReasonML


let makeShadow = (~x: int, ~y: int, ~spread: int, ~color: string) => {  0;}
let shadow = makeShadow(~spread=5, ~x=10, ~y=10, ~color="black")
复制代码


是的,你可以在 TypeScript 中尝试使用对象作为参数来模拟这种做法,但是随后你需要在每个函数调用时分配一个对象 :/


TypeScript


function makeShadow(args: {  x: number  y: number  spread: number  color: number}) {  return 0}
const shadow = makeShadow({ x: 10, y: 10, spread: 5, color: "black" })
复制代码

模块系统

在 TypeScript 中,我们显式导出和导入文件之间的所有内容。


Hello.ts


export const test = "Hello"
复制代码


import { test } from "./Hello.ts"
console.log(test)
复制代码


在 ReasonML 中,每个文件都是一个带有该文件名的模块。


Hello.re


let test = "Hello";
复制代码


Js.log(Hello.test);
复制代码


你可以 open 模块,使内容可用而无需模块名称前缀:


open Hello;
Js.log(test);
复制代码

编译速度

为了对比编译速度,我们来编译 TodoMVC,因为其实现和项目大小都是容易对比的。我们正在测试的是将代码转换为 JavaScript 所花费的时间。没有打包,压缩等操作。[注 1]


TypeScript + React.js


$ time tsc -p jstsc -p js 6.18s user 0.24s system 115% cpu 5.572 total
复制代码


6.18 秒


ReasonML + ReasonReact


$ bsb -clean-world$ time bsb -make-world[18/18] Building src/ReactDOMRe.mlast.d[9/9] Building src/ReactDOMRe.cmj[6/6] Building src/Fetch.mlast.d[3/3] Building src/bs_fetch.cmj[12/12] Building src/Json_encode.mlast.d[6/6] Building src/Json.cmj[7/7] Building src/todomvc/App.mlast.d[3/3] Building src/todomvc/App-ReasonReactExample.cmjbsb -make-world 0.96s user 0.73s system 161% cpu 1.049 total
复制代码


0.96 秒


现在这还包括编译 ReasonML 依赖项的时间,我们还可以测试只编译项目文件的时间:


ReasonML + ReasonReact, only src/


$ bsb -clean$ time bsb -make-worldninja: no work to do.ninja: no work to do.ninja: no work to do.[7/7] Building src/todomvc/App.mlast.d[3/3] Building src/todomvc/App-ReasonReactExample.cmjbsb -make-world 0.33s user 0.27s system 117% cpu 0.512 total
复制代码


0.33 秒


竟然有这么快!


BuckleScript 将安装时、构建时和运行时的性能视为一项重要功能


这是从 BuckleScript 文档中引用的。BuckleScript 是将 ReasonML 转换为 JavaScript 的工具。

目前 TypeScript 胜过 ReasonML 的地方

关于 ReasonML 的内容并不是都那么美好。它是一种相当新的语言……嗯,实际上它不是基于 OCaml 的,后者已经相当老了;但重点在于互联网上的资源仍然不是特别多。与 ReasonML 相比,用谷歌搜索 TypeScript 的问题更容易获得答案。


DefinitelyTyped 类型实在太多了,ReasonML 想要追上来还有很长的路要走。

结语

对于前端开发人员来说,ReasonML 语法应该让人觉得很熟悉,这意味着学习曲线的起点并不那么陡峭(但仍然比 TypeScript 要陡)。ReasonML 充分利用了其他语言和工具(例如 Immutable.js 和 eslint)中最好的东西,并将其带入了语言级别。它并没有试着成为一种完全纯粹的编程语言,你需要的话可以随时退回到突变和命令式编程。它非常快,它的速度是提升开发体验的重点所在。ReasonML 能带来 TypeScript 想要做到的一切(甚至更多),但是前者没有那些 JavaScript 怪癖。你应该试试它!

注释

[1] 编译时间测试平台是一部 MacBook Pro 2015,处理器是 Intel Core i5-5287U @2.90GHZ


TypeScript + React.js 源码(https://github.com/tastejs/todomvc/tree/master/examples/typescript-react)


ReasonML + ReasonReact 源码(https://github.com/reasonml-community/reason-react-example/tree/master/src/todomvc)


原文链接:


https://blog.dubenko.dev/typescript-vs-reason/


2019-12-27 16:291572

评论

发布
暂无评论
发现更多内容

架构成长之路 | 图解分布式共识算法Paxos教会协议

阿里技术

paxos协议 分布式算法

libpq SDK 发送 SQL 和解析结果

KaiwuDB

sql KaiwuDB

Abaqus有限元分析技术应用于卫星强度校核

思茂信息

abaqus abaqus软件 abaqus有限元仿真 有限元仿真技术

瓴羊Quick BI:企业数据分析的利器

夜雨微澜

插件化工程R文件瘦身技术方案 | 京东云技术团队

京东科技开发者

企业号 6 月 PK 榜 R文件 文件瘦身 插件化工程

使用华为云开发者插件一键部署应用到ECS

华为云PaaS服务小智

ide 云计算 插件 华为云

成败关键!一对一直播源码平台搭建需要的条件

山东布谷科技

软件开发 源码搭建 一对一直播源码 直播源码

2023 PlatformCon 平台工程大会回顾

杨振涛

平台工程 内部开发者平台 内部开发者门户 平台工程大会 平台团队

2023开放原子全球开源峰会高峰论坛成功举办

开放原子开源基金会

开源 开放原子全球开源峰会 开放原子 高峰论坛

原点安全携“金融机构消费者个人信息保护解决方案”亮相 2023 中国金融数字化转型发展大会

原点安全

数据安全 金融机构 个人信息保护

低代码开发平台为数智赋能,让开发变得更简单

引迈信息

前端 低代码 JNPF

this 之谜揭底:从浅入深理解 JavaScript 中的 this 关键字(二)

Immerse

深度学习应用篇-推荐系统[12]:经典模型-DeepFM模型、DSSM模型召回排序策略以及和其他模型对比

汀丶人工智能

人工智能 深度学习 推荐系统 搜索算法 6 月 优质更文活动

Junit执行器Runner探索之旅 | 京东云技术团队

京东科技开发者

单元测试 JUnit runner 源码解读 企业号 6 月 PK 榜

直播回顾|走进元服务,携手小强停车探索鸿蒙新流量阵地

HarmonyOS SDK

HMS Core

Spring Boot+Vite+Vue3二手商城

路北路陈

6 月 优质更文活动

深度学习应用篇-推荐系统[11]:推荐系统的组成、场景转化指标(pv点击率,uv点击率,曝光点击率)、用户数据指标等评价指标详解

汀丶人工智能

人工智能 深度学习 推荐系统 搜索推荐 6 月 优质更文活动

理解 与 计算 物联网产品的电池使用寿命

矜辰所致

物联网 低功耗计算 电量计算 6 月 优质更文活动

测试左移

BY林子

软件测试 敏捷测试

【有奖体验】叮!你有一张 3D 卡通头像请查收

Serverless Devs

Serverless 函数计算FC

Wallys launches high-performance Tri-Band solutions/ipq4019+qca9882.

Cindy-wallys

IPQ4019 QCA9882 ipq4029

里氏替换原则究竟如何理解?

磐远

Java 设计模式 设计原则 里氏替换

openEuler 开源汇智赢未来|2023开放原子全球开源峰会 openEuler 论坛成功召开

openEuler

开源 操作系统 openEuler 资讯

蚂蚁集团云原生智能容量技术 KapacityStack 正式开源

TRaaS

GitHub 开源 蚂蚁集团

系统重构实施,百亿级核心交易如何保证准确性?

鲸品堂

计费模式 对账系统 企业号 6 月 PK 榜

安全问题我们需要重视,立刻升级fastjson2

源字节1号

开源 软件开发 后端开发 小程序开发

DR6018 Routerboard: Unleashing Long-Distance Transfer Capabilities up 23Km!

Cindy-wallys

IPQ6010 ipq6018

BeautifulPrompt:PAI推出自研Prompt美化器,赋能AIGC一键出美图

阿里云大数据AI技术

人工智能 AIGC Prompt 企业号 6 月 PK 榜

如果 ReasonML 没有 JavaScript 的那些怪癖,你该不该试试它?_文化 & 方法_Oleksandr Dubenko_InfoQ精选文章