9
8

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

ReasonMLを使う理由…ではなく入門

Last updated at Posted at 2019-12-28

ReasonMLを使う理由は、まださほど使ったことないのでわかりません。

  • 型がある
  • null許容型が無い
  • 他言語と相互運用できる

という情報は、最初に手に入りました。

githubのスター数などを見る限り、なんとなくですが海外では結構流行っているように見えます。

TypeScriptやFlowの型定義生成もできますし、公開されているExampleの中にはGatsbyJSのサイトに組み込むような部分的な利用もできたり、ライブラリの作成用途としても活躍しそうです。

公式サイトは、ところどころ未翻訳ですが、日本語記事もあります。

以降も、章ごとに【公式で見る】リンクは付けておくので、ご活用ください。

自分自身も入門しながら、調べながら説明していくつもりです。

(※モジュールの項について、一部理解しきれず曖昧な記事になってます。分かり次第、更新する予定です。)

おかしいところがあったら、教えてください。

インストール 【公式で見る】

CLI導入initの流れは公式にあるので、せっかくなのでバニラから始めてみましょう。

まずは、ディレクトリを作成し、bs-platformをローカルインストールします。

基本的に、みんな大好きnpmです。

(Yarnももちろん可。開発はFacebookが始めたらしいですが、Yarn推奨というわけでもないです。)

mkdir reason-sample

cd reason-sample

npm init -y

npm install --save-dev bs-platform

これで、bsbコマンドが使えるようになります。

これは、BuckleScript(バックルスクリプト)というReasonMLをJavaScriptにコンパイルするツールです。

BuckleScriptは、

  • ReasonML -> JavaScript
  • OCaml -> JavaScript

という2種の変換をサポートするReasonMLの姉妹(おそらく姉)です。

ということで、

bsb = Buckle Script Build ツールかと。

Hello World

bsbでビルドするには、bsconfig.jsonが必要です。

最低限の構成です。プロジェクトルートに作ってください。

{
  "name": "reason-sample",
  "namespace": true,
  "sources": ["src"]
}

nameとソース配置ディレクトリを指定するだけです。

srcディレクトリにindex.reというファイルを作ります。

mkdir src
touch index.re

index.reを編集します。

Js.log("hello world");

Js.logは、console.logにコンパイルされました。

そして、

npx bsb -make-world

でビルドが始まり、libディレクトリが作成されます。

lib/js/src/index.jsがコンパイル後のJavaScriptです。

node lib/js/src/index.js

でHello worldは完了です。

【補足1】BuckleScriptについて

ここまで、ReasonMLというより、BuckleScriptのHello Worldみたいになってしまいました。

よりbsbコマンドについて知りたい方は、こちらを参照してください。

bsconfig.jsonの書き方は、こちらです。

あまり、翻訳は進んでないようですが、右上の「Aあ」で日本語に切り替えもできます。

翻訳に参加するのもありだと思います。

【補足2】開発環境

エディタープラグイン

⇒ 好きなIDEのものをお使いください。

REPL環境

⇒ 対話型実行環境です。

パフォーマンス計測

⇒ パフォーマンス計測して、共有できます。

bsb-native

⇒ OCamlにコンパイルできるそうです。ReasonMLをOCamlランタイムで動かしたい人向けですかね。何か面白いことができそうな予感が漂ってます。

ちょっとした準備

ここから何度も実行するので、package.jsonscripts

  "scripts": {
    "otameshi": "bsb -make-world && node lib/js/src/index.js",
  },

という風に、コンパイル+実行を定義しておくと便利です。

npm run otameshiでお試し実行できます。

ここからが構文などの入門になります。

let 束縛 【公式で見る】

ReasonMLで、宣言は「let」です。

let hoge = "hoge";

JavaScriptのconstみたいにイミュータブルです。

変数では無く「let 束縛」です。

let hoge = "hoge";

// エラー!
hoge = "huga";

でも、再宣言はできます。

let hoge = "hoge";

// これはOK
let hoge = "huga";

スコープは、{}の中だけです。いつもどおりです。

{
  let hoge = "hoge";
}

// hogeがみつからないよって言われる
print_endline(hoge);

さらっと出てきたprint_endline()は、console.logみたいなもんです。

【コーヒーブレーク】こんな書き方ができる

こんな感じの書き方ができます。

++が文字列結合です。

let message = {
  let a = "hello";
  let b = "world";

  a ++ " " ++ b;
}

print_endline(message);

変換されたJavaScriptを見てみましょう。

// Generated by BUCKLESCRIPT, PLEASE EDIT WITH CARE
'use strict';


var message = "hello world";

console.log(message);

exports.message = message;
/*  Not a pure module */

「Not a pure module」と教えてくれるのも気になりますが置いといて、

まず、スコープ{}内の式の結果は、コンパイルされて"hello world"という文字列になってます。

print_endline()は、console.logですね。

最後に、exports.message = message

これはCommonJSの記法で、別のモジュールからmessageを参照できるようにするコードですね。

どうやらReasonMLでのトップレベルスコープの「let 束縛」は、exportされるようです。

型! 【公式で見る】

まずは、型推論です。

let num = 10;
print_endline(num);

// numは数字だよ!って言われる
let num = "hoge";

10は「Int」型なので、後の"hoge"という文字列型は再宣言できません。

明示的な型宣言も可能です。

:+ 型で、束縛の型を指定できます。

let num: int = 10;
print_endline(string_of_int(num));

// 型が違うので怒られる
let num: int = "10";

ちゃっかりstring_of_int()を使いましたが、intからstringに変換する関数です。

print_endline()は、文字列を表示するので、型変換をしないとエラーになります。

これは、公式に出ているadd関数の宣言です。

let add = (x: int, y: int) : int => x + y;

このように戻り値の方も指定できるようです。

公式にはこんな感じの呪文の例も書かれています。

let drawCircle = (~radius as r: int) : unit => ...;

あとで「関数」のところで説明できるようになるので、楽しみにしておくか、

間は飛ばしてラベル付き引数を見てください。

【コーヒーブレーク】使われなければ再宣言できる

冒頭のコードを下記のようにすると、コンパイルが通ります。

let num = 10;

let num = "hoge";

コンパイル後のJavaScriptはこちらです。

// Generated by BUCKLESCRIPT, PLEASE EDIT WITH CARE
'use strict';


var num = "hoge";

exports.num = num;
/* No side effect */

ちゃっかり「No side effect」って教えてくれるのも気になりますが置いといて、

そもそもlet num = 10に対する部分は消えてます。

使われてないコードは、コンパイル後は消えているということです。

一応、Warningは出るので気づけます。

