Closed22

PureScript に入門してみる

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」というファイルが無いと言っている。

https://github.com/wasmerio/wasmer/issues/1651

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 の問題の説明があった。

https://github.com/purescript/purescript/blob/master/INSTALL.md

The PureScript REPL depends on the curses library (via the Haskell package terminfo).

PureScript REPL が Haskell パッケージ由来の curses というライブラリに依存している関係で、libtinfo.so.5 が必要になるらしい。

PureScript の実装って Haskell だったのか!

PureScript の GitHub リポジトリのページを見てみると、

https://github.com/purescript/purescript
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.dhallspago.dhallという2つのファイルが作られて、srctestという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というファイルフォーマットを初めて見た。

https://github.com/dhall-lang/dhall-lang

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 buildspago 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.

「テストを書け」という圧をかけてきている。

https://twitter.com/t_wada/status/1382114919101779968

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

この型の内容の意味はまだわからないけど、とりあえず型が確認できた。

チュートリアルでは、このあとこのPreludeData.Listというモジュールを使っていくとのこと。

プロジェクトオイラー #1

チュートリアルは、ここからプロジェクトオイラーの問題への挑戦に移行。

https://ja.wikipedia.org/wiki/プロジェクト・オイラー

プロジェクト・オイラー(英: 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
>

とくに音沙汰無い感じ。これが噂の変数の束縛というやつだろうか。正確な意味はよくわかっていない。

束縛

https://ja.wikipedia.org/wiki/自由変数と束縛変数

束縛変数という名称は英語で"bound variable"。bound はおそらく bind の過去分詞で、それで「束縛」。名前に紐付いた値が変わらず縛り付けられている、というふわっとした認識でいいのだろうか。

https://fumieval.hatenablog.com/entry/2018/10/31/150056

実は「束縛」には二つの意味がある。一つは、数学的な意味での変数の束縛、もう一つは、識別子と実体の結合という意味での束縛だ。

「AをBに束縛する」と言った場合後者で、プログラミングの文脈ではこちらを耳にすることが多いだろう。

プログラミングの文脈では

https://ja.wikipedia.org/wiki/束縛_(情報工学)#名前束縛

こっちのようだ。

名前束縛 (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というファイルを作成して、以下のプログラムを書く。

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 上のファイル、ディレクトリを開くことができる。

https://marketplace.visualstudio.com/items?itemName=ms-vscode-remote.remote-wsl

PureScript Languages Support という拡張機能を使えば、PureScript のシンタックスハイライトができる。

https://marketplace.visualstudio.com/items?itemName=nwolverson.language-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==などのたくさんの他の共通関数を提供する

modPreludeだった。というか==も関数だった?!構文どうなってるんだろう。

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.Foldablesum関数を読み込んでいる。

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.cborindex.jsの2つのファイルができている。

externs.cborの内容は、どうもバイナリ形式っぽい。

index.jsの内容は以下の通り。

output/Euler/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に以下のプログラムを記述。

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
output/Main/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があるので、この記述を削除する。

spago.dhall
{-
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ファイルを作成して、以下の内容にする。

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モジュールの部分は以下のようになっていた。

index.jsの一部
(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 に行くべき、とのこと。

https://book.purescript.org/

ここでは実践的な問題解決による PureScript の学習ができるらしい。

すでに Haskell や Elm のような言語をよく知っている場合でも、この PureScript by Example は有効とのこと。自分はどれもよく知らないので、次に進むしかない。

すでに知っている場合は、他に言語リファレンスを眺めてほしい、とのこと。PureScript by Example の次はこれだろうか。

https://github.com/purescript/documentation/tree/master/language

Pursuit も眺めてほしいとのこと。

https://pursuit.purescript.org/

Pursuit hosts API documentation for PureScript packages.

Pursuit はパッケージの API のリファレンスのようだ。いろんな人が作ったパッケージが公開されている。

WebGL のパッケージもあった。

https://pursuit.purescript.org/packages/purescript-webgl/2.0.1

特にコアライブラリを知ることは価値があるとのこと。

https://github.com/purescript

そして、その中でも prelude にはプログラムを書く上で使用頻度の高いたくさんの基本的な概念が提供されているとのこと。

https://pursuit.purescript.org/packages/purescript-prelude/5.0.0

これにて、Getting Started によるチュートリアルは終了とする。

次は「PureScript の訓練をする」のスクラップで学習の記録していく。

https://zenn.dev/mozukichi/scraps/e74219b6182c0a

そういえば、気になることがあったので、確かめてみた。

チュートリアルの途中で「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

できてしまった。

詳しくは学習を続けて構文を理解してからにしたほうが良さそうだ。

このスクラップは2021/04/20にクローズされました
作成者以外のコメントは許可されていません