Admittedly something small.
2016年9月16日

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

JavaScript Haskell purescript 関数型プログラミング


はじめに

スクリプト言語PureScriptの開発環境構築手順を紹介します。PureScriptにはAltJSとしての側面があり、ブラウザ環境やNode環境での実行が主眼に置かれており、JavaScriptでは難しかった大規模な開発に耐えうる極めて高い堅牢性や可読性を備えています。また、JavaScriptに変換されるため、膨大なJavaScriptの資産を比較的簡単に活用できるほか、PureScriptのソースコードとそこから出力されるJavaScriptコードを対比させながら学べるという利点もあります。

PureScriptは最高級の機能を備える関数型プログラミング言語としての側面も持ちます。関数型プログラミング言語の代名詞的存在であるHaskellは、互換性を保つためにその長い歴史の中で数々の欠陥を抱え込んできました。PureScriptはHaskellのコンセプトを更に洗練して、新規の言語としてすべてを作り直すもので、PureScriptはHaskellよりもHaskellらしいとすら言われることもあります。しかも、Haskellの最大の難点であった『遅延評価』を取り除くことで、Haskellに習熟していない開発者でも馴染みやすい言語仕様になっています。

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

前提となる知識

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

Node環境での開発

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

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

何をするにも、まずはコンパイラが必要です。nodeがインストールしてあれば、最新版のコンパイラをインストールするには次のコマンド一発だけです。

$ npm install -g purescript

ソースファイルの作成

HelloWorldのソースファイルを作成しましょう。新しいプロジェクトのために適当なディレクトリを作成して、そこにsrc/Main.pursというファイルを作成し、次のようなコードをコピペして保存してください。PureScriptソースファイルの拡張子は.pursを使います。

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!"

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');
Main.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!"
$ purs compile "bower_components/purescript-*/src/**/*.purs" "src/**/*.purs"
$ purs bundle "output/**/*.js" --module Main --main Main --output public/app.js

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

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

// 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を適当に作成しておきます。


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

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

$ node public/app.js
Hello, world!

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

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

注:Halogenは0.11にアップデートされたのですが、purescript-halogen-templeteのアップデートがまだです

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を使うための拡張が各種エディタについて提供されています。

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

このあとは

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

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

変更履歴

PureScriptもどんどんバージョンアップしてすぐコンパイル通らなくなって困るのですが、この記事はなるべくメンテナンスしたいと思います。


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


このエントリーをはてなブックマークに追加