はじめに
PureScriptはHaskellライクな純粋関数型AltJSです。
PureScriptにはHigher Kinded PolymorphismやExtensible effects、Rank N Typesなどの強力な型機能があり、JavaScriptをそのまま書くのに比べて劇的にプログラミングが楽になります。
しかしPureScriptはJavaScriptの関数を使うAltJSなので、どこかで既存のライブラリなどを呼び出す必要があります。
そして静的な型のないJavaScriptの関数をどのように純粋関数型の世界に持ってくるのかということに頭を悩まされることになります。
この記事では以下のことを説明します。
- PureScriptのFFIの基本
- FFIを書くのに便利なモジュールを紹介
- JavaScriptの関数が副作用を持つ場合のFFIの書き方の注意点
- JavaScriptの関数が副作用のある場合に、より簡単に型を付ける方法(拙作モジュールの紹介)
FFIはPureScriptを実際に使う上で最初の壁になると思われるので、少しでもその壁を越える手助けになれば幸いです。
本当はさらにpurescript-affモジュールを使って非同期モナドにする方法も紹介したかったのですが、ここまででかなり長くなってしまったので、purescript-affについては次の記事で紹介したいと思います。
PureScriptのFFIの基本
まず、PureScriptの言語機能のFFIについて見直してみます。
PureScript 0.7でFFIの部分が大きく変更されたので、前にPureScriptをちょっと勉強したことあるという人ももう一度確認してみてください。
PureScript 0.7から FFIの方式がインラインでおこなう方法から別ファイルに分離 になりました。
これは将来的にPureScriptがJavaScript以外のバックエンドを持つ場合に備えて、明示的なJavaScriptへの依存を減らすためらしいですが、JavaScript部分が別ファイルになっていたほうがエディタの入力補助などを受けられてよいと思います。
ではPureScriptのFFIの基本について、我々の聖典である『PureScript by Example』から具体例を引用してみましょう。
ここではJavaScriptの文字列をURIエンコードするencodeURIComponent
という関数をPureScriptから使えるようにしてみます。
まずPureScript側は型だけを定義します。
module Data.URI where
foreign import encodeURIComponent :: String -> String
JavaScript側は同名の関数をエクスポートします。
ここで// module Data.URI
というコメントは意味を持つので注意してください。
ちゃんと同じモジュール名を指定しないといけません。
"use strict";
// module Data.URI
exports.encodeURIComponent = encodeURIComponent;
試しにこの関数をREPLで動かしてみましょう。
bower.json はたとえばこんな感じになります。
{
"name": "ffi-example",
"devDependencies": {
"purescript-console": "^0.1.0"
}
}
PureScript 0.7から今まで以上に色々モジュール分離がおこなわれ、psciで実行した結果を表示するためのpurescript-consoleまで標準では使えなくなってしまいました。
なのでpurescript-consoleをdevDependenciesに入れる必要があります(この機能くらいpsciに入れてくれればよいのでは、と思ってしまいますが…)。
ファイル構成は以下のようになります。
ffi-example
├── bower.json
└── src
└── Data
├── URI.js
└── URI.purs
これでREPLを起動すると、
$ bower i && pulp psci
以下ように動作を確認することができます。
> Data.URI.encodeURIComponent "http://www.nicovideo.jp"
"http%3A%2F%2Fwww.nicovideo.jp"
これでPureScript上で文字列をURIエンコードすることができました。
これがPureScriptの言語機能のFFIになります。
余談 pulp について
余談ですが、最近PureScript界隈ではビルドツールにpulpを使うことが多くなりました。
gulpやgruntを使う場合と違い、ビルド用のタスクを書かなくてもPureScriptのコンパイルができるのに加えて、browserify機能やウォッチ機能も最初から付いており、とても楽に使うことができます。
またpulpは標準のPureScriptツールの機能も便利に使えるようになっています。
たとえばpulp init
でプロジェクトを生成すればpurescript-consoleが依存に含まれているのですぐにREPLを起動することができます。
また上でREPLを起動したときのようにpulp psci
を使えば.psciファイルを生成して、プロジェクトのPureScriptを読み込んだ状態でpsciを起動してくれますし、
pulp docs
ではpsc-docsに適切に引数を与えてドキュメントを生成してくれます。
たとえば上記のモジュールではpulp docs
だけで
## Module Data.URI
#### `encodeURIComponent`
``` purescript
encodeURIComponent :: String -> String
```
のようなドキュメントが生成されます。
ライブラリを自作したらついでにドキュメントも生成して公開するとよいでしょう。
最近pulpが標準的に使われるようになったのは上記のようにPureScriptを使う上で非常に便利というのもあると思いますが、
JavaScript界隈で個々のツールが多機能化する一方で、gulpやgruntなどのビルドツールを使わずにnpm run
だけでタスクを実行しようという流れがあるので、その影響もあると思います。
PureScriptでも汎用のビルドツールを使って面倒になるより、専用のpulpを使ったほうが楽でいいということなんでしょうね。
FFIを書くのに便利なモジュールを紹介
purescript-functions
次に標準ライブラリのpurescript-functionsの紹介をします。
purescript-functionsは多引数の関数を扱うためのモジュールで、FFIを書くときに役に立ちます。
まず基本的なことですが、PureScriptの世界の関数はすべてカリー化されています。つまり一引数の関数しか扱うことができません。
なのでPureScriptの言語標準機能だけでJavaScriptの複数の引数を持つ関数を使いたい場合はいちいちFFIを使ってJavaScriptの関数をラッピングする必要があります。
たとえば2引数のJavaScriptの関数をPureScriptで使うためには、以下のようにJavaScriptのラッパー関数を書く必要があります。
exports._parseInt = function(s) {
return function(r) {
return parseInt(s, r);
};
};
これをPureScriptで以下のように型付けすると、
foreign import _parseInt :: String -> Int -> Int
やっと使えるようになるわけです。
> _parseInt "ff" 16
255
しかし引数の渡し方が違うという理由で、部分適用したいわけでもないのに、いちいちカリー化するようなラッピング関数を記述するのは面倒です。
このために使うのがpurescript-functionsです。
purescript-functionsのFn2
という型を使うとparseInt
に直接型を付けることができます。
import Data.Function
foreign import parseInt :: Fn2 String Int Boolean
Fn2を使うとJavaScriptのコードは
exports._parseInt = parseInt;
まで短くすることができます。
この関数はrunFn2
を使うと実行することができます。
> runFn2 _parseInt "ff" 16
255
runFn2をいちいち使うのは面倒なので普通のPureScript関数でラッピングすることになるでしょう。
parseInt :: String -> Int -> Int
parseInt s r = runFn2 _parseInt s r
このように最終的に使いやすいようにラッピングする必要はあるかもしれませんが、JavaScriptのレベルでやるのとPureScriptでやるのでは型や表現力の差で書きやすさに大きな違いがあると思います。
逆にPureScriptの関数を複数の引数を持つ関数として使いたい場合(たとえばJavaScriptのコールバックに渡したい場合など)は mkFn
という関数を使うとPureScriptの関数を非カリー化(uncurrying)することができます。
たとえば以下の divides
というPureScriptの関数をmkFn2
を使って定義した場合、
import Data.Function
import Math
import Prelude
divides :: Fn2 Number Number Boolean
divides = mkFn2 $ \n m -> m % n == 0.0
このコードをJavaScriptにコンパイルすると以下のような2引数の関数になります。
var divides = function(n, m) {
return $$Math["%"](m)(n) === 0.0;
};
このようにpurescript-functionsを使うとJavaScriptの記述量を減らすことができ、場合によってはコンパイルで生成されるJavaScriptの量も少なくすることができます。
purescript-nullable
次に小さなモジュールですが、purescript-nullableを紹介します。
null値の扱いは様々な言語で問題になりますが、PureScriptでもJavaScriptのnullをうまく扱う必要があります。
関数型プログラミングではnullになるかもしれない値をOption型やMaybe型のようなもので表現することが一般的ですが、
PureScriptでもFFIによりMaybe型にラッピングすることが基本になると思います。
しかし、JavaScriptのnullに対して直接型付けする方法もあります。
それがこの節で紹介するpurescript-nullableモジュールのNullable
です。
ここではDOMの中からIDで検索しエレメントを取得することができるJavaScriptのgetElementById
関数にPureScriptの型を与えることを考えてみましょう。
DOMの型を全部定義すると大変なのでpurescript-domモジュールから借りてくることにしましょう。
注意すべきなのはgetElementById
は要素が見つからない場合nullを返す関数ということです。
なので返り値をただのElement
にすると、要素が見つからなかった場合に実行時エラーが起きることになります。
よって最後に返される型をpurescript-nullableのNullableを使ってNullable Element
にしてみます。
すると以下のような型付けが考えられます。
import Control.Monad.Eff
import Data.Nullable
import DOM(DOM())
import DOM.Node.Types(Document(), Element())
foreign import getElementById :: forall eff. String -> Document -> Eff (dom :: DOM | eff) (Nullable Element)
JavaScript側は以下のようになります。
exports.getElementById = function(id) {
return function(document) {
return function() {
return document.getElementById(id);
};
};
};
JavaScript側では特に何もせずにgetElementByIdを普通に呼び出しているだけです。
つまりnullを返すかもしれないということがPureScriptの型で表現されているだけなのです。
しかし、このNullable Element
はそのまま使うことはできないので、
purescript-nullableのtoMaybe関数を使ってMaybe型に変換して使うことになります。
toMaybe <$> getElementById "hoge" document // たとえば Just <div id="hoge">...</div> のような返り値になる
purescript-functionsのFn
とrunFn
もそうでしたが、このようにPureScriptで直接扱いづらいJavaScriptのコードはforeign import
で外部型を定義し、
その中にデータを入れて、必要なときにrunXXX
やtoXXX
などの関数で取り出すという方法が有効です。
JavaScriptの関数が副作用を持つ場合のFFIの書き方の注意点
purescript-functionsはたいへん便利なんですが、一つ注意点がありまして、Eff関数は直接型付けできません 。
実はPureScriptの普通の関数とEffの関数とではJavaScriptになったときの形がちょっと異なります。
上記のgetElementByIdのラッピング関数をもう一度見てみましょう。
exports.getElementById = function(id) {
return function(document) {
return function() {
return document.getElementById(id);
};
};
};
よく見ると最後の返り値をfunction ()
でラッピングしていることがわかると思います。
getElementByIdは副作用を持ち、Eff (dom :: DOM | eff) (Nullable Element)
の値を返すわけですが、
このように返り値がEffになる場合は、function ()
でラッピングしなければならないのがPureScriptのルールです。
よってpurescript-functionsモジュールを使って直接型を付けることはできないということになります。
『PureScript by Example』でも「10.15 Representing Side Effects」でそのことに触れられています。
JavaScriptのMath.random
関数を使いたい場合、PureScriptでは
foreign import random :: forall eff. Eff (random :: RANDOM | eff) Number
という型を付けて、JavaScriptではfunction()
でラッピングしています。
exports.random = function() {
return Math.random();
};
またPureScriptのEff関数をコールバックとしてJavaScriptに渡す場合でも問題が発生します。
PureScriptのEff関数はJavaScriptになったときにfunction()
が外側に付いてしまっているので、動作がおかしいことになるのです。
この解決策もFFIのJavaScriptでおこなうことになります。
たとえばコールバックを引数にとるJavaScriptの関数readFile
があったとして、これに対してPureScriptのEff関数callback
をコールバックとして与えたいという場合を考えます。
PureScriptの型は以下のようになるでしょう。
foreign import readFile :: forall eff. String -> (String -> Eff (fs :: FS | eff) Unit) -> Eff (fs :: FS | eff) Unit
これに対してJavaScript側は以下のようにコールバックの呼び出し方に工夫が必要になります。
exports.readFile = function(filename) {
return function(callback) {
return function() {
readFile(filename, function (data) {
callback(data)();
};
};
};
};
注目してほしいのはcallback(value)();
の部分で、最後の()
はPureScriptのEff関数を呼び出しているわけです。
このあたりのEff関数の扱い方がFFIの注意点になります。
JavaScriptの関数が副作用のある場合により簡単に型を付ける方法(拙作モジュールの紹介)
(以下の節はPureScriptの標準的なやり方ではないので、無視してもらってもかまいません)
前節でEff関数のFFIの注意点について述べましたが、実際問題として、JavaScript関数の多くが副作用を持つことになります。
またコールバック関数の多くも副作用を持つことになるでしょう。
これらに対していちいちJavaScriptのラッパー関数を書くのでは、個人的にちょっと不便ではないかと思いました。
そこで hexx/purescript-eff-functions というモジュールを作ってみました。
このモジュールにはImportEffFnX
とExportEffFnX
という二つの型があります。
前者が副作用のあるJavaScript関数に対して使う型で、後者がPureScriptのEff関数をコールバックとしてJavaScriptに渡す場合に使う型です。
ImportEffFnX
を使うと上のrandom
関数は以下のように書き換えることができます。
PureScript側が以下のようになり、
foreign import _random :: forall eff. ImportEffFn0 (Eff (random :: RANDOM | eff) Number)
random :: forall eff. Eff (random :: RANDOM | eff) Number
random = runImportEffFn0 _random
JavaScript側は以下のようになります。
exports._random = Math.random;
purescript-functionsモジュールを使ったときと同じように、副作用を持つJavaScriptの関数でも型を直接付けられるようになりました。
PureScriptのEff関数をJavaScriptにコールバックとして渡す場合はどうでしょうか。
今度は上記のreadFile
関数をExportEffFnX
を使って書き換えてみます。
すると、PureScript側が以下のようになり、
type Callback eff = ExportEffFn1 String (Eff (fs :: FS | eff) Unit)
foreign import _readFile :: forall eff. ImportEffFn2 String (Callback eff) (Eff (fs :: FS | eff) Unit)
readFile :: forall eff. String -> (String -> Eff (fs :: FS | eff) Unit) -> Eff (fs :: FS | eff) Unit
readFile filename callback = runImportEffFn2 _readFile filename (mkExportEffFn1 callback)
JavaScript側は以下のようになります。
exports._readFile = readFile;
基本的な動作はpurescript-functionsと同じで、run系は実行する関数になり、mk系は作る関数になります。
PureScript側の記述は多くなりましたが、JavaScript側はすっきりしました。
JavaScript側のFFIでがんばる方法の問題点は、Eff関数の仕様がわかりづらい点と、記述のミスが実行時のエラーになるという点です。
拙作のpurescript-eff-functionsモジュールを使うと、この点をpurescript-functionsを使った場合と同じように改善することができます。
purescript-eff-functionsでJavaScriptのthisの問題を解決する
この他に、さらにもう一つ、どうしてもFFIを使わなければならない問題があります。
悪名高いJavaScriptのthisの問題です。
JavaScriptの関数中のthisは呼び出し方によって変わります。
オブジェクトのメソッドのように呼び出す場合thisはレシーバを指すようになりますが、関数として呼び出す場合はグローバルオブジェクトを指します。
メソッドとして呼び出されることが想定されているものを、関数として呼び出してしまうとthisがおかしいことになり、実行時エラーになってしまいます。
しかし、この呼びわけをPureScriptから指定するのは困難です。
purescript-functionを使うと、JavaScriptを関数として呼び出すことしかできないためです。
仕方ないので、これもFFIのJavaScriptでがんばることになります。
そこで拙作のpurescript-eff-functionsモジュールにはMethodEffFnX
という型を用意し、この問題を解決しています。
たとえば上記のgetElementById
関数ですが、実はdocument
に対してメソッドして呼び出さないと機能しない関数です。
これをMethodEffFnX
を使って書き換えてみましょう。
type Document = { getElementById :: forall t eff. MethodEffFn1 t String (Eff (dom :: DOM | eff) (Nullable Element)) }
getElementById :: forall eff. String -> Document -> Eff (dom :: DOM | eff) (Nullable Element)
getElementById id document = runMethodEffFn1 document.getElementById document id
今回はJavaScript側の記述は必要ありません。
このようにPureScriptのレコード型を使って、JavaScriptのオブジェクトに直接型を付けるのもやりやすくなります。
実際にpurescript-eff-functionsのrunMethodEffFn1
がどうなっているかというと、JavaScriptのcall
を使っているだけなんですが。
exports.runMethodEffFn1 = function (fn) {
return function (t) {
return function (a) {
return function () {
return fn.call(t, a);
};
};
};
};
以上、拙作のpurescript-eff-functionsモジュールを使うと、FFIのJavaScriptの記述量がさらに減らせることが理解していただけたと思います。
冒頭に述べたようにJavaScriptで書くよりPureScriptで書くほうが記述能力が高く、強力な型のサポートが得られます。
なので、なるべく工夫してなるべくJavaScriptよりもPureScriptで書くようにしていきたいと考えています。
まとめ
今回はPureScriptのFFIの基礎の説明をしました。
慣れるとわりと機械的にJavaScriptの関数に型を付けていけるようになると思います。
さらにpurescript-affを使うと、JavaScriptで多発することになる非同期処理でもPureScriptらしい高機能で統一されたインターフェースを与えることができるのですが、
そちらは次回紹介したいと思います。
(あー、来年ぐらいには仕事でPureScriptを使いたいなあ……)