Edited at
ElmDay 12

ElmのNative moduleを書く

More than 3 years have passed since last update.

Elmとjavascriptのやりとりには、どちらかと言えば高レベルなportを使用するものの他に、

低レベルで、PureScript等で言うffiに相当する、Nativeモジュールという機能が有ります。

Elmのcoreモジュールやelm-htmlなど他のjavascriptライブラリとのやりとりにはこれを使っています。

Elmはまだ発展途上でcoreモジュールに足りない機能も多々有るので、Nativeモジュールは書ける方が良いでしょう。

本稿では現時点でcoreモジュールに無い、takeWhileの実装を通じてNativeモジュールの書きかたを解説します。


開発開始

なにはともあれelm-makeコマンドを使ってプロジェクトの雛形を作りましょう。

$ elm-make

Some new packages are needed. Here is the upgrade plan.

Install:
elm-lang/core 1.0.0

Do you approve of this plan? (y/n) y
Downloading elm-lang/core
Packages configured successfully!
Compiled 32 files


javascriptを書く

まずjavascriptを書きましょう。

NativeモジュールはNative以下に配置する様に決まっているので、今回はNative.ListProcとでもしておきましょう。以下が雛形となります。


Native/ListProc.js

Elm.Native.ListProc = {};

Elm.Native.ListProc.make = function(elm) {
elm.Native = elm.Native || {};
elm.Native.ListProc = elm.Native.ListProc || {};
if (elm.Native.ListProc.values) return elm.Native.ListProc.values;

/* ここに実装を書く */

return elm.Native.ListProc.values = {
/* ここにエクスポートする関数を列挙する */
};
};



log関数を書く

いきなり2引数かつ高階関数は難易度が高いので、引数をconsoleに出力して、そのまま返すlog : String -> String関数を書いてみましょう。

まずは実装部分です


log.js

    function log (x) {

console.log(x);
return x;
}

これはまぁ自明ですね。これをエクスポートしましょう。


export.js

    return elm.Native.ListProc.values = {

log: log
};

これでlog関数についてのjavascript部分の記述は完了しました。


ラッパーを書く

これをラップして型安全にするelmのモジュールを書きましょう。


ListProc.elm

module ListProc where

import Native.ListProc

log : String -> String
log = Native.ListProc.log


何も解説する事は無いかと思います。適切な型を教えてあげるだけでOKです。


main

このlog関数を使用するmain関数を定義して実行してみましょう。


main.elm

import ListProc

import Text(plainText)

main = plainText <| ListProc.log "Hello, World!"


$ elm-make main.elm --output main.html

Error when searching for modules imported by module 'ListProc':
Could not find module 'Native.ListProc'

Potential problems could be:
* Misspelled the module name
* Need to add a source directory or new dependency to elm-package.json

警告が出ました。どうやらNativeモジュールを使用するにはelm-package.jsonで宣言する必要がある様です。

elm-package.jsonを以下の様に変更しましょう。


elm-package.json.diff

--- elm-package.json.bak    2014-12-12 23:40:42.000000000 +0900

+++ elm-package.json 2014-12-12 23:40:54.000000000 +0900
@@ -7,7 +7,8 @@
"."
],
"exposed-modules": [],
+ "native-modules": true,
"dependencies": {
"elm-lang/core": "1.0.0 <= v < 2.0.0"
}
}

$ elm-make main.elm --output main.html

Compiled 2 files
Successfully generated main.html

無事コンパイル出来たのでmain.htmlを開き、Hello, World!と表示されるとともに、consoleに出力されている事を確認してください。

さて、これでとりあえずNativeモジュールの書きかたは解ったので、サクっとtakeWhileを実装しましょう。


takeWhileString

ひとまずStringのみを取るtakeWhileString : (Char -> Bool) -> String -> Stringを書きましょう

まずは実装部分です。


takeWhileString.js

    // 別のモジュールを使用する時はこの様にする。

// Elm.Native.UtilsにはElmの型を構築する関数などが入っている。
var Utils = Elm.Native.Utils.make(elm);

function takeWhileString(p, l) {
for(var i = 0; i < l.length; i++) {

// Utils.chrを用いてChar型にする。
// 1引数関数にはそのまま引数を渡せばOK
// 2引数以上ならA2(f, a1, a2)の様にA?関数に渡す。
if(!p(Utils.chr(l[i]))) {
return l.slice(0,i);
}
}
return l;
}


次にこれをエクスポートする記述を足しましょう。


export.js

    return elm.Native.ListProc.values = {

// 2引数以上の時はF?関数で包む。
takeWhileString: F2(takeWhileString)
};

そしてラッパーを書いて、


ListProc.elm

module ListProc where

import Native.ListProc

takeWhileString : (Char -> Bool) -> String -> String
takeWhileString = Native.ListProc.takeWhileString


メインを変更してテストしましょう。


main.elm

import ListProc

import Text(plainText)

main = plainText <| ListProc.takeWhileString ((/=) ' ') "Hello, World!"


$ elm-make main.elm --output test.html

Compiled 1 file
Successfully generated test.html

生成されたtest.htmlを開くと"Hello,"と表示されると思います。これでtakeWhileStringの実装が完了しました。

Listに対する実装は割愛させていただきます。

Elmの多相型にはnumber(Int,Float)/comparable(Int,Float,Char,String,lists,tuples)/appendable(String,List,Text)の3種類があります。

takeWhileの場合appendableを取る様に出来そうですが、エラーメッセージが固定で(++)がどうこうと出るので、特段の理由が無い限りは多相型としない方が良さそうです。

List.takeWhile, String.takeWhileの様にモジュールで分けるべきでしょう。


おわりに

以上、駆け足ではありますが、elmのNativeモジュールの書きかたをざっと流しました。

インラインでffiを書けるpurescript等の様な手軽さは有りませんが、jsとして分離するのでエディタの支援が容易に受けられる、他のaltJSで記述できる、などの利点があると思います。

足りない機能はどんどんNativeモジュールで実装しましょう!!

今回のコードはこちらにまとめてあります。