実例によるPureScript

ウェブのための関数型プログラミング

Phil Freeman, "PureScript by Example - Functional Programming for the Web"

10 外部関数インタフェース

10.1 この章の目標

この章では、PureScriptコードからJavaScriptコードへの呼び出し、およびその逆を可能にする、PureScriptの外部関数インタフェース(foreign function interface, FFI)を紹介します。これから扱うのは次のようなものです。

この章の終わりにかけて、再び住所録のコード例について検討します。この章の目的は、FFIを使ってアプリケーションに次のような新しい機能を追加することです。

10.2 プロジェクトの準備

このモジュールのソースコードは、第7章及び第8章の続きになります。今回もそれぞれのディレクトリから適切なソースファイルがGruntfileに含められています。

この章では型付けされていないデータを操作するためのデータ型と関数を提供するpurescript-foreignライブラリというBower依存関係がひとつ新しく追加されます。

新しいNPM依存関係もあります。この章のGruntfileは、grunt-contrib-connectパッケージを使用してコンパイル後に静的ファイルサーバを実行するようになっています。これは、ウェブページがローカルファイルから配信されているときに起こる、ローカルストレージとブラウザ固有の問題を避けるためです。この章の例を実行するには、まずgruntを実行して、それからブラウザで http://localhost:8000/ を開いてください。

10.3 免責事項

JavaScriptとの共同作業をできる限り簡単にするため、PureScriptは単純な多言語関数インタフェースを提供します。しかしながら、FFIはPureScriptの高度な機能であることには留意していただきたいと思います。FFIを安全かつ効率的に使用するには、扱うつもりであるデータの実行時の表現についてよく理解していなければなりません。この章では、PureScriptの標準ライブラリのコードに関連する、そのような理解を与えることを目指しています。

PureScriptのFFIはとても柔軟に設計されています。実際には、外部関数に最低限の型だけを与えるか、それとも型システムを利用して外部のコードの誤った使い方を防ぐようにするか、開発者が選ぶことができるということを意味しています。標準ライブラリのコードは、後者の手法を好む傾向にあります。簡単な例としては、JavaScriptの関数で戻り値が nullをされないことを保証することはできません。実のところ、既存のJavaScriptコードはかなり頻繁にnullを返します!しかし、PureScriptの型は通常null値を持っていません。そのため、FFIを使ってJavaScriptコードのインターフェイスを設計するときは、これらの特殊な場合を適切に処理するのは開発者の責任です。

10.4 JavaScriptからPureScriptを呼び出す

少なくとも単純な型を持った関数については、JavaScriptからPureScript関数を呼び出すのはとても簡単です。

例として以下のような簡単なモジュールを見てみましょう。

module Test where

gcd :: Number -> Number -> Number
gcd 0 m = m
gcd n 0 = n
gcd n m | n > m = gcd (n - m) m
gcd n m = gcd (m - n) n

この関数は、減算を繰り返すことによって2つの数の最大公約数を見つけます。関数を定義するのにPureScriptを使いたくなるかもしれない良い例となっていますが、JavaScriptからそれを呼び出すためには条件があります。 PureScriptでパターン照合と再帰を使用してこの関数を定義するのは簡単で、実装する開発者は型検証器の恩恵を受けることができます。

このモジュールをpscで次のようにコンパイルし、結果のJavaScriptををNodeにロードしてみましょう。

$ psc Test.purs > Test.js

$ node Test.js

この関数をJavaScriptから呼び出す方法を理解するには、PureScriptの関数は常に引数がひとつのJavaScript関数へと変換され、引数へは次のようにひとつづつ適用していかなければならないことを理解するのが重要です。

> var test = PS.Test.gcd(15)(20);

TestモジュールはグローバルなPSオブジェクトのメンバTestへとコンパイルされることに注意してください。これはpscコンパイラのデフォルトの動作ですが、グローバル名前空間は次のようにコマンドラインオプションを使用して変更することができます。

$ psc Test.purs --browser-namespace=MyNamespace > Test.js

psc-makeを使用してCommonJSのモジュールにコードをコンパイルすると、コンパイルされたモジュールは、デフォルトではoutputフォルダに配置されます。生成されたこれらのモジュールをnode_modulesディレクトリにコピーすると、NodeJS(もしくはその他のCommonJS互換環境)のrequire関数を使用して、モジュールを参照することができるようになります。

var Test = require('Test');

このモジュールで定義された関数は、先ほどと同様に使うことができます。

Test.gcd(15)(20);

10.5 名前の生成を理解する

PureScriptはコード生成時にできるだけ名前を保存することを目的としています。具体的には、トップレベルでの宣言では、JavaScriptのキーワードでなければ任意の識別子が保存されます。

識別子としてJavaScriptの予約語を使う場合は、名前はダブルダラー記号でエスケープされます。たとえば、次のPureScriptコードを考えてみます。

null = []

これは以下のようなJavaScriptへコンパイルされます。

var $$null = [];

また、識別子に特殊文字を使用したい場合は、単一のドル記号を使用してエスケープされます。たとえば、このPureScriptコードを考えます。

example' = 100

これは以下のJavaScriptにコンパイルされます。

