PureScript に入門してみる
PureScript に入門してみる。普段使用している環境が Windows なので、WSL2 の Ubuntuを使って試す。
$ cat /etc/os-release
NAME="Ubuntu"
VERSION="20.04.2 LTS (Focal Fossa)"
ID=ubuntu
ID_LIKE=debian
PRETTY_NAME="Ubuntu 20.04.2 LTS"
VERSION_ID="20.04"
まずは、公式の Getting Started に従って環境構築を進める。
Node.jsのインストール
PureScript のコンパイラをインストールするには npm を使うようなので、まずは Node.js をインストールする。
Node.js のバージョンを切り替えるツールに nvm とか n とかあった気がするけど、昔の記憶なので、今の主流がどれなのか調べてみる。
こういったバージョン切り替えツールのことは "version manager" と呼ぶらしい。
Node.js Version Manager
とりあえずGitHubリポジトリのスター数で比較してみる。(2021年4月15日現在)
node.js version manager | star |
---|---|
nvm | 48.1k |
n | 14.4k |
fnm | 3.7k |
fnm っていうのが Rust で実装された高速なやつみたい。今回は PureScript が目的なので、とりあえず今回は今でも主流っぽい nvm で試す。
nvm をインストール
curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.38.0/install.sh | bash
.bashrc に以下の記述がされているのを確認した。
export NVM_DIR="$([ -z "${XDG_CONFIG_HOME-}" ] && printf %s "${HOME}/.nvm" || printf %s "${XDG_CONFIG_HOME}/nvm")"
[ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh" # This loads nvm
.bashrc を動かしておいて、nvm が入ったか確認してみる。
$ source .bashrc
$ nvm --version
0.38.0
バージョン 1.0.0 いってなかったんか。
Node.js をインストール
nvm から Node.js の最新版をインストールする。
$ nvm install node
Downloading and installing node v15.14.0...
Downloading https://nodejs.org/dist/v15.14.0/node-v15.14.0-linux-x64.tar.xz...
################################################################################################################# 100.0%
Computing checksum with sha256sum
Checksums matched!
Now using node v15.14.0 (npm v7.7.6)
Creating default alias: default -> node (-> v15.14.0)
node と npm がインストールされたことを確認する。
$ node --version
v15.14.0
$ npm --version
7.7.6
入った。
PureScript をインストール
PureScript のコンパイラを npm からインストールする。
$ npm install -g purescript
エラー。
npm ERR! [ FAILURE ] Verify the prebuilt binary works correctly
npm ERR! Error: Command failed: /home/*****/.nvm/versions/node/v15.14.0/lib/node_modules/purescript/purs.bin --version
npm ERR! /home/*/.nvm/versions/node/v15.14.0/lib/node_modules/purescript/purs.bin: error while loading shared libraries: libtinfo.so.5: cannot open shared object file: No such file or directory
libtinfo.so.5: cannot open shared object file: No such file or directory
「libtinfo.so.5」というファイルが無いと言っている。
sudo apt install libtinfo5
しろ、と。
$ sudo apt update
$ sudo apt install libtinfo5
$ npm install -g purescript
$ purs --version
0.14.0
入った。
purs
コマンドを実行すると、コマンドの使い方が表示された。
$ purs
Usage: purs COMMAND
The PureScript compiler and tools
Available options:
--version Show the version number
-h,--help Show this help text
Available commands:
bundle Bundle compiled PureScript modules for the browser
compile Compile PureScript source files
docs Generate documentation from PureScript source files
in a variety of formats, including Markdown and HTML
graph Module dependency graph
hierarchy Generate a GraphViz directed graph of PureScript type
classes
ide Start or query an IDE server process
publish Generates documentation packages for upload to
Pursuit
repl Enter the interactive mode (PSCi)
For help using each individual command, run `purs COMMAND --help`. For example,
`purs compile --help` displays options specific to the `compile` command.
purs 0.14.0
If you encounter problems during installation, see the compiler's Install Guide for troubleshooting suggestions.
「インストールで問題が起きたらここ見てね」っていう説明があって、リンク先のページを見たら libtinfo5 の問題の説明があった。
The PureScript REPL depends on the
curses
library (via the Haskell packageterminfo
).
PureScript REPL が Haskell パッケージ由来の curses
というライブラリに依存している関係で、libtinfo.so.5 が必要になるらしい。
PureScript の実装って Haskell だったのか!
PureScript の GitHub リポジトリのページを見てみると、
Haskell | 71.0% |
PureScript | 25.8% |
Yacc | 1.3% |
Less | 0.7% |
CSS | 0.6% |
Shell | 0.3% |
Other | 0.3% |
Haskell だった。
開発環境のセットアップ
Spago is the recommended package manager and build tool for PureScript.
PureScript のパッケージマネージャーとビルドツールとして Spago というツールが推奨とのこと。
Spago をインストール。
$ npm install -g spago
$ spago --version
0.20.0
入った。
次に、空のディレクトリを作って、そこでspago init
するとのこと。
ディレクトリ名は適当に「purs-project」にしておく。
$ mkdir purs-project
$ cd purs-project
$ spago init
[info] Initializing a sample project or migrating an existing one..
[info] Updating package-set tag to "psc-0.14.0-20210409"
Fetching the new one and generating hashes.. (this might take some time)
[info] Generating new hashes for the package set file so it will be cached.. (this might take some time)
[info] Set up a local Spago project.
[info] Try running `spago build`
もりもり初期環境が作られたっぽい。
$ tree
.
├── packages.dhall
├── spago.dhall
├── src
│ └── Main.purs
└── test
└── Main.purs
2 directories, 4 files
プロジェクトルートにpackage.dhall
とspago.dhall
という2つのファイルが作られて、src
とtest
という2つのディレクトリが作られて、それぞれ中にMain.purs
というファイルが作られた。
packages.dhall
は Spago の設定が入っているらしい。npm で言うpackage.json
的な雰囲気を醸し出しているが、spago.dhall
が依存ライブラリ情報が入っているらしい。
src/Main.purs
がプロジェクトのエントリポイントで、test/Main.purs
が中身からっぽのテストスイートらしい。
tree -a
してみたら、ひっそりと.gitignore
と.purs-repl
というのが居た。
$ tree -a
.
├── .gitignore
├── .purs-repl
├── packages.dhall
├── spago.dhall
├── src
│ └── Main.purs
└── test
└── Main.purs
.gitignore
の内容は以下のとおり。
/bower_components/
/node_modules/
/.pulp-cache/
/output/
/generated-docs/
/.psc-package/
/.psc*
/.purs*
/.psa*
/.spago
まあ、今はここはそんな詳しく見なくてもいいだろう。
.purs-repl
の中身は以下のとおり。
import Prelude
REPL を起動したときにimport
するパッケージが書かれているっぽい。ここに何か書けば、REPL 起動時に自動で実行したい何かを書けるということなんだろう。
Getting Started の説明のここの段階では、spago init
で生成されたそれぞれのファイルの中身については深く触れていなかったが、中身が気になるので見てみる。
package.dhall
の中身の全文は長めなので全部転載しないが、その大半がコメントによる説明で、実質書かれている内容が以下のとおり。
let upstream =
https://github.com/purescript/package-sets/releases/download/psc-0.14.0-20210409/packages.dhall sha256:e81c2f2ce790c0e0d79869d22f7a37d16caeb5bd81cfda71d46c58f6199fd33f
in upstream
パッケージをインストールするときにデータを持ってくる先を設定している風に見える。
そもそもdhall
というファイルフォーマットを初めて見た。
Dhall is a programmable configuration language optimized for maintainability.
こちらも Haskell 製で、プログラマブルな設定用言語らしい。いわゆる DSL というやつか。
spago.dhall
の中身は以下の通り。
{-
Welcome to a Spago project!
You can edit this file as you like.
-}
{ name = "my-project"
, dependencies = [ "console", "effect", "prelude", "psci-support" ]
, packages = ./packages.dhall
, sources = [ "src/**/*.purs", "test/**/*.purs" ]
}
これは依存パッケージとか、プロジェクトの設定のようだ。npm で言う package.json に相当するものと思って問題ないようだ。ファイル名に騙されないようにしないといけない。
ここまでできると(というか、まだspago init
しかしてないけど)、spago build
とspago test
が実行できるらしい。やってみる。
$ spago build
[info] Installing 4 dependencies.
[...省略]
Compiling Type.Proxy
[...省略]
[info] Build succeeded.
もりもりとパッケージインストールとコンパイルが走ってビルドができたようだ。
$ ls
output packages.dhall spago.dhall src test
新たにoutput
というディレクトリが作られていて、そのなかに更に依存しているパッケージごとのディレクトリが作られていて、更にその中にビルドで生成されたファイルがあるようだった。
$ ls output
Control.Applicative Data.Eq.Generic Data.Monoid.Endo Data.Semiring Main
Control.Apply Data.EuclideanRing Data.Monoid.Generic Data.Semiring.Generic PSCI.Support
Control.Bind Data.Field Data.Monoid.Multiplicative Data.Show Prelude
Control.Category Data.Function Data.NaturalTransformation Data.Show.Generic Record.Unsafe
Control.Monad Data.Functor Data.Ord Data.Symbol Test.Main
Control.Semigroupoid Data.Generic.Rep Data.Ord.Generic Data.Unit Type.Data.Row
Data.Boolean Data.HeytingAlgebra Data.Ordering Data.Void Type.Data.RowList
Data.BooleanAlgebra Data.HeytingAlgebra.Generic Data.Ring Effect Type.Proxy
Data.Bounded Data.Monoid Data.Ring.Generic Effect.Class cache-db.json
Data.Bounded.Generic Data.Monoid.Additive Data.Semigroup Effect.Class.Console
Data.CommutativeRing Data.Monoid.Conj Data.Semigroup.First Effect.Console
Data.DivisionRing Data.Monoid.Disj Data.Semigroup.Generic Effect.Uncurried
Data.Eq Data.Monoid.Dual Data.Semigroup.Last Effect.Unsafe
てんこ盛り。なんかモナモナした感じのものもある。
spago test
も実行してみる。
$ spago test
[info] Build succeeded.
🍝
You should add some tests.
[info] Tests succeeded.
すぐに完了した。中身が empty と言っていたので
You should add some tests.
「テストを書け」という圧をかけてきている。
spago install
コマンドを使って依存パッケージを追加でインストールすることもできるらしい。Getting Started チュートリアルではspago install lists
として、lists
というパッケージをインストールしているので、やってみる。
$ spago install lists
[info] Installing 26 dependencies.
[...省略]
インストールしたライブラリは.spago
ディレクトリの中に作られるらしく、.spago/lists/{version}
という感じで、ライブラリごとのディレクトリの中に、さらにバージョンごとにディレクトリが作られるようだ。
$ ls -a
. .. .gitignore .purs-repl .spago output packages.dhall spago.dhall src test
$ ls .spago
ls .spago
bifunctors control exists invariant newtype prelude run.js type-equality
console distributive foldable-traversable lazy nonempty profunctor safe-coerce unfoldable
const effect functors lists orders psci-support tailrec unsafe-coerce
contravariant either identity maybe partial refs tuples
すでに色々入っていた。spago build
のときに入っていたのだろうか。
$ ls .spago/lists
v6.0.0
バージョン番号の名前のディレクトリがあった。
$ ls .spago/lists/v6.0.0/
CHANGELOG.md LICENSE LICENSE-GHC.md README.md bench bower.json package.json src test
ライブラリの本体があった。
PSCi
PSCi というのは、いわゆる REPL のようだ。これはspago repl
で実行できるらしい。「.purs-repl
ファイルを作ってロードするモジュールを含められるよ」という説明があった。
If you invoke the PSCi executable directly, you would need to load these files by hand.
PSCi を直に起動する場合は.purs-repl
のようなファイルは手動で読み込む必要があるとのこと。しかし、その直で PSCi を起動する方法は知らない。.purs-repl
を自動で読み込む機能はspago repl
によるものということか。
「PSCi」という名前の「PS」が PureScript で、最後の「i」が interactive っぽいんだけど「C」が何の略かわからない。あ、「SCript」ということか?
とりあえずspago repl
してみる。
$ spago repl
Compiling Record.Unsafe
[...省略]
Compiling Data.List.NonEmpty
PSCi, version 0.14.0
Type :? for help
import Prelude
>
最初にドドドッとコンパイルが走ってから REPL モードに入った。さっきlists
ライブラリを入れたからだろうか。
ここで:?
を入力するとヘルプが見れるようだ。
> :?
The following commands are available:
:? Show this help menu
:quit Quit PSCi
:reload Reload all imported modules while discarding bindings
:clear Discard all imported modules and declared bindings
:browse <module> See all functions in <module>
:type <expr> Show the type of <expr>
:kind <type> Show the kind of <type>
:show import Show all imported modules
:show loaded Show all loaded modules
:show print Show the repl's current printing function
:paste paste Enter multiple lines, terminated by ^D
:complete <prefix> Show completions for <prefix> as if pressing tab
:print <fn> Set the repl's printing function to <fn> (which must be fully qualified)
Further information is available on the PureScript documentation repository:
--> https://github.com/purescript/documentation/blob/master/guides/PSCi.md
:quit
で終了のようなので、いったん終了して、もう1回 REPL を起動してみる。
> :quit
See ya!
()
$ spago repl
PSCi, version 0.14.0
Type :? for help
import Prelude
>
今度はコンパイルが走らずに一瞬で REPL モードに入った。おそらくライブラリやコードに変更があったときに依存しているもの全部コンパイルが入るんだろう。
PSCi では Tab キーを使ってオートコンプリート機能が使えるらしい。
> im
im
とだけ入力したところで Tab キーを押すと、
> import
import
まで補完された。
:type
コマンド
:type
というコマンドで値の型を確認することができるらしい。チュートリアルでは Prelude というモジュールの中にあるmap
という関数の型を確認している。
> import Prelude
> :type map
forall (f :: Type -> Type) (a :: Type) (b :: Type). Functor f => (a -> b) -> f a -> f b
この型の内容の意味はまだわからないけど、とりあえず型が確認できた。
チュートリアルでは、このあとこのPrelude
とData.List
というモジュールを使っていくとのこと。
プロジェクトオイラー #1
チュートリアルは、ここからプロジェクトオイラーの問題への挑戦に移行。
プロジェクト・オイラー(英: Project Euler、名称はレオンハルト・オイラー由来)は、数学やプログラミングなどに興味を持つ大人や学生が主な利用者であり、プログラミング (コンピュータ)による一連の計算問題の解決を目的としたウェブサイトである。
その第1問の問題。
If we list all the natural numbers below 10 that are multiples of 3 or 5, we get 3, 5, 6 and 9. The sum of these multiples is 23.
Find the sum of all the multiples of 3 or 5 below 1000.
10未満の3または5の倍数である3,5,6,9の合計は23。では、1000未満の3または5の倍数の合計を出せ。みたいな感じだ。
チュートリアルでは、まずData.List
モジュールのrange
関数を使っている。
> import Data.List
> range 0 999
(0 : 1 : 2 : 3 : 4 : 5 : 6 : 7 : 8 : 9 : 10 : 11 : 12 : 13 : 14 : ......[ずっと続く]
0から999までのリストがずらっと表示された。
このリストに名前を与えるにはns = range 0 999
のように書くとのこと。
> ns = range 0 999
>
とくに音沙汰無い感じ。これが噂の変数の束縛というやつだろうか。正確な意味はよくわかっていない。
束縛
束縛変数という名称は英語で"bound variable"。bound はおそらく bind の過去分詞で、それで「束縛」。名前に紐付いた値が変わらず縛り付けられている、というふわっとした認識でいいのだろうか。
実は「束縛」には二つの意味がある。一つは、数学的な意味での変数の束縛、もう一つは、識別子と実体の結合という意味での束縛だ。
「AをBに束縛する」と言った場合後者で、プログラミングの文脈ではこちらを耳にすることが多いだろう。
プログラミングの文脈では
こっちのようだ。
名前束縛 (Name binding) あるいは名前結合とは、値を識別子に対応付けることを意味する。値に束縛された識別子を、その値への参照と呼ぶ。
先のブログでは以下のようにも説明している。
Haskellは、x = 42のような定義を与えることによって、変数で値を参照できる。だからといって「Haskellでは変数を値にバインドする」と言い切ってしまうことには大きな問題がある。理由は簡単で、変数に対して値以外もバインドできるからだ。例えばy = x + 1という宣言を考えてみよう。この宣言はyをバインドするが、その対象はx + 1という計算であり、式を評価した結果の値ではない。
これは、Haskell は遅延評価だから言えることなのだろうか。例えば JavaScript では関数を値として評価することができて、かつ正格評価なので、JavaScript で値としての関数を変数に代入することは「名前と値を結びつけた」と表現していいってことだろうか。
とりあえずこの件は置いておいて、チュートリアルを進める。
プロジェクトオイラー第1問の続き
ここでns
を評価してみたら、また0から999のリストが表示された。
> ns
(0 : 1 : 2 : 3 : 4 : 5 : 6 : 7 : 8 : 9 : 10 : 11 : 12 : 13 : 14 : [...ずっと続く]
ここでfilter
関数を使って、基準を満たさない要素を濾過するとのこと。「フィルター」をあえて日本語で「濾過」と書いてみた。たまに「カタカナ語を使うな!日本語で言ったほうがわかりやすい!」という主張を聞くので「フィルター」を「濾過」にした。本当にわかりやすい?
> multiples = filter (\n -> mod n 3 == 0 || mod n 5 == 0) ns
> multiples
(0 : 3 : 5 : 6 : 9 : 10 : 12 : 15 : 18 : 20 : 21 : [...ずっと続く]
multiples
を評価してみたら、1000未満の3または5の倍数が表示された。
filter
が関数で、引数として(\n -> mod n 3 == 0 || mod n 5 == 0)
という関数とns
を与えているように見える。合っているだろうか。
\n
の\
これは、引数を指定するときに必要な記述なんだろうか。
mod
という関数も使われている。これは剰余だろう。
filter
関数はData.List
モジュールにある関数と説明があったんだけど、mod
関数はPrelude
モジュールにある関数だろうか。関数から、それが属するモジュールを調べる方法はあるんだろうか。
> :type multiples
List Int
multiples
の型を確認したらList Int
という型だった。Array<int>
みたいな感じだろう。<>
こういう記号を使わずに、横にそのまま書くのか。
Data.Foldable
モジュールにsum
関数があって、これを使って今回の問題を解決できるとのこと。
> import Data.Foldable
> sum multiples
233168
合計が出た!
コンパイル
さっきまでは REPL でプログラムを実行していたけど、次はプログラムをコンパイルする。
まずは、プログラムのファイルを作成する。src/Euler.purs
というファイルを作成して、以下のプログラムを書く。
module Euler where
import Prelude
import Data.List (range, filter)
import Data.Foldable (sum)
ns = range 0 999
multiples = filter (\n -> mod n 3 == 0 || mod n 5 == 0) ns
answer = sum multiples
個人的に Visual Studio Code をよく使うので、それで書く。
Visual Studio Code の Remote - WSL という拡張機能を使えば、WSL 上のファイル、ディレクトリを開くことができる。
PureScript Languages Support という拡張機能を使えば、PureScript のシンタックスハイライトができる。
モジュールについての大事な点について説明されている。
- ファイルごとにモジュールヘッダー(おそらく
module Euler where
の部分のこと)で始まる
このサンプルでのmodule Euler where
の部分の意味は「このモジュールの名前はEuler
とする」ということだと思う。しかしwhere
が何を意味しているのかわからない。あとで解説があるんだろうか。
A module name consists of one or more capitalized words separated by dots.
この部分の説明は「モジュールの名前は1つか、それ以上のドット区切りの capitalized words になる」みたいな意味のようだけど "capitalized words" の意味がわからない。Google翻訳してみたら「大文字の単語」となった。Euler
から見るに、大文字始まりということだろうか。
- モジュールは満杯の名前を使って輸入される
「フルネームを使ってインポートされる」をむりやり日本語で書いてみた。カタカナ語は日本語にしたほうがわかりやすいんだよね?
まあ、import
するモジュールの名前に相対パスのような書き方はしないということだろう。
-
Prelude
モジュールはmod
や==
などのたくさんの他の共通関数を提供する
mod
はPrelude
だった。というか==
も関数だった?!構文どうなってるんだろう。
mod n 3 == 0
mod n 3
の評価結果に対する(?)==
関数を呼び出して、引数に0
を渡している?まあ、このへんはあとあとやっていくかもしれないので置いておく。
-
Prelude
とだけ書くとモジュールの中のすべての関数を暗に読み込んでいるが、それらを明確にリストすることができる
モジュールの中のどの関数を読み込むのかを明示的に書くこともできるということか。
- ガイドラインでは暗に1つのモジュールを読み込んでいる
どの関数を読み込むのかを明示しなくてもいい感じか?
サンプルプログラムを見ていく。
import Prelude
import Data.List (range, filter)
import Data.Foldable (sum)
ここはモジュールを読み込んでいる。Data.List
モジュールのあとに(range, filter)
というのがある。これが読み込む関数を明示的に書く方法か。そのあと、Data.Foldable
のsum
関数を読み込んでいる。
ns = range 0 999
multiples = filter (\n -> mod n 3 == 0 || mod n 5 == 0) ns
ここは REPL でやったのと同じ内容だ。
answer = sum multiples
最後に合計値にanswer
という名前を与えている。
このファイルのプログラムを REPL から直接読み込むこともできるらしい。
$ spago repl
> import Euler
> answer
233168
合計値が参照できた。
新しく作ったモジュールを JavaScript のプログラムにコンパイルするために Spago を使うことができる。
$ spago build
Compiling Unsafe.Coerce
[...省略]
Compiling Euler
[...省略]
Warning 1 of 11:
in module Control.MonadZero
at .spago/control/v5.0.0/src/Control/MonadZero.purs:48:1 - 48:43 (line 48, column 1 - line 48, column 43)
[...省略]
Warning 11 of 11:
in module Euler
at src/Euler.purs:12:1 - 12:23 (line 12, column 1 - line 12, column 23)
モジュールがコンパイルされたが、警告が11個出た。
コンパイルされたファイルはoutput
ディレクトリにできるとのこと。
$ ls output
Control.Alt Data.Equivalence Data.Maybe.Last Data.Semiring.Generic
[...省略]
Data.Boolean Data.Functor.Product2 Data.Profunctor Euler
[...省略]
ひっそりとEuler
ができあがってる。
$ ls output/Euler
externs.cbor index.js
externs.cbor
とindex.js
の2つのファイルができている。
externs.cbor
の内容は、どうもバイナリ形式っぽい。
index.js
の内容は以下の通り。
// Generated by purs version 0.14.0
"use strict";
var Data_EuclideanRing = require("../Data.EuclideanRing/index.js");
var Data_Foldable = require("../Data.Foldable/index.js");
var Data_List = require("../Data.List/index.js");
var Data_List_Types = require("../Data.List.Types/index.js");
var Data_Semiring = require("../Data.Semiring/index.js");
var ns = Data_List.range(0)(999);
var multiples = Data_List.filter(function (n) {
return Data_EuclideanRing.mod(Data_EuclideanRing.euclideanRingInt)(n)(3) === 0 || Data_EuclideanRing.mod(Data_EuclideanRing.euclideanRingInt)(n)(5) === 0;
})(ns);
var answer = Data_Foldable.sum(Data_List_Types.foldableList)(Data_Semiring.semiringInt)(multiples);
module.exports = {
ns: ns,
multiples: multiples,
answer: answer
};
Human Readable だ。
Data_List.range(0)(999);
カリー化された関数が使われてる様子が伺える。
The compiler will display several warnings about missing type declarations.
コンパイラは型宣言が無いことについて警告を表示する、とのこと。11個の警告はこれか。チュートリアルではプログラムを簡潔にするためにとりあえず省略しているとのこと。
テスト
テストコードを書くためにassert
というライブラリをインストールする。
$ spago install assert
test/Main.purs
に以下のプログラムを記述。
module Test.Main where
import Prelude
import Euler (answer)
import Test.Assert (assert)
main = do
assert (answer == 233168)
先程作ったEuler
モジュールを読み込むためにimport Euler (answer)
が記述されている。モジュール名のあとの名前は個別の関数を書ける、ということだったんだけどanswer
は関数だった…?いや、関数以外にも読み込めるということか。
テスト用のassert
関数を読み込んでいる。
import Test.Assert (assert)
main = do
assert (answer == 233168)
main
がエントリポイントだとして、do
というのが書かれている。「do記法というもの」とか「関数型プログラミングで手続き的プログラミングのようなことが書ける」だというふわっとした認識はあるけど、その正体はよくわかっていない。軽く調べてみたら糖衣構文だとのこと。とりあえず置いておく。
assert (answer == 233168)
はなんとなくわかる。
ここの例はただanswer
の値が正しい答えと同じかどうか見ているだけだけど、実際のテストではmain
関数で複数のテストを合成したEffect
モナドをつかうだろう、とのこと。説明にモナっとしたのが入ってきた。
テストの実行はspago test
。
$ spago test
Compiling Test.Main
Warning found:
in module Test.Main
at test/Main.purs:8:1 - 9:28 (line 8, column 1 - line 9, column 28)
No type declaration was provided for the top-level declaration of main.
It is good practice to provide type declarations as a form of documentation.
The inferred type of main was:
Effect Unit
in value declaration main
See https://github.com/purescript/documentation/blob/master/errors/MissingTypeDeclaration.md for more information,
or to contribute content related to this warning.
[info] Build succeeded.
[error] Some of your project files import modules from packages that are not in the direct dependencies of your project.
To fix this error add the following packages to the list of dependencies in your config:
- foldable-traversable
ん?エラーが出た。
foldable-traversable
パッケージを依存パッケージリストに追加せよ、と。これはチュートリアルで説明されていなかった。
$ spago install foldable-traversable
$ spago test
[info] Build succeeded.
[info] Tests succeeded.
さくっとテストが通った。
最初に出ていた警告は「main
の型宣言をしろ」「main
の型はEffect Unit
じゃない?」と。まだチュートリアルで扱っていない内容なので、スルーしておく。
実行可能ファイルの作成
src/Main.purs
の内容を以下のように変更。
module Main where
import Prelude
import Euler (answer)
import Effect.Console (log)
main = do
log ("The answer is " <> show answer)
Effect.Console
というモジュールにlog
という関数があり、これを使うと標準出力ができるようだ。
標準出力に"The answer is " <> show answer
という内容を出力。
<>
は文字列連結っぽい。
show answer
ではshow
関数にanswer
を渡している。show
は何のためのものだろうか。
これでspago run
すると、実行がされるようだ。
$ spago run
Compiling Main
Warning found:
in module Main
at src/Main.purs:8:1 - 9:40 (line 8, column 1 - line 9, column 40)
No type declaration was provided for the top-level declaration of main.
It is good practice to provide type declarations as a form of documentation.
The inferred type of main was:
Effect Unit
in value declaration main
See https://github.com/purescript/documentation/blob/master/errors/MissingTypeDeclaration.md for more information,
or to contribute content related to this warning.
[info] Build succeeded.
[warn] None of your project files import modules from some projects that are in the direct dependencies of your project.
These dependencies are unused. To fix this warning, remove the following packages from the list of dependencies in your config:
- effect
The answer is 233168
またmain
の型宣言についての警告が出た。今は無視。
[warn] None of your project files import modules from some projects that are in the direct dependencies of your project.
These dependencies are unused. To fix this warning, remove the following packages from the list of dependencies in your config:
- effect
effect
パッケージは使われていないので、この警告を消したかったら依存パッケージから消せ、と。今は無視しておこう。
最後にThe answer is 233168
と、メッセージがしっかり表示されている。
output/Main
ディレクトリが作られている。
$ ls output/Main
externs.cbor index.js
// Generated by purs version 0.14.0
"use strict";
var Data_Show = require("../Data.Show/index.js");
var Effect_Console = require("../Effect.Console/index.js");
var Euler = require("../Euler/index.js");
var main = Effect_Console.log("The answer is " + Data_Show.show(Data_Show.showInt)(Euler.answer));
module.exports = {
main: main
};
できあがってる。
ブラウザ向けにコンパイルする
spago bundle-app
コマンドを使えば、ブラウザ向けの JavaScript コードを出力できるようだ。
$ spago bundle-app
[info] Build succeeded.
[warn] None of your project files import modules from some projects that are in the direct dependencies of your project.
These dependencies are unused. To fix this warning, remove the following packages from the list of dependencies in your config:
- effect
[info] Bundle succeeded and output file to index.js
また使われていない依存パッケージについての警告が出た。ちょっと気持ち悪いので、対処しておく。警告によるとeffect
パッケージが使われていないということなので、これを削除する。
spago.dhall
ファイルの中のdependencies
の中にeffect
があるので、この記述を削除する。
{-
Welcome to a Spago project!
You can edit this file as you like.
-}
{ name = "my-project"
, dependencies =
[ "assert"
, "console"
- , "effect"
, "foldable-traversable"
, "lists"
, "prelude"
, "psci-support"
]
, packages = ./packages.dhall
, sources = [ "src/**/*.purs", "test/**/*.purs" ]
}
あ、シンタックスハイライトの「diff dhall」でdhallのコメントアウトの終わりの-}
が削除行として認識されてしまった……。
これでとりあえず警告は出なくなった。
$ spago bundle-app
[info] Build succeeded.
[info] Bundle succeeded and output file to index.js
ディレクトリのトップにindex.js
が生成されていた。中身は482行あり、ファイルサイズは17KBほど。依存パッケージが全部組み込まれている模様。(いわゆる"バンドル"された)
このバンドルされたファイルの内容には、使用していないコードは削除されているとのこと。
これをHTMLファイルから読み込むことで、ブラウザで動かすことができるらしい。
index.html
ファイルを作成して、以下の内容にする。
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Euler Exercise</title>
</head>
<body>
<script src="./index.js"></script>
<body>
</html>
このindex.html
をブラウザで開いてみるとページは真っ白だが、デベロッパーツールでコンソールを見てみると……
The answer is 233168
出力がされていた!
spago bundle-app
で生成されていたindex.js
の内容のうち、今回作成したEuler
モジュールの部分は以下のようになっていた。
(function($PS) {
// Generated by purs version 0.14.0
"use strict";
$PS["Euler"] = $PS["Euler"] || {};
var exports = $PS["Euler"];
var Data_EuclideanRing = $PS["Data.EuclideanRing"];
var Data_Foldable = $PS["Data.Foldable"];
var Data_List = $PS["Data.List"];
var Data_List_Types = $PS["Data.List.Types"];
var Data_Semiring = $PS["Data.Semiring"];
var ns = Data_List.range(0)(999);
var multiples = Data_List.filter(function (n) {
return Data_EuclideanRing.mod(Data_EuclideanRing.euclideanRingInt)(n)(3) === 0 || Data_EuclideanRing.mod(Data_EuclideanRing.euclideanRingInt)(n)(5) === 0;
})(ns);
var answer = Data_Foldable.sum(Data_List_Types.foldableList)(Data_Semiring.semiringInt)(multiples);
exports["answer"] = answer;
})(PS);
exports
($PS["Euler"]
)に追加されていたのはanswer
のみとなっていた。これはMain
側で参照していたのがanswer
のみだったので、最適化されたのだろうか。
spago build
をしたら CommonJS modules の形式で小分けされてビルドがされるので、Node.js を使った巨大プロジェクト開発では有効だとのこと。
$ node
Welcome to Node.js v15.14.0.
Type ".help" for more information.
> const Euler = require("./output/Euler/index.js")
undefined
> Euler.answer
233168
Node.js から呼び出すことができた!
次のステップは PureScript by Example に行くべき、とのこと。
ここでは実践的な問題解決による PureScript の学習ができるらしい。
すでに Haskell や Elm のような言語をよく知っている場合でも、この PureScript by Example は有効とのこと。自分はどれもよく知らないので、次に進むしかない。
すでに知っている場合は、他に言語リファレンスを眺めてほしい、とのこと。PureScript by Example の次はこれだろうか。
Pursuit も眺めてほしいとのこと。
Pursuit hosts API documentation for PureScript packages.
Pursuit はパッケージの API のリファレンスのようだ。いろんな人が作ったパッケージが公開されている。
WebGL のパッケージもあった。
特にコアライブラリを知ることは価値があるとのこと。
そして、その中でも prelude にはプログラムを書く上で使用頻度の高いたくさんの基本的な概念が提供されているとのこと。
これにて、Getting Started によるチュートリアルは終了とする。
次は「PureScript の訓練をする」のスクラップで学習の記録していく。
そういえば、気になることがあったので、確かめてみた。
チュートリアルの途中で「Prelude
にはmod
や==
などのたくさんの一般的な関数があるよ」という説明があって「==
も関数なのか?!」と思ったので確認。
spago repl
で:type
を使うと値の型を確認することができるので、==
の型を確認してみる。
$ spago repl
> :type ==
Unexpected or mismatched indentation at line 1, column 1
期待しない、またはミスマッチな字下げ、と出てしまった。もしかしたら括弧で囲ってあげたらいけるかも?
> :type (==)
forall (a :: Type). Eq a => a -> a -> Boolean
できたかもしれない!最終的に Boolean になる関数、ということでおそらくそれらしい。forall (a :: Type).
このへんの意味はまだわからない。
構文としては==
の左右に比較する値が書いてあったんだけど、引数がどう渡されているのかわからない。
もしかして== 1 1
こういう書き方もできるんだろうか。
> == 1 1
Unexpected token '==' at line 1, column 1
おっと。括弧で囲ってみる。
> (==) 1 1
true
できてしまった。
詳しくは学習を続けて構文を理解してからにしたほうが良さそうだ。