未使用のコードを削減することで、ファイルのサイズを抑えてくれているのですね。

文字列 【公式で見る】

文字列は""で囲みます。

エスケープは\

文字列結合は、++

でできます。

文字列内は改行ができます。

ただし、Unicodeをサポートしていませんので、ひらがな等を含めたい場合は、その為の書き方があります。後で説明します。

let string = "hello 
world";

print_endline(string);

{||}というで囲むと、中がすべてエスケープされます。

let greetingAndOneSlash = {|Hello
World
\
Hehe...
|};

print_endline(greetingAndOneSlash);

JavaScriptにコンパイルされると、

var greetingAndOneSlash = "Hello\nWorld\n\\\nHehe...\n";

という風に、エスケープされていることがわかります。

{js||js}で囲むことでUnicodeが扱えます。

let code = {js||js};

print_endline(code);

{j||j}で囲むことで置き換えができます。Unicodeもサポートしてます。

let hoge = "world";
let helloWorld = {j|hello, $hoge|j};

print_endline(helloWorld);

$hoge"world"に置き換わります。

真偽値 【公式で見る】

true / falseです。いつも通りです。

演算子もほぼいつも通りです。

true && false // false
true || false // true
!true // false
1 > 1 // false
1 >= 1 // true
1 < 1 // false
1 <= 1 // true
(1, 2) == (1, 2) // true
(1, 2) === (1, 2) // false
(1, 2) != (1, 2) // false
(1, 2) !== (1, 2) // true

あとにも書きますが、(1, 2)というのは、タプルです。

==は構造的な比較なので、内部の値が一致しているタプル同士はイコールと判定されます。

===は参照の比較なので、別のインスタンスとしてノットイコールと判定されます。

お分かりの通り、==は構造的な比較で、ネストされたデータについて再帰的に比較を行う為、パフォーマンスに影響します

注意して使ってください。

IntとFloat 【公式で見る】

Intは、32-bitの整数型です。

演算子もいつもどおりです。

let hoge:int = 3 + 2 * 4 - 1 / 2;

print_endline(string_of_int(hoge));

計算結果は、10.5ですが出力は11です。

let hoge:int = 3 + 2 * 4 - 1 / 2 * 2;

print_endline(string_of_int(hoge));

これで、10になるかと思いきや11でした。

バグなのか分からないので、今度Issue挙げてみます。

Intで割り算は避けた方がいいかもです。

Floatは、小数が扱えます。

演算子は、+. -. *. /.というように.が後につきます。

let hoge:float = 3.1 +. 2.2 *. 4.3 -. 1.4 /. 2.5 *. 2.6;

print_endline(string_of_float(hoge));

こっちは計算合ってました。

タプル 【公式で見る】

()で囲むとタプル。

// 型推論
let tuple1 = (1, 2.0, "3", '4', true);

// 型指定
let tuple2: (int, string) = (12, "34");

// 値を取り出す
let (_, y, z) = (1, 2, 3);

print_endline(string_of_int(y + z));

公式にもありますが、タプルは

  • 不変
  • 順序がある
  • 作成時にサイズが決まる
  • いろんな型をまぜこぜOK

らしいです。

パターンマッチングに使う方法が公式で紹介されています。

switchとか関数はあとでやりますが、なんとなく分かるかと思います。

コンパイル後のJavaScriptコードを見たら、タプルはただの配列でした。

// (1,2,3)
[1,2,3]

不変な配列みたいな感じですね。

Record 【公式で見る】

Recordは、JavaScriptのオブジェクトに近いようですが、早くて不変で型が持てます。

// person型
type person = {
  age: int,
  name: string
};

// インスタンス
let me:person = {
  age: 5,
  name: "zzzzz"
};

let age = string_of_int(me.age);
let name = me.name;

let result = {j|$nameの精神年齢は$ageです|j};

print_endline(result);

型の宣言は必須です。

let me = {
  // ageなんてプロパティは無いと怒られる
  age: 5,
  name: "zzzzz"
};

プロパティの型が違えば、コンパイルエラーです。

// person型
type person = {
  age: int,
  name: string
};

// ageの型がダメ
let me:person = {
  age: "5",
  name: "zzzzz"
};

インスタンスの構造が型に即して無くてもエラーです。

// person型
type person = {
  age: int,
  name: string
};

// 勝手にプロパティ追加しちゃだめ
let me:person = {
  age: 5,
  name: "zzzzz",
  extend: "hoge"
};

Recordは不変ですが、...を新しい束縛を作ることができます。

// person型
type person = {
  age: int,
  name: string
};

// インスタンス
let me = {
  age: 5,
  name: "zzzzz"
};

// 新しいインスタンス(1歳成長しました)
let grown = {
  ...me,
  age: me.age + 1
}

let age = string_of_int(grown.age);
let name = grown.name;

let result = {j|$nameの精神年齢は$ageです|j};

print_endline(result);

省略記法も使うことができます。

type address = {
  zip: string,
  country: string,
  state: string,
  city: string,
  street: string,
  room: string
}

type person = {
  age: int,
  name: string,
  // address: addressと一緒
  address
};

let zip = "111-1111";

let me = {
  age: 5,
  name: "zzzzz",
  address: {
    // zip: zipと一緒
    zip,
    country: "Japan",
    state: "Tokyo",
    city: "Shubuya",
    street: "oooooo",
    room: "102"
  }
};

【コーヒーブレーク】型は結局どうなるか

コンパイル後のJavaScriptを見てみましょう。

最初の例をコンパイルした結果です。

// Generated by BUCKLESCRIPT, PLEASE EDIT WITH CARE
'use strict';


var age = String(5);

var name = "zzzzz";

var result = "" + (String(name) + ("の精神年齢は" + (String(age) + "です")));

console.log(result);

var me = {
  age: 5,
  name: "zzzzz"
};

exports.me = me;
exports.age = age;
exports.name = name;
exports.result = result;
/* age Not a pure module */

Classもないし、チェック処理もないです。

ほんとにただオブジェクト作ってるだけです。

当たり前ですが、あくまで静的解析でバグを検知してくれているだけなので、

ランタイム上で動くコードにReasonMLの型の概念はありません。

おかげでコンパイル時に型チェックは入りますが、コンパイル後のコードは軽量で高速です。

ありがたいですね。

Variant! 【公式で見る】

型!の時とおなじように、「!」がついています。

それだけReasonMLにとって大事な機能ということらしいです。

ReasonMLのデータ構造における宝石の王冠とも表現されるVariantとはなんなのでしょうか?

とりあえずコードを見た方が早いです。

type animal =
  | Dog
  | Cat
  | Hamaguchi;

let ilove = Dog;

