twitter buttonfacebook buttongoogle plus buttonhatena bookmark buttonpocket button
ちょっと小さいのはたしかですが。
2016年9月16日

純粋関数型スクリプト言語PureScriptのはじめかた。コンパイラの使いかたからサーバサイド/クライアントサイドアプリケーション開発まで

JavaScriptHaskellpurescript関数型プログラミング


はじめに

スクリプト言語**PureScript**の開発環境構築から簡単なアプリケーションの作成までの手順を紹介します。この記事ではHalogenというユーザインターフェイスのフレームワークや、Hyperというサーバサイドフレームワークを動かすところまでを紹介しています。そのため、コマンドライン環境のような基本的な環境だけではなく、シングルページアプリケーションのような実用性の高いアプリケーションを作って試せるようになっています。ブラウザ環境で動けば、ウェブページに組み込んで自分が作ったアプリケーションを簡単に他の人に見せられますし、Electronを使って単独で動くウィンドウアプリケーションなんかも作ることができるなど夢が広がりますね。

PureScriptとはどんな言語?

PureScriptにはいろいろな側面がありますが、まずはAltJS、つまりJavaScriptを主なコンパイルターゲットとする言語であるのが大きな特徴でしょう。ブラウザ環境やNode環境での実行が主眼に置かれており、JavaScriptでは難しかった大規模な開発に耐えうる極めて高い堅牢性や可読性を備えています。また、膨大なJavaScriptのライブラリ資産を比較的簡単に活用できるほか、PureScriptのソースコードとそこから出力されるJavaScriptコードを対比させながら学べるという利点もあります。なお、C++やErlangのバックエンドの開発も行われています。

他にも、最高級の機能を備える関数型プログラミング言語としての側面も持ちます。PureScriptの構文やコンセプトの大半は、関数型プログラミング言語の代名詞的存在であるHaskellがもとになっています。しかしPureScriptはそのHaskellのコンセプトを更に洗練し、長い歴史の中で見えてきたHaskellの数々の問題点をことごとく修正し、まったくの新規の言語としてすべてを作り直しているようなものです。そのため、PureScriptはHaskell自身よりもHaskellらしいとすら言われることもあります。また、Haskellの最大の難点といえば『遅延評価』ですが(※諸説あります)、これを取り除くことでHaskellに習熟していない開発者でも馴染みやすい言語仕様になっています。

そして、PureScriptはスクリプト言語であることも忘れてはいけません。スクリプト言語といえば、JavaScriptとかRubyとかPythonとか、そういう動的型付けで様々なシンタックスシュガーが山盛りな言語というイメージですが、PureScriptは極めて厳格な静的型付け、そして機能を絞り込んだ言語仕様や抽象度の高いAPIなどを持つ、どう考えてもその対極に位置する言語です。でも、なにしろ名前に『スクリプト』って入っているので誰がなんと言おうとPureScriptはスクリプト言語です。スクリプト言語らしい、ゆるい気持ちで取り組みましょう。

前提となる知識

PureScriptは主にAltJSとしてJavaScript環境で開発される以上、JavaScriptの周辺ツールを頻繁に使うことになります。

  • HTML/CSS/JavaScript
  • Node環境でのJavaScript開発。CommonJSモジュール
  • パッケージマネージャ。npm/Bower
  • Browserify(Webpack/Rollup)
  • reactに代表される仮想DOMの概要

nodeやbower、browserifyといった基本的なツールは、予めインストールしておいてください。ここに挙げられているものでよく知らないものがあれば、先に簡単に調べておきましょう。

なお、Bowerはもはや非推奨のツールです。現在PureScriptも徐々にBowerへの依存を排除しつつあり、最終的にはBowerから完全に脱却すると思われます。ただ、psc-packageという独自のパッケージマネージャも試験的に開発されていますが、現時点ではまだBowerを使わずに実用的な開発をするのは難しいです。当面はBowerを使いましょう。

Node環境での開発

まずは一番シンプルに、Nodeのコマンドライン環境でPureScriptコードを動かすところまで説明します。

コンパイラのインストール

何をするにも、まずはコンパイラが必要です。nodeがインストールされていることを確認したら、次のコマンドで最新版のコンパイラpursとビルドツールpulp、パッケージマネージャbowerをインストールしてください(もちろんBowerがすでにインストールされているのであれば省いて構いません)。