var example$prime = 100;

この方式は、ユーザー定義の中置演算子の名前を生成するためにも使用されます。

(%) a b = ...

これは次のようにコンパイルされます。

var $percent = ...

コンパイルされたPureScriptコードがJavaScriptから呼び出されることを意図している場合、識別子は英数字のみを使用し、JavaScriptの予約語を避けることをお勧めします。ユーザ定義演算子がPureScriptコードでの使用のために提供される場合でも、JavaScriptから使うための英数字の名前を持った代替関数を提供しておくことをお勧めします。

10.6 実行時のデータ表現

型はプログラムはある意味で「正しい」ことをコンパイル時に判断できるようにします。つまり、実行時には中断されません。しかし、これは何を意味するのでしょうか?PureScriptでは式の型は実行時の表現と互換性がなければならないことを意味します。

そのため、PureScriptとJavaScriptコードを一緒に効率的に使用できるように、実行時のデータ表現について理解することが重要です。これは、与えられた任意のPureScriptの式について、その値が実行時にどのように評価されるかという挙動を理解できるべきであることを意味しています。

PureScriptの式は、実行時に特に単純な表現を持っているということは朗報です。実際に標準ライブラリのコードについて、その型を考慮すれば式の実行時のデータ表現を把握することが可能です。

単純な型については、対応関係はほとんど自明です。たとえば、式が型Booleanを持っていれば、実行時のその値vtypeof v === 'boolean'を満たします。つまり、型Booleanの式はtrueもしくはfalseのどちらか一方の(JavaScriptの)値へと評価されます。実のところ、nullundefinedに評価される、型BooleanのPureScriptの式はありません。

NumberStringの型の式についても同様のことが成り立ちます。Number型の式はnullでないJavaScriptの数へと評価されますし、String型の式はnullでないJavaScriptの文字列へと評価されます。

もっと複雑な型についてはどうでしょうか?

すでに見てきたように、PureScriptの関数は引数がひとつのJavaScriptの関数に対応しています。厳密に言えば、任意の型abについて、式fの型がa -> bで、式xが型aについての適切な実行時表現の値へと評価されるなら、fはJavaScriptの関数へと評価され、xを評価した結果にfを適用すると、それは型bの適切な実行時表現を持ちます。簡単な例としては、String -> String型の式は、nullでないJavaScript文字列からnullでないJavaScript文字列への関数へと評価されます。

ご想像のとおり、PureScriptの配列はJavaScriptの配列に対応しています。しかし、PureScriptの配列は均質であり、つまりすべての要素が同じ型を持っていることは覚えておいてください。具体的には、もしPureScriptの式eが任意の型aについて型[a]を持っているなら、eはすべての要素が型aの適切な実行時表現を持った(nullでない)JavaScript配列へと評価されます。

PureScriptのレコードがJavaScriptのオブジェクトへと評価されることはすでに見てきました。ちょうど関数と配列の場合のように、そのラベルに関連付けられている型を考慮すれば、レコードのフィールドのデータの実行時の表現についても推論することができます。もちろん、レコードのそれぞれのフィールドは、同じ型である必要はありません。

10.7 代数的データ型の実行時表現

PureScriptコンパイラは、代数的データ型のすべての構築子についてそれぞれ関数を定義し、新たなJavaScriptオブジェクト型を作成します。これらの構築子はこれらのプロトタイプに基づいて新しいJavaScriptオブジェクトを作成する関数に対応しています。

たとえば、次のような単純な代数的データ型を考えてみましょう。

data ZeroOrOne a = Zero | One a

PureScriptコンパイラは、次のようなコードを生成します。

function One(value0) {
    this.value0 = value0;
};

One.create = function (value0) {
    return new One(value0);
};

function Zero() {
};

Zero.value = new Zero();

ここで2つのJavaScriptオブジェクト型ZeroOneを見てください。JavaScriptの予約語newを使用すると、それぞれの型の値を作成することができます。引数を持つ構築子については、コンパイラはvalue0value1などと呼ばれるフィールドに対応するデータを格納します。

PureScriptコンパイラは補助関数も生成します。引数のない構築子については、コンパイラは構築子が使われるたびにnew演算子を使うのではなく、データを再利用できるようにvalueプロパティを生成します。ひとつ以上の引数を持つ構築子では、適切な表現を持つ引数を取り適切な構築子を適用するcreate関数をコンパイラは生成します。

2引数以上の構築子についてはどうでしょうか?その場合でも、PureScriptコンパイラは新しいオブジェクト型と補助関数を作成します。しかし今回は、補助関数は2引数のカリー化された関数です。たとえば、次のような代数的データ型を考えます。

data Two a b = Two a b

このコードからは、次のようなJavaScriptコードを生成されます。

function Two(value0, value1) {
    this.value0 = value0;
    this.value1 = value1;
};

Two.create = function (value0) {
    return function (value1) {
        return new Two(value0, value1);
    };
};

ここで、オブジェクト型Twoの値は予約語newまたはTwo.create関数を使用すると作成することができます。

