v2.2.0
Communicate with JavaScript

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'

alertdeprecatedのような他の OCaml 組み込み attributes も Melange で使用できます。

新しい attributes を定義する

2 つ目のアプローチは、JavaScript オブジェクトのプロパティにバインドするために使用されるmel.set属性のような、Melange のために特別に設計された新しい属性を導入することです。Melange によって導入された属性の完全なリストは、ここにあります。

Attribute アノテーションは、コード内の配置と、どの種類のシンタックス・ツリー・ノードにアノテーションするかによって、1 文字、2 文字、または 3 文字の@を使用することができます。アトリビュートの詳細については、OCaml のマニュアル・ページ (opens in a new tab)を参照してください。

Melange の attribute であるmel.setmel.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

バージョン 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 の値に変換されます:

MelangeJavaScript
intnumber
nativeintnumber
int32number
floatnumber
stringstring
arrayarray
tuple (3, 4)array [3, 4]
boolboolean
[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 Noneundefined
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}
int64array of 2 elements [high, low] high is signed, low unsigned
char'a' -> 97 (ascii code)
bytesnumber 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_listArray.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 は、レコード、フィールド、引数、関数などの場所で使用されます:

Extension nodes:

これらの拡張ノードを使用するには、melange PPX プリプロセッサをプロジェクトに追加する必要があります。そのためには、dune ファイルに以下を追加してください:

(library
 (name lib)
 (modes melange)
 (preprocess
   (pps melange.ppx)))

同じフィールドのpreprocessmelange.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 の値idundefinedかどうかをチェックし、それに応じて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 のオブジェクトは、さまざまなユースケースで使用されます:

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で、'aOCaml のオブジェクト (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.objextensionを使って 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.getmel.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_indexmel.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))で定義されており、getsetなどの操作が可能です。

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.newmel.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: setTimeoutclearTimeoutのバインディングは、ここでは学習のために示していますが、これらはすで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:

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 引数は、いくつかの指定された文字列値しか取ることができません:asciiutf8などです。他のいくつかの関数は、VS Code API のcreateStatusBarItem関数のように、alignmentは引数は指定された整数値 1 また 2 (opens in a new tab)のみ取ることができます。

これらの引数を単なるstringintとして型付けすることはできますが、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.intmel.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 は1undefinedの値を足そうとするので、結果として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 関数mapmap 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 を提供しています。

先ほどの例でどのように使えるか見てみましょう。umel.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 を追加することなく、すべてがうまく機能するようになりました。

umel.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コールバックの内部では、thisxを指していることになります。x.onloadunit -> 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 ではnullundefinedは異なるモデルで扱われますが、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_optundefined_to_optnullableidentityの 4 つのディレクティブがサポートされています。

nullableは、nullundefinedoption型に変換するので、推奨されます。

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にはいくつかの違いがあります:

  • jsConvertermel.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

actionToJsaction型の値から整数を返します。これは、多相 Variant でmel.intを使うときに説明したのと同じ方法で、Clickは 0 から始まり、Submitは 3(mel.asでアノテーションされているため)、そしてCancelは 4 となります。

actionFromJsoption型の値を返しますが、これはすべての整数が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)を参照してください。

関数nameGetageGetは各レコード・フィールドのアクセサです:

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]

アクセサ nameGetageGet は生成されますが、コンストラクタ 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値をインポートするには、requireimportのどちらかを使います(選択したモジュールシステムによって異なります)。

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 側でデフォルトのインポートとして使用できるようになります。