$ npm install --global purescript pulp bower

ここでは--globalオプションをつけてグローバルにインストールしていますが、もちろんお好みでローカルにインストールしても構いません。

プロジェクトの作成

まずはプロジェクトを作成しましょう。どこかに空のディレクトリを作成して、そこに移動しましょう。そしてpulp initコマンドを実行します。ここでは、helloディレクトリを作って、その中でpulp initを実行しています。

$ mkdir hello
$ cd hello
$ pulp init

成功すると、次のようなファイルやディレクトリで構成されるサンプルプロジェクトが作成されると思います。

  • .gitignore
  • .purs-repl
  • bower.json
  • bower_components
    • ...
  • src
    • Main.purs
  • test
    • Main.purs

srcがPureScriptソースコードを保存するディレクトリです。src/Main.pursというファイルを開いてみましょう。PureScriptソースファイルの拡張子は.pursを使います。次のようなコードが書かれているかと思います。

module Main where

import Prelude
import Control.Monad.Eff (Eff)
import Control.Monad.Eff.Console (CONSOLE, log)

main :: forall e. Eff (console :: CONSOLE | e) Unit
main = do
  log "Hello sailor!"

pulpを利用したビルドと実行

次に、このサンプルプロジェクトのプログラムを動かしてみましょう。アプリケーションを起動するには**pulp runコマンド**を実行します。pulp runを実行すると自動的に再ビルドも行われます。

$ pulp run
* Building project in path/to/hello
Compiling Data.Boolean
Compiling Control.Semigroupoid

...

Compiling Main
Compiling PSCI.Support
* Build successful.
Hello sailor!

こんな感じで、コンパイルされたモジュールの名前がだらーっと出力されたあと、アプリケーションが起動してHello sailor!が出力されたことがわかります。

なお、プログラムの実行はせずにビルドだけを行うには、pulp buildコマンドを使います。また、テストを実施するには**pulp testコマンドを使います。pulp testコマンド**を実行すると、src/Main.pursにあるMainモジュールではなく、test/Main.pursファイルのTest.Mainモジュールが実行されます。

PureScriptパッケージのインストール

PureScriptコンパイラには一切のライブラリが付属しておらず、コンパイラのみでは標準出力はおろか足し算すらできません。すべてのライブラリがコンパイラから完全に分離されているので、まずはプロジェクトにbowerで必要なPureScriptパッケージをインストールしなければなりません。標準出力を行うにはpurescript-consoleパッケージを使いますので、インストールしておきましょう。bower installでパッケージ名を指定するだけです。

$ bower install --save purescript-console

このように、プロジェクトで必要になるたびにbowerでパッケージを集めてきます。開発が進むと他にもいろいろなパッケージが欲しくなると思いますが、PureScriptのパッケージを探すには、pursuitというドキュメント検索サービスを使うか、bowerのサイトで"purescript"というキーワードで検索するといいと思います。bowerに登録されていないライブラリも結構あるので、githubで探してみるのもいいと思います。

コンパイル

PureScriptのツール群はpursという単一の実行可能ファイルにまとめられており、このpursに続いて任意のコマンドを入力することでそれぞれのツールを起動します。PureScriptソースコードをコンパイルするには**purs compileコマンド**を実行しますが、引数にはコンパイルに必要なソースファイルのパスをすべて列挙して与えます。パスにはglobが効きますので、purs compile "**/*.purs"のように指定すれば、.pursの拡張子を持つファイルを片っ端から見つけ出してまとめてコンパイルできます。ただしそのコマンドでは、複数のパッケージをインストールした時に、テスト用のコードのモジュール名が衝突してコンパイルに失敗するケースがあります。基本的には次のようにして、srcディレクトリだけを対象に含めるように指定するといいと思います。

$ purs compile "bower_components/purescript-*/src/**/*.purs" "src/**/*.purs"

また、パスをダブルクォーテーションで囲まずに渡すと、環境によってはシェルがパスのワイルドカードを勝手に展開してしまい、それがpscのglobの挙動と微妙に異なっているためコンパイルが通らないことがありました。psc自身にglobが備わっているので、シェルにワイルドカードを展開されないようにしてください。

成功すれば、outputディレクトリ以下にコンパイルされたJavaScriptコードが出力されているはずです。例として、Main.pursがコンパイルされた結果のoutput/Main/index.jsを掲載しておきます。

