雑記帳
初めての PureScript

PureScript 公式チュートリアルを追いかける (Chapter 2)

イメージ
Chapter 2 - Getting Started
PureScript を試しにやってみたところ、
  • これで JavaScript プログラムを快適に作れるようになるのではないのか?
という希望が持てたこともあり、引き続きチュートリアルの追っかけを続けることにした。
今回は、公式チュートリアルのチャプター2 をやっていく。
また、公式では「GitHub から例題集のデータを拾ってきて、それを使って進めていく」という流れになっているが、ここでは毎回初期化直後の骨組みから出発するという感じでやってみる。
(チャプター1は PureScript とはどういった言語であるのかのアブストと、ちょうど前回やった Hello World プログラムのビルドテストみたいな感じだったので、前回をチャプター1の内容という扱いにしておく。)
演習問題
底辺の長さ x、高さ y の直角三角形の斜辺の長さを求める関数 diagonal を書け!
いざ問題を解く!
取り敢えず Haskell のノリで書いてみる
まずは、
  • EffectIO
  • logputStrLn
ということだけを意識し、Haskell を書く要領でコーディングしてみた。
module Main where

import Prelude
import Data.Number (sqrt)
import Effect (Effect)
import Effect.Console (log)

main :: Effect Unit
main = do
  log (show $ diagonal 3 4)

diagonal x y = sqrt $ x^2 + y^2
これでトランスパイルが通ったら最高だなと思いながら、ビルドをかけてみる。
C:\Users\hikaru\Desktop\PureScript>spago bundle-app
[info] Installing 3 dependencies.
[info] Searching for packages cache metadata..
[info] Recent packages cache metadata found, using it..
[info] Installing "prelude"
[info] Installing "console"
[info] Installing "effect"
[info] Installation complete.
[warn] "Failed to run `purs graph \".spago/console/v6.0.0/src/**/*.purs\" \".spago/effect/v4.0.0/src/**/*.purs\" \".spago/prelude/v6.0.1/src/**/*.purs\" \"src/**/*.purs\" \"test/**/*.purs\"`. Error was:\n\"Error found:\\r\\nin module Main\\r\\nat src\\\\Main.purs:4:1 - 4:26 (line 4, column 1 - line 4, column 26)\\r\\n\\r\\n  Module Data.Number was not found.\\r\\n  Make sure the source file exists, and that it has been provided as an input to the compiler.\\r\\n\\r\\n\\r\\nSee https://github.com/purescript/documentation/blob/master/errors/ModuleNotFound.md for more information,\\r\\nor to contribute content related to this error.\\r\\n\\r\\n\\r\\n\""
Error found:
in module Main
at src\Main.purs:4:1 - 4:26 (line 4, column 1 - line 4, column 26)

  Module Data.Number was not found.
  Make sure the source file exists, and that it has been provided as an input to the compiler.


See https://github.com/purescript/documentation/blob/master/errors/ModuleNotFound.md for more information,
or to contribute content related to this error.


