JavaScript とのコミュニケーション
Melange は JavaScript との相互運用性が非常に高く、外部の JavaScript コードを利用するための様々な機能を提供しています。これらのテクニックを学ぶために、まず言語コンセプトについて説明し、次に Melange の型が JavaScript のランタイム型にどのようにマッピングされるかを見ていきます。最後に、JavaScript とのコミュニケーション方法を示すために、さまざまな使用例を例として示します。
言語のコンセプト
以下のセクションで説明する概念は、OCaml 言語のごく一部です。しかし、JavaScript とのコミュニケーション方法と、そのために Melange が提供する機能を理解する上では不可欠です。
Attributes and extension nodes
JavaScript と相互作用するために、Melange はこれらの相互作用を表現するブロックを提供するように言語を拡張する必要があります。
例えば、そのために新しい構文構造(キーワードなど)を導入するのも 1 つの方法です:
javascript add : int -> int -> int = {|function(x,y){
return x + y
}|}
しかし、これでは Melange の主な目標のひとつである OCaml との互換性が壊れてしまいます。
幸いなことに、OCaml はパーサーや言語との互換性を壊すことなく言語を拡張するメカニズムを提供しています。これらのメカニズムは 2 つの部分で構成されています:
- 第 1 に、拡張または置換されるコード部分を定義するための構文追加
- 第 2 に、PPX リライター (opens in a new tab)と呼ばれるコンパイル時の OCaml ネイティブ・プログラムで、上記で定義された構文追加を読み込み、拡張または置換を行います。
構文追加には、Extension nodes (opens in a new tab)とattributes (opens in a new tab)という 2 つの種類があります。
Extension nodes
Extension nodes は、extender と呼ばれる特定のタイプの PPX リライターによって置き換えられることになっているブロックです。Extension nodes は、識別のために%
文字を使用します。extender は Extension nodes を有効な OCaml AST(抽象構文木)に置き換えます。
Melange が拡張ノードを使用して JavaScript とコミュニケーションする例として、Melange プログラム内で「生の」JavaScript を生成する方法があります:
[%%mel.raw "var a = 1; var b = 2"]
let add = [%mel.raw "a + b"]
これは、以下のような JavaScript コードを生成します:
var a = 1
var b = 2
var add = a + b
パーセント 1 文字と 2 文字の違いについては、OCaml のドキュメント (opens in a new tab)を参照してください。
Attributes
Attributes は、コードの特定の部分に追加情報を提供するために適用される「装飾」です。Melange では、JavaScript コード生成の表現力を高めるために、既存の OCaml 組み込み Attributes を再利用するか、新しい Attributes を定義するかの 2 つの方法で Attributes を使用します。
OCaml attributes の再利用
最初のアプローチは、既存のOCaml の組み込み attributes (opens in a new tab)を JavaScript の生成に活用することです。
Melange プロジェクトで使用できる OCaml 属性の代表的な例として、unboxed
属性があります。これは、シングルフィールドのレコードとバリアントを 1 つのタグでコンパイルし、生の値に最適化するものです。これは、混在させたくない型エイリアスを定義する場合や、異種コレクションを使用する JavaScript コードにバインドする場合に便利です。後者の例は variadic 関数の引数のセクションで説明します。
例えば:
type name =
| Name of string [@@unboxed]
let student_name = Name "alice"
は、以下のようにコンパイルされます:
var student_name = 'alice'
alert
やdeprecated
のような他の OCaml 組み込み attributes も Melange で使用できます。
新しい attributes を定義する
2 つ目のアプローチは、JavaScript オブジェクトのプロパティにバインドするために使用されるmel.set
属性のような、Melange のために特別に設計された新しい属性を導入することです。Melange によって導入された属性の完全なリストは、ここにあります。
Attribute アノテーションは、コード内の配置と、どの種類のシンタックス・ツリー・ノードにアノテーションするかによって、1 文字、2 文字、または 3 文字の@
を使用することができます。アトリビュートの詳細については、OCaml のマニュアル・ページ (opens in a new tab)を参照してください。
Melange の attribute であるmel.set
と mel.as
を使ったサンプルです:
type document
external setTitleDom : document -> string -> unit = "title" [@@mel.set]
type t = {
age : int; [@mel.as "a"]
name : string; [@mel.as "n"]
}
プリプロセッサ、attributes、Extension nodes の詳細については、OCaml ドキュメントの PPX リライタのセクション (opens in a new tab)を参照してください。
External 関数
Melange が JavaScript と通信するために公開しているシステムのほとんどは、external
と呼ばれる OCaml の構成要素の上に構築されています。
external
は、OCaml でC コードとのインターフェイス (opens in a new tab)となる値を宣言するためのキーワードです:
external my_c_function : int -> string = "someCFunctionName"
これはlet
バインディングのようなものだが、external のボディが文字列である点が異なります。この文字列は文脈によって特定の意味を持つ。ネイティブの OCaml では、通常その名前の C 関数を指します。Melange の場合は、実行時の JavaScript コードに存在し、Melange から使用される関数または値を指します。
Melange では、external を使用してグローバルな JavaScript オブジェクトにバインドすることができます。また、特定の[@mel.xxx]
属性で装飾することで、特定のシナリオでのバインディングを容易にすることもできます。利用可能な属性については、次のセクションで詳しく説明します。
一度宣言すれば、external
を通常の値として使用することができます。Melange の external 関数は期待される JavaScript の値に変換され、コンパイル時に呼び出し元にインライン化され、コンパイル後は完全に消去されます。JavaScript の出力には現れないので、バンドルサイズにコストはかかりません。
Note: 外部関数と[@mel.xxx]
属性をインターフェースファイルでも使用することをお勧めします。これにより、結果として得られる JavaScript の値を呼び出し先で直接インライン化することができ、いくつかの最適化が可能になるからです。
特別な identity external
特筆すべき external には次のものがあります:
type foo = string
type bar = int
external danger_zone : foo -> bar = "%identity"
これは、foo
型をbar
型に変換することだけを行う最後の脱出口です。以下のセクションで、もし external 関数を書くのに失敗したら、この関数を使うことに戻ることができますが、そうしないようにしましょう。
Abstract types
この後のセクションで、値が代入されずに型が定義されているバインディングの例に出会うでしょう。以下はその例です:
type document
これらの型は「抽象型」と呼ばれ、JavaScript と通信する際に、値に対する操作を定義する external 関数とともに一般的に使用される。
抽象型は、不要な詳細を省きつつ、JavaScript に由来する特定の値に対する型を定義することを可能にします。例えば、前述の document
型はいくつかのプロパティを持ちます。抽象型を使用することで、すべてのプロパティを定義するのではなく、Melange プログラムが必要とするドキュメント値の必要な部分のみに焦点を当てることができます。次の例を考えてみましょう:
type document
external document : document = "document"
external set_title : document -> string -> unit = "title" [@@mel.set]
後続のセクションでは、mel.set
属性の詳細と、document
のようなグローバルな値へのバインディング方法について掘り下げます。
抽象型とその有用性の包括的な理解については、OCaml Cornell textbook (opens in a new tab)の「カプセル化」のセクションを参照してください。
Pipe operators
Melange には二つの pipe 演算子があります:
- pipe last
|>
: OCaml でサポート (opens in a new tab)され、Melange にも継承されています - pipe first
|.
,->
: Melange 独自のものです
二つの違いについて見ていきましょう
Pipe last
バージョン 4.01 以降、OCaml には逆引き演算子または「パイプ」演算子(|>
)が追加されました。OCaml のバックエンドとして、Melange はこの演算子を継承しています。
パイプ演算子は次のように実装できます(実際の実装は少し異なります (opens in a new tab)):
let ( |> ) f g = g f
この演算子は、ある値に対して複数の関数を順番に適用し、各関数の出力が次の関数の入力になる(パイプライン)場合に便利です。
例えば、次のように定義された関数square
があるとします:
let square x = x * x
以下のように使用することができます:
let ten = succ (square 3)
パイプ演算子を使えば、左から右の順に結合性を保った (opens in a new tab) ten
を書くことができます:
let ten = 3 |> square |> succ
複数の引数を取ることができる関数を扱う場合、パイプ演算子は、関数が処理中のデータを最後の引数として取るときに最もよく機能します。例えば:
let sum = List.fold_left ( + ) 0
let sum_sq =
[ 1; 2; 3 ]
|> List.map square (* [1; 4; 9] *)
|> sum (* 1 + 4 + 9 *)
OCaml 標準ライブラリ (opens in a new tab)のList.map
関数は、第 2 引数にリストを取るので、上記の例は簡潔に書くことができます。この規約は「データ・ラスト」と呼ばれることもあり、OCaml のエコシステムでは広く採用されている。データ・ラストとパイプ演算子|>
は currying と相性が良いので、OCaml 言語にぴったりです。
しかし、エラー処理に関しては、データ・ラストの使用にはいくつかの制限があります。この例では、間違った関数を使ったとします:
let sum_sq =
[ 1; 2; 3 ]
|> List.map String.cat
|> sum
コンパイラーは当然エラーを出します:
4 | [ 1; 2; 3 ]
^
Error: This expression has type int but an expression was expected of type
string
List.map(String.cat)
で間違った関数を渡していることを教えてくれるのではなく、エラーはリストそのものを指していることに注意してください。この動作は、コンパイラーが左から右へと型を推論する、型推論の動作方法と一致しています。[1; 2; 3 ] |> List.map String.cat
は、List.map String.cat [ 1; 2; 3 ]
と等価なので、型の不一致は、String.cat
が処理された後、リストが型チェックされるときに検出されます。
このような制限に対処する目的で、Melange は Pipe first 演算子 |.
(OCaml) / ->
(Reason)を導入しました。
Pipe first
上記の制約を克服するために、Melange は Pipe first 演算子|.
(OCaml) / ->
(Reason)を導入しました。
その名前が示すように、Pipe first 演算子はデータが第 1 引数として渡される関数に適しています。
Melange に含まれる Belt ライブラリ(OCaml (opens in a new tab), Reason (opens in a new tab))の関数は、Data First の規約を念頭に置いて設計されているため、Pipe first 演算子との組み合わせが最適です。
例えば、上の例をBelt.List.map
と Pipe first 演算子を使って書き直すことができます:
let sum_sq =
[ 1; 2; 3 ]
|. Belt.List.map square
|. sum
Belt.List.map
に間違った関数が渡された場合のエラーの違いを見てみましょう:
let sum_sq =
[ 1; 2; 3 ]
|. Belt.List.map String.cat
|. sum
コンパイラーはこのエラー・メッセージを表示します:
4 | |. Belt.List.map String.cat
^^^^^^^^^^
Error: This expression has type string -> string -> string
but an expression was expected of type int -> 'a
Type string is not compatible with type int
エラーはBelt.List.map
に渡された関数を指しています。
Melange では、Chainingのセクションで示したように、データファーストまたはデータラストという 2 つの規約を使用して JavaScript にバインディングを記述することができます。
データファーストとデータラストの演算子の違いと、そのトレードオフについては、こちらのブログ記事 (opens in a new tab)を参照してください。
データ型とランタイム表現
Melange の各型は以下のように JavaScript の値に変換されます:
Melange | JavaScript |
---|---|
int | number |
nativeint | number |
int32 | number |
float | number |
string | string |
array | array |
tuple (3, 4) | array [3, 4] |
bool | boolean |
[Js.Nullable.t](OCaml (opens in a new tab) / Reason (opens in a new tab)) | null / undefined |
Js.Re.t(OCaml (opens in a new tab) / Reason (opens in a new tab)) | RegExp (opens in a new tab) |
Option.t None | undefined |
Option.t Some( Some .. Some (None)) (OCaml), Some(Some( .. Some(None))) (Reason) | internal representation |
Option.t Some 2 (OCaml), Some(2) (Reason) | 2 |
record {x = 1; y = 2} (OCaml) / {x: 1; y: 2} (Reason) | object {x: 1, y: 2} |
int64 | array of 2 elements [high, low] high is signed, low unsigned |
char | 'a' -> 97 (ascii code) |
bytes | number array |
list [] | 0 |
list [ x; y ] (OCaml) / [x, y] (Reason) | { hd: x, tl: { hd: y, tl: 0 } } |
variant | 以下を参照 |
polymorphic variant | 以下を参照 |
単一の非 null
コンストラクタを持つバリアント:
type tree = Leaf | Node of int * tree * tree
(* Leaf -> 0 *)
(* Node(7, Leaf, Leaf) -> { _0: 7, _1: 0, _2: 0 } *)
複数の非 null コンストラクタを持つバリアント:
type t = A of string | B of int
(* A("foo") -> { TAG: 0, _0: "Foo" } *)
(* B(2) -> { TAG: 1, _0: 2 } *)
Polymorphic variants:
let u = `Foo (* "Foo" *)
let v = `Foo(2) (* { NAME: "Foo", VAL: "2" } *)
それでは、これらの型のいくつかを詳しく見ていきましょう。まず、JavaScript の値として透過的に表現される共有型について説明し、次に、より複雑な実行時表現を持つ非共有型について説明します。
NOTE: Melange コードと通信する JavaScript コードから非共有データ型の実行時表現を手動で読み書きすることで、これらの表現が将来変更される可能性があるため、実行時エラーにつながる可能性があります。
Shared types
以下は、Melange と JavaScript の間でほぼ「そのまま」共有できる型です。具体的な注意点は、該当するセクションに記載しています。
Strings
JavaScript の文字列は、UTF-16 でエンコードされた Unicode テキストの不変なシーケンスです。OCaml の文字列は不変のバイト列であり、テキストコンテンツとして解釈される場合、最近では UTF-8 でエンコードされたテキストであると仮定されます。これは JavaScript コードとやりとりする際に問題となります:
let () = Js.log "你好"
これは、不可解なコンソール出力につながります。これを修正するために、Melange ではjs
識別子を使って引用符付きの文字列リテラル (opens in a new tab)を定義できます:
let () = Js.log {js|你好,
世界|js}
これは JavaScript の文字列補間に似ていますが、変数にのみ適用されます(任意の式には適用されません):
let world = {j|世界|j}
let helloWorld = {j|你好,$world|j}
補間変数を括弧で囲むこともできます: {j|你 好,$(world)|j}
文字列を扱うために、Melange 標準ライブラリはStdlib.String
モジュール(OCaml (opens in a new tab) / Reason (opens in a new tab))でいくつかのユーティリティを提供しています。文字列を扱うための JavaScript ネイティブ関数へのバインディングは、Js.String モジュール
(OCaml (opens in a new tab) / Reason (opens in a new tab))にあります。
Floating-point numbers
OCaml の浮動小数点数はIEEE 754 (opens in a new tab)で、仮数は 53 ビット、指数は-1022 ~ 1023 です。これはJavaScript の数値 (opens in a new tab)と同じエンコーディングであるため、これらの型の値は Melange コードと JavaScript コードの間で透過的に使用できます。Melange 標準ライブラリはStdlib.Float
モジュール(OCaml (opens in a new tab) / Reason (opens in a new tab))を提供しています。浮動小数点値を操作する JavaScript API へのバインディングは、Js.Float
モジュール(OCaml (opens in a new tab) / Reason (opens in a new tab))にあります。
Integers
Melange では、JavaScript のビット演算の固定幅変換のため、整数は 32 ビットに制限されています。Melange の整数は JavaScript の数値にコンパイルされますが、これらを互換的に扱うと、精度の違いにより予期せぬ動作になる可能性があります。JavaScript のビット演算は 32 ビットに制限されていますが、整数自体は数値と同じ (opens in a new tab)浮動小数点フォーマットを使って表現されるため、Melange に比べて JavaScript で表現可能な整数の範囲が広くなっています。大きな数値を扱う場合は、代わりに浮動小数点を使うことが推奨されます。例えば、Js.Date
のようなバインディングでは float が使われます。
Melange 標準ライブラリにはStdlib.Int
モジュール(OCaml (opens in a new tab) / Reason (opens in a new tab))が用意されています。JavaScript の整数を扱うバインディングはJs.Int
モジュール(OCaml (opens in a new tab) / Reason (opens in a new tab))にあります。
Arrays
Melange の配列は JavaScript の配列にコンパイルされます。しかし JavaScript の配列とは異なり、Melange 配列のすべての値は同じ型である必要があることに注意してください。
もう 1 つの違いは、OCaml の配列は固定サイズですが、Melange 側ではこの制約が緩和されていることです。配列の長さを変更するには、Js.Array.push
などの関数を使用します。Js.Array
モジュール(OCaml (opens in a new tab) / Reason (opens in a new tab))の JavaScript API バインディングで使用できます。
Melange の標準ライブラリにも配列を扱うモジュールがあり、Stdlib.Array
モジュール(OCaml (opens in a new tab) / Reason (opens in a new tab))で利用できます。
Tuples
OCaml のタプルは JavaScript の配列にコンパイルされます。これは、異種値を持つ JavaScript 配列を使用するバインディングを書くときに便利ですが、たまたま固定長でした。
実際の例としては、ReasonReact (opens in a new tab)というReact (opens in a new tab)用の Melange バインディングがあります。このバインディングでは、コンポーネント効果の依存関係は OCaml タプルとして表現され、Melange によって JavaScript 配列にきれいにコンパイルされます。
例えば、以下のようなコードです:
let () = React.useEffect2 (fun () -> None) (foo, bar)
このコードは以下のように JavaScript にコンパイルされます:
React.useEffect(function () {}, [foo, bar])
Booleans
bool
型の値は JavaScript のブール値にコンパイルされます。
Records
Melange レコードは JavaScript オブジェクトに直接マッピングされます。レコードフィールドに非共有データ型(バリアントなど)が含まれている場合、これらの値は JavaScript で直接使用せず、別途変換する必要があります。
レコードを使用した JavaScript オブジェクトとのインターフェイスに関する広範なドキュメントは、以下のセクションにあります。
Regular expressions
%mel.re
拡張ノードを使用して作成された正規表現は、JavaScript の対応するものにコンパイルされます。
例:
let r = [%mel.re "/b/g"]
このコードは以下のように JavaScript にコンパイルされます:
var r = /b/g
上記のような正規表現はJs.Re.t
型です。Js.Re
モジュール(OCaml (opens in a new tab) / Reason (opens in a new tab))は、正規表現を操作する JavaScript 関数へのバインディングを提供します。
Non-shared data types
以下の型は Melange と JavaScript で大きく異なるため、JavaScript から操作することは可能ですが、変換してから操作することが推奨されます。
- Variants と Polymorphic variants:JavaScript から操作する前に、読みやすい JavaScript の値に変換しておくとよいでしょう。Melange ではいくつかのヘルパーを用意しています
- 例外
- Option(Variant 型):
Js.Nullable
モジュール(OCaml (opens in a new tab) / Reason (opens in a new tab))のJs.Nullable.fromOption
関数とJs.Nullable.toOption
関数を使用して、null
またはundefined
値に変換する方が良いでしょう - List(Variant 型):
Stdlib.Array
モジュール(OCaml (opens in a new tab) / Reason (opens in a new tab))のArray.of_list
とArray.to_list
を使います - Character
- Int64
- Lazy な値
attributes と extension nodes のリスト
Attributes:
これらの attributes は、external
定義の注釈に使用されます:
mel.get
: JavaScript オブジェクトのプロパティを、.
記法を使って静的に名前から読み込むmel.get_index
: JavaScript オブジェクトのプロパティを[]
括弧記法で動的に読み込むmel.module
: JavaScript モジュールの値にバインドするmel.new
: JavaScript クラスのコンストラクタにバインドするmel.obj
: JavaScript オブジェクトを作成するmel.return
:null
可能な値からOption.t
値への自動変換mel.send
: JavaScript オブジェクトのメソッドを Pipe first で呼び出すmel.send.pipe
: JavaScript オブジェクトのメソッドを Pipe last で呼び出すmel.set
: JavaScript オブジェクトのプロパティを、.
記法を使って静的にセットするmel.set_index
: JavaScript オブジェクトのプロパティを[]
を使って動的に設定するmel.scope
: JavaScript オブジェクト内部のより深いプロパティにアクセスするmel.splice
: 非推奨の属性で、mel.variadic
の代替形式mel.variadic
: 配列から多様な引数を取る関数にバインドする
これらの attributes は、external
定義における引数の注釈に使用されます:
u
: 関数の引数を uncurried として定義(手動)mel.int
: 関数の引数を int にコンパイルするmel.string
: 関数の引数を文字列にコンパイルするmel.this
:this
ベースのコールバックにバインドするmel.uncurry
: 関数の引数を uncurried として定義する(自動)mel.unwrap
: Variant 値のアンラップ
これらの attributes は、レコード、フィールド、引数、関数などの場所で使用されます:
mel.as
: JavaScript の出力コードで生成される名前を再定義します。定数関数の引数、Variants、Polymorphic variants(exteranl 関数のインライン化または型定義)、レコードフィールドで使用されますderiving
: 型のゲッターとセッターを生成しますmel.inline
: 定数値を強制的にインライン化しますoptional
: レコードのフィールドを省略します(deriving
と組み合わせる)
Extension nodes:
これらの拡張ノードを使用するには、melange PPX プリプロセッサをプロジェクトに追加する必要があります。そのためには、dune
ファイルに以下を追加してください:
(library
(name lib)
(modes melange)
(preprocess
(pps melange.ppx)))
同じフィールドのpreprocess
をmelange.emit
に追加することができる。
以下は、Melange がサポートしているすべての Extension nodes のリストです:
mel.debugger
:debugger
文を挿入するmel.external
: グローバル値を読むmel.obj
: JavaScript オブジェクトリテラルを作成するmel.raw
: 生の JavaScript コードを書くmel.re
: 正規表現を挿入する
Generate raw JavaScript
Melange ファイルから JavaScript コードを直接記述することは可能です。これは安全ではありませんが、プロトタイプを作成するときや、逃げ道として便利です。
これを行うには、mel.raw
拡張 (opens in a new tab)を使用します:
let add = [%mel.raw {|
function(a, b) {
console.log("hello from raw JavaScript!");
return a + b;
}
|}]
let () = Js.log (add 1 2)
{||}
文字列は引用符付き文字列 (opens in a new tab)と呼ばれます。これらは JavaScript のテンプレート・リテラルに似ており、複数行にまたがるという意味で、文字列の内部で文字をエスケープする必要はありません。
角括弧で囲った Extension 名を使用すると ([%mel.raw <string>]
) 、実装が直接 JavaScript である式 (関数本体やその他の値) を定義するのに便利です。これは、同じ行ですでに型シグネチャを付けることができるので便利で、コードをより安全にすることができます。例えば:
let f : unit -> int = [%mel.raw "function() {return 1}"]
角括弧のない Extension 名(%mel.raw <string>
)は、構造体 (opens in a new tab)やシグネチャ (opens in a new tab)の定義に使われます。
例:
[%%mel.raw "var a = 1"]
Debugger
Melange では、mel.debugger
Extension を使用して debugger;
式を注入することができます:
let f x y =
[%mel.debugger];
x + y
出力:
function f(x, y) {
debugger // JavaScript developer tools will set a breakpoint and stop here
return (x + y) | 0
}
Detect global variables
Melange は、JavaScript の実行環境で定義されたグローバルを使用するための比較的型安全なアプローチを提供しています: mel.external
。
[%mel.external id]
は JavaScript の値id
がundefined
かどうかをチェックし、それに応じてOption.t
値を返します。
例:
let () = match [%mel.external __DEV__] with
| Some _ -> Js.log "dev mode"
| None -> Js.log "production mode"
例:
let () = match [%mel.external __filename] with
| Some f -> Js.log f
| None -> Js.log "non-node environment"
Inlining constant values
Some JavaScript idioms require special constants to be inlined since they serve
as de-facto directives for bundlers. A common example is process.env.NODE_ENV
:
JavaScript のイディオムの中には、インライン化するために特別な定数を必要とするものがあります。一般的な例は process.env.NODE_ENV
です:
if (process.env.NODE_ENV !== 'production') {
// Development-only code
}
このコードは以下のようになります:
if ('development' !== 'production') {
// Development-only code
}
この場合、Webpack などのバンドラーは、if
文が常に特定のブランチで評価されることを判別し、他のブランチを排除することができます。
Melange は、生成された JavaScript で同じ目標を達成するためにmel.inline
attribute を提供します。例を見てみましょう:
external node_env : string = "NODE_ENV" [@@mel.scope "process", "env"]
let development = "development"
let () = if node_env <> development then Js.log "Only in Production"
let development_inline = "development" [@@mel.inline]
let () = if node_env <> development_inline then Js.log "Only in Production"
以下に示す生成された JavaScript を見ればわかります:
development
変数がエミットされる- 変数
process.env.NODE_ENV !== development
としてif
文で使用される
- 変数
development_inline
変数が最終出力に存在しない- この値は
if
文内でインライン化される:process.env.NODE_ENV !== "development"
- この値は
var development = 'development'
if (process.env.NODE_ENV !== development) {
console.log('Only in Production')
}
if (process.env.NODE_ENV !== 'development') {
console.log('Only in Production')
}
Bind to JavaScript objects
JavaScript のオブジェクトは、さまざまなユースケースで使用されます:
- 固定の型のレコード (opens in a new tab)
- マップまたは辞書
- クラス
- インポート/エクスポートするモジュール
Melange では、これら 4 つのユースケースに基づいて JavaScript オブジェクトのバインディングメソッドを分けています。このセクションでは、最初の 3 つについて説明します。JavaScript モジュールオブジェクトへのバインディングについては、「他の JavaScript モジュールからの関数の使用」で説明します。
Objects with static shape (record-like)
Using OCaml records
JavaScript オブジェクトに固定フィールドがある場合、それは概念的にOCaml のレコード (opens in a new tab)のようなものです。Melange はレコードを JavaScript オブジェクトにコンパイルするため、JavaScript オブジェクトにバインドする最も一般的な方法はレコードを使用することです。
type person = {
name : string;
friends : string array;
age : int;
}
external john : person = "john" [@@mel.module "MySchool"]
let john_name = john.name
以下のように JavaScript が生成されます:
var MySchool = require('MySchool')
var john_name = MySchool.john.name
Exteranl 関数については前のセクションで説明しました。mel.module
attribute はここに書かれています。
Melange 側と JavaScript 側で異なるフィールド名を使用したい、または使用する必要がある場合は、mel.as
デコレータを使用できます:
type action = {
type_ : string [@mel.as "type"]
}
let action = { type_ = "ADD_USER" }
以下のように JavaScript が生成されます:
var action = {
type: 'ADD_USER',
}
これは、Melange で表現できない JavaScript の属性名にマッピングするのに便利です。たとえば、生成したい JavaScript の名前が予約語 (opens in a new tab)である場合などです。
mel.as
デコレーターにインデクスを渡すことで、Melange レコードを JavaScript の配列にマッピングすることも可能です:
type t = {
foo : int; [@mel.as "0"]
bar : string; [@mel.as "1"]
}
let value = { foo = 7; bar = "baz" }
以下のように JavaScript が生成されます:
var value = [7, 'baz']
Using Js.t
objects
レコードの代わりに、Melange は JavaScript オブジェクトを生成するために使用できる別の型を提供しています。この型は'a Js.t
で、'a
はOCaml のオブジェクト (opens in a new tab)です。
オブジェクトとレコードを比較した場合の利点は、事前に型宣言を行う必要がないため、プロトタイピングや JavaScript のオブジェクトリテラルを素早く生成するのに役立ちます。
Melange では、Js.t
オブジェクトの値を作成する方法や、オブジェクト内のプロパティにアクセスする方法を提供しています。値を作成するには、[%mel.obj]
拡張子を使用し、##
infix 演算子でオブジェクトのプロパティを読み込むことができます:
let john = [%mel.obj { name = "john"; age = 99 }]
let t = john##name
以下のように JavaScript が生成されます:
var john = {
name: 'john',
age: 99,
}
var t = john.name
オブジェクト型にはレコード型にはない柔軟性があることに注意してください。例えば、あるオブジェクト型を、より少ない値やメソッドを持つ別のオブジェクト型に強制することができますが、レコード型を、より少ないフィールドを持つ別のオブジェクト型に強制することは不可能です。そのため、いくつかのメソッドを共有する異なるオブジェクト型を、共通のメソッドだけが見えるデータ構造の中に混在させることができます。
例えば、文字列型のフィールドname
を含むすべてのオブジェクト型で操作する関数を作ることができます:
let name_extended obj = obj##name ^ " wayne"
let one = name_extended [%mel.obj { name = "john"; age = 99 }]
let two = name_extended [%mel.obj { name = "jane"; address = "1 infinite loop" }]
オブジェクトとポリモーフィズムについてもっと読むには、OCaml のドキュメント (opens in a new tab)かOCaml のマニュアル (opens in a new tab)をチェックしてください。
Using external functions
Js.t
値とmel.obj
extensionを使って JavaScript のオブジェクト・リテラルを作成する方法についてはすでに説明しました。
Melange はさらにmel.obj
attribute を提供しており、外部関数と組み合わせて JavaScript オブジェクトを作成することができます。これらの関数が呼び出されると、関数のラベル付き引数に対応するフィールドを持つオブジェクトが生成されます。
これらのラベル付き引数のいずれかがオプショナルとして定義され、関数適用時に省略された場合、結果の JavaScript オブジェクトは対応するフィールドを除外します。これにより、実行時オブジェクトを作成し、オプショナルキーが実行時に発行されるかどうかを制御することができます。
例えば、次のような JavaScript オブジェクトにバインドする必要がある場合:
var homeRoute = {
type: 'GET',
path: '/',
action: () => console.log('Home'),
// options: ...
}
最初の 3 つのフィールドは必須で、オプション・フィールドは任意です。バインディング関数は次のように宣言します:
external route :
_type:string ->
path:string ->
action:(string list -> unit) ->
?options:< .. > ->
unit ->
_ = ""
[@@mel.obj]
関数末尾の空文字列は、構文的に有効にするために使用されることに注意してください。この文字列の値はコンパイラによって無視されます。
オプションの引数options
があるので、その後ろにunit
型のラベルのない引数が追加されます。これにより、関数の適用時にオプション引数を省略することができます。ラベル付きオプション引数の詳細については、OCaml のマニュアル (opens in a new tab)を参照してください。
関数の戻り値の型は、ワイルドカード型 _
を使って指定しないでおきます。Melange は自動的に結果の JavaScript オブジェクトの型を推測します。
route 関数では、_type
引数はアンダースコアで始まります。OCaml の予約語であるフィールドを持つ JavaScript オブジェクトにバインドする場合、Melange ではラベル付き引数にアンダースコアの接頭辞を使用できます。その結果、JavaScript オブジェクトのフィールド名からアンダースコアが取り除かれます。これはmel.obj
attribute の場合のみ必要で、それ以外の場合はmel.as
attribute を使用してフィールド名を変更することができます。
このように関数を呼び出すと:
let homeRoute = route ~_type:"GET" ~path:"/" ~action:(fun _ -> Js.log "Home") ()
以下のような JavaScript が生成され、options
フィールドは引数に与えられていないため、含まれません:
var homeRoute = {
type: 'GET',
path: '/',
action: function (param) {
console.log('Home')
},
}
Bind to object properties
JavaScript オブジェクトのプロパティにのみバインドする必要がある場合、mel.get
とmel.set
を使って.
記法でアクセスすることができます:
(* Abstract type for the `document` value *)
type document
external document : document = "document"
external set_title : document -> string -> unit = "title" [@@mel.set]
external get_title : document -> string = "title" [@@mel.get]
let current = get_title document
let () = set_title document "melange"
以下のように JavaScript が生成されます:
var current = document.title
document.title = 'melange'
Alternatively, if some dynamism is required on the way the property is accessed,
you can use mel.get_index
and mel.set_index
to access it using the bracket
notation []
:
また、動的にプロパティへアクセスする場合は、mel.get_index
とmel.set_index
を使って、括弧記法[]
でアクセスできます:
type t
external create : int -> t = "Int32Array" [@@mel.new]
external get : t -> int -> int = "" [@@mel.get_index]
external set : t -> int -> int -> unit = "" [@@mel.set_index]
let () =
let i32arr = (create 3) in
set i32arr 0 42;
Js.log (get i32arr 0)
以下のように JavaScript が生成されます:
var i32arr = new Int32Array(3)
i32arr[0] = 42
console.log(i32arr[0])
Objects with dynamic shape (dictionary-like)
JavaScript のオブジェクトが辞書として使われることもあります。このような場合:
- オブジェクトに格納された値はすべて同じ型に属する
- キーと値のペアは、実行時に追加または削除できる
このような JavaScript オブジェクトを使用する場合、Melange では特定の型Js.Dict.t
を公開しています。この型の値および値を扱う関数は、Js.Dict
モジュール(OCaml (opens in a new tab) / Reason (opens in a new tab))で定義されており、get
、set
などの操作が可能です。
Js.Dict.t
型の値は、JavaScript オブジェクトにコンパイルされます。
JavaScript classes
JavaScript のクラスは特殊なオブジェクトです。クラスと相互作用するために、Melange は例えばnew Date()
をエミュレートするmel.new
を公開しています:
type t
external create_date : unit -> t = "Date" [@@mel.new]
let date = create_date ()
以下のように JavaScript が生成されます:
var date = new Date()
扱いたい JavaScript クラスが別の JavaScript モジュールにある場合、mel.new
とmel.module
をチェインさせることができます:
type t
external book : unit -> t = "Book" [@@mel.new] [@@mel.module]
let myBook = book ()
以下のように JavaScript が生成されます:
var Book = require('Book')
var myBook = new Book()
Bind to JavaScript functions or values
Using global functions or values
グローバルに利用可能な JavaScript 関数へのバインディングは、オブジェクトと同様にexternal
を利用します。しかし、オブジェクトとは異なり、attributes を追加する必要はありません:
(* Abstract type for `timeoutId` *)
type timeoutId
external setTimeout : (unit -> unit) -> int -> timeoutId = "setTimeout"
external clearTimeout : timeoutId -> unit = "clearTimeout"
let id = setTimeout (fun () -> Js.log "hello") 100
let () = clearTimeout id
NOTE:
setTimeout
とclearTimeout
のバインディングは、ここでは学習のために示していますが、これらはすでJs.Global
モジュール(OCaml (opens in a new tab) / Reason (opens in a new tab))で利用可能です。
以下のように JavaScript が生成されます:
var id = setTimeout(function (param) {
console.log('hello')
}, 100)
clearTimeout(id)
グローバルバインディングは値にも適用できます:
(* Abstract type for `document` *)
type document
external document : document = "document"
let document = document
以下のように JavaScript が生成されます:
var doc = document
Using functions from other JavaScript modules
mel.module
は、他の JavaScript モジュールに属する値にバインドすることができます。モジュールの名前か相対パスを文字列で指定します。
external dirname : string -> string = "dirname" [@@mel.module "path"]
let root = dirname "/User/github"
以下のように JavaScript が生成されます:
var Path = require('path')
var root = Path.dirname('/User/github')
Binding to properties inside a module or global
モジュールやグローバル JavaScript オブジェクト内のプロパティにバインディングを作成する必要がある場合、Melange はmel.scope
attribute を提供します。
例えば、vscode
パッケージ (opens in a new tab)の特定のプロパティコマンドに対するバインディングを書きたい場合、次のようにします:
type param
external executeCommands : string -> param array -> unit = ""
[@@mel.scope "commands"] [@@mel.module "vscode"] [@@mel.variadic]
let f a b c = executeCommands "hi" [| a; b; c |]
以下のようにコンパイルされます:
var Vscode = require('vscode')
function f(a, b, c) {
Vscode.commands.executeCommands('hi', a, b, c)
}
mel.scope
属性は、ペイロードとして複数の引数を取ることができます。
例:
type t
external back : t = "back"
[@@mel.module "expo-camera"] [@@mel.scope "Camera", "Constants", "Type"]
let camera_type_back = back
以下のように JavaScript が生成されます:
var ExpoCamera = require('expo-camera')
var camera_type_back = ExpoCamera.Camera.Constants.Type.back
mel.module
を使わずに、グローバル値へのスコープ付きバインディングを作成することができます:
external imul : int -> int -> int = "imul" [@@mel.scope "Math"]
let res = imul 1 2
以下のように JavaScript が生成されます:
var res = Math.imul(1, 2)
また、mel.new
と併用することもできます:
type t
external create : unit -> t = "GUI"
[@@mel.new] [@@mel.scope "default"] [@@mel.module "dat.gui"]
let gui = create ()
以下のように JavaScript が生成されます:
var DatGui = require('dat.gui')
var gui = new DatGui.default.GUI()
Labeled arguments
OCaml にはラベル付き引数 (opens in a new tab)があり、これはオプションでも可能で、external
でも動作します。
ラベル付き引数は、Melange から呼び出される JavaScript 関数の引数に関する詳細情報を提供するのに便利です。
例えば、次のような JavaScript 関数を Melange から呼び出すとします:
// MyGame.js
function draw(x, y, border) {
// let’s assume `border` is optional and defaults to false
}
draw(10, 20)
draw(20, 20, true)
Melange バインディングを記述する際、ラベル付き引数を追加することで、より明確にすることができます:
external draw : x:int -> y:int -> ?border:bool -> unit -> unit = "draw"
[@@mel.module "MyGame"]
let () = draw ~x:10 ~y:20 ~border:true ()
let () = draw ~x:10 ~y:20 ()
以下のように JavaScript が生成されます:
var MyGame = require('MyGame')
MyGame.draw(10, 20, true)
MyGame.draw(10, 20, undefined)
生成される JavaScript 関数は同じですが、Melange での使い方がより明確になります。
Note: この例では最後の引数の方は unit で、()
をborder
の後に加えなければなりません。なぜなら、border
は最後の位置でオプションの引数だからです。最後の param が unit 型でない場合には警告が出ますが、これはOCaml のドキュメント (opens in a new tab)で詳しく説明されています。
Melange 側で関数を適用する際、ラベル付けされた引数を自由に並べ替えることができることに注意してください。生成されるコードでは、関数宣言時に使用された元の順序が維持されます:
external draw : x:int -> y:int -> ?border:bool -> unit -> unit = "draw"
[@@mel.module "MyGame"]
let () = draw ~x:10 ~y:20 ()
let () = draw ~y:20 ~x:10 ()
以下のように JavaScript が生成されます:
var MyGame = require('MyGame')
MyGame.draw(10, 20, undefined)
MyGame.draw(10, 20, undefined)
Calling an object method
JavaScript のメソッドを呼び出す必要がある場合、Melange にはmel.send
という属性があります。
以下のスニペットでは、ライブラリ
melange.dom
で提供されているDom.element
型を参照します。dune
ファイルに(libraries melange.dom)
をインクルードすることで、プロジェクトに追加することができます:
(* Abstract type for the `document` global *)
type document
external document : document = "document"
external get_by_id : document -> string -> Dom.element = "getElementById"
[@@mel.send]
let el = get_by_id document "my-id"
以下のように JavaScript が生成されます:
var el = document.getElementById('my-id')
mel.send
を使用する場合、第一引数は呼び出したい関数を持つプロパティを保持するオブジェクトになります。これは pipe first 演算子(Ocaml: |.
/ Reason: ->
)とうまく組み合わされます。
バインディングを OCaml の pipe last 演算子|>
で使用するように設計したい場合、代替のmel.send.pipe
属性があります。それを使って上の例を書き換えてみましょう:
(* Abstract type for the `document` global *)
type document
external document : document = "document"
external get_by_id : string -> Dom.element = "getElementById"
[@@mel.send.pipe: document]
let el = get_by_id "my-id" document
mel.send
と同じコードを生成します:
var el = document.getElementById('my-id')
Chaining
この種の API は JavaScript でよく見られます: foo().bar().baz()
。この種の API は、Melange external で 設計することができます。どちらの規約を使用するかによって、2 つの attributes が利用できます:
- データファーストの場合、
mel.send
属性とpipe first 演算子(Ocaml:|.
/ Reason:->
)を組み合わせます。 - データラストの場合、
mel.send.pipe
属性と OCaml のpipe last 演算子|>
を組み合わせます。
Let’s see first an example of chaining using data-first convention with the pipe
first operator |.
->
:
まず、pipe first 演算子(Ocaml: |.
/ Reason: ->
)を使ったデータ・ファーストによるチェインの例を見てみましょう:
(* Abstract type for the `document` global *)
type document
external document : document = "document"
[@@mel.send]
external get_by_id : document -> string -> Dom.element = "getElementById"
[@@mel.send]
external get_by_classname : Dom.element -> string -> Dom.element
= "getElementsByClassName"
let el = document |. get_by_id "my-id" |. get_by_classname "my-class"
以下のように JavaScript が生成されます:
var el = document.getElementById('my-id').getElementsByClassName('my-class')
では、pipe last 演算子 |>
の場合:
(* Abstract type for the `document` global *)
type document
external document : document = "document"
[@@mel.send.pipe: document]
external get_by_id : string -> Dom.element = "getElementById"
[@@mel.send.pipe: Dom.element]
external get_by_classname : string -> Dom.element = "getElementsByClassName"
let el = document |> get_by_id "my-id" |> get_by_classname "my-class"
以下のように pipe first の場合と同じ JavaScript が生成されます:
var el = document.getElementById('my-id').getElementsByClassName('my-class')
Variadic function arguments
JavaScript の関数は任意の数の引数を取ることがあります。このような場合、Melange ではmel.variadic
attribute を external
に付加することができます。ただし、1 つだけ注意点があります。variadic 引数はすべて同じ型に属する必要があります。
external join : string array -> string = "join"
[@@mel.module "path"] [@@mel.variadic]
let v = join [| "a"; "b" |]
以下のように JavaScript が生成されます:
var Path = require('path')
var v = Path.join('a', 'b')
さらにダイナミズムが必要な場合は、OCaml attributes のセクション で説明した OCaml unboxed
(opens in a new tab) attribute を使用して、異なる型の要素を配列に挿入し、ラップされていない JavaScript の値に Melange をコンパイルする方法があります:
type hide = Hide : 'a -> hide [@@unboxed]
external join : hide array -> string = "join" [@@mel.module "path"] [@@mel.variadic]
let v = join [| Hide "a"; Hide 2 |]
以下のようにコンパイルされます:
var Path = require('path')
var v = Path.join('a', 2)
Bind to a polymorphic function
JavaScript ライブラリの中には、引数の型や形が変化する関数を定義しているものがあります。そのような関数にバインドするには、それがどの程度動的かによって 2 つのアプローチがあります。
Approach 1: 複数の external 関数
オーバーロードされた JavaScript 関数が取りうるフォームを数多く列挙できるのであれば、柔軟なアプローチとしては、それぞれのフォームに個別にバインドすることです:
external drawCat : unit -> unit = "draw" [@@mel.module "MyGame"]
external drawDog : giveName:string -> unit = "draw" [@@mel.module "MyGame"]
external draw : string -> useRandomAnimal:bool -> unit = "draw"
[@@mel.module "MyGame"]
3 つの external 関数がすべて同じ JavaScript 関数draw
にバインドされていることに注目してください。
Approach 2: Polymorphic variant + mel.unwrap
場合によっては、関数の引数の数は一定だが、引数の型が異なることがある。このような場合、引数を Variant としてモデル化し、外部でmel.unwrap
attribute を使用することができます。
次の JavaScript 関数にバインドしたいとします:
function padLeft(value, padding) {
if (typeof padding === 'number') {
return Array(padding + 1).join(' ') + value
}
if (typeof padding === 'string') {
return padding + value
}
throw new Error(`Expected string or number, got '${padding}'.`)
}
As the padding
argument can be either a number or a string, we can use
mel.unwrap
to define it. It is important to note that mel.unwrap
imposes
certain requirements on the type it is applied to:
- It needs to be a polymorphic variant (opens in a new tab)
- Its definition needs to be inlined
- Each variant tag needs to have an argument
- The variant type can not be opened (can’t use
>
)
padding
引数は数値でも文字列でもよいので、mel.unwrap
を使って定義することができます。重要なのは、mel.unwrap
が適用される型に一定の要件を課すことです:
- 多相 Variant (opens in a new tab)である必要がある
- 定義がインライン化されていること
- 各 Variant タグは引数を持つ必要がある。
- Variant 型はオープンできない(
>
は使えない)
external padLeft:
string
-> ([ `Str of string
| `Int of int
] [@mel.unwrap])
-> string
= "padLeft"
let _ = padLeft "Hello World" (`Int 4)
let _ = padLeft "Hello World" (`Str "Message from Melange: ")
以下のように JavaScript を生成します:
padLeft('Hello World', 4)
padLeft('Hello World', 'Message from Melange: ')
非共有データ型のセクションで見たように、JavaScript 側に直接 Variant を渡すのは避けるべきです。mel.unwrap
を使うことで、Melange から Variant を使うことができ、JavaScript は Variant 内の生の値を得ることができます。
Using polymorphic variants to bind to enums
JavaScript の API の中には、限られた値のサブセットを入力として受け取るものがあります。例えば、Node のfs.readFileSync
の第 2 引数は、いくつかの指定された文字列値しか取ることができません:ascii
、utf8
などです。他のいくつかの関数は、VS Code API のcreateStatusBarItem
関数のように、alignment
は引数は指定された整数値 1
また 2
(opens in a new tab)のみ取ることができます。
これらの引数を単なるstring
やint
として型付けすることはできますが、JavaScript 関数がサポートしていない値を使って external 関数を呼び出すことを防ぐことはできません。多相 Variant を使って実行時エラーを回避する方法を見てみましょう。
値が文字列の場合、mel.string
attribute を使用することができます:
external read_file_sync :
name:string -> ([ `utf8 | `ascii ][@mel.string]) -> string = "readFileSync"
[@@mel.module "fs"]
let _ = read_file_sync ~name:"xx.txt" `ascii
以下のように JavaScript を生成します:
var Fs = require('fs')
Fs.readFileSync('xx.txt', 'ascii')
このテクニックを mel.as
attribute と組み合わせることで、多相 Variant 値から生成される文字列を変更することができます。例えば:
type document
type style
external document : document = "document"
external get_by_id : document -> string -> Dom.element = "getElementById"
[@@mel.send]
external style : Dom.element -> style = "style" [@@mel.get]
external transition_timing_function :
style ->
([ `ease
| `easeIn [@mel.as "ease-in"]
| `easeOut [@mel.as "ease-out"]
| `easeInOut [@mel.as "ease-in-out"]
| `linear ]
[@mel.string]) ->
unit = "transitionTimingFunction"
[@@mel.set]
let element_style = style (get_by_id document "my-id")
let () = transition_timing_function element_style `easeIn
以下のような JavaScript を生成します:
var element_style = document.getElementById('my-id').style
element_style.transitionTimingFunction = 'ease-in'
Melange は文字列値を生成する以外に、整数値を生成するmel.int
も提供しています。mel.int
はmel.as
と組み合わせることもできます:
external test_int_type :
([ `on_closed | `on_open [@mel.as 20] | `in_bin ][@mel.int]) -> int
= "testIntType"
let value = test_int_type `on_open
この例では、on_closed
は 0 としてエンコードされ、on_open
は attribute mel.as
により 20 となり、in_bin
は 21 となります。なぜなら、Variant タグにmel.as
アノテーションが与えられていない場合、コンパイラは前の値からカウントアップして値を割り当て続けるからです。
以下のような JavaScript を生成します:
var value = testIntType(20)
Using polymorphic variants to bind to event listeners
Polymorphic Variant は、イベントリスナーや他の種類のコールバックなどをラップするためにも使うことができます:
type readline
external on :
readline ->
([ `close of unit -> unit | `line of string -> unit ][@mel.string]) ->
readline = "on"
[@@mel.send]
let register rl =
rl |. on (`close (fun event -> ())) |. on (`line (fun line -> Js.log line))
以下のような JavaScript を生成します:
function register(rl) {
return rl
.on('close', function ($$event) {})
.on('line', function (line) {
console.log(line)
})
}
Constant values as arguments
JavaScript の関数を呼び出して、引数の 1 つが常に一定であることを確認したいことがあります。この場合、[@mel.as]
attribute とワイルドカードパターン _
を組み合わせます:
external process_on_exit : (_[@mel.as "exit"]) -> (int -> unit) -> unit
= "process.on"
let () =
process_on_exit (fun exit_code ->
Js.log ("error code: " ^ string_of_int exit_code))
以下のような JavaScript を生成します:
process.on('exit', function (exitCode) {
console.log('error code: ' + exitCode.toString())
})
mel.as "exit"
とワイルドカードの_
パターンを組み合わせると、Melange は JavaScript 関数の第 1 引数を"exit"
という文字列にコンパイルするように指示します。
次のようにmel.as
引用符で囲まれた文字列を渡すことで、任意の JSON リテラルを使用することもできます: mel.as {json|true|json}
または mel.as {json|{"name":"John"}|json}
Binding to callbacks
OCaml では、すべての関数がとる引数の数(アリティ)は 1 です。つまり、次のような関数を定義すると、アリティは 1 になります:
let add x y = x + y
この関数の型はint -> int -> int
となります。これは、add 1
を呼び出すことでadd
を部分的に適用できることを意味し、add 1
は加算の第 2 引数を期待する別の関数を返します。このような関数は "curried" 関数と呼ばれ、OCaml の currying に関する詳細は "OCaml Programming: Correct + Efficient + Beautiful"のこの章 (opens in a new tab)を参照してください。
これは、すべての関数呼び出しが常にすべての引数を適用するという、JavaScript における関数呼び出しの規約とは相容れないものです。例の続きとして、JavaScript に上のようなadd
関数が実装されているとしましょう:
var add = function (a, b) {
return a + b
}
add(1)
を呼び出すと、関数は完全に適用され、b
の値はundefined
になります。そして、JavaScript は1
とundefined
の値を足そうとするので、結果としてNaN
を得ることになります。
この違いと Melange バインディングへの影響を説明するために、JavaScript 関数のバインディングを次のように書いてみましょう:
function map(a, b, f) {
var i = Math.min(a.length, b.length)
var c = new Array(i)
for (var j = 0; j < i; ++j) {
c[j] = f(a[i], b[i])
}
return c
}
素朴な external 関数宣言は以下のようになります:
external map : 'a array -> 'b array -> ('a -> 'b -> 'c) -> 'c array = "map"
残念ながら、これは完全には正しくありません。問題はコールバック関数にあり、型は'a -> 'b -> 'c
です。つまり、map
は上記のadd
のような関数を期待することになります。しかし、OCaml では、2 つの引数を持つということは、1 つの引数を取る関数を 2 つ持つということなのです。
問題をもう少し明確にするために、add
を書き換えてみましょう:
let add x = let partial y = x + y in partial
以下のようにコンパイルされます:
function add(x) {
return function (y) {
return (x + y) | 0
}
}
ここで、もし external 関数map
をmap arr1 arr2 add
と呼んでadd
関数と一緒に使ったら、期待通りには動かないでしょう。JavaScript の関数の適用は OCaml と同じようにはいかないので、map
実装の関数呼び出し、f(a[i],b[i])
は、引数x
を 1 つしか取らない JavaScript の外部関数add
に適用され、b[i]
は捨てられるだけです。この操作から返される値は、2 つの数値の加算ではなく、内側の匿名コールバックとなります。
OCaml と JavaScript の関数とそのアプリケーションの間のこのミスマッチを解決するために、Melange は「uncurried」である必要がある外部関数をアノテートするために使用できる特別な attribute @u
を提供しています。
Reason 構文では、Reaon のパーサーと深く連携しているため、この attribute は明示的に書く必要はありません。"uncurried"として関数をマークしたい場合、関数の型に.
を追加します。('a, 'b) => 'c
の代わりに(. 'a, 'b) => 'c
と書きます。
上の例では:
external map : 'a array -> 'b array -> (('a -> 'b -> 'c)[@u]) -> 'c array
= "map"
ここで('a -> 'b -> 'c [@u])
(Reason: (. 'a, 'b) => 'c
)はアリティ 2 であると解釈されます。一般に、'a0 -> 'a1 ... 'aN -> 'b0 [@u]
は'a0 -> 'a1 ... 'aN -> 'b0
と同じですが、前者はアリティが N であることが保証されているのに対し、後者は未知です。
add
を使ってmap
を呼び出そうとすると、次のようになります:
let add x y = x + y
let _ = map [||] [||] add
以下ようなエラーが起こります:
let _ = map [||] [||] add
^^^
This expression has type int -> int -> int
but an expression was expected of type ('a -> 'b -> 'c) Js.Fn.arity2
これを解決するために、関数定義にも@u
(Reason: .
)を追加します:
let add = fun [@u] x y -> x + y
たくさんの external 関数を書く場合、関数定義の注釈はかなり面倒になります。
この冗長さを回避するために、Melange はmel.uncurry
という別の attribute を提供しています。
先ほどの例でどのように使えるか見てみましょう。u
をmel.uncurry
に置き換えるだけです:
external map :
'a array -> 'b array -> (('a -> 'b -> 'c)[@mel.uncurry]) -> 'c array = "map"
通常のadd
関数でmap
を呼び出そうとすると、次のようになります:
let add x y = x + y
let _ = map [||] [||] add
追加する attribute を追加することなく、すべてがうまく機能するようになりました。
u
とmel.uncurry
の主な違いは、後者が external のみで動作することです。mel.uncurry
はバインディングに使用する推奨オプションであり、u
はパフォーマンスが重要で、OCaml の関数から生成された JavaScript 関数を部分的に適用しないようにしたい場合に有用です。
Modeling this
-based Callbacks
多くの JavaScript ライブラリには、例えばthis
キーワード (opens in a new tab)に依存するコールバックがあります:
x.onload = function (v) {
console.log(this.response + v)
}
x.onload
コールバックの内部では、this
はx
を指していることになります。x.onload
をunit -> unit
型で宣言するのは正しくありません。その代わりに、Melange は特別な属性mel.this
を導入しています:
type x
external x : x = "x"
external set_onload : x -> ((x -> int -> unit)[@mel.this]) -> unit = "onload"
[@@mel.set]
external resp : x -> int = "response" [@@mel.get]
let _ =
set_onload x
begin
fun [@mel.this] o v -> Js.log (resp o + v)
end
以下のような JavaScript を生成します:
x.onload = function (v) {
var o = this
console.log((o.response + v) | 0)
}
第 1 引数はthis
ために予約されることに注意してください。
Wrapping returned nullable values
JavaScript ではnull
とundefined
は異なるモデルで扱われますが、Melange ではどちらも'a option
(Reason: option('a)
)として扱うと便利です。
Melange は null になり得る戻り値の型をバインディング境界でどのようにラップするかをモデル化するために、external の値mel.return
attribute を認識します。mel.return
を持つexternal
値は戻り値をoption
型に変換し、Js.Nullable.toOption
のような関数による余分なラッピング/アンラッピングの必要性を回避します。
type element
type document
external get_by_id : document -> string -> element option = "getElementById"
[@@mel.send] [@@mel.return nullable]
let test document =
let elem = get_by_id document "header" in
match elem with
| None -> 1
| Some _element -> 2
以下のような JavaScript を生成します:
function test($$document) {
var elem = $$document.getElementById('header')
if (elem == null) {
return 1
} else {
return 2
}
}
上の[@mel.return nullable]
(Reason: [@mel.return nullable]
)のように、mel.return
attribute は attribute ペイロードを取ります。現在、null_to_opt
、undefined_to_opt
、nullable
、identity
の 4 つのディレクティブがサポートされています。
nullable
は、null
やundefined
をoption
型に変換するので、推奨されます。
identity
は、コンパイラが戻り値に対して何もしないようにします。これはほとんど使われませんが、デバッグのために紹介します。
Generate getters, setters and constructors
前のセクションで見たように、Melange には JavaScript から操作するのが簡単ではない値にコンパイルされる型があります。このような型の値と JavaScript コードからの通信を容易にするために、Melange には変換関数を生成するのに役立つderiving
attribute と、これらの型から値を生成する関数が含まれています。特に、Variant と polymorphic variants についてです。
さらに、deriving
はレコード型で使用することができ、setter や getter、作成関数を生成することができます。
Variants
Creating values
Variant 型の @deriving
アクセサを使用して、各ブランチのコンストラクタ値を作成します。
type action =
| Click
| Submit of string
| Cancel
[@@deriving accessors]
Melange は各 Variant タグに 1 つのlet
定義を生成し、以下のように実装します:
- ペイロードを持つ Variant タグでは、ペイロード値をパラメータとする関数になります
- ペイロードを持たない Variant タグでは、タグの実行時の値を持つ定数になります
- 上記のアクションタイプ定義に
deriving
のアノテーションを付けると、Melange は以下のようなコードを生成します:
type action =
| Click
| Submit of string
| Cancel
let click = (Click : action)
let submit param = (Submit param : action)
let cancel = (Cancel : action)
コンパイル後の JavaScript コードは以下のようになります:
function submit(param_0) {
return /* Submit */ {
_0: param_0,
}
}
var click = /* Click */ 0
var cancel = /* Cancel */ 1
生成された定義は小文字であり、JavaScript コードから安全に使用できることに注意してください。例えば、上記の JavaScript で生成されたコードがgenerators.js
ファイルにあった場合、定義は次のように使うことができます:
const generators = require('./generators.js')
const hello = generators.submit('Hello')
const click = generators.click
Conversion functions
Variant 型で @deriving jsConverter
を使うと、JavaScript の整数と Melange の Variant 値を行き来できるコンバータ関数を作成できます。
@deriving accessors
にはいくつかの違いがあります:
jsConverter
はmel.as
attribute と連動しますが、accessors
は連動しませんjsConverter
はペイロードを持つ Variant タグをサポートしていませんjsConverter
は値を前後に変換する関数を生成するが、accessors
は値を生成する関数を生成します
上記の制約を考慮した上で、jsConverter で動作するように適合させた前の例のバージョンを見てみましょう:
type action =
| Click
| Submit [@mel.as 3]
| Cancel
[@@deriving jsConverter]
これにより、以下の型の関数がいくつか生成されます:
val actionToJs : action -> int
val actionFromJs : int -> action option
actionToJs
はaction
型の値から整数を返します。これは、多相 Variant でmel.int
を使うときに説明したのと同じ方法で、Click
は 0 から始まり、Submit
は 3(mel.as
でアノテーションされているため)、そしてCancel
は 4 となります。
actionFromJs
はoption
型の値を返しますが、これはすべての整数がaction
型の Variant タグに変換できるわけではないからです。
Hide runtime types
型安全性を高めるために、jsConverter { newType }
のペイロードを@deriving
で使用することで、生成される関数から Variant(int
)の実行時表現を隠すことができます:
type action =
| Click
| Submit [@mel.as 3]
| Cancel
[@@deriving jsConverter { newType }]
この機能は、JavaScript の実行時表現を隠すために抽象型に依存しています。以下の型を持つ関数を生成します:
val actionToJs : action -> abs_action
val actionFromJs : abs_action -> action
actionFromJs
の場合、前のケースとは異なり、戻り値はoption
型ではありません。これは "correct by construction"の例であり、abs_action
を作成する唯一の方法はactionToJs
関数を呼び出すことです。
Polymorphic variants
@deriving jsConverter
attribute は多相 Variant にも適用できます。
NOTE: Variant と同様に、
@deriving jsConverter
attribute は Polymorphic Variant タグがペイロードを持っているときは使えません。JavaScript で多相 Variant がどのように表現されるかについては、実行時の表現のセクションを参照してください。
例を見てみましょう:
type action =
[ `Click
| `Submit [@mel.as "submit"]
| `Cancel
]
[@@deriving jsConverter]
Variant の例と同様に、以下の 2 つの関数が生成されます:
val actionToJs : action -> string
val actionFromJs : string -> action option
jsConverter { newType }
ペイロードは多相 Variant でも使用できます。
Records
Accessing fields
レコード型の@deriving accessors
を使用して、レコード・フィールド名のアクセサ関数を作成します。
type pet = { name : string } [@@deriving accessors]
let pets = [| { name = "Brutus" }; { name = "Mochi" } |]
let () = pets |. Belt.Array.map name |. Js.Array.join ~sep:"&" |. Js.log
Melange はレコードに定義されたフィールドごとに関数を生成します。この場合、pet
型のレコードからそのフィールドを取得できる関数 name
となります:
let name (param : pet) = param.name
以上のことを考慮すると、出来上がる JavaScript はこうなります:
function name(param) {
return param.name
}
var pets = [
{
name: 'Brutus',
},
{
name: 'Mochi',
},
]
console.log(Belt_Array.map(pets, name).join('&'))
Convert records into abstract types
JavaScript のオブジェクトにバインドする場合、Melange はオブジェクトを実行時の表現として正確に使用するため、一般的にはレコードを使用することをお勧めします。この方法については、JavaScript オブジェクトへのバインディングのセクションで説明しました。
しかし、レコードだけでは不十分な場合があります。JavaScript オブジェクトを生成する際に、キーが存在したり、存在しなかったりする場合があります。
例えば、次のようなレコードを考えてみましょう:
type person = {
name : string;
age : int option;
}
このユースケースの例は、{ name = "John"; age = None }
を期待して、age
キーが現れない{name: "Carl"}
のような JavaScript を生成することです。
@deriving abstract
attribute はこの問題を解決するために存在します。レコード型に存在する場合、@deriving abstract
はレコード定義を抽象化し、代わりに以下の関数を生成します:
- 型の値を作成するコンストラクタ関数
- レコード・フィールドにアクセスするための getter/setter
@deriving abstract
は、OCaml 型定義の attribute アノテーションから派生した(生成された)関数のセットによってのみ、レコード型の JavaScript オブジェクトを効果的にモデル化します。
次の Melange のコードを考えてみてください:
type person = {
name : string;
age : int option; [@mel.optional]
}
[@@deriving abstract]
Melange will generate a constructor to create values of this type. In our example, the OCaml signature would look like this after preprocessing:
type person
val person : name:string -> ?age:int -> unit -> person
Melange はperson
型を抽象化し、コンストラクタ、ゲッター、セッター関数を生成します。この例では、前処理後の OCaml シグネチャーは以下のようになります:
type person
val person : name:string -> ?age:int -> unit -> person
val nameGet : person -> string
val ageGet : person -> int option
person
関数を使用すると、person
の値を作成することができます。Melange はこの型を抽象化しているため、この型の値を作成する唯一の方法です。{ name = "Alice"; age = None }
のようなリテラルを直接使っても、型チェックは行われません。
以下に使用例を示します:
let alice = person ~name:"Alice" ~age:20 ()
let bob = person ~name:"Bob" ()
これにより、以下の JavaScript コードが生成されます。JavaScript のランタイム・オーバーヘッドがないことに注意してください:
var alice = {
name: 'Alice',
age: 20,
}
var bob = {
name: 'Bob',
}
person
関数はラベル付き引数を使用してレコード・フィールドを表す。オプションの引数age
があるため、unit
型の最後の引数を取ります。このラベル付きでない引数により、関数適用時にオプションの引数を省略することができる。オプションのラベル付き引数の詳細については、OCaml マニュアルの該当セクション (opens in a new tab)を参照してください。
関数nameGet
とageGet
は各レコード・フィールドのアクセサです:
let twenty = ageGet alice
let bob = nameGet bob
以下のような JavaScript を生成します:
var twenty = alice.age
var bob = bob.name
この関数は、モジュール内の他の値と衝突する可能性を防ぐために、レコードのフィールド名に Get
を付加して命名されます。getter 関数に短い名前を付けたい場合は、 { abstract = light }
という別のペイロードをderiving
に渡すことができます:
type person = {
name : string;
age : int;
}
[@@deriving abstract { light }]
let alice = person ~name:"Alice" ~age:20
let aliceName = name alice
以下のような JavaScript を生成します:
var alice = {
name: 'Alice',
age: 20,
}
var aliceName = alice.name
この例では、ゲッター関数はオブジェクト・フィールドと同じ名前を共有しています。前の例とのもう 1 つの違いは、このケースではオプションのフィールドを除外しているため、person コンストラクタ関数が最後のユニット引数を必要としなくなったことです。
Note:
mel.as
attribute は、レコード型がderiving
でアノテーションされている場合でもレコードフィールドに適用することができ、静的な形状を持つオブジェクトへのバインディングのセクションで示したように、結果の JavaScript オブジェクトのフィールド名を変更することができます。しかし、実行時の表現を配列に変更するためにmel.as
デコレータにインデックスを渡すオプション([@mel.as "0"]
のような)は、deriving
を使用している時には使用できません。
Compatibility with OCaml features
@deriving getSet
attribute とその軽量版は、Melange が OCaml から受け継いだ機能であるミュータブル・フィールドとプライベート型と一緒に使うことができます。
レコード型にミュータブルフィールドがある場合、Melange はそのための setter 関数を生成します。例えば:
type person = {
name : string;
mutable age : int;
}
[@@deriving getSet]
let alice = person ~name:"Alice" ~age:20
let () = ageSet alice 21
以下のような JavaScript を生成します:
var alice = {
name: 'Alice',
age: 20,
}
alice.age = 21
インターフェイスファイルで mutable
キーワードが省略された場合、Melange はモジュールのシグネチャに setter 関数を含めず、他のモジュールがその型の値を変更できないようにします。
プライベート型を使用すると、Melange がコンストラクタ関数を作成しないようにすることができます。たとえば、person
型を private として定義するとします:
type person = private {
name : string;
age : int;
}
[@@deriving getSet]
アクセサ nameGet
と ageGet
は生成されますが、コンストラクタ person
は生成されません。これは JavaScript オブジェクトにバインドする際に便利で、Melange コードがそのような型の値を生成するのを防ぎます。
Use Melange code from JavaScript
ビルドシステムのセクションで述べたように、Melange は CommonJS と ES6 モジュールの両方を生成できます。どちらの場合も、手書きの JavaScript ファイルから Melange が生成した JavaScript コードを使用すると、期待通りに動作します。
以下の定義です:
let print name = "Hello" ^ name
CommonJS(デフォルト)を使用する場合、この JavaScript コードが生成されます:
function print(name) {
return 'Hello' + name
}
exports.print = print
ES6 を使用する場合(melange.emit
の(module_systems es6)
フィールドを通して)、このコードが生成されます:
function print(name) {
return 'Hello' + name
}
export { print }
つまり、JavaScript ファイルのprint
値をインポートするには、require
かimport
のどちらかを使います(選択したモジュールシステムによって異なります)。
Default ES6 values
ES6 モジュールで JavaScript のインポートを扱う場合、次のような特殊なケースが発生します:
import ten from 'numbers.js'
このインポートは、numbers.js
がデフォルトのエクスポートを持つことを期待しています:
export default ten = 10
Melange からこのようなエクスポートをエミュレートするには、default
値を定義します。
例えば、numbers.ml
(Reason: numbers.re
)というファイルの場合:
let default = 10
そうすることで、Melange はdefault
のエクスポートに値を設定し、JavaScript 側でデフォルトのインポートとして使用できるようになります。