Admittedly something small.

ちょっと小さいのはたしかですが。

純粋関数型スクリプト言語PureScriptのはじめかた

2016年9月15日 ( 8年前に投稿 )

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

<script async src="//platform.twitter.com/widgets.js" charset="utf-8"></script>

はじめに

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

PureScriptとはどんな言語?

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

他にも、最高級の機能を備える関数型プログラミング言語としての側面も持ちます。PureScriptの言語仕様やコンセプトの大半は、関数型プログラミング言語の代表的存在のひとつであるHaskellがもとになっています。しかしPureScriptはそのHaskellのコンセプトを更に洗練し、長い歴史の中で見えてきたHaskellの数々の問題点をことごとく修正し、まったくの新規の言語としてすべてを作り直しているようなものです。そのため、PureScriptはHaskell自身よりもHaskellらしいとすら言われることもあります。

そして忘れてはいけないのは、PureScriptはスクリプト言語であることです。言語仕様はまったくスクリプト言語っぽくないですが、名前に『スクリプト』って入っているので。たぶん。スクリプト言語らしい、ゆるい気持ちで取り組みましょう。

前提となる知識

PureScriptは主にAltJSとしてJavaScript環境で開発される以上、JavaScriptの周辺ツールを頻繁に使うことになります。以下のような事項については、概要だけでもいいので頭に入れておきましょう。

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

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

なお、Bowerはもはや非推奨のツールになっています。現在PureScriptも徐々にBowerへの依存を排除しつつあり、いずれBowerから完全に脱却し、psc-packageという独自のパッケージマネージャへ移行すると思われます。しかし、現時点ではpsc-packageはまだ実験的な段階にとどまっていますので、特に理由がなければ今はBowerを使いましょう。どうしてもbowerは嫌でござるという人は、次の記事(英語)を参考にpsc-packageを使ってみてください。

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

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

files.png

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

module Main where

import Prelude
import Effect (Effect)
import Effect.Console (log)

main :: Effect Unit
main = do
  log "Hello sailor!"

ビルドと実行

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

$ pulp run
* Building project in path/to/hello
Compiling Type.Data.Row
Compiling Type.Data.RowList
Compiling Data.Symbol

...

Compiling Main
Compiling PSCI.Support
Compiling Effect.Class.Console
* Build successful.
Hello sailor!

こんな感じで、コンパイルされたモジュールの名前がだらーっと出力されたあと、コンパイル全体が成功するとBuild successfulと出力されます。同時に、コンパイルされた結果のJavaScriptコードが、outputディレクトリ以下に出力されているはずです。それからアプリケーションが起動して、Hello sailor!が出力されたことがわかります。

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

PureScriptモジュールの追加

PureScriptコンパイラに付属するモジュールは最低限の組み込みの型を定義する擬似的なモジュールであるPrimのみで、他には一切のライブラリが含まれないため、コンパイラのみでは標準出力はおろか足し算すらできません。さきほどはpulpが最低限必要なパッケージを自動的にインストールしてくれたためコンパイルが成功しましたが、今後開発が進むとパッケージを手動でインストールすることもあるでしょう。その場合は、bowerを使って必要なPureScriptパッケージをインストールします。

pulp initで生成されたbower.jsonの中の"dependencies"のところを見ると、purescript-preludepurescript-consoleという2つのモジュールへの依存関係が書かれています。

  "dependencies": {
    "purescript-prelude": "^4.0.1",
    "purescript-console": "^4.1.0",
    "purescript-effect": "^2.0.0"
  },

PureScriptのパッケージといえどただの通常のbowerコンポーネントですから、PureScriptパッケージをプロジェクトに追加するにはbower install --save パッケージ名というようなコマンドを実行するだけです。

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

purescript-node-httpによるサーバーサイドアプリケーションの開発

次は、purescript-node-httpというパッケージを使って、アクセスするとHelloとだけ返す簡単なサーバサイドウェブアプリケーションを作ってみます。さきほど作ったひな形のプロジェクトのsrc/Main.pursを編集して、次のようなコードを記述します。

module Main where

import Prelude (Unit, bind, pure, unit, void)
import Effect (Effect)
import Effect.Console (log)
import Node.HTTP (createServer, listen, responseAsStream)
import Node.Stream (end, writeString)
import Node.Encoding (Encoding(UTF8))
import Data.Maybe (Maybe(Nothing))

main :: Effect Unit
main = do
  server <- createServer \req res -> void do 
    let writable = responseAsStream res
    writeString writable UTF8 "Hello!" do 
      end writable (pure unit)

  listen server { hostname: "localhost", port: 8080, backlog: Nothing } do 
    log "Listening on localhost:8080!"

このコードは、Webサーバを起動し、リクエストがあったらHello!とレスポンスを返すだけのプログラムです。createServerでサーバを作成し、listenでリクエストの読み取りを開始しています。そしてリクエストが来たらコールバックが呼ばれ、writeStringでレスポンスを書き込みます。この流れは素のNode/JavaScriptで簡単なHTTPサーバを書くときとほとんど同じであることが、なんとなくわかると思います。

次に、必要なパッケージをインストールしましょう。先ほど述べたとおりpulp initでインストールされるパッケージは最低限ですので、Node.HTTPモジュールなどを使えるように、purescript-node-httpパッケージを追加でインストールしましょう。それからpulp runでアプリケーションを起動してみましょう。