let cried = switch ilove {
| Dog => {js|わん|js}
| Cat => {js|なあぁ|js}
| Hamaguchi => {js|気合いだ|js}
};

print_endline(cried);

まず、animal型は、「Dog・Cat・Hamaguchi」のどれかであると宣言します。

このとき、Dogというように大文字で始まる必要があります。

(言ってませんでしたが、型は小文字始まりです)

これらはコンストラクターと呼ばれます。

私は犬が好きなので、let ilove = Dog;とします。

そして、その値をswitchにかけると、中でマッチングされ、結果が返ります。

ここでは、Dogなので「わん!」と表示されます。

animalという型に「さまざまな」コンストラクターを持てるから、Variantなんでしょうか。

コンストラクタは引数を持てます。

type animal =
  | Dog(int)
  | Cat(int)
  | Hamaguchi;

let ilove = Dog(3);

let description = switch ilove {
| Dog(age) => {j|$age歳の犬です|j}
| Cat(age) => {j|$age歳の猫です|j}
| Hamaguchi => {js|気合いだ|js}
};

print_endline(description);

犬猫の情報として、年が指定できるようになり、switchの際は指定した年を参照できます。

ちなみにコンパイル後は、if文すらないです。

気になる方は、実際にコンパイルしてみてください。

optionlist

標準ライブラリには、2つのVariantsが事前に用意されています。

**option**は、Null許容を表現できます。

というのも、ReasonMLの型は全て非Null許容なので、値が入らないことを示すためにNoneを用いることができます。

type nullableInt = None | Some(int);

let numOrNull:nullableInt = None;

let result =
  switch numOrNull {
  | None => 0
  | Some(num) => num
  };

print_endline(string_of_int(result))

None => は、numOrNullがNoneの場合にマッチします。

Some(num) => は値が入っている場合にマッチします。

**list**は、ある任意の型aの値を空か1つ以上保持する。。。

つまりリストを表現できます。

type numberList = list(int);

let huga:numberList = [1, 2];

let result =
  switch huga {
  | [] => {js|空ですよ|js}
  | [a, b, ...rest] => string_of_int(a + b) ++ {js|ですよ|js}
  };

print_endline(result);

[] => の時は、リストが空の場合にマッチします。

[a, b, ...rest] => は、空でないリストにマッチし、一番目の値はa、二番目の値はbに入り、残りの要素はrest入ります。

nullとundefined、そしてOption 【公式で見る】

先ほども紹介したoptionを使って、null許容を表現できます。

電話番号は、電話を持っている人しかもっていないので、電話番号無し=Noneの場合もあるとします。

let personHasAPhone = true;

let phoneNumber =
  if (personHasAPhone) {
    Some("090-aaaa-bbbb")
  } else {
    None
  };

let result =
  switch phoneNumber {
  | None => ""
  | Some(number) => number
  };

print_endline(result);

このようにswitchで必ずNoneをハンドリングさせることで、意図しないバグの発生確率を下げることができます。

NoneはReasonML内での識別子ですが、JavaScript上ではundefinedになります。

Someコンストラクタは、Some(Some(x))のようにSomeを入れ子にできますが、その結果をそのままJavaScriptにコンパイルすると、

Some(None)Js_primitive.some(undefined);のような意図しないコンパイル結果が出る可能性があるので避けましょう。

また、サードパーティなどのJavaScript側から渡された値に、optionのような多態性のある型を割り当てるのもNGです。

intなどの具体的な型を割り当てましょう。

これについては、型システムは実行時のバリデーションを含まないので、ハンドリングできると勘違いしてしまうリスクを防いでいると勝手に思ってます。

また先ほど書いたようにNoneundefinedです。nullは含みません。

なので、JavaScriptから渡された値について「null or undefined」を判定する場合は、Js.Nullableモジュールを使うべきです。

結論的には、JavaScript側との相互運用において、Some(None)をJS側に出したり、JS側から貰った値をoptionとして扱おうとすると、ハマるからやめといてね。。。ってことになりそうです。

間違ってたら誰か教えてください。すいません。

Js.Nullableモジュールについては、JavaScriptとの相互運用について記載するときに、説明しようと思います。

リストと配列

ReasonMLには、リストと配列があり、それぞれ別物として扱われるようです。

リスト

特徴として、

  • 値は全部同じ型(タプルみたいに型まぜこぜはダメ)
  • 不変
  • 先頭への値の追加が高速

となります。

[]でリストの宣言ができます。

let list = [2, 3, 4];

let newList = [1, ...list];

switch newList {
| [] => print_endline("It is empty...")
| [a, ...rest] => print_endline("value at first index is " ++ string_of_int(a))
};

[1, ...list]のようにして、新しい束縛の宣言とともに、先頭に値を追加しています。

先頭以外のインデックスへの差し込みは、計算量が増える(O(n))ので、気を付けてください。