newtypeの場合はまた少し異なります。newtypeは単一の引数を取る単一の構築子を持つよう制限された代数的データ型であることを思い出してください。この場合には、実際はnewtypeの実行時表現は、その引数の型と同じになります。

例えば、電話番号を表す次のようなnewtypeを考えます。

newtype PhoneNumber = PhoneNumber String

これは実行時にはJavaScriptの文字列として表されます。newtypeは型安全性の追加の層を提供しますが、実行時の関数呼び出しのオーバーヘッドがないので、ライブラリを設計するのに役に立ちます。

10.8 量化された型の実行時表現

量化された型(多相型)の式は、制限された表現を実行時に持っています。実際には、量化された型の式が比較的少数与えられたとき、とても効率的に解決できることを意味しています。

例えば、次の多相型を考えてみます。

forall a. a -> a

この型を持っている関数にはどんなものがあるでしょうか。少なくともひとつはこの型を持つ関数が存在しています。すなわち、Preludeで定義されている恒等関数idです。

id :: forall a. a -> a
id a = a

実のところ、idの関数はこの型の唯一の(全)関数です!これは間違いなさそうに見えます(この型を持ったidとは明らかに異なる式を書こうとしてみてください)が、しかし、これを確かめるにはどうしたらいいでしょうか。これは型の実行時表現を考えることによって確認することができます。

量化された型forall a. tの実行時表現はどうなっているのでしょうか。さて、この型の実行時表現を持つ任意の式は、型aをどのように選んでも型tの適切な実行時表現を持っていなければなりません。上の例では、型forall a. a -> aの関数は、String -> StringNumber -> Number、, [Boolean] -> [Boolean]などといった型について、適切な実行時表現を持っていなければなりません。 これらは、数から数、文字列から文字列の関数でなくてはなりません。

しかし、それだけでは十分ではありません。量化された型の実行時表現は、これよりも更に厳しくなります。任意の式がパラメトリック多相的でなければなりません。つまり、その実装において、引数の型についてのどんな情報も使うことができないのです。この追加の条件は、考えられる多相型のうち、次のようなJavaScriptの関数として問題のある実装を禁止します。

function invalid(a) {
    if (typeof a === 'string') {
        return "Argument was a string.";
    } else {
        return a;
    }
}

確かにこの関数は文字列から文字列、数から数へというような関数ではありますが、追加の条件を満たしていません。引数の実行時の型を調べているからです。したがって、この関数は型forall a. a -> aの正しい実装だとはいえないのです。

関数の引数の実行時の型を検査することができなければ、唯一の選択肢は引数をそのまま返すことだけであり、したがって idは、forall a. a -> aのまったく唯一の実装なのです。

パラメトリック多相(parametric polymorphism)とパラメトリック性(parametricity)についての詳しい議論は本書の範囲を超えています。しかしながら、PureScriptの型は、実行時に消去されているので、PureScriptの多相関数は(FFIを使わない限り)引数の実行時表現を検査することができないし、この多相的なデータの表現は適切であることに注意してください。

10.9 制約された型の実行時表現

型クラス制約を持つ関数は、実行時に面白い表現を持っています。関数の振る舞いはコンパイラによって選ばれた型クラスのインスタンスに依存する可能性があるため、関数には選択したインスタンスから提供された型クラスの関数の実装が含まれてた型クラス辞書(type class dictionary)と呼ばれる追加の引数が与えられています。

例えば、Show型クラスを使用している制約された型を持つ、次のような単純なPureScript関数について考えます。

shout :: forall a. (Show a) => a -> String
shout a = show a ++ "!!!" 

このコードから生成されるJavaScriptは次のようになります。

var shout = function (dict) {
    return function (a) {
        return show(dict)(a) + "!!!";
    };
};

shoutは1引数ではなく、2引数の(カリー化された)関数にコンパイルされていることに注意してください。最初の引数dictShow制約の型クラス辞書です。dictには型ashow関数の実装が含まれています。

最初の引数として明示的にPreludeの型クラス辞書を渡すと、JavaScriptからこの関数を呼び出すことができます。

shout(Prelude.showNumber())(42);

演習

  1. (簡単) これらの型の実行時の表現は何でしょうか。

    forall a. a
    forall a. a -> a -> a
    forall a. (Ord a) => [a] -> Boolean

    これらの型を持つ式についてわかることはなんでしょうか。

  2. (やや難しい) psc-makeを使ってコンパイルし、NodeJSのrequire関数を使ってモジュールをインポートすることで、JavaScriptからpurescript-arraysライブラリの関数を使ってみてください。

10.10 PureScriptからのJavaScriptコードを使う

PureScriptからJavaScriptコードを使用する最も簡単な方法は、foreign import宣言を使用し、既存のJavaScriptの値に型を与えることです。

たとえば、特殊文字をエスケープすることによりURIのコンポーネントを符号化するJavaScriptのencodeURIComponent関数について考えてみます。

$ node

node> encodeURIComponent('Hello World')
'Hello%20World'

nullでない文字列からnullでない文字列への関数であり、副作用を持っていないので、この関数はその型String -> Stringについて適切な実行時表現を持っています。

次のような外部インポート宣言を使うと、この関数に型を割り当てることができます。