[error] Failed to build.
おっと、コードのトランスパイル以前に、そもそも必要なモジュールがカレントの作業スペースにインストールされてなかった。
ということで numbers モジュールをインストール。
C:\Users\hikaru\Desktop\PureScript>spago install numbers
[info] Installing 8 dependencies.
[info] Searching for packages cache metadata..
[info] Unable to find packages cache metadata, downloading from GitHub..
[info] Installing "numbers"
[info] Installing "unsafe-coerce"
[info] Installing "functions"
[info] Installing "safe-coerce"
[info] Installing "newtype"
[info] Installing "maybe"
[info] Installing "control"
[info] Installing "invariant"
[info] Installation complete.
では、リトライ。
C:\Users\hikaru\Desktop\PureScript>spago bundle-app
[ 2 of 77] Compiling Data.Unit
[ 1 of 77] Compiling Unsafe.Coerce
[ 3 of 77] Compiling Record.Unsafe
[ 4 of 77] Compiling Type.Proxy
[ 5 of 77] Compiling Data.Void
[ 6 of 77] Compiling Data.NaturalTransformation
[ 7 of 77] Compiling Data.Boolean
[ 8 of 77] Compiling Control.Semigroupoid
[ 9 of 77] Compiling Safe.Coerce
[10 of 77] Compiling Data.Symbol
[11 of 77] Compiling Control.Lazy
[12 of 77] Compiling Control.Category
[13 of 77] Compiling Data.Function.Uncurried
[14 of 77] Compiling Data.HeytingAlgebra
[16 of 77] Compiling Data.Semiring
[15 of 77] Compiling Data.Semigroup
[17 of 77] Compiling Data.Show
[18 of 77] Compiling Data.Ring
[19 of 77] Compiling Data.Generic.Rep
[21 of 77] Compiling Data.BooleanAlgebra
[20 of 77] Compiling Data.Eq
[22 of 77] Compiling Data.CommutativeRing
[24 of 77] Compiling Data.Ordering
[23 of 77] Compiling Data.EuclideanRing
[25 of 77] Compiling Data.Ord
[26 of 77] Compiling Data.DivisionRing
[27 of 77] Compiling Data.Field
[28 of 77] Compiling Data.Reflectable
[29 of 77] Compiling Data.Function
[30 of 77] Compiling Data.Monoid
[31 of 77] Compiling Data.Bounded
[32 of 77] Compiling Data.Functor
[33 of 77] Compiling Data.Monoid.Generic
[36 of 77] Compiling Control.Alt
[34 of 77] Compiling Control.Extend
[35 of 77] Compiling Control.Apply
[37 of 77] Compiling Data.Bounded.Generic
[38 of 77] Compiling Control.Plus
[39 of 77] Compiling Control.Comonad
[40 of 77] Compiling Control.Applicative
[41 of 77] Compiling Control.Alternative
[42 of 77] Compiling Control.Bind
[43 of 77] Compiling Control.Monad
[44 of 77] Compiling Prelude
[45 of 77] Compiling Control.MonadPlus
[46 of 77] Compiling Data.Number.Format
[47 of 77] Compiling Data.Ord.Generic
[48 of 77] Compiling Data.Semigroup.Generic
[49 of 77] Compiling Data.Show.Generic
[50 of 77] Compiling Data.Monoid.Dual
[52 of 77] Compiling Data.Monoid.Disj
[51 of 77] Compiling Data.HeytingAlgebra.Generic
[54 of 77] Compiling Data.Ring.Generic
[53 of 77] Compiling Data.Semiring.Generic
[55 of 77] Compiling Data.Monoid.Conj
[56 of 77] Compiling Data.Monoid.Multiplicative
[57 of 77] Compiling Data.Semigroup.First
[58 of 77] Compiling Data.Eq.Generic
[59 of 77] Compiling Data.Semigroup.Last
[61 of 77] Compiling Effect
[60 of 77] Compiling Data.Monoid.Additive
[62 of 77] Compiling Data.Monoid.Endo
[63 of 77] Compiling Effect.Unsafe
[64 of 77] Compiling Effect.Uncurried
[65 of 77] Compiling Effect.Class
[66 of 77] Compiling Effect.Console
[67 of 77] Compiling Data.Newtype
[68 of 77] Compiling Effect.Class.Console
[69 of 77] Compiling Test.Main
[70 of 77] Compiling Data.Monoid.Alternate
[71 of 77] Compiling Data.Functor.Invariant
[72 of 77] Compiling Data.Maybe
[73 of 77] Compiling Data.Maybe.First
[74 of 77] Compiling Data.Maybe.Last
[75 of 77] Compiling Data.Number
[76 of 77] Compiling Main
[77 of 77] Compiling Data.Number.Approximate
Error found:
in module Main
at src\Main.purs:12:24 - 12:25 (line 12, column 24 - line 12, column 25)

  Unknown operator (^)


See https://github.com/purescript/documentation/blob/master/errors/UnknownName.md for more information,
or to contribute content related to this error.


[error] Failed to build.
モジュールの問題は解消されたものの、やはりそう上手くはトランスパイルは通らなかった。
Haskell では (^) が用意されているので、いつものノリで使ってしまったけれど、PureScript の Prelude モジュールには含まれていないっぽい。
冪乗の使用やめて再ビルド
もしかしたら、探せばこの手の演算子が定義されたモジュールはあるのかもしれないけど、
diagonal x y = sqrt $ x*x + y*y
というように一旦冪乗の使用をやめて、トランスパイルをかけてみる。
C:\Users\hikaru\Desktop\PureScript>spago bundle-app
[1 of 1] Compiling Main
Warning found:
at src\Main.purs:12:1 - 12:32 (line 12, column 1 - line 12, column 32)

  No type declaration was provided for the top-level declaration of diagonal.
  It is good practice to provide type declarations as a form of documentation.
  The inferred type of diagonal was:

    Number -> Number -> Number


in value declaration diagonal

See https://github.com/purescript/documentation/blob/master/errors/MissingTypeDeclaration.md for more information,
or to contribute content related to this warning.


