より実用的なPureScriptのFFI（purescript-aff編）

# より実用的なPureScriptのFFI（purescript-aff編）

# はじめに

前回の記事([より実用的なPureScriptのFFI（基礎編） - Qiita](http://qiita.com/pab_tech/items/0eb43d73c135d93fa09c))ではPureScriptからJavaScriptを呼び出す仕組みであるFFIの基礎について述べました。
今回はJavaScriptのコールバックを非同期モナドにする [purescript-aff](https://github.com/slamdata/purescript-aff) について紹介したいと思います。
前回と今回の方法を合わせて使えば、簡単に既存のJavaScriptの関数を、PureScriptらしい高機能で統一されたインターフェースで使うことができるようになります。

# JavaScriptのコールバック地獄

JavaScriptはブラウザでもNodeでも基本的にシングルスレッドで動作するために非同期処理が中心になります。
このため次の処理をするコールバックを受け取るような関数多くなります。
これが多段になり、エラー処理も入ってくるようになるとコールバック地獄と呼ばれるような状態になることはよく指摘されます。

JavaScriptではこの問題を解決するためにpromiseやgeneratorを用いるのが主流ですが、
PureScriptでは[Read PureScript by Example | Leanpub](https://leanpub.com/purescript/read#leanpub-auto-callback-hell)の「12. Callback Hell」にContTを使った方法が紹介されています。
今回紹介するpurescript-affはそれをより発展された方法と言えます。

# purescript-affの概要

[slamdata/purescript-aff](https://github.com/slamdata/purescript-aff)ではpurescript-affについて`ErrorT (ContT Unit (Eff e) a`と同じようなものと紹介されています。
これはつまりpurescript-affは非同期処理、エラー処理、副作用の三つの機能があるということです。
実際問題として、JavaScriptのコールバックはこの三つがセットになっていることが多いので、purescript-affでラップすることによりそれらをまとめて扱うことができるようになるわけです。

また purescript-aff はSemigroup、Monoid、Apply、Applicative、Bind、Monad、Alt、Plus、MonadPlus、MonadEff、MonadErrorのインスタンスが既に定義されているので、Affにするだけで多くの機能が使えるようになります。

# コールバックを受け取るような関数をAffにするには？

## まずPureScriptの型をつける（前回の復習）

ここでは[Read PureScript by Example | Leanpub](https://leanpub.com/purescript/read#leanpub-auto-callback-hell)と同じようにNode.jsのFile Systemを題材にコールバックを受け取るJavaScriptの関数をpurescript-affに対応させる方法を解説します。

AffにするのはreadFileとwriteFileの二つです。
TypeScriptの[DefinitelyTyped/node.d.ts at master · borisyankov/DefinitelyTyped](https://github.com/borisyankov/DefinitelyTyped/blob/master/node/node.d.ts)の型を見てみますと以下のようになっています。

```typescript
export function readFile(filename: string, callback: (err: ErrnoException, data: Buffer) => void): void;

export function writeFile(filename: string, data: any, callback: (err: ErrnoException) => void): void;
```

それぞれreadFileはエラー値とデータが引数のコールバックを受け取り、writeFileはエラー値のコールバックを受け取ります。
これらの関数に復習ついでに前回説明した方法でPureScriptの型を付けてみます。

まずPureScriptのコールバックの型を考えてみます。

```purescript
type ReadCallback eff = Either Error String -> Eff (fs :: FS | eff) Unit

type WriteCallback eff = Maybe Error -> Eff (fs :: FS | eff) Unit
```

`ReadCallback`はエラー値もしくはデータを受け取るので、引数をEitherで取るようにします。

`WriteCallback`はエラー時にエラー値を受け取るので、引数をMaybeで取るようにします。

しかし、このままではJavaScriptにコールバックとしては渡せないので、JSコールバック用の型も考えます。

```purescript
type JSReadCallback eff = ExportEffFn2 (Nullable Error) String (Eff (fs :: FS | eff) Unit)

type JSWriteCallback eff = ExportEffFn1 (Nullable Error) (Eff (fs :: FS | eff) Unit)
```

前回説明したとおり`ExportEffFn2`は2引数を受け取るJSコールバックを表現するデータコンストラクタです。
`ExportEffFn2`は1引数のものです。

このPureScriptのコールバックとJavaScriptのコールバックを変換する関数も作ります。

```purescript
toJSReadCallback :: forall eff. ReadCallback eff -> JSReadCallback eff
toJSReadCallback f = mkExportEffFn2 \e s -> f $ maybe (Right s) Left (toMaybe e)

toJSWriteCallback :: forall eff. WriteCallback eff -> JSWriteCallback eff
toJSWriteCallback f = mkExportEffFn1 $ toMaybe >>> f
```

これでコールバックの準備はできました。
そして次はJavaScriptの`readFile`と`writeFile`にPureScriptの型をつけてみます。
前回やった直接型を付ける方法を採用します。

JavaScript側は例によってこれだけです。

```javascript
"use strict";

// module FS

exports.fs = require('fs');
```

PureScript側はまとめてレコードで表現します。

```purescript
foreign import fs ::
  { readFile  :: forall eff. ImportEffFn2 String (JSReadCallback eff) (Eff (fs :: FS | eff) Unit)
  , writeFile :: forall eff. ImportEffFn3 String String (JSWriteCallback eff) (Eff (fs :: FS | eff) Unit)
  }
```

これで`readFile`と`writeFile`にPureScriptの型を付けることができました。
前回述べたとおりこれではちょっと使いづらいので、使いやすくするようなラッパー関数を作ります。

```purescript
readFile :: forall eff. String -> ReadCallback eff -> Eff (fs :: FS | eff) Unit
readFile path callback = runImportEffFn2 fs.readFile path $ toJSReadCallback callback

writeFile :: forall eff. String -> String -> WriteCallback eff -> Eff (fs :: FS | eff) Unit
writeFile path dat callback = runImportEffFn3 fs.writeFile path dat $ toJSWriteCallback callback
```

これでJavaScriptの`readFile`と`writeFile`をPureScriptの世界に持ってくることできました。
ここまでが前回やった内容になります。

## しかし、型付けできたはいいけど、使いづらい…

PureScriptの型を付けることができましたが、このままではJavaScriptのコールバックの問題がそのまま残っています。

たとえば上記の関数をそのまま使ってファイルのコピーをしようと思うと以下のような冗長な記述が必要になるでしょう。

```purescript
copyFile :: forall eff. String -> String -> CopyCallback eff -> Eff (fs :: FS | eff) Unit
copyFile from to callback =
  readFile from $ either
    (\error -> callback $ Just error)
    (\dat -> writeFile to dat callback)
```

コールバックのネストと、エラーコールバックの呼び出しが二箇所に登場しています。
これが多段になるとさらに恐ろしいことになるのはJavaScriptと同じです。

## そこでAffにする

そこでこのコールバックの問題を解決するために`readFile`と`writeFile`をAffにすることを考えてみます。

Affにするには`Control.Monad.Aff`の`makeAff`という関数を使います。

```purescript
makeAff :: forall e a. ((Error -> Eff e Unit) -> (a -> Eff e Unit) -> Eff e Unit) -> Aff e a
```

`makeAff`はちょっとややこしいですが、エラー用のコールバックと正常系のコールバックを受け取るようなEff関数に対して、Affを作ってくれます。
`readFile`と`writeFile`は、この`makeAff`の引数の型とはちょっと異なりますが、基本的に同様の機能を持つ関数です。
`makeAff`と`either`関数や`maybe'`などを使って、この二つの関数をAffにしてみましょう。

```purescript
readFileAff :: forall eff. String -> Aff (fs :: FS | eff) String
readFileAff path = makeAff \errorCallback datCallback -> readFile path $ either errorCallback datCallback

writeFileAff :: forall eff. String -> String -> Aff (fs :: FS | eff) Unit
writeFileAff path dat = makeAff \errorCallback unitCallback -> writeFile path dat $ maybe' unitCallback errorCallback
```

`readFileAff`と`writeFileAff`が`readFile`と`writeFile`をAff化した関数になります。
`readFileAff`と`writeFileAff`の型を見てみますと、`readFile`と`writeFile`に比べてコールバックを受け取らなくなり、かわりに返り値のAffの値が`Unit`ではなくなり、それぞれコールバックで受け取っていたデータの型になっています。

たとえば`readFile`ではファイルから読み込んだデータはコールバックに渡されていましたが、`readFileAff`では返り値のAffの値になっています。
`writeFileAff`も同様です。

# Affを使ってみる

では、`readFileAff`と`writeFileAff`を使って`copyFile`のAff版である`copyFileAff`を作ってみましょう。
これは非常に簡単です。

```purescript
copyFileAff :: forall eff. String -> String -> Aff (fs :: FS | eff) Unit
copyFileAff from to = readFileAff from >>= writeFileAff to
```

Affはモナドなので、いつもの見慣れたモナド関数の合成で終わってしまいました。

## Affを実行する

Affを実行するにはいくつかの手段があります。
適当に実行させて結果も気にしないというのであれば`launchAff`を使うことができます。

先ほどの例で言えば

```purescript
main :: Eff (fs :: FS, err :: EXCEPTION) Unit
main = launchAff $ copyFileAff "a.txt" "b.txt"
```

で、この`main`関数を実行するとファイルのコピーをおこなうことができます。

`launchAff`は実質エラー処理をおこなわない場合にしか使うことができません。
Affが非同期に実行された場合（たいてい場合そうだと思いますが）、`launchAff`の例外も非同期に発生するためです。

ちゃんとエラー処理をしたい場合は`runAff`を使います。

```purescript
main :: Eff (fs :: FS, console :: CONSOLE) Unit
main = runAff (const $ log "error!") (const $ log "done") $ copyFileAff "a.txt" "b.txt"
```

エラー処理の有無でこの二つを使い分けて実行してください。

## EffをAffに組み込む

逆にEff関数をAffの中で使いたい場合は`liftEff`を使います。
たとえばコピーするときに中身のデータをダンプしたい場合は、以下のように途中に`liftEff`を使ってEff関数を挟むことができます。

```purescript
copyFileAndLogAff :: forall eff. String -> String -> Aff (fs :: FS, console :: CONSOLE | eff) Unit
copyFileAndLogAff from to = do
  dat <- readFileAff from
  liftEff $ log dat
  writeFileAff to dat
```

ファイルを読み込んだあとで中身を`log`関数を使って標準出力に出力し、ファイルに書き込む関数ができあがりました。

以上、Affを使えばコールバックが不必要になることが理解していただけたと思います。

# Affのエラー処理

次にAffのエラー処理を見てみます。
上記で紹介したように`runAff`でもエラー処理をおこなうことができますが、他にも色々な方法があります。

## Attempt

エラーから復旧したい場合は`attempt`を使います。
通常Affの値は正常値の値になるわけですが、attemptを使うとEitherでエラー値も取り出すことができます。
これによりエラーから復旧することができます。

たとえばファイルを読み込むのに成功したらコピーをおこない、失敗した場合はデフォルトの値をファイルに書き込む関数を作ってみます。

```purescript
copyFileWithDefaultAff :: forall eff. String -> String -> String -> Aff (fs :: FS | eff) Unit
copyFileWithDefaultAff from to defaultData = do
  errorOrData <- attempt $ readFileAff from
  writeFileAff to $ either (const defaultData) id errorOrData
```

この関数はファイルの読み込みに失敗したら引数のdefaultDataの値が保存されます。

## Alt

最初に述べたようにAffにはAltのインスタンスがあります。
AffのAltインスタンスは最初に成功した結果を返すような動作をします。

```purescript
instance altAff :: Alt (Aff e) where
  alt a1 a2 = attempt a1 >>= either (const a2) pure
```

たとえば二つのファイルのうち最初に読み込みに成功したデータを保存するような関数は以下のように書けます。

```purescript
copyFileOrAff :: forall eff. String -> String -> String -> Aff (fs :: FS | eff) Unit
copyFileOrAff fromA fromB to = do
  dat <- readFileAff fromA <|> readFileAff fromB
  writeFileAff to dat
```

この関数はまず`fromA`のファイルを読み込もうとし成功した場合はそのまま中身をコピーし、失敗した場合は`fromB`のファイルの内容をコピーしようとします。

またAffはPlusのインスタンスもあります。
`empty`はエラーになります。

```purescript
instance plusAff :: Plus (Aff e) where
  empty = throwError $ error "Always fails"
```

よって、たとえば複数のファイルのうち最初に読み込みに成功したデータを保存するような関数は以下のように書けます。

```purescript
copyFileFromOneOfAff :: forall eff. Array String -> String -> Aff (fs :: FS | eff) Unit
copyFileFromOneOfAff froms to = do
  dat <- foldr alt empty $ readFileAff <$> froms
  writeFileAff to dat
```

この関数は受け取ったファイル名の配列のうち最初に読み込みに成功した内容をコピーします。

## MonadError

では、関数中でエラーを発生させたい場合はどうすればいいでしょうか。
その場合MonadErrorの関数を使うことができます。

たとえばファイルをコピーする際にあるサイズを越えるものはエラーにしたい場合を考えてみると、MonadErrorの`throwError`を使って以下のように書けます。

```purescript
copyFileWithinAff :: forall eff. String -> String -> Int -> Aff (fs :: FS | eff) Unit
copyFileWithinAff from to within = do
  dat <- readFileAff from
  when (length dat > within) $ throwError $ error "Size over!"
  writeFileAff to dat
```

これでファイルの中身のサイズが`within`を越えるものはエラーにすることができました。

またMonadErrorの`catchError`を使ってエラーから復旧することもできます。
`attempt`で書いた`copyFileWithDefaultAff`を`catchError`を使って書き直すと以下のようになります。

```purescript
copyFileWithDefaultAff :: forall eff. String -> String -> String -> Aff (fs :: FS | eff) Unit
copyFileWithDefaultAff from to defaultData = do
  dat <- readFileAff from `catchError` recover
  writeFileAff to dat
    where
      recover :: Error -> Aff (fs :: FS | eff) String
      recover = const $ pure defaultData
```

以上、Affで様々なエラー処理を記述できることを確認しました。

# Affにはまだまだ色々な機能があるよ

Affには以上説明した機能の他に

- Affのキャンセル機能
- Aff間で共有できる変数
- Affの並列実行

などの機能もありますが、これらはFFIを説明するという目的を越えますので、説明はまたの機会にします。

# まとめ

今回はFFIで作ったコールバックを受け取る関数をpurescript-affに対応する方法を紹介しました。
JavaScriptに多いコールバック呼び出しをAffにすることで、非同期処理をモナド関数として書くことができ、エラー処理を分離し、Effと同じようにrow typesで副作用を扱うこともできます。
前回と今回の方法を合わせればJavaScriptの関数をPureScriptらしい高機能で統一されたインターフェースで使うことができるようになります。