写点什么

如果 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:291809

评论

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

你不知道的SSD那些事

焱融科技

分布式 存储 SSD nvme

patroni 通过服务启动报错

hobson

数据库 高可用 AntDB

Go语言分布式系统配置治理

田晓亮

微服务

杂谈-JSONP探索

卡尔

Java jsonp

原创 | 使用JUnit、AssertJ和Mockito编写单元测试和实践TDD (十三)编写测试-生命周期方法

编程道与术

Java 编程 TDD 单元测试 JUnit

Vue生态篇(一)

shirley

Java Vue

每个人都是领导者的工程团队

hongfei

工程能力 项目实践

【Java 25周年有奖征文获奖名单公布!!!】关于Java,你最想赞扬、吐槽、期待的变化是什么?

InfoQ写作社区官方

写作平台 Java25周年 热门活动

情绪的力量:如何使用情绪来达成目标

董一凡

情绪

程序员修炼的务实哲学

博文视点Broadview

程序员 软件 编程思维 工程师 编程之路

数据产品经理实战-数据门户搭建(上)

第519区

数据中台 开发数据

# LeetCode 863. All Nodes Distance K in Binary Tree

liu_liu

算法 LeetCode

奈学:传授“带权重的负载均衡实现算法”独家设计思路

奈学教育

分布式

开源分布式文件系统大检阅

焱融科技

开源 sds 存储 焱融科技 文件存储

Redis持久化了解一波!

不才陈某

redis 程序员 后端

ARTS - Week Two

shepherd

js algorithm

知识也会生宝宝?

史方远

个人成长 随笔杂谈

ARTS 第二周打卡

陈文昕

从 0 到 1 搭建技术中台之发布系统实践:集泳道、灰度、四端和多区域于一体的设计与权衡

伴鱼技术团队

架构 系统设计 系统架构 系统性思考 架构设计

互联网时代的界限管理

非著名程序员

程序员 职场 提升认知 界限管理

Vue生态篇(二)

shirley

Vue

这是一个测试文档

Geek_073cad

我的 Windows 利器

玄兴梦影

工具 Win

我为什么开始技术写作?

架构精进之路

技术创作

线程池续:你必须要知道的线程池submit()实现原理之FutureTask!

一枝花算不算浪漫

源码分析 并发编程

# LeetCode 215. Kth Largest Element in an Array

liu_liu

算法 LeetCode

MySQL的各种日志

超超不会飞

MySQL

美团可能会强势涉足 ToB

罗小布

创业 互联网巨头 深度思考 互联网

我常用的浏览器插件

彭宏豪95

chrome 效率工具 浏览器 插件

Python 自动化办公之"你还在手动操作“文件”或“文件夹”吗?"

JackTian

Python 自动化

一个人,沿着童年的路究竟可以走多远?

zhoo299

童年 NASA 航天

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