Closed9

PureScript の訓練をする #2 入門

Kazuki MiyanishiKazuki Miyanishi

PureScript by Example の Chapter 2 Getting Started に入っていきます。

https://book.purescript.org/chapter2.html

このチャプターのゴールは以下のもののよう。

  • PureScript の開発環境のセットアップ
  • 練習問題をいくつか解く
  • テストを使って解答を確認

https://github.com/purescript/documentation/blob/master/guides/Getting-Started.md

まずは上記のページに従って開発環境のセットアップと言語の基本について学ぶ、とあるけど、これはすでに実施済み。

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

練習問題を解く

練習問題を解くための開発環境として以下のリポジトリをクローンするとのこと。

git clone https://github.com/purescript-contrib/purescript-book.git
$ ls purescript-book/
CNAME  CONTRIBUTING.md  README.md  book.toml  deploy_key.enc  exercises  scripts  text

クローンされました。

このリポジトリには PureScript のサンプルコードとチャプターごとの練習問題のためのユニットテストが含まれるとのこと。

練習問題を解くための初期設定が必要になるみたいで、その初期設定のために以下の2つのスクリプトがあるみたい。

  • resetSolutions.sh: 練習問題のリセット
  • removeAnchors.sh: アンカーの削除

これらはscriptsディレクトリの中にある。これらの意味がちょっとわからない。なので中身を覗いてみる。

resetSolutions.sh
#!/usr/bin/env bash

# This script automatically resets exercises so they are ready to be solved.
#  - Removes lines with a note to delete them.
#  - Moves the no-peeking directory outside of the compilation path.

# For all chapters
for d in exercises/*; do
  # if directory (excludes LICENSE file)
  if [ -d $d ]; then
    perl -ni -e 'print if !/Note to reader: Delete this line/' $d/test/Main.purs
  fi
  # if there's a no-peeking directory
  if [ -d $d/test/no-peeking ]; then
    mv $d/test/no-peeking $d/no-peeking
  fi
done

上のほうにコメントが書いてある。

  • 削除の注意書きがある行を削除する
  • no-peeking ディレクトリをコンパイルパスの外に移動する

よくわからん。bashのコードを見てみると、最初にexercisesディレクトリ以下のtest/Main.pursファイルについて、Perl にオプション-niprint if !/Note to reader: Delete this line/を実行している。

Perl のオプションなんて覚えてないので、ヘルプで見てみる。

$ perl --help

Usage: perl [switches] [--] [programfile] [arguments]
[...省略]
  -i[extension]     edit <> files in place (makes backup if extension supplied)
[...省略]
  -n                assume "while (<>) { ... }" loop around program

iオプションは拡張子と一緒につけて使うみたいだけど、省略もできるようだ。

edit <> files in place (makes backup if extension supplied)

<>を編集する。<>って何だ。

https://tutorial.perlzemi.com/blog/20080722121673.html

ファイルを一行づつ読み込むには行入力演算子「<>」を使用します。読み込む行がなくなると、undefを返却します。

Perl のコマンドの最後に与えてるファイルの内容を<>から読み込むようにしているということだろうか。

-nオプションは、プログラムの周りにwhile (<>) { ... }のループで囲うとのこと。

print if !/Note to reader: Delete this line/

Note to reader: Delete this lineのパターンにマッチしないものだけprintという意味のように見える。

試しに実験。

$ cat hoge.txt
hoge
foo
foo
hoge
bar
hoge
bar
$ perl -ni -e 'print if !/hoge/' hoge.txt
$ cat hoge.txt
foo
foo
bar
bar

hogeの行だけ消えた。

<>でファイルの入力というのはわかったけど、出力してかつファイルに書き込みもするというのは想像できなかった。

試しにexercises/chapter2/test/Main.pursの中を覗いてみたら、コメントでNote to reader: Delete this lineが書かれている行があった。これが練習問題の解答になっている行かもしれないようなので、内容は見ないでそっと閉じておいた。

Kazuki MiyanishiKazuki Miyanishi

scripts/resetSolutions.shの以下の部分について一応見ておく。

if [ -d $d/test/no-peeking ]; then
    mv $d/test/no-peeking $d/no-peeking
fi

これは単純に、test/no-peekingディレクトリを移動させているだけだ。試しにこのディレクトリを覗いてみたらSolutions.pursというファイルがあった。解答の内容が書かれているような感じなので、中は見ないでおく。

no-peekingを直訳したら「覗き見禁止」だった。危ない危ない。

ということで、このresetSolutions.shを実行しておく。

$ ./scripts/resetSolutions.sh

音沙汰は無いが、テストコードから解答らしき行が消えて、no-peekingディレクトリも移動されていた。

Kazuki MiyanishiKazuki Miyanishi

scripts/removeAnchors.shの内容は以下の通り。

#!/usr/bin/env bash

# This script removes all code anchors to improve readability

# All .purs files in the exercises directories (excluding hidden files)
ALL_PURS=$(find exercises \( ! -regex '.*/\..*' \) -type f -name '*.purs')