foreign import encodeURIComponent :: String -> String

また、PureScriptで記述された関数のように、この関数をPureScriptから使ってみます。たとえば、この宣言をモジュールとして保存してpsciにロードすると、先ほどの計算を再現することができます。

> encodeURIComponent "Hello World"
"Hello%20World"

このアプローチは、簡単なJav​​aScriptの値には適していますが、もっと複雑な値に使うには限界があります。ほとんどの既存のJavaScriptコードは、基本的なPureScriptの型の実行時表現によって課せられた厳しい条件を満たしていないからです。このような場合のためには、適切な実行時表現に従うことを強制するようにJavaScriptコードをラップするという別の方法があります。

10.11 JavaScriptの値のラッピング

外部インポート宣言は、型注釈の直前に文字列リテラルを含めることで、JavaScriptコードのブロックと対にすることができます。そのJavaScriptコードは、コンパイル時に生成されたコードに直接挿入されます。

これはPureScriptの型を与えるためにJavaScriptコードの既存の部分をラップする場合に特に便利です。このようにしたくなる理由はいくつかあります。

外部インポート宣言を使用して、配列についてのhead関数を作成したいとしましょう。JavaScriptでは次のような関数になるでしょう。

function head(arr) {
    return arr[0];
}

しかし、この関数には問題があります。型forall a. [a] -> aを与えようとしても、空の配列に対してこの関数はundefinedを返します。したがって、この特殊な場合を処理するために、ラッパー関数を使用する必要があります。

簡単な方法としては、空の配列の場合に例外を投げる方法があります。

foreign import head
  "function head(arr) {\
  \  if (arr.length) {\
  \    return arr[0];\
  \  } else {\
  \    throw new Error('Empty array!');\
  \  }\
  \}" :: forall a. [a] -> a

バックスラッシュを使用するとその行から次の1行まで継続することができ、JavaScriptの実装を複数行に分離できることに注意してください。

10.12 外部型の定義

失敗した場合に例外を投げるという方法は、あまり理想的とはいえません。PureScriptのコードでは、欠けた値のような副作用は型システムを使って扱うのが普通です。この手法としてはMaybe型構築子を使う方法もありますが、この節ではFFIを使用した別の解決策を扱います。

実行時には型aのように表現されますがundefinedの値も許容するような新しい型Undefined aを定義したいとしましょう。

外部インポート宣言とFFIを使うと、外部型(foreign type)を定義することができます。構文は外部関数を定義するのと似ています。

foreign import data Undefined :: * -> *

この予約語dataは値ではなく定義している型を表していることに注意してください。型シグネチャの代わりに、新しい型のを与えます。このとき、種Undefined* -> *であると宣言しています。つまりUndefinedは型構築子です。

これでheadの定義を簡素化することができます。

foreign import head
  "function head(arr) {\
  \  return arr[0];\
  \}" :: forall a. [a] -> Undefined a

2点変更がある注意してください。head関数の本体ははるかに簡単で、もしその値が未定義であったとしてもarr[0]を返し、型シグネチャはこの関数が未定義の値を返すことがあるという事実を反映するよう変更されています。

この関数はその型の適切な実行時表現を持っていますが、型Undefined aの値を使用する方法がありませんので、まったく役に立ちません。しかし、FFIを使用して新しい関数を幾つか書くことによって、それを修正することができます!

次の関数は、値が定義されているかどうかを教えてくれる最も基本的な関数です。

foreign import isUndefined
  "function isUndefined(value) {\
  \  return value === undefined;\
  \}" :: forall a. Undefined a -> Boolean

PureScriptからisUndefinedheadを一緒に使用すると、便利な関数を定義することができます。

isEmpty :: forall a. [a] -> Boolean
isEmpty = isUndefined <<< head

ここで、定義されたこの外部関数はとても簡単であり、PureScriptの型検査器を使うことによる利益をなるべく多く得るということを意味します。一般に外部関数は可能な限り小さく保ち、アプリケーションの処理はPureScriptコードへ移動しておくことをおすすめします。

10.13 多変数​関数

PureScriptのPreludeには、興味深い外部型がいくつかも含まれています。すでに扱ってきたように、PureScriptの関数型は単一の引数だけを取りますが、カリー化を使うと複数の引数の関数をシミュレートすることができます。これには明らかな利点があります。関数を部分適用することができ、関数型の型クラスインスタンスを与えることができます。ただし、効率上のペナルティが生じます。パフォーマンス重視するコードでは、複数の引数を受け入れる本物のJavaScript関数を定義することが必要な場合があります。Preludeではそのような関数を安全に扱うことができるようにする外部型が定義されています。

たとえば、PreludeのData.Functionモジュールには次の外部型宣言があります。

foreign import data Fn2 :: * -> * -> * -> *

これは3つの型引数を取る型構築子Fn2を定義します。Fn2 a b cは、型abの2つの引数、返り値の型cをもつJavaScript関数の型を表現しています。

Preludeでは0引数から10引数までの関数について同様の型構築子が定義されています。

次のようにmkFn2関数を使うと、2引数の関数を作成することができます。

import Data.Function

