- Published on
ReasonML 使用一週隨筆
- Authors
- Name
- 米K朗基羅
- @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 搭配 typeof
或 instance 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
及 REPLrtop
- 包含了格式化工具
套件管理
- 還是 npm/yarn
- esy
前端
- ReasonReact
- 建議使用 React Hooks API
- Redux 已經做在 ReasonML 語言層,所以不需要
- 內建陽春 router
工具
- rationale
- 類似 Ramda
- 定義很多類 Haskell 的 operator
- Immutable.re
- reason-loadable
- 動態載入 ReasonReact component
- 底層透過 bs-dynamic-import 達到動態載入的效果
進階主題
參考資料
剛開始對語法和一些規範還不太熟悉,除了官方文件以外還可以參考:
遇到的問題
這裡會將遇到的問題簡單分為: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,用起來感覺不是很自然
- 雖然不夠完美,但 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
小結
綜合以上想法,自己覺得使用體驗還是蠻不錯的。