// Generated by psc version 0.10.5
"use strict";
var Prelude = require("../Prelude");
var Control_Monad_Eff = require("../Control.Monad.Eff");
var Control_Monad_Eff_Console = require("../Control.Monad.Eff.Console");
var main = Control_Monad_Eff_Console.log("Hello, world!");
module.exports = {
    main: main
};

Node環境でHelloWorld

コンパイルされて出力されたJavaScriptソースファイルはCommonJSモジュールになっているので、JavaScriptで直接書かれた通常のモジュールのように扱うことができます。例えば次のようにすればNode環境で先ほどのHelloWorldを実行することができます。

$ node
> const Main = require('./output/Main/index')
undefined
> Main.main()
Hello, world!
{}

Hyperによるサーバーサイドアプリケーションの開発

次は、Hyperというかなり本格的なサーバサイドアプリケーション開発用のライブラリを使って、アクセスするとHelloとだけ返す簡単なサーバサイドウェブアプリケーションを作ってみましょう。サンプルコードをこちらからコピペしてきました。

module Examples.HelloHyper where

import Prelude
import Control.IxMonad ((:*>))
import Control.Monad.Eff (Eff)
import Control.Monad.Eff.Console (CONSOLE)
import Hyper.Node.Server (defaultOptionsWithLogging, runServer)
import Hyper.Response (closeHeaders, respond, writeStatus)
import Hyper.Status (statusOK)
import Node.HTTP (HTTP)

main :: forall e. Eff (console :: CONSOLE, http :: HTTP | e) Unit
main =
  let app = writeStatus statusOK
            :*> closeHeaders
            :*> respond "Hello, Hyper!"
  in runServer defaultOptionsWithLogging {} app

HyperのBowerパッケージの名前はpurescript-hyperです。忘れないようにパッケージをインストールし、それからコンパイルして、うまく行ったらnodeでアプリケーションを起動してみましょう。起動するときはモジュールの名前を間違えないように気をつけてください。

$ bower install --save purescript-hyper
$ purs compile "bower_components/purescript-*/src/**/*.purs" "src/**/*.purs"
$ node
> const HelloHyper = require('./output/Examples.HelloHyper/index')
undefined
> HelloHyper.main()
undefined
> Listening on http://localhost:3000

これで、あとはブラウザで http://localhost:3000/ にアクセスすればHello, Hyper!と表示されると思います。このコンパイルで生成されたアプリケーションは、もちろんNodeが動く環境ならどんなサーバでも実行することができます。ただし、上の手順で起動するのは少々面倒なので、次のような起動用のスクリプトを用意して起動してもいいでしょう。

const HelloHyper = require('./output/Examples.HelloHyper/index');
HelloHyper.main();
$ node main.js
Listening on http://localhost:3000

また、以降の節で紹介するpurs bundleコマンドを使えば、すべてのモジュールを結合しつつ、単独でそのまま起動できるモジュールを作ることができます。

ブラウザ環境での実行

次は、ブラウザ環境でPureScriptアプリケーションを実行してみましょう。ブラウザ環境ではCommonJSモジュールは直接インポート出来ませんので、purs bundleというコマンドを使って、事前にモジュールを単一のJavaScriptファイルへと結合しておくのがいいと思います。コードは先程のHelloWorldを再び使いましょう。

module Main where

import Prelude
import Control.Monad.Eff (Eff)
import Control.Monad.Eff.Console (CONSOLE, log)

main :: Eff (console :: CONSOLE) Unit
main = log "Hello, world!"

pscでコンパイルした結果はただのCommonJSモジュールなので、browserifywebpackで結合することもできるのですが、psc-bundleで結合したほうが劇的にサイズが小さくなるので、なるべくpsc-bundleを使ったほうがいいです。

$ purs compile "bower_components/purescript-*/src/**/*.purs" "src/**/*.purs"
$ purs bundle "output/**/*.js" --module Main --main Main --output public/app.js