(ちなみに先頭への追加はO(1)

**「値の取り出し」**はswitchで行います。

リストへのアクセスでswitchを使う機会は多いです。

【補足】将来的に、先頭以外のインデックスへの挿入に対してもパフォーマンスを保証するリストを追加する可能性はあるらしいです。

配列

  • 変更可能
  • ランダムアクセス・更新が高速
  • 固定サイズ

[||]で配列の宣言ができます。

let array = [|1, 2, 4|];

let before = array[2];

array[2] = 3;

let after = array[2];

print_endline({j|$beforeから$afterに変更しました|j})

固定サイズで、中身を頻繁に変更する場合は「配列」を使いましょう。

関数 【公式で見る】

やっとこさ出てきた関数ですが、公式でも「ここまで関数を説明しませんでした。信じられますか?」と書いてます。(笑)

真面目な話、ここまでに紹介した機能を駆使すれば大抵の処理は書けそうですし、ここでの「関数」はある意味で可読性を落とす原因になるようにも思えます。

なので、ライブラリの関数のラッパーとか、四捨五入みたいなちょっとしたユーティリティなどの「中身知らない系」の処理作成に向いてそうです。

それはさておき、

構文は、JavaScriptのアロー関数に似ています。

// 基本的な書き方
let greet = (name) => "Hello " ++ name;

// 実行
print_endline(greet("world"));

// 複数の引数
let add = (x, y, z) => x + y + z;

// ブロック
let greetMore = (name) => {
  let part1 = "Hello";
  part1 ++ " " ++ name
};

引数無しの関数

引数無しの関数は、ReasonMLでは「副作用」をもたらすものとして使われます。

/* receive & destructure the unit argument */
let logSomething = () => {
  print_endline("hello");
  print_endline("world")
};

/* call the function with the value of type unit */
logSomething();

単純に、() => ...と書くわけですが、ReasonMLにおいて()unitとしての「値」として扱われるそうです。

()unitとして可能な唯一の値らしいです。

不便さと裏腹に、都合の悪い書き方として、あえて()を書かせる思想?みたいです。

副作用を記載する際は、このように「引数無しの関数」を使いましょう。

ラベル付き引数

引数に名前をつけることができ、実行する際に名前付きで指定できます。

let concat = (~a, ~b) => string_of_int(a) ++ b;

print_endline(concat(~a=2, ~b="3"));

~a~a as aの省略記法なので、

let concat = (~a as a, ~b as b) => string_of_int(a) ++ b;

print_endline(concat(~a=2, ~b="3"));

とおなじで、別名を付けることもでき、

let concat = (~a as zen, ~b as go) => string_of_int(zen) ++ go;

print_endline(concat(~a=2, ~b="3"));

関数のスコープ内での引数のエイリアスを定義できます。

カリー化

ReasonMLは、パフォーマンス劣化なく、自動でカリー化を行っています。

let add = (a, b) => a + b;

let add = (a) => (b) => a + b;

で、addの中身は一緒です。

つまり、先に書いたadd関数に引数を1個しか渡さなかった場合は、関数((b) => a + b)が返ってくるということです。

let add = (a, b) => a ++ b;

// output: 23
print_endline(add("2", "3"));

// output: 23
print_endline(add("2")("3"));

オプションの引数

引数を任意にするには、引数に=?を付けるだけです。

これにより引数がoption型になり、NoneまたはSome('a)として扱えます。

'aは任意の型を指します。

let add = (~a, ~b=?, ()) => {
  let num = switch b {
  | None => "0"
  | Some(num) => string_of_int(num)
  };

  a ++ num
}

print_endline(add(~a="2", ()));

~bは指定されていないので、Noneとなり、"20"が出力されます。

引数の最後に、unit()を付ける理由を説明します。

先ほどのカリー化の説明をもとにすると、

let add = (~a, ~b=?) => ...

という関数に対して

add(~a)

は、

(~b=?) => ...

となりますよね。

しかし、引数bは任意なので、bを指定しないで実行したいのか、カリー化したいだけなのか判断がつきません。

下記のコードはコンパイルエラーとなり、bを指定するよう怒られます。

let add = (~a, ~b=?) => ...

print_endline(add(~a="2"));

そこで、OCamlにおける経験則として、引数にunit()を配置することをとすることで、

その位置までの引数を無視する、つまりNoneを指定して関数を実行することを明示できます。

よって、こんな風にbNoneが指定された状態の関数を作りだし、最後にcを指定して実行できます。

let add = (~a, ~b=?, (), ~c=?) => {
  a ++ switch b {
  | None => "0"
  | Some(num) => string_of_int(num)
  } ++ switch c {
  | None => "0"
  | Some(num) => string_of_int(num)
  };
}

let add2 = add(~a="2", ());

print_endline(add2(~c=3));

こんなまどろっこしいこと普通はやらなそうですが、例えばNPMライブラリとして公開する場合には、

exports.addの引数を一部束縛した状態の関数として、exports.add2を公開できます。

こういった仕様があることを知っておくに越したことはなさそうですね。

Explicitly Passed Optional

オプショナルな引数を、別の関数に渡すときは、どういう風に書くことになるでしょう。

let concat = (~a, ~b=?, ()) => a ++ switch b {
| None => "0"
| Some(num) => string_of_int(num)
};

let add = (~a, ~b=?, ()) => switch b {
| None => concat(~a=a, ())
| Some(num) => concat(~a=a, ~b=num, ())
};

print_endline(add(~a="1", ()));

よく見てもらうと、add関数がやっていることは、引数bがNoneかどうか判断し、concat関数に対して指定するかしないかを切り替えているだけです。

こうした冗長な記述に対する省略記法があります。

let concat = (~a, ~b=?, ()) => a ++ switch b {
| None => "0"
| Some(num) => string_of_int(num)
};

// このように省略できる
let add = (~a, ~b=?, ()) => concat(~a, ~b=?b, ());

print_endline(add(~a="1", ()));

関数を呼び出す際の引数の指定で、=?を使いoption型の値を指定することで、switch + None + Someのパターンマッチングに対する省略記法となります。

デフォルト値の指定

任意の引数に対するデフォルト値の指定ができます。

let add = (~a, ~b=0, ()) => a ++ string_of_int(b);

print_endline(add(~a="1", ()));

bは指定されない場合、0になり、intとして扱われます。

再帰的関数

基本的に、let a = () => a()のような自身への参照の仕方はできないのですが、

recをつかうことで、可能になります。

let rec floorSum = (a) => switch a {
| 0 => a
| n => a + floorSum(n - 1)
};

print_endline(string_of_int(floorSum(3)));

相互で呼び出される再帰的関数

a()の中でb()を呼び、b()の中でa()が呼ばれるパターンです。

recandを使います。

let rec callSecond = () => callFirst()
and callFirst = () => callSecond();

良い具体例が浮かばず、公式をパクりました。すいません。

引数の型や、関数の型(インターフェース)を指定する

ここまでで説明しなかったので、一応書いておきます。

// 引数の型の指定
let add = (x: int, y: int) => x + y;

// 関数の型=インターフェースの指定
let add: int => int => int = (x, y)=> x + y;

print_endline(string_of_int(add(3, 1)));

【コーヒーブレーク】Intが戻り値の関数

一番最後の例のコンパイル結果を見てみましょう。

// Generated by BUCKLESCRIPT, PLEASE EDIT WITH CARE
'use strict';


function add(x, y) {
  return x + y | 0;
}

console.log(String(4));

exports.add = add;
/*  Not a pure module */

x + y | 0ってなんぞ?って思いませんか?

これは、戻り値がNaNになるのを防止しているのだと思います。

ReasonMLで作ったIntが戻り値の関数は、| 0が付くのかもしれません。

引数にnullとかが渡るとエラーではなく「0」になるとすると、実行時例外は防げるものの、意図しない動作になる可能性もありますよね。

もちろん完全にReason Worldの中だけなら問題ないのですが、ReasonMLで作りNPMで外部公開したライブラリを使ってもらって、意図しない挙動になるのは困ります。

可用性の観点ではもちろん良いですが、綿密な計算を行うシステムに多少不向きであるのはJavaScriptと同様ですね。

一応、知っておくべきでしょう。

If-Else 【公式で見る】

If-Elseは条件を評価し、対応するブロック内の式の評価結果を戻り値として返します。

let result = if (true) {
  "true result"
} else {
  "false result"
}

print_endline(result);

IfとElseブロックの評価結果の型は同一である必要があります。

ある条件で「ログを出力をする」などの戻り値のない副作用などの処理分岐はこのようにかきます。

let result = if (true) {
  let log = () => print_endline("it is true");

  log()
};

これは

let result = if (true) {
  let log = () => print_endline("it is true");

  log()
} else {
  ()
};

のシンタックスシュガーで、戻り値がunit()になります。

よってこのような式はコンパイルエラー(unit() ≠ string)です。

let result = if (true) {
  "true result"
}

三項演算子

もちろん三項演算子も使えます。

let res = true ? "true" : "false";

print_endline(res);

Pipe First [【公式で見る】](https://reasonml.github.io/docs/en/pipe-first

-> 演算子を使うことで、パイプ表現ができます。

たとえば、このように関数の入れ子が多すぎる式は

let add = (num) => num + 2;
let minus = (num) => num - 3;
let times = (num) => num * 4;

print_endline(
  string_of_int(
    times(minus(add(1)))
  )
);

このようにパイプで表現できます。

let add = (num) => num + 2;
let minus = (num) => num - 3;
let times = (num) => num * 4;

1
  ->add
  ->minus
  ->times
  ->string_of_int
  ->print_endline

a(b)を、b->aと表現できるということになります。

複数の引数を持つ関数の場合は、

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

1 ->add(2, 3)
  ->string_of_int->print_endline;

というように書けます。ラベル付き引数の場合でも同様です。

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

1 ->add(~y=2, ~z=3)
  ->string_of_int->print_endline;

ただし、パイプを使いすぎるのは、良いこととは限らず、新しくReasonMLを始めた人は、この軽快な書き方に頼って、すべてパイプにしようとしてしまうらしいです。

公式では、このように中心的な処理に対する付加的な処理としてパイプをつけることを勧めてます。

parseData(person)
  ->getAge
  ->validateAge

このようにIntの値を表示させたいときに、ちょっと便利だったりもします。

1024
  ->string_of_int->print_endline

JavaScriptのメソッドチェーン

JavaScriptでは、

[1,2,3].map(x => x + 1).filter(x => x % 2 === 0);

というようにメソッドチェーン形式で式を記載できます。

これをReasonMLでやろうとすると

filter(map([|1,2,3|], x => x + 1), x => x mod 2 === 0);

となり、ちょっぴり見づらくなってしまいますが、

[|1,2,3|]
  -> map(x => x + 1)
  -> filter(x => x mod 2 === 0)

と書けるので、嬉しいね。っていう話でした。

filtermapはデフォルトで存在するわけではなく、Js側からもらってこないといけません。

BuckleScript binding APIを利用する必要があります。

実際はこのように記載することで、利用することができます。

[@bs.send] external map : (array('a), 'a => 'b) => array('b) = "map";
[@bs.send] external filter : (array('a), 'a => 'b) => array('b) = "filter";

[|1,2,3|]
  -> map(x => x + 1)
  -> filter(x => x mod 2 === 0)

VariantsへのPipe

optionで、Some('a)ってありましたよね。

Someはコンストラクタであって、関数ではないですが、パイプすることができます。

let result = name->preprocess->Some;

let result = Some(preprocess(name));

上記2つは一緒です。(また良い例が浮かばず、公式パクりで申し訳ない。)

コンストラクタは、あくまで関数ではなく、このパイプ表現以外で、関数と同様に扱える場面は無いそうです。

パイプは、実はただ1つ前を( )で囲む変換を施しているだけなのかもしれないですね。

ただこれ、どういうときに使うんでしょうね。

preprocessが外部コードの時とかになるのでしょうか?

JavaScript相互運用の説明をするときに、謎は解けるかもしれません!

Pipe プレースホルダー

プレースホルダーは、関数を実行する際の引数を_にすることで、ある値を受け取り、_の位置に指定して実行される関数にできる記法です。(語彙が崩壊してる…)

例を見せます。

let add = (x, y, z) => x + y + z;

// プレースホルダー記法は
let add4 = add(1, _, 3);

// これと同じ意味
let add4 = y => add(1, y, 3);

add4(2)
 ->string_of_int->print_endline;

このプレースホルダーを使うことで、パイプする引数をどの位置に渡すかを指定できます。

let concat = (x, y) => x ++ y;

"1"->concat("2")
   ->print_endline; // result:12

"1"->concat("2", _)
   ->print_endline; // result:21

2個目は、プレースホルダーを使って、2番目の引数に"1"を渡しているので、結果が変わります。

型をもっと【公式で見る】

すでに「型!」は説明しましたが、更なる使い方の説明になります。

型引数!

いわゆるジェネリック型が定義できます。

ジェネリック型には、'を付けます。

たまに出てきた'aはジェネリック型だったということです。

下記のように、データ構造が等しく、別の型のデータを持つ場合に、

type intTuple = (int, int);
type floatTuple = (float, float);

型引数を使えると、よりインスタントに型を生成できます。

type twinTuple('t) = ('t, 't);

type intTuple = twinTuple(int);
type floatTuple = floatTuple(int);

optionと同等の、ジェネリック型を作成することもできます。

type ninni('concrete) = None | Some('concrete);

let num: ninni(int) = None;

let resnum = switch num {
| None => 0
| Some(n) => n
};
resnum->string_of_int->print_endline;

let str: ninni(string) = None;
let resstr = switch str {
| None => "empty"
| Some(s) => s
};
resstr->print_endline;

公式には、より複雑に組成された構造が紹介されてます。

興味があったら、見てください。

相互に参照する再帰的な型

お互いにお互いの型を参照するケースです。

typeandを使います。

type person = { owns: list(dog) }
and dog = { ownedBy: option(person) };

let me: person = {
  owns: []
};

let norainu: dog = {
  ownedBy: None
};

Destructuring

デストラクチャリングです。

構造データを分解して、束縛に代入できます。

タプルの分解例です。

let tuple = (1, 2);
let (a, b) = tuple;

(a + b)
  -> string_of_int -> print_endline;

レコードの分解例です。

type example = { a: int, b: int };
let record: example = { a: 1, b: 1 };

let { b, a } = record;

(a + b)
  -> string_of_int -> print_endline;

// ラベル付けもできます
let { b: bbb, a: aaa } = record;

(aaa + bbb)
  -> string_of_int -> print_endline;

型アノテーションもつけることができます。

let (a: int, b: int) = someInts;
let {a: (aaa: int), b: (bbb: int)} = someInts;

なぜか公式では触れられていませんが、配列もできます。

let [a, b] = [1, 2];

(a + b)
  -> string_of_int -> print_endline;

関数のラベル付き引数も分解できます。

let addTuple = (~tuple as (x, y)) => x + y;

addTuple(~tuple=(1, 2))
  -> string_of_int -> print_endline;

分解していない状態の引数に、別のラベルを付けることができます。

type record = {
  x: int,
  y: int
};

let add = (~record as {x, y} as pair) => x + y + pair.x + pair.y;

add(~record={ x: 1, y: 2})
  -> string_of_int -> print_endline;

パターンマッチング!【公式で見る】

Variantの章は読まれましたか?

読んでいないなら、先にそちらを読んでください。と公式がおっしゃっています。

switchで良く使った|によるものです。

たとえば、HTTPレスポンスのハンドリングに使えます。

type person = {
  name: string,
  age: int
};

type response =
  | Ok(list(person))
  | BadRequest(string)
  | InternalServerError;

let res = Ok([{
  name: "taro",
  age: 10
}, {
  name: "jiro",
  age: 8
}]);

switch (res) {
| Ok(persons) => {
    let [taro, jiro] = persons;
    print_endline(taro.name ++ " and " ++ jiro.name);
  }
| BadRequest(message) => print_endline(message);
};

上の例では、レスポンスには、「OKBadRequestInternalServerError」を定義しました。

しかしswitchでは、「OKBadRequest」しかマッチングさせていません。

この場合エラーにはなりませんが、警告が出ます。

// `InternalServerError`のことを忘れていませんか?
You forgot to handle a possible case here, for example:
InternalServerError

みんな大嫌いUnhandledPromiseRejectionWarning()とか防げそうですね。(笑)

条件分岐としての使い方もできます。

let num = 4;

let result = switch num {
| 1 => "1"
| 2 => "2"
| 3 => "3"
| _ => {js|数値です|js}
};

result
 ->print_endline;

_は、他がすべてマッチしなかったときのフォールスルーです。

コンストラクタに含まれる値を指定したマッチングもできます。

type person = {
  name: string,
  age: int
};

type response =
  | Ok(list(person))
  | BadRequest(string)
  | InternalServerError;

let res = BadRequest("CODE1-1");

switch (res) {
| Ok(persons) => {
    let [taro, jiro] = persons;
    print_endline(taro.name ++ " and " ++ jiro.name);
  }
| BadRequest("CODE1-1" | "CODE1-2" | "CODE1-3") => print_endline({js|コード1のエラーです|js});
| BadRequest(message) => print_endline({j|不明なエラー: $message|j});
| InternalServerError => print_endline("サーバーエラー")
};

リストなどのデータ構造とのマッチングもできます。

let items = [1,2,3];

let result = switch items {
| [] => "0"
| [x, y] => string_of_int(x + y)
| [x, y, z] => string_of_int(x + y + z)
| [a, ...rest] => "too many items!"
};

result
  -> print_endline;

もちろんレコードでもできます。

type person = {
  name: string,
  age: int
};

let taro: person = {
  name: "taro",
  age: 10
};

let who = switch taro {
| {name: "taro", age: 10} => "this is taro."
| {name: "jiro", age: 8} => "this is jiro."
| _ => "who?"
};

print_endline(who);

when句

when句を使うことで、マッチングに条件を追加できます。

type person = {
  name: string,
  age: int
};

type response =
  | Ok(list(person))
  | BadRequest(string)
  | InternalServerError;

let res = BadRequest("no-error");

switch (res) {
| Ok(persons) => {
    let [taro, jiro] = persons;
    print_endline(taro.name ++ " and " ++ jiro.name);
  }
| BadRequest(message) when message === "no-error" => print_endline({js|エラーではなかったようです|js});
| BadRequest(message) => print_endline({j|不明なエラー: $message|j});
| InternalServerError => print_endline("サーバーエラー")
};

例外のハンドリング

例外はまだ説明していませんが、こういうことができることだけ説明します。

exception句を使うことで、評価された値がエラーを含む場合のマッチングができます。

switch (mayThrowError()) {
| result => print_endline(result)
| exception A_Error => print_endline("error message!")
};

ネストされたパターン

データ構造に対するパターンマッチングができることを説明しました。

データ構造がネストしていても、それは可能です。

type friend = {
  name: string
}

type person = {
  name: string,
  friend: friend
};

let taro = {
  name: "taro",
  friend: {
    name: "jiro"
  }
};


let result = switch taro {
| { friend: { name: "jiro" } } => {js|jiroが友達です|js}
| _ => "who?"
};

print_endline(result);

パターンはどこでも使える

実はswitchに限らず、let bindingでも使えます。

type name =
  | FirstName(string)
  | LastName(string);


let taro = FirstName("taro");

let FirstName(name) | LastName(name) = taro;

print_endline(name);

Mutation 【公式で見る】

refを使うことで、let bindingを変更できるようにします。

箱が作られ、その中に値が入っている状態をイメージします。

let num = ref(0);

let zero = num^;

num := 3;

num^
  ->string_of_int->print_endline;

zero
  ->string_of_int->print_endline;

箱の中の値の変更は、:=で、

箱の中の値の取り出しは、^で行います。

numは、箱(ref)そのもの、zeroは値になります。

ただしrefを使う前に、letが再宣言できることを忘れないでください。

let hoge = "ho";
let hoge = hoge ++ "ge";

print_endline(hoge)

命令的ループ 【公式で見る】

mapfilterを使う以外に、列挙型の操作に使える構文が用意されています。

Forループ

for x in xFrom to xTo {}が使えます。

for (i in 1 to 10) {
  print_int(i);
}

print_intなんて便利なものがあったとは露知らず。)

downtoで、大きい値から小さい値へのループもできます。

for (i in 10 downto 1) {
  print_int(i);
}

Whileループ

while(condition) {}です。

let x = ref(10);

while(x^ > 0) {
  print_int(x^);

  x := x^ - 1;
}

JSX 【公式で見る】

ReasonMLでは、JSX記法が使えます。

JSX記法が必ずしも可読性に寄与するかどうかの議論は、こちらが参考になります。

let hoge = (~foo, ~children, ()) => {
  print_endline(foo);

  switch children {
  | [child1, child2] => print_endline(child1 ++ " " ++ child2)
  | _ => print_endline("invalid children")
  };
}

let bar = "bar";
let child1 = "taro";
let child2 = "jiro";

<hoge foo={bar}>child1 child2</hoge>;

hogeは関数で、foo属性は引数fooに、子要素に指定したchild1とchild2は引数childrenにリストで入ります。

これは<hoge>が、下記のコードに変換されるためです。

([@JSX] div(~foo=bar, ~children=[child1, child2], ()));

公式を見ると、他にもいくつかのルールが紹介されていますので、よかったら見てみてください。

External 公式で見る

ReasomMLには、externalまたは、FFI foreign function interface という機能があります。

下記のコードを見てください。

[@bs.val] external jsAlert: string => unit = "alert";

jsAlert("work only in browser.");

これがJavaScriptにコンパイルされると、

// Generated by BUCKLESCRIPT, PLEASE EDIT WITH CARE
'use strict';


alert("work only in browser.");

/*  Not a pure module */

となります。

@bs.valの役割は、"alert"というJavaScriptのグローバル空間の値に対するアクセスを提供することです。

つまりこのコードはブラウザ環境でしか動きません。

ReasonMLにおける相互運用機能とは、コンパイラ(ここではBuckleScript)に対するヒントなので、

コンパイル後のコードが、

  • Cのコンパイラが読めるならC言語との相互運用も可能ですし、

  • ブラウザで動くなら、JavaScriptとの相互運用も可能だということです。

あくまで、ReasonML内で使うにはインタフェースを定義して、型を明示する必要があるということです。

この型システムはReasonML内でのモノで、外界のコードには適用されません。

これは、実行時エラーの主な原因にもなると思います。

外界のコードが持つ良い点も悪い点も受け入れるということなので、導入には慎重になるべきだと思います。

最後にもう一つ、@bs.sendを紹介します。

[@bs.send] external map : (array('a), 'a => 'b) => array('b) = "map";

map([|1, 2, 3|], x => x * 2)

これは、

// Generated by BUCKLESCRIPT, PLEASE EDIT WITH CARE
'use strict';


/* array */[
    1,
    2,
    3
  ].map((function (x) {
        return (x << 1);
      }));

/*  Not a pure module */

にコンパイルされます。

第1引数に指定する値が持つメソッドとして、コンパイルされます。

JavaScriptのメソッドチェーンを、ReasonMLの関数にマッピングするときに便利です。

Pipe Firstでも説明しましたが、Pipeを使ってメソッドチェーン風に書くこともできます。

他にもいろんなbs-blablaがあり、こちらで紹介されているので興味がある方は参照してみてください。

例外処理 【公式で見る】

exceptionで定義したエラーをraiseで発生させています。

tryを用いて、エラーハンドリングします。

exception A_Error;

let hoge = (x) => {
  if (x mod 2 === 0) {
    raise(A_Error)
  } else {
    1
  }
};

let res = try (hoge(2)) {
  | A_Error => 0
};

print_int(res);

variantのような使い方も可能です。

exception A_Error(string);

let hoge = () => {
  raise(A_Error("error message"))
};

try (hoge()) {
  | A_Error(message) => print_endline(message)
};

ただし公式には、tryはデモンストレーションとして使って見せたもので、実際はswitch + exceptionを使ってくれと書いてあります。

パターンマッチング - 例外のハンドリング

これらは例外的なケースでのみ利用することが推奨され、乱用をさけるよう公式では書かれています。

StringListは、例外を発するので、使うときは気をつけてください。

オブジェクト 【公式で見る】

Recordに似てますが、JavaScriptにおけるオブジェクトというよりはclassみたいな感じです。

.を使って、'closed'オブジェクトを定義します。

'closed'オブジェクトには、型に定義されていないメソッドを後から追加することができません。

type person = {
  .
  name: string
};

let taro: person = {
  val _name = "taro";
  pub name = _name;
};

print_endline(taro#name);

pubpublicメソッド。

priprivateメソッドです。

valはプロパティで、privateです。

.が2個、つまり..`は'open'なオブジェクトを定義します。

'open'なオブジェクトは、定義されたインターフェース以外にメソッドやプロパティを持つことができます。

イメージですが、personを抽象クラスとして、kosekiに拡張しています。

thisの説明の都合で、pub adjustNameからpri changeNameを呼び出せます。

type person('a) = {
  ..
  name: string
} as 'a;

type koseki = {
  .
  name: string,
  adjustName: string => unit
};

let taro: person(koseki) = {
  val _name = ref("toro");
  pub name = _name^;
  pub adjustName = (name) => this#changeName(name);
  pri changeName = (name) => {
    _name := name;
  };
};

taro#adjustName("taro");

print_endline(taro#name);

モジュール 【公式で見る】

ReasonMLにおけるモジュール化です。

moduleで定義し、型定義やlet bindingsを含むことができ、ネストもできます。

module Example = {
  type meanless = Hoge | Huga | Piyo;

  let hoge = Hoge;
  
  let getMeanlessName = (x) => switch x {
  | Hoge => "hoge"
  | Huga => "huga"
  | Piyo => "piyo"
  };
}

let piyo: Example.meanless = Example.Piyo;

Example.getMeanlessName(piyo)
  -> print_endline;

open

openを使うことで、モジュール名 + .を付けなくてよくなります。

module Example = {
  type meanless = Hoge | Huga | Piyo;

  let hoge = Hoge;
  
  let getMeanlessName = (x) => switch x {
  | Hoge => "hoge"
  | Huga => "huga"
  | Piyo => "piyo"
  };
}

open Example;

let piyo: meanless = Piyo;

getMeanlessName(piyo)
  -> print_endline;

この例だと、グローバル空間への展開ですが、ローカルにも展開できます。

module Example = {
  type meanless = Hoge | Huga | Piyo;

  let hoge = Hoge;
  
  let getMeanlessName = (x) => switch x {
  | Hoge => "hoge"
  | Huga => "huga"
  | Piyo => "piyo"
  };
}

let result = {
  open Example;

  let piyo: meanless = Piyo;

  getMeanlessName(piyo)
}

// グローバルスコープで meanless は参照できないからエラー
let huga: meanless = Huga;

print_endline(result);

モジュール拡張

別のモジュールでincludeを使い、ベースとなるモジュールを拡張できます。

こちらはmixin的な使い方です。

module Base = {
  let x = "x";
};

module Inheried = {
  include Base;
  let y = "y";
};

print_endline(Inheried.x);

.reファイルはモジュールそのもの

index.reとは別に、sample.reファイルを作成してください。

sample.re
let a = 1;

このsample.reは、Sampleモジュールとして、他のファイルから呼び出しできます。

index.re
print_int(Sample.a);

もちろん、index.reIndexモジュールとして利用できます。

シグネチャ

モジュールの仕様?インターフェース?のことをシグネチャと呼びます。

module typeを使って、シグネチャを定義します。

// Animalのシグネチャ
module type Animal = {
  type species = Dog | Cat | Pig;

  let getCry: species => string;
};

// Animalのモジュール(実装)
module Animal: Animal = {
  type species = Dog | Cat | Pig;

  let getCry = specie => switch (specie) {
  | Dog => {js|ワン|js}
  | Cat => {js|にゃあ|js}
  | Pig => {js|ブヒ|js}
  };
};

let result = {
  open Animal;

  getCry(Dog);
};

print_endline(result);

シグネチャの拡張

モジュールと同じで、includeで拡張できます。

module type Base = {
  let a: string;
};

module type Inherited = {
  include Base;

  let b: int;
};

またすでに実装されているモジュールを拡張して、シグネチャとして定義することもできます。

module Base = {
  let a = "a";
};

module type Inherited = {
  include (module type of Base);

  let b: int;
};

.reiファイルは、シグネチャそのもの

animal.reiファイルを作成します。

animal.rei
type species = Dog | Cat | Pig;

let getCry: species => string;

これに対して、同名の.reファイルを実装する必要があります。

animal.re
type species = Dog | Cat | Pig;

let getCry = specie => switch (specie) {
| Dog => {js|ワン|js}
| Cat => {js|にゃあ|js}
| Pig => {js|ブヒ|js}
};

これで、Animalモジュールを利用できます。

index.re
let result = {
  open Animal;

  getCry(Dog);
};

print_endline(result);

モジュール関数(functors)

functorsとは、モジュールを引数として渡せる関数のことみたいです。

functorsは、

  • letではなく、moduleで宣言する
  • モジュールを引数に取り、モジュールを返す
  • 引数は、アノテーションが必要
  • 大文字から始まる

を満たします。

公式の例を使わせてください。

module type Comparable = {
  type t;
  let equal: (t, t) => bool;
};

module MakeSet = (Item: Comparable) => {
  /* let's use a list as our naive backing data structure */
  type backingType = list(Item.t);
  let empty = [];
  let add = (currentSet: backingType, newItem: Item.t) : backingType =>
    /* if item exists */
    if (List.exists((x) => Item.equal(x, newItem), currentSet)) {
      currentSet /* return the same (immutable) set (a list really) */
    } else {
      [
        newItem,
        ...currentSet /* prepend to the set and return it */
      ]
    };
};

これらを使い

module IntPair = {
  type t = (int, int);
  let equal = ((x1, y1), (x2, y2)) => x1 == x2 && y1 == y2;
  let create = (x, y) => (x, y);
};

/* IntPair abides by the Comparable signature required by MakeSet */
module SetOfIntPairs = MakeSet(IntPair);

let result = SetOfIntPairs.add([
  (1, 2),
  (3, 4)
], IntPair.create(1, 2))

Js.log(result);

このように、ジェネリック的な使い方ができます。

Promise 【公式で見る】

ReasonMLには、JavaScriptのPromiseの機能へのBuckleScriptによるバインディングがビルトインサポートされてます。

let promise = Js.Promise.make((~resolve, ~reject) => {
  Js_global.setTimeout(() => resolve(. "hello "), 2000);
  ()
});

promise
|> Js.Promise.then_(value => {
     Js.Promise.resolve(value ++ "world");
   })
|> Js.Promise.then_(value => {
     print_endline(value);
     Js.Promise.resolve(value);
   })
|> Js.Promise.catch(error => {
     Js.log(error);
     Js.Promise.resolve("failed");
   })

Js_global.setTimeoutを使って2秒後にメッセージを出しています。

resolve関数を呼び出す際に.がついています。

Js.Promise.make: ( ( ~resolve: (. 'a) => unit, ~reject: (. exn) => unit ) => unit ) => Js.Promise.t('a)

このように、resolve関数の引数の定義に.がついていて、これはresolve関数をカリー化しないでねっていうコンパイラへの指示になります。

つまり任意の引数を除き、すべての引数を1度に指定する必要があります。

|>は、パイプ演算子の一種です。

let add = (a, b) => a + b;

let result = 1 |> add(2);

print_int(result);

|>の前の値を引数として、|>の後ろの関数を実行します。

カリー化により、add(2)は引数を1つ取る関数(b => 1 + b)なので、1を引数に与えて実行した結果、3を返します。

ライブラリを使う

Redex 公式

Redexは、ReasonML用のライブラリインデックスです。

作ったライブラリは、こちらから登録することができます。

npm publishするかどうかは任意で、実験的な機能や、開発中のライブラリを一旦Redexに登録しておいて、先に公開だけしたり、Contributeしてもらったりすることができます。

大抵のライブラリは、NPMのレジストリに公開してあって、そこからnpm installできます。

その後、bsconfig.json内のbs-dependenciesまたはbs-dev-dependenciesを編集します。

bs-dependencies, bs-dev-dependencies

たとえばbs-cssというパッケージをインストールする際は、

npm install --save bs-css

bs-dependenciesに配列で、依存パッケージを指定します。

bsconfig.json
{
  "name": "reason-sample",
  "sources": ["src"],
  "bs-dependencies": ["bs-css"]
}

これで、Cssモジュールが使えます。

Open Css

npmのJSライブラリの利用 bs-blablaで見る

@bs.moduleを使えば、requireで別のJSファイルへの参照

恐らくRedexに載ってるパッケージの多くが、これを使って既存のJavaScriptライブラリのラッパーを作り公開しているのではないかと思います。

このように記載したReasonMLが、

[@bs.module] external uuidv4: unit => string = "uuid/v4";

Js.log(uuidv4());

このようにコンパイルされることで、npmライブラリをrequireできます。

var V4 = require("uuid/v4");

console.log(V4());

さいごに

OCamlは触ったことないですが、ReasonMLは抵抗少なめでした。

コードは可読性の為に冗長になることがあるので、コンパイルにより不要なコードが削除されたり、パフォーマンスが良いプログラムを組めるのはうれしいです。

実行時例外は普通に起こるかと思われますが、JavaScriptよりバグが少なく書けるかなと言う感じです。

JavaScriptとの比較ばかりしているので、AltJSとしての紹介になってしまいましたが、あくまでそれはBuckleScriptとの組み合わせの話です。

ReasonMLには、型とかnullが辛いと思われてるような別の言語との相互運用の可能性も残されているように感じます。

シンプルな言語だと舐めてかかったら、いろいろ機能を持っていて、ただ実際にはlet binding・Variants・パターンマッチング・型などコアとする部分があり、そこに対して便利さの為にモジュールが追加されていったような感じがしました。気のせいですかね。

各章にその機能に関連したReasonMLの思想や実際の実装上の活用法が、「Design Decisions」や「Tips & Tricks」として書かれているので、ぜひ読んで頂きたいところです。

自身も入門しながらのせいで、ところどころ質問混ざっちゃってますが、誰かのお役に立てたら幸いです。

さて、ここまで長くなりましたが、読んで頂きありがとうございました~。

9
8
1

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
9
8

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?