HaskellerのためのPureScriptことはじめ
<br>
<br>
<br>
<br>
<br>
**【注意事項】この記事はとてもとても古いです。現在のPureScriptとは大きく言語仕様が変わっており、この記事の情報はあまり宛てにならないと思います。あまりに現状のPureScriptとかけ離れているので削除も考えましたが、何かの参考になるかもしれないので一応残しておきます。PureScriptについては、もし英語でも構わないのであれば、PureScriptのオリジナルの開発者であるPhilさん本人による[PureScript by Example](https://leanpub.com/purescript/read)がもっとも信頼できる情報源です。どうしても日本語の情報を探しているなら、かなり古いですが[関数型なAltJS、PureScriptの入門書を邦訳しました。](/post/f1e048fc9a8ca51eddb2/)も参考にしてください。**
<br>
<br>
<br>
<br>
<br>
<br>
----
## **[PureScript](http://www.purescript.org/)**とは:
* Haskellライクな構文とライブラリ
* Row Polymorphismによる柔軟な型システム
* JavaScriptへトランスコンパイルされるAltJS
そんなPureScriptについて調べた内容のざっくりとしたメモです。べつにHaskellユーザじゃなくても読めますのでどうぞ。
## 言語仕様編
### 正格評価
PureScriptは**正格評価**です
### 型とKind
#### * (Type)
Haskellと同じように、`*`は普通のデータ型を表すkindです。`* -> *`は型をひとつとって型を返すkindです。例: `Number :: *`, `String :: *`, `Maybe :: * -> *`, `Maybe Number :: *`, `(->) :: * -> * -> *`, `Number -> Number :: *`, `[] :: * -> *`, `[] Number :: *`, `[Number] :: *`
#### ! (Effect)
PureScriptのkindには`*`とは別に`!`というものもあります。この`!`は入出力や副作用を表すEffectの型のkindです。例: `Data.Trace.Trace :: !`, `Control.Monad.Eff.Random.Random :: !`
`foreign import`を使えば自分で新たなEffectを定義することもできます。
```
foreign import data Counter :: !
```
自分で定義したこの`!`は、FFIで`Eff`と一緒に使います。
#### \# (Row)
Rowは名前と型の組み合わせを複数個順序なしでまとめたものです。副作用を表す型Effでどんな副作用が含まれているかを表したり、オブジェクト型を作り出したりするのに使います。`# *`はフィールドに`*`だけをもつRowで、`# !`はフィールドに`!`だけを持つRowのことです。Rowを作るには`()`を使います。
###### `()`
例: `() :: # *` または `() :: # !`, `( number :: Number ) :: # *`, `( number :: Number, string :: String ) :: # *`, `( trace :: Trace ) :: # !`, `( number :: Number, trace :: Trace )` は * と ! を混ぜているのでエラー
###### `Object`
`Object`は `# *`から`*`を作ってくれます。JavaScriptのオブジェクトのような型は`()`でRowを作ってから`Object`に渡すと作ることができます。
例: `Object :: # * -> *`, `Object () :: *`, `Object ( number :: Number ) :: *`, `Object ( trace :: Trace )` はObjectは # ! は受け取れないのでエラー
`Object ( number :: Number )`は`number`という名前で`Number`という型のプロパティを持ったオブジェクトの型に相当します。
```
earth :: Object ( answer :: Number )
earth = { answer : 42 }
```
###### `{}`
`{}`は`Object ()`のシンタックスシュガーです。JavaScriptのオブジェクトの型を作るときにちょっと便利です。
例: `{} :: *`, `{ number :: Number } :: *`, `{ trace :: Trace }` は `Object ( trace :: Trace )` と同じ理由でエラー
```
earth :: { answer :: Number }
earth = { answer : 42 }
```
###### そのほか
例: `Eff :: # ! -> * -> *`, `Eff () :: * -> *`, `Eff ( trace :: Trace ) :: * -> *`, `Eff ( trace :: Trace ) Number :: *`, `Eff ( number :: Number )` は `Eff` は `# *` を受け取れないのでエラー, `Eff ( trace :: Trace ) Trace` も `*` に `!` を渡そうとしているのでエラー
### オブジェクト(レコード)
`{ answer : 42 }`というようなRecord LiteralでJavaScriptのようなオブジェクトリテラルを書けます。レコードでパターンマッチングもできます。
```
f { foo = "Foo", bar = n } = n
f _ = 0
```
(チュートリアルを読んでると、`{ answer :: Number }`みたいなのをHaskellと同様にRecordと呼んでいたり、`{ answer : 42 }`はRecord Literalと呼んだり、パターンマッチングするときは`Record Pattern`なのですが、Rowから型を作るときの型コンストラクタは`Object`だし、RecordとObjectという呼び方が混在している気もします。このあたりまだよくわかりません)
###### dataの直積型とオブジェクトの使い分け:
* Haskellと同様に、のちほどフィールドを追加しそうなデータ型はオブジェクトにします。`type` で別名も定義しておくと良さそうです
* インスタンスが必要な場合や、データ型が循環する場合では、`newtype`でさらにオブジェクトを包むデータ型を別に定義します
* 直和が必要な場合は`newtype`ではなく `data`でオブジェクトを包むことになります
* 2次元ベクトル型や単方向リストのように、フィールドの内容が明らかで今後増減しそうになくフィールドの個数が少ないデータ型は、直接`data`でデータ型を定義しても構いません。ただし、パターンマッチングは関数定義か`case`式のみで使用可能で、(関数ではない)値の定義や`let`式ではパターンマッチングが使えないらしく、直積型を`data`で定義すると不便なことがあります。dataによる直積型とオブジェクト型(レコード型)は異なるので、Haskellのように一旦dataで直積型を定義しておいてあとからフィールドラベルを付け加えるというようなことは簡単にはできません。筆者もいろいろ調査中ですが、この点についてはもしかしたら今後改善されるのかもしれません。
```hs
module Main(main) where
import Debug.Trace
-- dataで直積型を定義。
data Vector2 = Vector2 Number Number
p :: Vector2
p = Vector2 10 20
-- 関数定義でのパターンマッチングは可能
addVector :: Vector2 -> Vector2 -> Vector2
addVector (Vector2 x1 y1) (Vector2 x2 y2) = Vector2 (x1 + x2) (y1 + y2)
-- case式でもパターンマッチングは可能
px :: Number
px = case p of
Vector2 x y -> x
-- do記法の <- でもパターンマッチングが可能
main = do Vector2 x y <- return $ Vector2 10 20
trace $ show x
-- パースエラー(Haskellでは可能)
Vector2 x y = Vector2 10 20
-- パースエラー(Haskellでは可能)
py = let Vector2 x y = p in y
-- パースエラー(Haskellでは可能)
main = do let Vector2 x y = Vector2 10 20
trace $ show x
-- こういうWorkaroundは可能だけど、さすがにちょっとよみづらい
py = (\(Vector2 x y) -> y) p
-- dataを直接使う場合は、フィールドを取り出す関数を自力で定義するのが正攻法か
xop (Vector2 x _) = x
py = xop p
```
### Row Polymorphism
`( name::String, age::Number )`というRowは`name::String`と`age::Number`という2つのフィールド**のみ**を持った『閉じた』Rowです。それに対し、型変数`t`を加えた`( name::String, age::Number | t)`は**少なくとも**`name::String`と`age::Number`という2つのフィールドを持つ『開いた』Rowです。
`Object ()`のシンタックスシュガーである`{}`も同様で、`{ name::String, age::Number }`は閉じたオブジェクト、`{ name::String, age::Number | t}`は開いたオブジェクトの型になります。
閉じたRowには過不足なく同じ名前で同じ型のフィールドが揃っていなければその型の値として扱えません。
```
type Entry = { firstName :: String, lastName :: String, phone :: String }
john :: Entry
john = { firstName: "John", lastName: "Smith", phone: "555-555-5555" }
```
```
fullName :: { firstName :: String, lastName :: String } -> String
fullName person = person.firstName ++ " " ++ person.lastName
main = trace $ fullName john -- Johnには余計なフィールドphoneが含まれているからERROR
```
それに対し、開いたRowにすると余分なフィールドがあっても受け取れます。`r`にその場に応じて適切な型が補完されると考えるとよいでしょう。この場合、`fullName`の呼び出しでは型変数`r`が`phone :: String`であるとすれば型を一致させることができます。
```
fullName :: forall r. { firstName :: String, lastName :: String | r } -> String ---r を追加
fullName person = person.firstName ++ " " ++ person.lastName
main = trace $ fullName John -- Johnには余計なフィールドphoneが含まれているけどOK
```
Row Polymorphismにより、入出力の時にいろんな`Eff`を混ぜて書くのが楽になります。逆に言えば、`Eff`を`Trace`するための`Eff`、例外を扱うための`Eff`、乱数を生成するための`Eff`……という風に細かく分けて定義し、必要に応じて合成させることでより詳細に制御できます。`Eff`について詳しくは後述します。
### 型変数
型変数を導入する際 `forall`は 必須です
### main
エントリポイントである `main` の型は `main :: Eff :: # ! -> * -> *`というような型です。`main`で`Trace`と`Random`の`Eff`を混ぜて使いたい時の`main`の型は次のようになります。
```
module Main where
import Prelude
import Control.Monad.Eff
import Control.Monad.Eff.Random
import Debug.Trace
main :: Eff (trace :: Trace, random :: Random) Unit
main = do
n <- random
print n
```
もっとも、`main`の型注釈は書かなくても型推論でたぶんなんとかなります。
### モジュール
`module ... where` は必須です。
### magic-do
`do`は`>>=`のシンタックスシュガーですから、
```hs
do trace "hoge"
trace "piyo"
```
のようなコードは`>>=`の入れ子になってしまいそうですが、`Eff`はコンパイラが特別扱いしてうまく平坦なコードにして出力してくれます。このmagic-doはコンパイラオプションでオフにすることもできます。magic-doが効くのは`Eff`だけですから、それ以外の独自のモナドで副作用を記述しようとすると恐ろしいコードが吐かれます(後述)。
### 末尾再帰
末尾再帰もちゃんと最適化してwhileのループにしてくれますので心配いりません。
```hs
go n = do
print n
go (n + 1)
main = go 1
```
```js
var go = function (__copy_n) {
return function __do() {
var n = __copy_n;
tco: while (true) {
Debug_Trace.print(Prelude.showNumber)(n)();
var __tco_n = n + 1;
n = __tco_n;
continue tco;
};
};
};
```
### セクション
演算子のセクションはありません。演算子を`()`で囲んで関数にし、`(+) 42`みたいに書くことはできます。
## ライブラリ編
[PureScript](https://github.com/purescript) や [PureScript Contrib](https://github.com/purescript-contrib) を覗くといろいろあります。
### [Prelude](https://github.com/purescript/purescript/tree/master/prelude)
Preludeに入っているのは本当に基本的なものだけです。`Maybe`や`Either`も別モジュールです。ちゃんと`Fanctor` -> `Applicative` -> `Monad` の階層になっていたり、後発なだけあって全体的に整理されてすっきりしている印象です。
#### Boolean/Number/String
`Boolean`, `Number`, `String`がJavaScriptの`boolean`, `number`, `string`にそのまま対応します。`Boolean`な値は小文字から始まる`true`と`false`です。`true`や`false`はパターンマッチングでも使えるデータコンストラクタなのに!ふしぎ!
#### 標準出力
`trace`, `print`がそれぞれHaskellの`putStrLn`, `print`に相当します。
#### 関数合成
Haskellのような`.`による関数合成はできません。関数は`Semigroupoid`という型クラスのインスタンスがあって、`f . g` は `f <<< g`か`g >>> f`と書きます。
#### Unit
UnitはHaskellのような`() :: ()`ではなく`unit :: Unit`です。
### Foreign Function Interface
PureScriptのFFIは単純で、
* PureScriptから呼ぶ関数はカリー化して定義しておく(PureScriptの関数はJavaScriptレベルでもカリー化されて定義されるので、逆にJavaScriptからPureScriptの関数を呼ぶ場合は引数を一つづつ渡す)
* Effモナドは単にnullary function
* Haskell同様の `foreign import " ... " hoge :: Hoge -> Piyo` というような構文で呼び出す先の型を定義する
というだけです。仕組みは簡単なものの、何しろカリー化しなければならないので愚直に`foreign import`キーワードを使ってPureScriptからJavaScriptの関数を呼ぶのはとても面倒です。ffiを使う場合は[purescript-easy-ffi](https://github.com/pelotom/purescript-easy-ffi)というモジュールを使いましょう。`unsafeForeignFunction `という関数を呼ぶだけです。
```
stringify :: forall a. Number -> a -> String
stringify = unsafeForeignFunction ["n", "x"] "JSON.stringify(x, null, n)"
```
purescript-easy-ffiがあれば、 `foreign import`で直接外部の関数を定義する方法や、`Data.Function`を組み合わせる方法はほとんど使う必要はないと思います。
###### 参考
* [24 Days of PureScript | day 3](https://gist.github.com/paf31/8e9177b20ee920480fbc#day-3---purescript-easy-ffi-and-purescript-oo-ffi)
### 例外処理
JavaScriptのtry/catchのような`Eff`まみれの例外処理をしたければ、[`purescript-exceptions`](https://github.com/purescript/purescript-exceptions) の `Control.Monad.Eff.Exception` をインポートします。 `error`で例外オブジェクトを作り `throwException`で投げて`catchException`で受けとります。
```
module Main where
import Prelude
import Control.Monad.Eff
import Debug.Trace
import Control.Monad.Eff.Exception
main :: forall t. Eff (trace :: Trace, ex :: Exception) Unit
main = do
catchException (\err -> print (message err)) do
print "begin"
throwException (error "Exception")
print "end" -- throwException で抜けるのでこちらは呼ばれない
```
`throwException`はJavaScriptレベルでは`throw`しているだけで、`catchException`も`try-catch`で捕まえているだけです。
### STモナド
`ST`モナドを`runPure`/`runST`で走らせた場合は、コンパイラが特別に処理してくれてただの変数になります
```
collatz :: Number -> Number
collatz n = runPure (runST (do
r <- newSTRef n
count <- newSTRef 0
untilE $ do
modifySTRef count $ (+) 1
m <- readSTRef r
writeSTRef r $ if m % 2 == 0 then m / 2 else 3 * m + 1
return $ m == 1
readSTRef count))
```
```
var collatz = function (n) {
return Control_Monad_Eff.runPure(function __do() {
var _60 = n;
var _59 = 0;
(function () {
while (!(function __do() {
_59 = 1 + _59;
var _58 = _60;
_60 = _58 % 2 === 0 ? _58 / 2 : 3 * _58 + 1;
return _58 === 1;
})()) {
};
return {};
})();
return _59;
});
};
```
ただし、以下のように`ST`を他の`Eff`と混ぜて使った場合は普通にいろんな関数呼び出しにコンパイルされてました。残念。
```
main :: forall a . Eff (trace :: Trace, random :: Random, st :: ST a) Unit
main = do
x <- newSTRef 0
forE 0 100 $ \i -> do
n <- random
modifySTRef x $ (+) n
return unit
readSTRef x >>= print
```
### CanvasとFreeモナド
ブラウザ環境でCanvasに描くためのバインディング[Graphics.Canvas](https://github.com/purescript-contrib/purescript-canvas)もあります。ゲームを作ったりするときに役に立ちそうです。
CanvasのFreeモナド版[Graphics.Canvas.Free](https://github.com/paf31/purescript-free-canvas)もあるみたいです。Freeモナドじゃないほうの生の`Graphics.Canvas`で書こうとすると
```
context <- getContext2D canvas
dimensions <- getCanvasDimensions canvas
clearRect context { x:0, y:0, w:dimensions.width, h:dimensions.height }
save context
setLineWidth 2 context
setShadowOffsetX 1 context
setShadowOffsetY 1 context
...
```
みたいにひたすら`context`に付きまとわれるCanvasの描画コマンド群が、Freeモナドを使うと`context`が消えて次のようにすっきり書くことができます。
```
context <- getContext2D canvas
dimensions <- getCanvasDimensions canvas
clearRect context { x:0, y:0, w:dimensions.width, h:dimensions.height }
runGraphics context $ do
save
setLineWidth 2
setShadowOffsetX 1
setShadowOffsetY 1
...
```
`Graphics.Canvas`だけを使って`Eff`モナドで書けばmagic-doのおかげで以下のようなシンプルなJavaScriptを吐いてくれるのですが、
```js
Graphics_Canvas.save(_53)();
Graphics_Canvas.setLineWidth(2)(_53)();
Graphics_Canvas.setShadowOffsetX(1)(_53)();
Graphics_Canvas.setShadowOffsetY(1)(_53)();
Graphics_Canvas.setShadowColor("#808080")(_53)();
Graphics_Canvas.setStrokeStyle("#FF8000")(_53)();
....
```
`Graphics.Canvas.Free`を使うと、以下の様な恐ろしいコードを吐いてきます。
```js
return Graphics_Canvas_Free.runGraphics(_55)(Prelude[">>="](Control_Monad_Free.bindFree(Data_Coyoneda.functorCoyoneda))(Graphics_Canvas_Free.save)(function () {
return Prelude[">>="](Control_Monad_Free.bindFree(Data_Coyoneda.functorCoyoneda))(Graphics_Canvas_Free.setLineWidth(2))(function () {
return Prelude[">>="](Control_Monad_Free.bindFree(Data_Coyoneda.functorCoyoneda))(Graphics_Canvas_Free.setShadowOffsetX(1))(function () {
return Prelude[">>="](Control_Monad_Free.bindFree(Data_Coyoneda.functorCoyoneda))(Graphics_Canvas_Free.setShadowOffsetY(1))(function () {
return Prelude[">>="](Control_Monad_Free.bindFree(Data_Coyoneda.functorCoyoneda))(Graphics_Canvas_Free.setShadowColor("#808080"))(function () {
return Prelude[">>="](Control_Monad_Free.bindFree(Data_Coyoneda.functorCoyoneda))(Graphics_Canvas_Free.setStrokeStyle("#FF8000"))(function () {
return Prelude[">>="](Control_Monad_Free.bindFree(Data_Coyoneda.functorCoyoneda))(Graphics_Canvas_Free.translate(20)(20))(function () {
return Prelude[">>="](Control_Monad_Free.bindFree(Data_Coyoneda.functorCoyoneda))(Graphics_Canvas_Free.scale(2.0)(1.5))(function () {
return Prelude[">>="](Control_Monad_Free.bindFree(Data_Coyoneda.functorCoyoneda))(Graphics_Canvas_Free.rotate(state.rotation))(function () {
....
```
1回きりの描画ならこれでも構わないのですが、ゲームみたいに頻繁に画面を更新するアプリケーションだとさすがにちょっと心配です。
## 参考文献というかSee Also
まともにPureScriptに取り組む気があるなら、**"PureScript by Example"** と **"24 Days of PureScript"** は必須です。それ以外はお好みに応じてどうぞ。
* **[purescript/wiki](https://github.com/purescript/purescript/wiki)** とりあえずWikiがいろんな情報の入口です
* **[PureScript by Example](https://leanpub.com/purescript/) PureScript開発者本人によって書かれたチュートリアルで、これさえ読めばPureScriptについてはだいたいすべてわかります。開発環境のインストールの方法から関数型言語の基礎的な概念まで丁寧に説明されており、PureScriptの入門に最適だろうと思います。主にJavaScriptのユーザ向けに書かれており、Haskellのような言語に通じていなくても読むことができます(というか、Haskellに通じている人ならすでに知っている内容のほうが多いかもしれません)。** ~~長いので筆者はまだ全部は読んでませんが、十分な量がある充実したテキストなのでそのうち全部目を通そうと思います。~~ 読んだので邦訳しました。読んでください → **[関数型なAltJS、PureScriptの入門書を邦訳しました。](/post/f1e048fc9a8ca51eddb2/)**
* **[24 Days of PureScript](https://gist.github.com/paf31/8e9177b20ee920480fbc)** PureScriptをいじろうと思うなら絶対に目を通しておくべき記事その2
* [Handling Native Effects with the Eff Monad](https://github.com/purescript/purescript/wiki/Handling-Native-Effects-with-the-Eff-Monad) 入出力はHaskellとだいぶ違います。
* [Getting Started with Purescript for Web Development](http://curtis.io/posts/purescript-for-web-development.html) "PureScript by Example"のサンプルコードはNode上で動かすものが多いですが、この記事はブラウザ環境で動かす方法。といってもビルドの方法がちょっと違ったりブラウザ環境専用のモジュールが必要になるだけの話です。
* [pursuit](http://pursuit.purescript.org/) - PureScriptのドキュメント検索エンジン。Hoogleみたいなアレ
## 参考になりそうな小さめのプロジェクト
とりあえず筆者も何かゲームでも作ってみようと思うので、PureScriptで書かれたゲームのデモを幾つか:
* [purescript-asteroids](https://github.com/waterson/purescript-asteroids/) アステロイド。DOMでイベントリスナを登録してSTを書き換えてというベタな方法で書かれている
* [purescript-is-magic](https://github.com/bodil/purescript-is-magic/) マイリトルポニーをジャンプさせてTroll Faceを避けるゲーム。purescript-signalでリアクティブに書かれている
* [purescript-demo-mario](https://github.com/michaelficarra/purescript-demo-mario) purescript-signalでマリオがジャンプするデモ。これもpurescript-signal
## 材料
* [purescript-simple-dom](https://github.com/aktowns/purescript-simple-dom) 基本的なDOMの操作はひと通り揃っているみたいです。別に[purescript-dom](https://github.com/purescript-contrib/purescript-dom/) (`DOM`) というモジュールもありますが、そちらはすっからかん
* [purescript-canvas](https://github.com/purescript-contrib/purescript-canvas) Canvasへの描画。まだ`drawImage`すらなくて辛い
## 感想
PureScriptは筆者が長年追い求めていた理想のAltJSに限りなく近い。すごい。
## さいごに
そのうちPureScriptで何か作ってみようと思います。いろいろわかり次第また追記するつもりです。
(この記事は同じ筆者が Qiita に投稿した記事の複製です。オリジナル記事)