purs bundleコマンドのオプションは次のとおりです。

  • --moduleオプションでモジュールを指定すると、そのモジュールに関連したモジュールだけが結合されます。このオプションを指定しなくても動きますが、ファイルサイズが劇的に小さくなるので指定したほうがいいです。ただし、コマンドの実行に少し時間がかかると思います
  • --outputオプションで出力先ファイルを選べます。指定しない場合は結合の結果が標準出力に垂れ流されます
  • --mainオプションでモジュールを指定すると、そのモジュールのmainが実行されるコードが追加されます。この場合、コンパイル後のコードの最後にPS["Main"].main();という一行が追加されます。

短いのでコンパイル後のコードを全文掲載しておきます。

// Generated by psc-bundle 0.11.4
var PS = {};
(function(exports) {
    "use strict";

  exports.log = function (s) {
    return function () {
      console.log(s);
      return {};
    };
  };
})(PS["Control.Monad.Eff.Console"] = PS["Control.Monad.Eff.Console"] || {});
(function(exports) {
  // Generated by purs version 0.11.4
  "use strict";
  var $foreign = PS["Control.Monad.Eff.Console"];
  var Control_Monad_Eff = PS["Control.Monad.Eff"];
  var Data_Show = PS["Data.Show"];
  var Data_Unit = PS["Data.Unit"];
  exports["log"] = $foreign.log;
})(PS["Control.Monad.Eff.Console"] = PS["Control.Monad.Eff.Console"] || {});
(function(exports) {
    "use strict";
  var Control_Monad_Eff = PS["Control.Monad.Eff"];
  var Control_Monad_Eff_Console = PS["Control.Monad.Eff.Console"];
  var Prelude = PS["Prelude"];        
  var main = Control_Monad_Eff_Console.log("Hello, world!");
  exports["main"] = main;
})(PS["Main"] = PS["Main"] || {});
PS["Main"].main();

これを読み込むpublic/index.htmlを適当に作成しておきます。

<!doctype html>
<script src="app.js"></script>

public/index.htmlをブラウザで開くと、コンソールにHello, world!と表示されるはずです。

psc-bundleで結合したコードは、もちろんNode環境でも次のようにして実行できます。

$ node public/app.js
Hello, world!

単体のアプリケーションを作る場合は、起動したあとでいちいちrequireを呼び出してモジュールをロードするのは面倒ですから、purs bundleでバンドルしてしまうのがいいと思います。

Halogenによるクライアントサイドウェブアプリケーション開発

PureScriptのUIフレームワークではpurescript-halogenというのが一番メジャーです。このフレームワークのテンプレートプロジェクトpurescript-halogen-templateがあるので、これをgitでも何でも構わないのでダウンロードしてきましょう。my-halogen-projectのところはお好みでどうぞ。

$ git clone https://github.com/slamdata/purescript-halogen-template.git my-halogen-project
$ cd my-halogen-project

それから、npm installbower updateでこのテンプレートプロジェクトが依存するパッケージをかき集めます。

$ npm install --production
$ bower update

これができたらビルド可能になるので、先ほどと同じコマンドでpscを叩いてコンパイルし、それからpsc-bundleを叩いて結合します。

$ purs compile "bower_components/purescript-*/src/**/*.purs" "src/**/*.purs"
$ purs bundle "output/**/*.js" --module Main --main Main --output dist/app.js

dist/index.htmlを開くと、ボタンをクリックするたびに表示が切り替わるページが表示されるのがわかると思います。あとはお好みでuglifyなんかで圧縮してもいいと思います。

$ purs bundle "output/**/*.js" --module Main --main Main | uglifyjs > dist/app.js

これで、PureScriptで本格的なシングルページアプリケーションを開発する準備が整いました。この記事ではコンパイラを直接叩きましたが、必要に応じてシェルスクリプトやnpmスクリプトなどで自動化しておくと楽かと思います。このHalogenというフレームワークを使ってどのようにアプリケーションを開発すればいいのかは、また別に記事を書きたいと思っています。

ビルドタスクの自動化

先に説明したコマンドをいちいち手で打つのは面倒くさいので、筆者はnpmスクリプトに次のような感じで書いておくことが多いです。

  "scripts": {
    "prebuild": "purs compile \"bower_components/purescript-*/src/**/*.purs\" \"src/**/*.purs\"",
    "build": "purs bundle \"output/**/*.js\" --module Main --main Main --output public/index.js"
  },