for f in $ALL_PURS; do
  # Delete lines starting with an '-- ANCHOR' comment
  perl -ni -e 'print if !/^\s*-- ANCHOR/' $f
done

コードを読みやすくするためにexercisesディレクトリ以下の全.pursファイルから、-- ANCHORから始まる行をすべて削除するとのこと。最初から削除されていない理由はあとでわかるだろうか。

と思ったら、書いてあった。

these anchors are used for copying code snippets into the book's rendered markdown, and you probably don't need this clutter in your local repo):

この本の Markdown のレンダリングのために使われているだけで、練習問題を解く上では必要のないもののようだ。

ということでこれも実行。

$ ./scripts/removeAnchors.sh

説明では以下も実行しろと言わんばかりの記述がある。

git add .
git commit --all --message "Exercises ready to be solved"

ローカルリポジトリに変更を保存していけということか。

$ git add .
$ git commit -a -m "練習問題を解く準備完了"

ローカル用のリポジトリだから、コメントは日本語でいいや。

Kazuki MiyanishiKazuki Miyanishi

練習問題 Chapter 2

exercise/chapter2ディレクトリに入って、spago testを実行。

$ cd exercises/chapter2/
$ spago test
[info] Installing 54 dependencies.
...省略

依存パッケージのインストールとコンパイルがもりもり走った。

何やら警告が出ている。

Warning 1 of 25:

  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)

    A custom warning occurred while solving type class constraints:

      'MonadZero' is deprecated, use 'Monad' and 'Alternative' constraints instead


  in value declaration monadZeroArray

  See https://github.com/purescript/documentation/blob/master/errors/UserDefinedWarning.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:
- integers
- math
→ Suite: Euler - Sum of Multiples
  ✓ Passed: below 10
  ✓ Passed: below 1000

All 2 tests passed! 🎉
[info] Tests succeeded.

テストが通ったようだ。練習問題やるのに、今テスト通っちゃっていいんだろうか、と思ったけど、どうやら良いみたいだ。

何やらここでも警告が出ている。使ってない依存パッケージがある、とのことだ。これは練習問題で使うのだろうか。

Kazuki MiyanishiKazuki Miyanishi

公式ガイドの入門の方では1000未満の3または5の倍数の合計になっていたが、

ns = range 0 999

multiples = filter (\n -> mod n 3 == 0 || mod n 5 == 0) ns

今回の入門ではn未満の3または5の倍数の合計を求める式がすでに記述されている。

src/Euler.purs
ns n = range 0 (n - 1)

multiples n = filter (\n -> mod n 3 == 0 || mod n 5 == 0) (ns n)

ns関数が引数nを取るようになっている。

そして、テストスイートには包括的なテストが書かれている。

test/Main.purs
module Test.Main where

import Prelude
import Test.MySolutions
import Effect (Effect)
import Euler (answer)
import Test.Unit (suite, test)
import Test.Unit.Assert as Assert
import Test.Unit.Main (runTest)

