Haskell
purescript

PureScript使ってみた

More than 3 years have passed since last update.

PureScriptすごい!!いちばんHaskellっぽく書けるaltJSだ!!と思って使い初めて2日位ですが色々メモっておきます。

環境構築

追記 2014/12/05
とりあえず触ってみたい!って時にコンパイラをコンパイルしてコーヒー飲んで待つの辛いので、linux/macosユーザーであれば、githubでリリースされているバイナリを使用するのも良いかもしれません。
https://github.com/purescript/purescript/releases

なにはともあれコンパイラが無いと始まりません。hackageに上がってるのでサクっと入れましょう。

私はサンドボックス内にインストールしたかったので以下の様にインストールを行いました。

shell
$ cabal get purescript
Unpacking to purescript-0.6.2/
$ cd purescript-0.6.2
$ cabal sandbox init
$ cabal install
$ ls ./.cabal-sandbox/bin # ここにパス通した
hierarchy psc       psc-docs  psc-make  psci

node,npmも使うのでOSのパッケージマネージャなどで入れておきましょう。

MacOSX+homebrewならこんな感じで入ります。

shell
$ brew install node
==> Downloading https://downloads.sf.net/project/machomebrew/Bottles/node-0.10.29.
Already downloaded: /Library/Caches/Homebrew/node-0.10.29.mavericks.bottle.tar.gz
==> Pouring node-0.10.29.mavericks.bottle.tar.gz
==> Caveats
Bash completion has been installed to:
  /usr/local/etc/bash_completion.d
==> make install
==> /usr/local/bin/npm update npm -g --prefix /usr/local
==> Summary
?  /usr/local/Cellar/node/0.10.29: 1547 files, 18M

Hello, World!

ちゃんとインストール出来たかの確認も兼ねてHello, World!してみましょう。

拡張子は.purs, .purs.hsあたりにしとくのが普通みたいです。

hello.purs
module Main where

import Debug.Trace(trace)

main = trace "hello, world"

コンパイルして、nodeで実行してみましょう。

shell
$ psc --main=Main hello.purs | node
Hello, World!

上手くいきましたね!

トップレベル関数に型注釈を付けない奴は何やってもダメってghcさんも言っているので、型注釈も付けておきましょう。

hello.purs
module Main where

import Control.Monad.Eff(Eff(..))
import Debug.Trace(Trace(..), trace)

main :: Eff (trace :: Trace) Unit
main = trace "hello, world"

こんな感じで細かく副作用をコントロール出来るのがウリの一つだそうです。今回はtraceしか使ってないのでこうなりますね。

型のtraceの部分は関数traceの型(forall r. String -> Eff (trace :: Trace | r) Unit)と一致させないとダメみたいなので、traceで固定となります。なんか冗長な気もしますが我慢しましょう。

パッケージ管理

全部自分で書くくらいなら窓から投げ捨てた方がマシですので、当然PureScriptでもいろいろなライブラリが書かれています。

bowerなるnode製のパッケージマネージャで管理するのが普通らしいので、これを使ってランダム値を扱うpurescript-randomパッケージを入れてみましょう。

shell
# bowerのインストール
$ npm install -g bower
# purescriptを含むパッケージを検索
$ bower search purescript
Search results:

    purescript-maybe git://github.com/purescript/purescript-maybe.git
    purescript-arrays git://github.com/purescript/purescript-arrays.git
    purescript-monoid git://github.com/purescript/purescript-monoid.git
    purescript-either git://github.com/purescript/purescript-either.git
(省略)

# purescript-randomのインストール
$ bower install purescript-random

インストールしたパッケージはカレントディレクトリのbower_components以下に入るので、見てみましょう。
psc-docsを使うとpurescriptのソースファイルからmarkdown形式のドキュメントを生成することが出来ます。

shell
$ psc-docs bower_components/purescript-random/src/Control/Monad/Eff/Random.purs
# Module Documentation

## Module Control.Monad.Eff.Random

### Types

    data Random :: !


### Values

    random :: forall e. Eff (random :: Random | e) Number

使いかたは一目瞭然なので、使用してみましょう。

random_test.purs
module Main where

import Control.Monad.Eff
import Control.Monad.Eff.Random
import Debug.Trace

main :: Eff (random::Random, trace::Trace) Unit
main = do
    r <- random
    print r
shell
$ psc --main=Main test.purs | node
Error at test.purs line 4, column 1:
Error in module Main
Cannot import unknown module 'Control.Monad.Eff.Random'

おっと、Control.Monad.Eff.Randomも引数に入れてあげないとダメですね。

shell
$ psc --main=Main bower_components/purescript-random/src/Control/Monad/Eff/Random.purs test.purs | node
0.20829654578119516
$ psc --main=Main bower_components/purescript-random/src/Control/Monad/Eff/Random.purs test.purs | node
0.5878193469252437