divides :: Fn2 Number Number Boolean
divides = mkFn2 $ \n m -> m % n == 0

そして、 runFn2関数を使うと、2引数の関数を適用することができます。

> runFn2 divides 2 10
true

> runFn2 divides 3 10
false

ここで重要なのは、引数がすべて適用されるなら、コンパイラはmkFn2関数やrunFn2関数をインライン化するということです。そのため、生成されるコードはとてもコンパクトになります。

var divides = function (n, m) {
    return m % n === 0;
};

10.14 均質なレコード

外部型のさらなる例として、均質なレコード(homogeneous records)の型を定義してみましょう。これは、どんなラベルでも持つことができますが、どのプロパティも同じ型をもっているレコードです。

PureScriptではレコードの各プロパティは異なる型を持つことができます。これは多くの場合に便利ですが、JavaScriptコードにおけるいくつかの典型的なパターンに意味のある型を与えるのがうまくいかないときがあります。

均質なレコードの型はそのプロパティの(統一された)型によってパラメータ化されることになるので、これは次のような* -> *という種を持つことになります。

foreign import data HRec :: * -> *

外部値(foreign value)を使用すると、簡単に空の均質なレコードを定義することができます。

foreign import empty 
  "var empty = {}" :: forall a. HRec a

forall a. HRec a という型は、この空の均質なレコードは任意の型aについて型aのプロパティを持っているのを表していることに注意してください。emptyはどのプロパティも持っていないので、これが正しいのはまったくの自明です!

また、均質なレコードに新しいフィールドを挿入する関数を定義することができます。PureScriptの値は不変なので、JavaScriptコードで既存のレコードをコピーする必要があります。

foreign import insert
  "function insert(key, value, rec) {\
  \  var copy = {};\
  \  for (var k in rec) {\
  \    if (rec.hasOwnProperty(k)) {\
  \      copy[k] = rec[k];\
  \    }\
  \  }\
  \  copy[key] = value;\
  \  return copy;\
  \}" :: forall a. Fn3 String a (HRec a) (HRec a)

insert関数は3引数の関数を表現するために型コンストラクタFn3を使っています。JavaScriptで手作業でカリー化関数を書くことはとても面倒なので、Fn3を使うと便利です。この関数はレコードを複製し、複製へ新しいキーを追加します。

均質なレコードを使うと、通常のPureScriptレコードではできないような、いろいろな面白い操作を行うことができます。例えば、均質なレコードの値に対して関数をマッピングすることができます。

foreign import mapHRec
  "function mapHRec(f, rec) {\
  \  var mapped = {};\
  \  for (var k in rec) {\
  \    if (rec.hasOwnProperty(k)) {\
  \      mapped[k] = f(rec[k]);\
  \    }\
  \  }\
  \  return mapped;\
  \}" :: forall a b. Fn2 (a -> b) (HRec a) (HRec b)

つまり、HRecFunctorなのです!

instance functorHRec :: Functor HRec where
  (<$>) f rec = runFn2 mapHRec f rec

また、HRecFoldable型クラスのインスタンスにすることもでき、レコードの値に対して畳み込みをすることもできます。さらに興味深いことに、レコードのプロパティの値だけでなくラベルも受け取る累積関数について畳み込みを実行することができます!

foreign import foldHRec
  "function foldHRec(f, r, rec) {\
  \  var acc = r;\
  \  for (var k in rec) {\
  \    if (rec.hasOwnProperty(k)) {\
  \      acc = f(acc, k, rec[k]);\
  \    }\
  \  }\
  \  return acc;\
  \}" :: forall a r. Fn3 (Fn3 r String a r) r (HRec a) r

この章のソースコードには、次の演習に対する解決策の基礎として使用することができる HRecモジュールの関数が含まれています。

演習

  1. (簡単) psci でいくつかの簡単なレコードを構築し、 runFn3関数を使用してinsert関数を試してみてください。

  2. (やや難しい) 2つの均質なレコードの和集合を計算する関数 unionを書いてください。2つのレコードがラベルを共有している場合、2つめのレコードが優先させなければいけません。

  3. (やや難しい) 通常の(カリー化された)関数を使用するfoldHRecためのラッパー関数を書いてください。その関数は次のような型を持っていなければなりません。

    forall a r. (r -> String -> a -> r) -> r -> HRec a -> r

    この関数を定義するのにFFIは使用しないでください。

  4. (難しい) 均質なレコードのキーを検索する関数lookupを書いてください。その関数は次のような型を持っていなければなりません。

    forall a. String -> HRec a -> Maybe a

    この関数の次の2種類の実装を書いてください。最初のバージョンはfoldHRec関数を使用する必要があります。2つめのバージョンは、外部関数として定義しなければなりません。ヒント:次のような関数の定義を探してみると参考になるかもしれません。

    lookupHelper :: forall a r. Fn4 r (a -> r) String (HRec a) r

    第1引数及び第2引数は、それぞれ NothingJust関数に対応しています。

  5. (難しい) マッピング関数が追加の引数としてプロパティのラベルを受け取るmapHRec関数書いてください。その関数を使用してHRecShowインスタンスを簡素化してください。