main :: Effect Unit
main = do
  runTest do
    suite "Euler - Sum of Multiples" do
      test "below 10" do
        Assert.equal 23 (answer 10)
      test "below 1000" do
        Assert.equal 233168 (answer 1000)
    {-  Move this block comment starting point to enable more tests
    suite "diagonal" do
      test "3 4 5" do
        Assert.equal 5.0 (diagonal 3.0 4.0)
      test "5 12 13" do
        Assert.equal 13.0 (diagonal 5.0 12.0)
    suite "circleArea" do
      test "radius 1" do
        Assert.equal 3.141592653589793 (circleArea 1.0)
      test "radius 3" do
        Assert.equal 28.274333882308138 (circleArea 3.0)
    suite "leftoverCents" do
      test "23" do
        Assert.equal 23 (leftoverCents 23)
      test "456" do
        Assert.equal 56 (leftoverCents 456)
      test "-789" do
        Assert.equal (-89) (leftoverCents (-789))

-}

初めて見るものがたくさん登場している。しかし、このへんの細かい部分については、今は詳しく理解しなくて良いとのこと。

コメントアウトされているところには

Move this block comment starting point to enable more tests

このブロックコメントの開始点を移動させてもっとテストを有効にする

とある。

解説には

If you write your solutions in the Test.MySolutions module (test/MySolutions.purs), you can check your work against the provided test suite.

もしType.MySolutionsモジュール (test/MySolutions.purs ) にソリューションを書くなら、提供されたテストスイートに対して作業を確認できます。

とあるけど、その意味がよくわからない。

test/MySolutions.purs には

test/MySolutions.purs
module Test.MySolutions where

import Prelude

とだけ書いてある。

Kazuki MiyanishiKazuki Miyanishi

Exercise

(Medium) Write a diagonal function to compute the length of the diagonal (or hypotenuse) of a right-angled triangle when given the lengths of the two other sides.

(Medium) 他の側面の2つの長さが与えられたときに right-angled 三角形の diagonal (または hypotenuse ) の長さを計算するdiagonal関数を書け。

英語力不足で読解が厳しい。

最初の "(Medium)" は「中級レベル」みたいな意味だと思っていいだろうか。

right-angled は「直角」。diagonal は「対角線」。hypotenuse は「斜辺」とのこと。

整理すると

(中級)他の側面の2つの長さが与えられたときに直角三角形の対角線(または斜辺)の長さを計算するdiagonal関数を書け。

ということだ。2点間距離の計算っぽい。

Solution

まずはテストを有効にするとのこと。

コメント開始点を移動させて、以下のテストを有効にさせた。

