【注意事項】この記事はとてもとても古いです。現在のPureScriptとは大きく言語仕様が変わっており、この記事の情報はあまり宛てにならないと思います。あまりに現状のPureScriptとかけ離れているので削除も考えましたが、何かの参考になるかもしれないので一応残しておきます。PureScriptについては、もし英語でも構わないのであれば、PureScriptのオリジナルの開発者であるPhilさん本人によるPureScript by Exampleがもっとも信頼できる情報源です。どうしても日本語の情報を探しているなら、かなり古いですが関数型なAltJS、PureScriptの入門書を邦訳しました。も参考にしてください。
PureScriptとは:
- 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で直積型を定義しておいてあとからフィールドラベルを付け加えるというようなことは簡単にはできません。筆者もいろいろ調査中ですが、この点についてはもしかしたら今後改善されるのかもしれません。
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
は>>=
のシンタックスシュガーですから、
do trace "hoge"
trace "piyo"
のようなコードは>>=
の入れ子になってしまいそうですが、Eff
はコンパイラが特別扱いしてうまく平坦なコードにして出力してくれます。このmagic-doはコンパイラオプションでオフにすることもできます。magic-doが効くのはEff
だけですから、それ以外の独自のモナドで副作用を記述しようとすると恐ろしいコードが吐かれます(後述)。
末尾再帰
末尾再帰もちゃんと最適化してwhileのループにしてくれますので心配いりません。
go n = do
print n
go (n + 1)
main = go 1
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 や PureScript Contrib を覗くといろいろあります。
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というモジュールを使いましょう。unsafeForeignFunction
という関数を呼ぶだけです。
stringify :: forall a. Number -> a -> String
stringify = unsafeForeignFunction ["n", "x"] "JSON.stringify(x, null, n)"
purescript-easy-ffiがあれば、 foreign import
で直接外部の関数を定義する方法や、Data.Function
を組み合わせる方法はほとんど使う必要はないと思います。
参考
例外処理
JavaScriptのtry/catchのようなEff
まみれの例外処理をしたければ、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もあります。ゲームを作ったりするときに役に立ちそうです。
CanvasのFreeモナド版Graphics.Canvas.Freeもあるみたいです。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を吐いてくれるのですが、
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
を使うと、以下の様な恐ろしいコードを吐いてきます。
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 とりあえずWikiがいろんな情報の入口です
PureScript by Example PureScript開発者本人によって書かれたチュートリアルで、これさえ読めばPureScriptについてはだいたいすべてわかります。開発環境のインストールの方法から関数型言語の基礎的な概念まで丁寧に説明されており、PureScriptの入門に最適だろうと思います。主にJavaScriptのユーザ向けに書かれており、Haskellのような言語に通じていなくても読むことができます(というか、Haskellに通じている人ならすでに知っている内容のほうが多いかもしれません)。
長いので筆者はまだ全部は読んでませんが、十分な量がある充実したテキストなのでそのうち全部目を通そうと思います。読んだので邦訳しました。読んでください → 関数型なAltJS、PureScriptの入門書を邦訳しました。24 Days of PureScript PureScriptをいじろうと思うなら絶対に目を通しておくべき記事その2
Handling Native Effects with the Eff Monad 入出力はHaskellとだいぶ違います。
Getting Started with Purescript for Web Development "PureScript by Example"のサンプルコードはNode上で動かすものが多いですが、この記事はブラウザ環境で動かす方法。といってもビルドの方法がちょっと違ったりブラウザ環境専用のモジュールが必要になるだけの話です。
pursuit - PureScriptのドキュメント検索エンジン。Hoogleみたいなアレ
参考になりそうな小さめのプロジェクト
とりあえず筆者も何かゲームでも作ってみようと思うので、PureScriptで書かれたゲームのデモを幾つか:
- purescript-asteroids アステロイド。DOMでイベントリスナを登録してSTを書き換えてというベタな方法で書かれている
- purescript-is-magic マイリトルポニーをジャンプさせてTroll Faceを避けるゲーム。purescript-signalでリアクティブに書かれている
- purescript-demo-mario purescript-signalでマリオがジャンプするデモ。これもpurescript-signal
材料
-
purescript-simple-dom 基本的なDOMの操作はひと通り揃っているみたいです。別にpurescript-dom (
DOM
) というモジュールもありますが、そちらはすっからかん -
purescript-canvas Canvasへの描画。まだ
drawImage
すらなくて辛い
感想
PureScriptは筆者が長年追い求めていた理想のAltJSに限りなく近い。すごい。
さいごに
そのうちPureScriptで何か作ってみようと思います。いろいろわかり次第また追記するつもりです。