10.15 副作用の表現

EffモナドもPreludeの外部型として定義されています。その実行時表現はとても簡単です。型Eff eff aの式は、任意の副作用を実行し型aの適切な実行時表現で値を返す、引数なしのJavaScript関数へと評価されます。

Eff型の構築子の定義は、Control.Monad.Effモジュールで次のように与えられています。

foreign import data Eff :: # ! -> * -> *

Eff型の構築子は作用の行と返り値の型によってパラメータ化されおり、それが種に反映されることを思い出してください。

簡単な例として、purescript-randomパッケージで定義されるrandom関数を考えてみてください。その型は次のようなものでした。

random :: forall eff. Eff (random :: Random) Number

random関数の定義は次のように与えられます。

foreign import random
  "function random() {\
  \  return Math.random();\
  \}" :: forall eff. Eff (random :: Random | eff) Number

random関数は実行時には引数なしの関数として表現されていることに注目してください。これは乱数生成という副作用を実行しそれを返しますが、返り値はNumber型の実行時表現と一致します。それはnullでないJavaScriptの数です。

もう少し興味深い例として、 PreludeのDebug.Traceモジュールで定義されたtrace関数を考えてみましょう。trace関数は次の型を持っています。

forall eff. String -> Eff (trace :: Trace | eff) Unit

この定義は次のようになっています。

foreign import trace
  "function trace(s) {\
  \  return function() {\
  \    console.log(s);\
  \    return {};\
  \  };\
  \}" :: forall eff. String -> Eff (trace :: Trace | eff) Unit

実行時のtraceの表現は、引数なしの関数を返す、単一の引数のJavaScript関数です。内側の関数はコンソールにメッセージを書き込むという副作用を実行し、空のレコードを返します。Unitは空のレコード型のnewtypeとしてPreludeで定義されているので、内側の関数の戻り値の型はUnit型の実行時表現と一致していることに注意してください。

作用RandomTraceも外部型として定義されています。その種は!、つまり作用であると定義されています。例えば次のようになります。

foreign import data Random :: !

詳しくはあとで見ていきますが、このように新たな作用を定義することが可能なのです。

Eff eff a型の式は、通常のJavaScriptのメソッドのようにJavaScriptから呼び出すことができます。例えば、このmain関数は作用の集合effと何らかの型aについてEff eff aという型でなければならないので、次のように実行することができます。

PS.Main.main();

または、CommonJSの環境では次のようにします。

require('Main').main();

pscコンパイラを使用するときは、コマンドライン上で --mainコンパイラオプションを使用すると、このmainの呼び出しを自動的に生成することができます。

10.16 新しい作用の定義

この章のソースコードでは、2つの新しい作用が定義されています。最も簡単なのはControl.Monad.Eff.Alertモジュールで定義されたAlert作用です。これはその計算がポップアップウィンドウを使用してユーザに警告しうることを示すために使われます。

この作用は最初に外部型宣言を使用して定義されています。

foreign import data Alert :: !

Alertは種!が与えられており、Alertが型ではなく作用であることを示しています。

次に、alertアクションが定義されています。alertアクションはポップアップを表示し、作用の行にAlert作用を追加します。

foreign import alert
  "function alert(msg) {\
  \  return function() {\
  \    window.alert(msg);\
  \    return {};\
  \  };\
  \}" :: forall eff. String -> Eff (alert :: Alert | eff) Unit

このアクションはDebug.Traceモジュールのtraceアクションととてもよく似ています。唯一の違いは、traceアクションがconsole.logメソッドを使用しているのに対し、alertアクションはwindow.alertメソッドを使用していることです。このように、alertwindow.alertが定義されているウェブブラウザのような環境で使用することができます。

traceの場合のように、alert関数は型Eff (alert :: Alert | eff) Unitの計算を表現するために引数なしの関数を使っていることに注意してください。

この章で定義される2つめの作用は、Control.Monad.Eff.Storageモジュールで定義されているStorage作用です。これは計算がWeb Storage APIを使用して値を読み書きする可能性があることを示すために使われます。

この作用も同じように定義されています。

foreign import data Storage :: !

Control.Monad.Eff.Storageモジュールには、ローカルストレージから値を取得するgetItemと、ローカルストレージに値を挿入したり値を更新するsetItemという、2つのアクションが定義されています。この二つの関数は、次のような型を持っています。

getItem :: forall eff. String -> Eff (storage :: Storage | eff) Foreign
setItem :: forall eff. String -> String -> Eff (storage :: Storage | eff) Unit

興味のある読者は、このモジュールのソースコードでこれらのアクションがどのように定義されているか調べてみてください。

setItemはキーと値(両方とも文字列)を受け取り、指定されたキーでローカルストレージに値を格納する計算を返します。

getItemの型はもっと興味深いものです。getItemはキーを引数に取り、キーに関連付けられた値をローカルストレージから取得しようとします。window.localStoragegetItemメソッドはnullを返すことがあるので、返り値はStringではなく、purescript-foreignパッケージのData.Foreignモジュールで定義されているForeignになっています。

Data.Foreignは、型付けされていないデータ、もっと一般的にいえば実行時表現が不明なデータを扱う方法を提供しています。

