LoginSignup
16

More than 5 years have passed since last update.

より実用的なPureScriptのFFI(基礎編)

Last updated at Posted at 2015-09-26

はじめに

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のFnrunFnもそうでしたが、このようにPureScriptで直接扱いづらいJavaScriptのコードはforeign importで外部型を定義し、
その中にデータを入れて、必要なときにrunXXXtoXXXなどの関数で取り出すという方法が有効です。

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 というモジュールを作ってみました。

このモジュールにはImportEffFnXExportEffFnXという二つの型があります。
前者が副作用のある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を使いたいなあ……)

Register as a new user and use Qiita more conveniently

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