昨年末から時間さえ有れば『Scala関数型デザイン&プログラミング』の演習問題の解き直し、というのを久しぶりに(3年ぶり?)をやっていました。
過去に解いた時の記憶がだいぶ飛んでしまっていたので、久しぶりに解き直してみるか、と思ったものの、そのまま解いてもあまり面白くありません。そこで演習問題を都度Scala3ベースに置き換えながら解くことにしました。
なお、原著の『Functional Programming in Scala』は今年Scala3対応版の第2版が出版される予定で、GitHubのリポジトリにも既に第2版用のScala3対応版コードが用意されています(が、そちらを見てしまうと練習にはならないので、どうしても分からない時だけ見るようにしました)。
環境準備
JDKのインストール
今ならJava11か、Java17になります。
ただし、Java8以降であればどのディストリビューションを使っても大丈夫です。
sbtのインストール
公式サイトのドキュメントに沿ってインストールします。
sbt Reference Manual — Installing sbt
macOSならHomebrewからのインストールできます。
$ brew install sbt
演習問題のリポジトリの用意
GitHubからクローンします。デフォルトのブランチが既にScala3対応のsecond-edition
になっていますが、「自分で書き換えてみる」という趣旨なので、first-edition
の方のブランチをローカルに取得します。
$ git switch -c first-edition remotes/origin/first-edition
project/build.propertiesの書き換え
sbtのバージョンを置き換えます。
最新バージョンは公式サイトで確認します。
project/build.properties
を書き換えます。この記事を書いている時点での最新バージョンは1.6.1でした。
sbt.version=1.6.1
build.sbtの書き換え
scalaVersion
は本日時点での最新版の3.1.1
に書き換えます。また、first-edition
では特にテスティングフレームワークの指定はされていませんでしたが、テスト無しに演習問題の結果確認はできないので、ここではテスティングフレームワークとしてAirSpec
を追加します(libraryDependencies
と、testFrameworks
を追加し、ついでにテストの結果が見やすいように並列実行抑止の指定であるTest / parallelExecution := false
を追加しています)。
(second-edition
のリポジトリでは、munit
が指定されていました)
ThisBuild / scalaVersion := "3.1.1" lazy val root = (project in file(".")) .aggregate(exercises, answers) .settings( name := "fpinscala" ) lazy val exercises = (project in file("exercises")) .settings( name := "exercises", libraryDependencies += "org.wvlet.airframe" %% "airspec" % "21.12.1" % Test, testFrameworks += new TestFramework("wvlet.airspec.Framework"), Test / parallelExecution := false ) lazy val answers = (project in file("answers")) .settings( name := "answers" )
コンパイルの準備
リポジトリのディレクトリへ移動し、sbtを起動し、プロジェクトをexercise
に移動します。
sbt:fpinscala> projects [info] In file:/xxx/fpinscala/ [info] answers [info] exercises [info] * root sbt:fpinscala> project exercises [info] set current project to exercises (in build file:/xxx/fpinscala/)
この時点でコンパイルすると以下のディレクトリでエラーが発生します。かなり後半の演習問題ですし、ここで引っかかってしまうと先に進まなくなるので、この章の演習問題を解き終えるまではディレクトリごと別の場所に移動しておきます。直すのはその章の演習問題を解くまで持ち越しです。
exercises/src/main/scala/fpinscala/applicative/ exercises/src/main/scala/fpinscala/iomonad/ exercises/src/main/scala/fpinscala/streamingio/
> compile [info] compiling 15 Scala sources to /xxx/fpinscala/exercises/target/scala-3.1.1/classes ... [warn] -- [E121] Pattern Match Warning: /xxx/fpinscala/exercises/src/main/scala/fpinscala/datastructures/List.scala:31:9 [warn] 31 | case _ => 101 [warn] | ^ [warn] |Unreachable case except for null (if this is intentional, consider writing case null => instead). [warn] one warning found [warn] one warning found [success] Total time: 4 s, completed Jan 23, 2022, 9:06:34 PM
途中でワーニングが出ていますが、コンパイルは完了しました。
演習問題を解く前に
『Scala関数型デザイン&プログラミング』は、本を読むだけだとなかなか理解が追いつかないというか、必要なことは全部書かれていないので、並行して演習問題の回答や、豊富なWikiを読むことをおすすめします。
また、「Part III 関数型デザインに共通する構造」あたりを読み始める時にcatsの入門ドキュメントなど、関連するトピックを扱った記事を合わせて読むと理解が広がります。
Scala With Cats を読む前に知っておきたかったこと - MicroAd Developers Blog
演習問題自体はかなり難易度が高いものも多いので、解けない時は寄り道をして、リフレッシュしていきましょう。
AirSpecの使い方
テスティングフレームにはairframe-log
によるログ出力が便利なAirSpec
を使うことにしました。アサーションの数が多すぎず、覚えていられるくらいの数、というのもポイントです。
詳しい使い方は公式サイトを見てください。
ほとんどのテストは、shouldBe
の左側にテストしたいコードを書き、右側に期待値を書く、という書き方で事足ります。
たとえば、あらかじめ用意されているMyModule.abs
という関数のテストをGettingStartedSpec
というテストクラスを作って実行してみましょう。
まず、exercises/src/test/scala/fpinscala/gettingstarted/GettingStartedSpec.scala
というファイルを作って、以下のコードを書きます(Scala3らしく、classを囲む中括弧を省略しています...AirSpec traitの後のコロンと、end GettingStartedSpec
という記述に注意)。
package fpinscala.gettingstarted import wvlet.airspec.* class GettingStartedSpec extends AirSpec: test("abs") { MyModule.abs(1) shouldBe 1 MyModule.abs(0) shouldBe 0 MyModule.abs(-1) shouldBe 1 }
sbtからテストを実行します。
sbt:exercises> test [info] compiling 1 Scala source to /xxx/fpinscala/exercises/target/scala-3.1.1/test-classes ... GettingStartedSpec: - abs 10.04mses / Test / compileIncremental 0s [info] Passed: Total 1, Failed 0, Errors 0, Passed 1 [success] Total time: 1 s, completed Jan 24, 2022, 1:21:04 AM
テストが完了し、無事にすべて成功しました。
次にテストの失敗を試します。最後のテストの期待値を書き換えます。
package fpinscala.gettingstarted import wvlet.airspec.* class GettingStartedSpec extends AirSpec: test("abs") { MyModule.abs(1) shouldBe 1 MyModule.abs(0) shouldBe 0 MyModule.abs(-1) shouldBe -1 }
sbtからテストを実行します。
sbt:exercises> test [info] compiling 1 Scala source to /xxx/fpinscala/exercises/target/scala-3.1.1/test-classes ... GettingStartedSpec: - abs 16.66ms << failed: 1 didn't match with -1 (GettingStartedSpec.scala:10) [error] Failed: Total 1, Failed 1, Errors 0, Passed 0 [error] Failed tests: [error] fpinscala.gettingstarted.GettingStartedSpec [error] (Test / test) sbt.TestsFailedException: Tests unsuccessful [error] Total time: 1 s, completed Jan 24, 2022, 1:39:24 AM
テストは失敗し、何が一致しなかったのか、どの行で発生しなかったのかが分かるログが出力されているので、これを手がかりに解析を行う、という流れになります。
余談ですがAirSpecのドキュメントには、ログが見づらくなるのでテストが遅くなってもいいから並列処理を止めたい、という時はbuild.sbt
にTest / parallelExecution := false
を追加する、と書かれています。この辺りの挙動の詳細は以下のブログ記事が参考になります。
今回はすべて同じプロジェクトにまとまっていますので、上記の指定だけで十分です。
Scala3で導入されたOptional Bracesについて
Scala3からOptional Bracesという記法が導入され、これまでScalaにおける基本的なコードのまとまりの単位であった中括弧を使ったC言語やJavaっぽい記法に加え、インデントベースのPythonっぽい記法が可能になりました。
詳しくは公式ドキュメントを参照してください。
Optional Braces | Scala 3 Language Reference | Scala Documentation
今回、演習問題を解くにあたっては、Optional Bracesを使って書き換えていきます。
しかし、先ほどのテストコードの例ではまだ中括弧が残っていたままでした。
test("abs") { MyModule.abs(1) shouldBe 1 MyModule.abs(0) shouldBe 0 MyModule.abs(-1) shouldBe -1 }
これはScala3.1.1時点では、関数の引数に渡すコードブロックの中括弧を省略できないためです。将来は標準でそれができるようになる見込みですが、先ほどのドキュメントの最後に書かれているとおり、現状ではexperimental
オプションの指定が必要です。
import language.experimental.fewerBraces
しかも、このオプションは正式リリース版では使えず、nightlyビルドでしか使えません。
nightlyビルドはcentaral repositoryにもアップされていますので、build.sbt
に適当に最新のnightlyビルドのバージョンを指定することで下記のように記述できます。
package fpinscala.gettingstarted import wvlet.airspec.* class GettingStartedSpec extends AirSpec: import language.experimental.fewerBraces test("abs"): MyModule.abs(1) shouldBe 1 MyModule.abs(0) shouldBe 0 MyModule.abs(-1) shouldBe 1 end GettingStartedSpec
この記法だとコードブロックを渡していると読み取りづらい気もしますし、現状はexperimental
扱いなので、将来的には変更される可能性があります。この記法は今回は採用しないこととします。
おわりに
これで演習問題を解く準備ができました。
次回からは、実際に第2章の演習問題を解いていきます。