Published on

ReasonML 使用一週隨筆

Authors
  • avatar
    Name
    米K朗基羅
    Twitter
    @kvzl_

Update: 現在改叫 ReScript 囉

前言

有一大部分是 ReasonML 學習筆記,想看真.心得的話可以直接跳到使用心得


語言特性

簡單列出幾點相較於 JavaScript/TypeScript,個人認為比較特別的:

基於 OCaml

ReasonML 是基於 OCaml 修改而來的語言,語義和 OCaml 差不多,但也繼承了一些 OCaml 的歷史包袱。

除了 OCaml 本身的特色以外,還做了一些調整:

  • 融入很多類似 JavaScript 的語法
  • 支援 JSX,定義 React DOM 時方便很多

型別系統

個人感覺比 TypeScript 更嚴謹一點,但大致上還是可以看到許多 TypeScript 也有的特性,像是:

  • Variant 類似 Enum
  • Record 類似 Interface
  • Tuple 類似固定長度的 typed array
  • null 零容忍,TypeScript 也提供了 strictNullChecks 選項
  • ...等等

Pattern Matching

類似 switch/case,但能夠直接 match 型別的 tag,如果在 TS 可能就要寫一堆 if-else 搭配 typeofinstance of

取自官方文件的例子:

type account =
  | None
  | Instagram(string)
  | Facebook(string, int);

let myAccount = Facebook("Josh", 26);
let friendAccount = Instagram("Jenny");

內建自動 Curry 及 Pipe Operator

可以從以下範例秒懂:

let add = (x, y) => x + y;
let addFive = add(5);

這段程式碼的語法和 JavaScript 完全相同,但執行結果則是完全不同。在 JavaScript 我們可能需要將 add 定義為 let add = (x) => (y) => x + y ,也就是 currying 的寫法;但這件事情在 ReasonML 中,編譯器會自動幫我們做好,我們就不必額外定義許多 curried function 了。

有了這個特性,搭配起 Pipe Operator 更是輕鬆自如!舉個簡單的 🌰:

let add = (x, y) => x + y;
let multiply = (x, y) => x * y;

let result = n => n
	|> multiply(n)
	|> add(3)

相關資源

快速了解一下 ReasonML 的 ecosystem:

編譯及開發
  • BuckleScript 用來把 OCaml/ReasonML 編譯為 JavaScript
    • ReasonML 完全可以用 OCaml 改寫,反之亦同
  • 能夠透過 Reason Native 編譯為原生的 OCaml 程式
  • reason-cli
    • 包含了格式化工具 refmt 及 REPL rtop
套件管理
  • 還是 npm/yarn
  • esy
前端
工具
進階主題
參考資料

剛開始對語法和一些規範還不太熟悉,除了官方文件以外還可以參考:


遇到的問題

這裡會將遇到的問題簡單分為:ReasonML 語言本身,以及 ReasonReact 的問題

ReasonML

怎麼 import CSS,甚至是 SCSS

可以透過 %raw來寫原始的 JavaScript 語法來達成:

[%raw {|require('./styles/app.scss')|}]

無法用 .name 取得 Record 中的 name 屬性

原本這樣寫會產生錯誤:

let obj = {name: 'Kevin'}
Js.log(obj.name) /* failed */

原來是必須要給 obj 型別定義才有辦法取得屬性:

type test = {name: string}
let obj: test = {name: 'Kevin'}
Js.log(obj.name) /* ok */

|>-> 有什麼差別

  • |>會將參數插入在最右邊,又稱作 Pipe forward

  • ->會將參數插入在最左邊,又稱作 Pipe first

參考資料

怎麼定義不合法識別字的 Object key

type t;
[@bs.get_index] external ff: (t, [@bs.as "xx"] _) => int = "";

let u = x => ff(x);

[@bs.get] external ff2: t => int = "xx";

let u2 = x => ff2(x);

轉換成 JavaScript

function u(x) {
  return x['xx']
}

function u2(x) {
  return x.xx
}

參考資料

ReasonReact

能不能像 vue 一樣用 object 來控制 class

試著寫了一個小小的 helper function:

let concatElm = elm => Js.Array.concat([|elm|]);

let join = Js.Array.joinWith;

let c = cls =>
  cls
  |> Js.Array.reduce(
       (result, (prop, value)) =>
         value == true ? concatElm(prop, result) : result,
       [||],
     )
  |> join(" ");

型別如下:

let c: Js.Array.t(('a, bool)) => string

使用方法:

let taskClassName =
	c([|
		("task", true),
		("task--active", active),
		("task--editing", editing),
	|]);

如何引入 SVG

方法 1:使用 svg-inline-loader 將 svg 內容轉成字串,再插入到 DOM 的 innerHTML

[@bs.module "./image.svg"]
external image : string = "default";

<div dangerouslySetInnerHTML={{ "__html": image }} />

方法 2:使用 file-loader產生 url,再透過 <img>引入

let image = [@raw {|require("./image.svg")|}];

<img src=image>

參考資料

如何 render 出 JSX 元素列表

方法 1:比較簡單

let thingDivs =
  [|"A", "B"|]
  |> Array.map(thing => <div> {React.string(thing)} </div>)
  |> React.array;

<div> thingDivs </div>;

方法 2:稍微複雜,但可以取得 index

<ul>
  {[|"foo", "bar", "baz"|]
  ->Belt.Array.mapWithIndex((key, x) => {
    let key = string_of_int(key);
    <li key>{React.string(x)}</li>;
  })
  ->React.array}
</ul>

參考資料

如何取得 event.target.value

ReactEvent.Form.target(event)##value;

或是

event->ReactEvent.Form.target##value;

參考資料

React Hooks 相關


使用心得

比較好與壞

喜歡的地方:

  • 比起其他真的很嚴格的函數式語言,ReasonML 對用慣 JavaScript/TypeScript 的人來說親切很多,我想這是因為:
    • 比較沒那麼嚴格、複雜
    • 引入很多 JavaScript 語法
  • 型別系統夠強大,基本的資料型態定義好以後,撰寫函數的過程中很少需要再自行標注型別
  • 編譯速度很快

有點可惜的地方:

  • 雖然編譯目標是 JavaScript,但為了和 OCaml 相容,還是必須設計出一些 JavaScript 專屬的 API,用起來感覺不是很自然
    • 像是定義 Object 的方式(12)、定義 Array 的方式(12
  • 雖然不夠完美,但 TypeScript 的型別也算是很夠用了(也有 fp-ts 這樣的工具可以用)。如果沒有一定要寫得很 functional,其實 TypeScript 還是可以用得很開心
    • 要更 functional 的話,Elm 或 PureScript 之類的可能也不錯,至少不像 ReasonML 有一些包袱
  • 行尾要加分號 😔

比起 TypeScript

雖然沒有認真學過 Type Theory,但還是能明顯感受到一些 ReasonML 比 TypeScript 型別更強大的原因:

  • operator 不會像 JavaScript 那樣 overloading,比如說像相加就有分 ++.++(雖然不太優雅就是了)
  • 沒有 null/undefined,空值都在 type-level 用 option 解決掉了
    • 即使 TypeScript 也有 optional,還是要面對無處不在的 null/undefined

更多和 TypeScript 的比較可以參考: TypeScript vs ReasonML – A Comparison | Oleksandr Dubenko


小結

綜合以上想法,自己覺得使用體驗還是蠻不錯的。