如果 ReasonML 没有 JavaScript 的那些怪癖,你该不该试试它?-深蓝源码网


时间: 2020-09-03 00:08:26 人气: 2274 评论: 0

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

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

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

声明一个变量

让我们从变量声明开始:

ReasonML
let a = "Hi";
TypeScript
const 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;

接口,记录

TypeScript
interface 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
						 技术沙龙 教程文章 热点综合					

评论