ちゃんとランダムな数字を出力出来ています!
PureScriptのブログ記事ではgruntを使用してビルドしたりしてますので、参考にすれば良いかと思います。

Foreign Function Interface(FFI)

PureScriptはまだまだ新しい言語なので、既存のライブラリには使いたい機能が無い事も多いです。

そんな時はFFIを使用して自分で使いたい機能をラップしましょう。

PureScriptのFFIは以下の様に単純なので直ぐに慣れるかと思います。

purescript-0.6からpython風の複数行文字列リテラルが入ったのでより書きやすくなりました。

purescript
module ForeignTest where

import Control.Monad.Eff
import DOM
import Data.Maybe
import Data.Function

-- effectとしてimport
foreign import data XHR :: !

-- typeとしてimport
foreign import data Ajax :: *

-- PureScriptの関数名とjavascriptの関数名は一致させる。
-- 関数は1引数ずつカリー化しておく。
-- foreign import multiply """
-- function multiply(a) {
--   return function (b) {
--     return a * b;
--   }
-- }""" :: Number -> Number -> Number

-- またはData.Functionをimportし、
-- 'Fn2 第一引数 第二引数 戻り値'のようにforeign importして、
foreign import multiplyImpl """
function multiplyImpl (a, b) {
  return a * b;
}""" :: Fn2 Number Number Number

-- runFn2を使用してpurescriptの関数に変換する。Fn?は0~5まで定義されている。
-- 0.6.0でFn?, runFn?に最適化が利くようになったので、こちらを使うほうが良い。
multiply :: Number -> Number -> Number
multiply = runFn2 multiplyImpl

-- 副作用を持つ場合など、モナドに包みたい時は一番内側に無引数の関数を追加する。
-- -- bindするときに関数が実行されるため。
foreign import newXMLHttpRequest """
function newXMLHttpRequest() {
  return new XMLHttpRequest();
}""" :: forall r. Eff (xhr :: XHR | r) Ajax

-- PureScriptの型を返したい時は引数で構築子を与える
foreign import getElementByIdImpl """
function getElementByIdImpl(cnsts, idnt) {
  return function(){
    var elm = document.getElementById(idnt);
    if(!!elm) {
       return cnsts.just(elm);
     } else {
       return cnsts.nothing;
     }
   }
 }""" :: forall r. Fn2 {just :: Node -> Maybe Node, nothing :: Maybe Node} String (Eff (dom :: DOM | r) (Maybe Node))

getElementById :: forall r. String -> Eff (dom :: DOM | r) (Maybe Node)
getElementById = runFn2 getElementByIdImpl {just: Just, nothing: Nothing}

嵌った所

だいぶドキュメント読め感強いですが、一応嵌った所を上げておきます。
また嵌り次第追記します。

レコード構文

Haskellのレコード構文

haskell
data Person = Persin { name :: String, age :: Int }

と全く同じ書式でデータ型の定義を書く事が出来ます。

purescript
data Person = Person { name :: String, age :: Number }

PersonはHaskellの様な

2引数でString, Intを取るコンストラクタ

では無く

1引数でString型のnameフィールド、Number型のageフィールドを過不足無く持つオブジェクトを取るコンストラクタ

となります。

当然、Haskellの様に

haskell
main = do
    let p = Person "arice" 12
    print $ age p

とは書けず、

purescript
unB :: B -> {name :: String, age :: Number}

unB (B i) = i

main = do
    let p = Person {name: "bob", age: 15}
    print (unB p).age

の様に書くことになります。

Haskellのレコード構文の様に使う場合は、あんまり便利では無い感強いのでtype宣言に留めておくのが良いのかな?と思います。

purescirpt
-- Person型はname :: String, age :: Numberを**過不足無く**持つ型。
-- 余分なフィールドを許容したいなら{name :: String, age :: Number | r}
-- どのようなフィールドを持っているかに興味が無いなら{ | r}
type Person = {name :: String, age :: Number}

main = do
    let p = {name: "Carol", age: 9} :: Person
    print p.age

Preludeの違いとか

真偽値型も勿論ありますが、頭文字は小文字です。

また、Haskellで言う()は以下の様に定義されておりますので、

purescript
data Unit = Unit {}

unit :: Unit
unit = Unit {}

()を使いたい所にはunitって書いておきましょう。

boolunit.purs
module Main where

import Debug.Trace

main = if true
       then print 1
       else return unit

公式のドキュメントには{ }と表示されてたりします(何故……昔はUnitとして{}が使用されていたみたいです。)が、もちろん{}と書いてもダメなので注意しましょう。

参考