test/Main.purs
-    {-  Move this block comment starting point to enable more tests
    suite "diagonal" do
      test "3 4 5" do
        Assert.equal 5.0 (diagonal 3.0 4.0)
      test "5 12 13" do
        Assert.equal 13.0 (diagonal 5.0 12.0)
+    {-  Move this block comment starting point to enable more tests

diagonal関数に3.04.0の引数を与えたら5.0が返ってくるテストと、5.012.0の引数を与えたら13.0が返ってくるテストがあるように見える。

この状態でテストを実行。

$ pwd
~/purescript-book/exercises/chapter2

$ spago test
Compiling Test.Main
Error found:
in module Test.Main
at test/Main.purs:21:27 - 21:35 (line 21, column 27 - line 21, column 35)

  Unknown value diagonal


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.

まだdiagonal関数は実装していないので、期待通りにテスト失敗。

まず問題のあるバージョンの関数を作ったときに何が起こるのかを見るために、以下のコードをtest/MySolutions.pursに追加するとのこと。

import Math (sqrt)

diagonal w h = sqrt (w * w + h)

そしてもう一度テスト実行。

$ spago test
Compiling Test.MySolutions
Compiling Test.Main
Warning 1 of 3:

  in module Test.MySolutions
  at test/MySolutions.purs:6:1 - 6:32 (line 6, column 1 - line 6, 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.

Warning 2 of 3:

  in module Test.Main
  at test/Main.purs:3:1 - 3:15 (line 3, column 1 - line 3, column 15)

    Module Prelude has unspecified imports, consider using the explicit form:

      import Prelude (Unit, discard)



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

Warning 3 of 3:

  in module Test.Main
  at test/Main.purs:4:1 - 4:24 (line 4, column 1 - line 4, column 24)

    Module Test.MySolutions has unspecified imports, consider using the explicit form:

      import Test.MySolutions (diagonal)



  See https://github.com/purescript/documentation/blob/master/errors/ImplicitImport.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:
- integers
→ Suite: Euler - Sum of Multiples
  ✓ Passed: below 10
  ✓ Passed: below 1000
→ Suite: diagonal
  ☠ Failed: 3 4 5 because expected 5.0, got 3.605551275463989
  ☠ Failed: 5 12 13 because expected 13.0, got 6.082762530298219

2 tests failed:

In "diagonal":
  In "5 12 13":
    expected 13.0, got 6.082762530298219

In "diagonal":
  In "3 4 5":
    expected 5.0, got 3.605551275463989

[error] Tests failed: exit code: 1

色々警告が出てしまった。型宣言についてと、importで使用する関数を指定しろ、という関係の警告に見える。あと「integer モジュール使ってないから削除しろ」という警告。今はとりあえず無視。

Suite: diagonalのテストがドクロマーク☠と共に失敗の表示になっている。

関数を以下のようにすると、テストがうまくいくとのこと。

diagonal w h = sqrt (w * w + h * h)

距離の計算式 \sqrt{x^2+y^2} だ。

実装を直して再テストすると、

→ Suite: Euler - Sum of Multiples
  ✓ Passed: below 10
  ✓ Passed: below 1000
→ Suite: diagonal
  ✓ Passed: 3 4 5
  ✓ Passed: 5 12 13

All 4 tests passed! 🎉
[info] Tests succeeded.

テストが成功した。

Kazuki MiyanishiKazuki Miyanishi

練習問題1

  1. (Easy) Write a function circleArea which computes the area of a circle with a given radius. Use the pi constant, which is defined in the Math module. Hint: don't forget to import pi by modifying the import Math statement.

半径を与えたら円の面積を計算するcircleArea関数を書け、とのこと。円周率はMathモジュールにあるpi定数が使えるみたい。

ヒントとして「import Math文を書いてpiを読み込むのを忘れないで」とのこと。

テストファイル(exercises/chapter2/test/Main.purs)に、この練習問題のテストが記述されているので、ブロックコメント行をずらして、以下の練習問題のテストを有効にする。

    suite "circleArea" do
      test "radius 1" do
        Assert.equal 3.141592653589793 (circleArea 1.0)
      test "radius 3" do
        Assert.equal 28.274333882308138 (circleArea 3.0)

test/MySolutions.pursファイルに追記していく感じでいいんだろうか。とりあえず、そうしよう。

module Test.MySolutions where

import Prelude
-import Math (sqrt)
+import Math (sqrt, pi)

diagonal w h = sqrt (w * w + h * h)
+
+circleArea radius = pi * radius * radius
$ spago test
 :
→ Suite: circleArea
  ✓ Passed: radius 1
  ✓ Passed: radius 3

テスト成功した!

Kazuki MiyanishiKazuki Miyanishi

練習問題2

  1. (Medium) Write a function leftoverCents which takes an Integer and returns what's leftover after dividing by 100. Use the rem function. Search Pursuit for this function to learn about usage and which module to import it from. Note: Your IDE may support auto-importing of this function if you accept the auto-completion suggestion.

rem関数を使って100で割った余りを返すleftoverCents関数を書け、とのこと。このrem関数の使い方は Pursuit を見て学べ、と。

注意点として「使用しているIDEで auto-completion suggestion を許可しているなら auto-importing がサポートされている」とのこと。使えるんだろうか。

rem関数の Pursuit のページは、おそらくこれだろう。

👉 Data.Int - purescript-integers - Pursuit

Date.Intモジュールのようだ。Visual Studio Code で auto-importing はどうやるんだろう。

とりあえず、求められている関数をexercises/chapter2/test/MySolutions.pursに追加する。

+leftoverCents n = rem n 100

auto-importing についてはよくわからなかったので保留。手動でimportを書く。

+import Data.Int (rem)
$ spago test
 :
→ Suite: leftoverCents
  ✓ Passed: 23
  ✓ Passed: 456
  ✓ Passed: -789

できた!

このスクラップは2021/06/25にクローズされました