演習

  1. (やや難しい) JavaScriptのWindowオブジェクトのconfirmメソッドのラッパを書き、Control.Monad.Eff.Alertモジュールにその関数を追加してください。

  2. (やや難しい) localStorageオブジェクトのremoveItemメソッドのラッパを書き、Control.Monad.Eff.Storageモジュールに追加してください

10.17 型付けされていないデータの操作

この節では、型付けされていないデータを、その型の適切な実行時表現を持った型付けされたデータに変換する、Data.Foreignライブラリの使い方について見て行きます。

この章のコードは、第8章の住所録の上にフォームの一番下に保存ボタンを追加することで作っていきます。保存ボタンがクリックされると、フォームの状態をJSONに直列化し、ローカルストレージに格納します。ページが再読み込みされると、JSON文書がローカルストレージから取得され、構文解析されます。

Mainモジュールではフォームデータの型を定義します。

newtype FormData = FormData
  { firstName  :: String
  , lastName   :: String
  , street     :: String
  , city       :: String
  , state      :: String
  , homePhone  :: String
  , cellPhone  :: String
  }

問題は、このJSONが正しい形式を持っているという保証がないことです。別の言い方をすれば、JSONが実行時にデータの正しい型を表しているかはわかりません。この問題はpurescript-foreignライブラリによって解決することができます。他にも次のような使いかたがあります。

それでは、pscipurescript-foreignライブラリを試してみましょう。二つのモジュールをインポートして起動します。

> :i Data.Foreign
> :i Data.Foreign.Class

Foreignな値を取得するためには、JSON文書を解析するのがいいでしょう。purescript-foreignはで次の2つの関数が定義されています。

parseJSON :: String -> F Foreign
readJSON :: forall a. (IsForeign a) => String -> F a

型構築子Fは、実際はData.Foreignで定義されている型同義語です。

type F = Either ForeignError

purescript-foreignライブラリの関数のほとんどは、Fモナドの値を返します。これは、型付けされた値を構築するのに、do記法やApplicative関手コンビネータを使うことができることを意味しています。

このIsForeign型クラスは、それらの型が型付けされていないデータから得られることを表しています。プリミティブ型や配列について定義された型クラスインスタンスは存在しますが、独自のインスタンスを定義することもできます。

それではpscireadJSONを使用していくつかの簡単なJSON文書を解析してみましょう。

> readJSON "\"Testing\"" :: F String
Right "Testing"

> readJSON "true" :: F Boolean 
Right true

> readJSON "[1, 2, 3]" :: F [Number]
Right [1, 2, 3]

EitherモナドではRightデータ構築子は成功を示していることを思い出してください。しかし、その不正なJSONや誤った型はエラーを引き起こすことに注意してください。

> readJSON "[1, 2, true]" :: F [Number]

Left (Error at array index 2: Type mismatch: expected Number, found Boolean)

purescript-foreignライブラリはJSON文書で型エラーが発生した位置を教えてくれます。

10.18 nullとundefined値の取り扱い

実世界のJSON文書にはnullやundefined値が含まれているので、それらも扱えるようにしなければなりません。

purescript-foreignでは、この問題を解決する3種類の構築子、NullUndefinedNullOrUndefinedが定義されています。先に定義したUndefined型の構築子と似た目的を持っていますが、省略可能な値を表すためにMaybe型の構築子を内部的に使っています。

それぞれの型の構築子について、ラップされた値から内側の値を取り出す関数、runNullrunUndefined runNullOrUndefinedが提供されています。null値を許容するJSON文書を解析するには、readJSONアクションまで対応する適切な関数を持ち上げます。

> runNull <$> readJSON "42" :: F (Null Number)
Right (Just 42)

> runNull <$> readJSON "null" :: F (Null Number)
Right Nothing

それぞれの場合で、型注釈が<$>演算子の右辺に適用されています。たとえば、readJSON "42"は型F (Null Number)を持っています。runNull関数は最終的な型F (Maybe Number)与えるためにFまで持ち上げられます。

NULL Numberは数またはnullいずれかの値を表しています。各要素がnullをかもしれない数値の配列のように、より興味深いの値を解析したい場合はどうでしょうか。その場合には、次のようにreadJSONアクションまで関数map runNullを持ち上げます。

> :i Data.Array

> map runNull <$> readJSON "[1, 2, null]" :: F [Null Number]
Right [Just 1, Just 2, Nothing]

一般的には、同じ型に異なる直列化戦略を提供するには、newtypesを使って既存の型をラップするのがいいでしょう。nullUndefinedNullOrUndefinedそれぞれの型は、Maybe型構築子に包まれたnewtypeとして定義されています。

10.19 住所録の項目の直列化

フォームデータはJSON.strongifyメソッドを使用して直列化されますが、これはData.JSONモジュールで定義されている次の関数でラップされています。

foreign import stringify
  "function stringify(x) {\
  \  return JSON.stringify(x);\
  \}" :: Foreign -> String