$ bower install --save purescript-node-http
bower purescript-node-http#*    cached https://github.com/purescript-node/purescript-node-http.git#5.0.0
bower purescript-node-http#*  validate 5.0.0 against https://github.com/purescript-node/purescript-node-http.git#*
bower purescript-arraybuffer-types#^2.0.0           cached https://github.com/jacereda/purescript-arraybuffer-types.git#2.0.0

...

├── purescript-tuples#5.0.0
└── purescript-unfoldable#4.0.0

purescript-math#2.1.1 bower_components/purescript-math
$ pulp run 
* Building project in path/to/hello
Compiling Type.Data.Row
Compiling Type.Equality
Compiling Record.Unsafe

...

* Build successful.
> Listening on http://localhost:8080

Build successfulと出力されたあと、最後の行でlocalhost:8080でListenしていることがわかります。これで、あとはブラウザで http://localhost:8080/ にアクセスすればHello!と表示されると思います。

ブラウザ環境での実行

次は、ブラウザ環境でPureScriptアプリケーションを実行してみましょう。ブラウザ環境ではCommonJSモジュールは直接インポート出来ませんので、pulp buildコマンドの--toオプションで出力先のファイルを指定することで、事前にモジュールを単一のJavaScriptファイルへと結合しておくのがいいと思います。先ほどと同じように、次のようにして新たな雛形を用意しましょう。

$ pulp init
$ pulp build --to public/app.js

さて、public/index.jsが作成されたと思いますので、これを読み込むpublic/index.htmlも適当に作成しておきます。

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

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

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

$ node public/app.js
Hello, world!

ここではpurs build --toでバンドルしましたが、pulp buildでコンパイルしてから、webpackなどを使ってバンドルする方法もあります。pulp自身は内部でbrowserifyを使ってバンドルを行っています。

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

最後に、ブラウザ環境でのGUIアプリケーション開発も試してみましょう。PureScriptのUIフレームワークではpurescript-halogenというのが一番メジャーなので、これを使います。これまでと同じようにプロジェクトを作成し、src/Main.pursを次のように編集します。

module Main where

import Data.Identity (Identity(..))
import Data.Maybe (Maybe(..))
import Effect (Effect)
import Halogen.Aff.Util (runHalogenAff, awaitBody)
import Halogen.Component (component)
import Halogen.HTML (text)
import Halogen.VDom.Driver (runUI)
import Prelude (const, ($), (>>=), pure, Unit, unit)

main :: Effect Unit
main = runHalogenAff $ awaitBody >>= runUI (component {
    render: const $ text "Hello, World",
    eval: \(Identity a) -> pure a,
    initialState: const unit,
    receiver : const Nothing
}) unit

それから、npm installbower updateでこのテンプレートプロジェクトが依存するパッケージをかき集めます。Bowerでpurescript-halogenパッケージをインストールすることを忘れないようにしましょう。

$ npm install
$ bower install --save purescript-halogen

これができたらビルド可能になります。pulp buildが成功したら、pulp serverコマンドで開発用サーバを起動しましょう。

$ pulp build --to public/app.js
$ echo '<script src="app.js"></script>' > public/index.html
$ pulp server

あとは、 http://localhost:1337/public/ を開くと、Helloと書かれたページができていることがわかります。

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

統合開発環境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を本格的に使うつもりなら、psc-ideはぜひともお勧めです。推論された関数の型の型注釈を補間してくれる機能や、カーソルを合わせると関数の型を表示してくれる機能なんかは、入門したてのころは特に役に立つと思います。

このあとは

  • "PureScript by Example" オリジナルの作者philさんが書いたPureScriptの入門書です。無料で読めます。PureScriptを使いたいならまっさきに読みたい。PureScriptに限らず、現代的な関数型プログラミングの一般的な入門としても適しています。SICPみたいな古文書を読んでる場合じゃないぞ!ただし、まだ内容がPureScript ver.0.11のままで、最新版のver. 0.12のコンパイラでは本文のサンプルコードの多くがコンパイルできません。つらい。
  • purescript/documentation ドキュメント用のリポジトリです。もちろん、どこもかしこもv0.11以前のコードなので、軒並みコンパイル通りません。つらい。
  • 24-days-of-purescript-2016 小ネタ集。記事中のサンプルコードはとうに古くなっていてコンパイル通りませんので、雰囲気だけお楽しみください。つらい。
  • Try PureScript! ブラウザ上でPureScriptの実行を試せる素敵サービス。v0.11対応です。つらい。

なお、「実例によるPureScript」以外は全部英語です。がんばろう。日本語の情報としては、上記の"PureScript By Example"の日本語版である実例によるPureScriptもありますが、こちらも当然v0.11のままなので、更新をお待ちください。つらい。

<!--

補足・pursコマンドを直接使用したコンパイル

PureScriptのツール群はpursという単一の実行可能ファイルにまとめられており、このpursに続いて任意のコマンドを入力することでそれぞれのツールを起動します。pursコマンドを直接呼び出す方法もありますが、

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

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

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

-->

<!--

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

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

-->

変更履歴

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

  • 2018/06/12 v0.12.0 に対応するように更新しました。Hyperがv0.12に対応するのがもう少し先になりそうな気がするので、サーバのサンプルの部分はpurescript-node-httpを使うように変えました。purescript-halogen-templateもあまり更新されていないので、ミニマルなサンプルコードに置き換えました。
  • 2017/09/24 全編にわたってpulpを使うように変更し、記事の長さも圧縮しました
  • 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でした。

(この記事は同じ筆者が Qiita に投稿した記事の複製です。オリジナル記事)