これで、npm run buildを実行すれば単体で実行可能なJavaScriptがpublic/index.jsに出力されます。ビルド自体はコンパイラを直接叩いてもそれほど難しくはないと筆者は思うんですが、一連の作業をさらに簡略化するためにはPureScript専用のビルドツール**pulp**を導入する方法があります。これを使うとブランクなプロジェクトの作成もコマンド一発、コンパイルもコマンド一発です。たまに意味の分からないエラーが出て頭を抱えることもあるんですが、楽なのは確かです。多くのPureScriptプロジェクトで使われているので、いろんなライブラリを試したりするときにいずれ使うことになると思います。

pulpの導入自体はnpm installで一発です。

$ npm install -g purescript pulp

あとは適当なディレクトリを作って、pulp initで空のプロジェクトが作成できます。またpulp runでそのプロジェクトをビルドし実行することができます。

$ pulp init
$ pulp run

筆者があまりpulpを使っていないのもあって、ここではこれ以上詳しい解説はしません。詳しい使い方についてはgithubのプロジェクトページなどを確認してください。

統合開発環境psc-ideの紹介

PureScriptのコンパイラにはpsc-ideというIDE、つまり統合開発環境が付属していています。『統合開発環境』といってもVisual StudioやEclipseのようなゴッツいものではなくて、コンパイラと連携してコードの問題の修復なんかを自動的に実行するためのインターフェイスを提供するコマンドライン上で動くプログラムです。このpsc-ideを使うための拡張が各種エディタについて提供されています。

  • Emacs: https://github.com/epost/psc-ide-emacs
  • Atom: https://github.com/nwolverson/atom-ide-purescript
  • Visual Studio Code: https://github.com/nwolverson/vscode-ide-purescript
  • Vim: https://github.com/FrigoEU/psc-ide-vim

PureScriptではモジュールのインポートがとにかく面倒くさいのですが、ide-purescriptを使ったら簡単にインポートを整理できるようになってだいぶ楽になりました。推論された関数の型の型注釈を補間してくれる機能や、カーソルを合わせると関数の型を表示してくれる機能なんかは、入門したてのころは特に役に立つと思います。

このあとは

  • "PureScript by Example" オリジナルの作者philさんが書いたPureScriptの入門書です。無料で読めます。PureScriptを使いたいならまっさきに読みたい。PureScriptに限らず、現代的な関数型プログラミングの一般的な入門としても適しています。SICPみたいな古文書を読んでる場合じゃないぞ!
  • Wiki Wikiにも色々情報があります
  • 24-days-of-purescript-2016 小ネタ集
  • purescript/documentation ドキュメント用のリポジトリができたらしいです

なお、これ全部英語です。がんばろう。

気になるIssueメモ(随時更新)

  • Inlining #2345 インライン化の話。PureScriptは現時点ではあまり最適化に力を入れていません。でも私もPureScriptのパフォーマンスの問題で躓いたことがあったので、もっと強力な最適化ができるといいとは思うのですが……。
  • Support ES6 and beyond? / Change psc to emit ES6 modules instead of CommonJS ES6対応。内部表現に破壊的変更が加わり、一部のFFIが壊れるかも。結局みんな静的解析が欲しくなるんじゃん。Commonjsモジュールとはなんだったのか。

変更履歴

PureScriptもどんどんバージョンアップしてすぐコンパイル通らなくなって困るのですが、この記事はなるべくメンテナンスしたいと思います。もしこの記事の内容が古くなっていてコンパイルできないようなことがあれば、ご連絡いただけると嬉しいです。この記事のコメント欄でもたぶん気付きますが、ツイッターへご連絡頂いたほうが確実です。

  • 2017/06/12 0.11.5 確認作業。この記事に大きな変更はありません
  • 2017/04/18 0.11アップデート作業。あとpurescript-halogen-templeteだけ
  • 2017/03/30 0.11アップデートの注意書きだけ加えました。そのうちまた更新します
  • 2017/01/25 環境によってコンパイルが通らない問題を修正しました
  • 2017/01/07 記事のメンテナンスをしました。pscのバージョンは**v0.10.5**です。バージョンがガンガン上がっていますが、コンパイラに破壊的変更はないので、この記事の内容にも大きな変更はありません。
  • 2016/11/12 記事のメンテナンスをしました。pscのバージョンは**v0.10.2**です。node-hhtpモジュールでAPIの変更があった部分を修正しました。
  • この記事を最初に書いた時点でのpscのバージョンは0.9.3でした。