保存ボタンをクリックすると、型FormDataの値が(Foreignの値に変換されたあとで)stringify関数に渡され、JSON文書として直列化されます。FormData型はレコードのnewtypeで、JSON.stringifyが渡された型FormDataの値はJSONオブジェクトとして扱われて直列化されます。newtypeはその基礎となるデータと同じ実行時表現を持っているためです。

生成されたJSONドキュメントを解析できるようにするためには、オブジェクトのプロパティを読み取れるようにしなければなりません。purescript-foreignライブラリはその機能を(!)演算子とreadPropアクションによって提供しています。

(!) :: (Index i) => Foreign -> i -> F Foreign
readProp :: forall a i. (IsForeign a, Index i) => i -> Foreign -> F a

型クラスIndexは外部値のプロパティをインデックスするために使われる型を表しています。IndexのインスタンスはString(オブジェクトプロパティにアクセスするため)とNumber(配列要素にアクセスするため)に対して提供されています。

readPropアクションを使うと、FormData型のIsForeignのインスタンスを定義することができます。次のようにIsForeign型クラスで定義されているread関数を実装する必要があります。

class IsForeign a where
  read :: Foreign -> F a

read関数を実装するには、FMonad構造を使って小さな部分からFormData構造体を次のように作っていきます。

instance formDataIsForeign :: IsForeign FormData where
  read value = do
    firstName   <- readProp "firstName" value
    lastName    <- readProp "lastName"  value
    street      <- readProp "street"    value
    city        <- readProp "city"      value
    state       <- readProp "state"     value
    homePhone   <- readProp "homePhone" value
    cellPhone   <- readProp "cellPhone" value
    return $ FormData
      { firstName  : firstName
      , lastName   : lastName
      , street     : street
      , city       : city
      , state      : state
      , homePhone  : homePhone
      , cellPhone  : cellPhone
      }

FormDataの構築子関数をF型構築子まで持ち上げると、このコードをFApplicative構造を使って書くこともできます。これは演習として残しておきます。

この型クラスのインスタンスは、ローカル·ストレージから取得したJSON文書を解析するためにreadJSON で次のように使われています。

loadSavedData = do
  item <- getItem "person"
  
  let
    savedData :: F (Maybe FormData)
    savedData = do
      jsonOrNull <- read item
      traverse readJSON (runNull jsonOrNull)

savedDataアクションは2つのステップにわけてFormData構造を読み取ります。まず、getItemから得たForeign値を解析します。jsonOrNullの型はコンパイラによってNull Stringだと推論されます(読者への演習: この型はどのように推論されているのでしょうか?)。traverse関数はreadJSONMaybe.String型の結果の(不足しているかもしれない)要素へと適用するのに使われます。readJSONについて推論される型クラスのインスタンスはちょうどさっき書いたもので、型F (Maybe FormData)の値で結果を返します。

traverseの引数にはreadが最初の行で得た結果jsonOrNullを使っているので、Fのモナド構造を使う必要があります。

結果のFormDataには3つの可能性があります。

gruntを実行し、それからブラウザでhttp://localhost:8000を開いて、これらのコードを試してみてください。保存ボタンをクリックするとフォームフィールドの内容をローカルストレージへ保存することができ、ページを再読込するとフィールドが再現されるはずです。

演習

  1. (簡単) readJSONを使って、[[1, 2, 3], [4, 5], [6]]のようなJavaScriptの数の2次元配列を表現するJSON文書を解析してください。 要素をnullにすることが許容されている場合はどうでしょうか。配列自体がnullにすることが許容されている場合はどうなりますか。

  2. (やや難しい) Applicativeコンビネータ<$><*>を使ってformDataIsForeign型クラスを書きなおしてください。

  3. (やや難しい) savedDataの実装の型を検証し、計算のそれぞれの部分式の推論された型を書き出してみましょう。

  4. (難しい) 次のnewtype型は、タグ付き共用体として直列化されなければならないEither a bの型の値を表しています。

    newtype Tagged a b = Tagged (Either a b)

    つまり、直列化されたJSON文書にはLeft構築子とRight構築子のどちらが値を構築するのに使われたかということを表すプロパティtagを含まなければいけません。実際の値は、JSON文書のvalueのプロパティに格納される必要があります。

    例えば、JSONデータ{ tag: "Left", value: 0 }Left 0へと復元されなければいけません。

    このTagged型構築子のIsForeignについての妥当なインスタンスを書いてください。

  5. (難しい、拡張)次のデータ型は、葉で値を持つ二分木を表しています。

    data Tree a = Leaf a | Branch (Tree a) (Tree a)

    JSONドキュメントとしてこの型の適切な表現を選択してください。JSON.stringifyと中間のレコードのnewtypeを使って二分木をJSONへ直列化する関数を書き、関連するIsForeignのインスタンスを書いてください。

10.20 まとめ

この章では、PureScriptから外部のJavaScriptコードを扱う方法、およびその逆の方法を学びました。また、FFIを使用して信頼できるコードを書く時に生じる問題について見てきました。

その他の例については、Githubのpurescript組織およびpurescript-contrib組織が、FFIを使用するライブラリの例を多数提供しています。残りの章では、型安全な方法で現実世界の問題を解決するために使うライブラリを幾つか見ていきます。