Error found:
in module Main
at src\Main.purs:10:24 - 10:25 (line 10, column 24 - line 10, column 25)

  Could not match type

    Int

  with type

    Number


while checking that type Int
  is at least as general as type Number
while checking that expression 3
  has type Number
in value declaration main

See https://github.com/purescript/documentation/blob/master/errors/TypesDoNotUnify.md for more information,
or to contribute content related to this error.


[error] Failed to build.
diagonal の型宣言がされてないという警告は置いておいて、再び違うエラー...
エラーの感じだと Haskell の場合、数値リテラルが「Num 型クラスの任意のインスタンス」である一方で、PureScript は違うのかな?
PureScript での数値リテラルの扱いが Haskell と違う?
ということでインタラクティブモードを起動して、数値リテラルの型を確認してみる。
C:\Users\hikaru\Desktop\PureScript>spago repl
PSCi, version 0.15.8
Type :? for help

import Prelude

> :t 0
Int

> :t 0.0
Number
まさにそうだった。
PureScript における数値リテラルに関しては、Haskell のような柔軟性はないっぽくて、小数点の有無で「Number 型」か「Int 型」のどちらかが割り当てられるっぽい。
でもそうなると、「掛け算の型ってどうなっているのか」が気になるし、実際に調べてみたところ...
> :t (*)
forall (a :: Type). Semiring a => a -> a -> a
まさかの 半環 (semiring) 型クラスの任意のインスタンスの要素に対して定義される形となっている。
半環というのは数学的構造の一種だけど、ここでサクッと説明できるようなものではないので、詳しい定義や説明は Wikipedia 参照。
話がちょっと逸れてしまったけど、sqrt の型は
> import Data.Number (sqrt)
> :t sqrt
Number -> Number
ということなので、数値リテラルに .0 をくっつけて、入力する数値の型を Int から Number に修正してあげればこのエラーは解消されるはず。
module Main where

import Prelude
import Data.Number (sqrt)
import Effect (Effect)
import Effect.Console (log)

main :: Effect Unit
main = do
  log (show $ diagonal 3.0 4.0)

diagonal x y = sqrt $ x*x + y*y
いざ実行。
C:\Users\hikaru\Desktop\PureScript>spago bundle-app
[1 of 1] Compiling Main
Warning found:
in module Main
at src\Main.purs:12:1 - 12:32 (line 12, column 1 - line 12, column 32)

  No type declaration was provided for the top-level declaration of diagonal.
  It is good practice to provide type declarations as a form of documentation.
  The inferred type of diagonal was:

    Number -> Number -> Number


in value declaration diagonal

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.

  index.js  960b

Done in 15ms
[info] Bundle succeeded and output file to index.js
キター!
ブラウザの Web Developer Tools から出力を確認してみたところ、
5.0
がしっかりと表示された!
今回のまとめ
  • PureScript では、演算子 (^) が Prelude モジュールに含まれない。
  • PureScript では、数値リテラルに対して小数点の有無で「Number 型」か「Int 型」のどちらかの決まった型が割り当てられるっぽい。
  • PureScript では、演算子 (*) は半環の一つの演算構造という扱いで、Number 型と Int 型はそれぞれ半環型クラスのインスタンスとして標準で定義されている。
おまけ
出力として得られた JavaScript コード
以下今回最終出力として得られた JavaScript コード。
(() => {
  // output/Data.Number/foreign.js
  var sqrt = Math.sqrt;

  // output/Data.Bounded/foreign.js
  var topChar = String.fromCharCode(65535);
  var bottomChar = String.fromCharCode(0);
  var topNumber = Number.POSITIVE_INFINITY;
  var bottomNumber = Number.NEGATIVE_INFINITY;

  // output/Data.Show/foreign.js
  var showNumberImpl = function(n) {
    var str = n.toString();
    return isNaN(str + ".0") ? str : str + ".0";
  };

  // output/Data.Show/index.js
  var showNumber = {
    show: showNumberImpl
  };
  var show = function(dict) {
    return dict.show;
  };

  // output/Effect.Console/foreign.js
  var log2 = function(s) {
    return function() {
      console.log(s);
    };
  };

  // output/Main/index.js
  var diagonal = function(x) {
    return function(y) {
      return sqrt(x * x + y * y);
    };
  };
  var main = /* @__PURE__ */ log2(/* @__PURE__ */ show(showNumber)(/* @__PURE__ */ diagonal(3)(4)));

  // <stdin>
  main();
})();