序文
Creative Scala は、Scala 歴ゼロのデベロッパー向けに書かれています。 関数型プログラミングを楽しく学べることを目指しました。 あなたが他のプログラミング言語の初歩は少しかじったことがあることを前提としますが、Scala やその他の関数型言語には慣れ親しんでいないことを想定しています。
この本の 3つの目標は:
- 関数型プログラミングを紹介して、あなたがプログラムを計算したり筋道を立てて推論できるようになることで、他の関数型プログラミングに関する入門書に進めるようになること。
- Scala を使って自分の興味のあることに探検していくのに十分な Scala を教えること。
- これらを 2次元コンピューターグラフィックスを使って楽しく、優しく、興味深い方法で提供すること。
この 3点です。
私たちの動機は自分たちがプログラミングを学んだり、関数型プログラミングを勉強したり、商用デベロッパーに Scala を教えてきた経験から来ています。
まず、私たちは、関数型プログラミングが未来であることを信じています。 プログラミング経験が浅いことを前提としているので、関数型プログラミングとあなたが経験したことがあるかもしれないオブジェクト指向プログラミングの違いの詳細はここでは割愛させていただきます。 コンピュータのプログラムについて考えたり書いたりするにはいくつの方法があり、私たちは関数型プログラミングという方法を選んだとだけ言っておきましょう。
関数型プログラミングを選んだ理由のほうが興味深いと思います。 プログラミングを教えるのにありがちな方法として私たちが「構文のごった煮」方式と呼んでいるものがあります。 この方式ではプログラミング言語は、(変数、for ループ、while ループ、メソッドなど) 構文機能の集合として教えられ、いつどの機能を使うのかは生徒に任せられます。 大学生としてプログラミングを習ったり、社会人としてプログラミングを教える立場になった両方の場合において私たちはこの方式が失敗するのを見てきました。生徒が問題をコードに体系的に分解するすべを持たないからです。 拙い授業の結果として多くの生徒が脱落する結果となりました。 残った生徒は、私たちのように既に広いプログラミング経験を持つ人が多かったと思います。
小学校の算数の時間の筆算の足し算のことを思い出してください。 これは暗算で足すには数が大きい場合に数字を足すための基本の方法です。 なので、例えば 266 + 385 を足すには桁をそろえて書き出して、10 を超えたら繰り上げを行うなどといった具合です。 算数は好きな授業じゃなかったかもしれませんが、いくつかの重要な教訓が隠されています。 第一は、問題を解くための体系的な方法が与えられたということです。 問題が筆算の足し算で解けると気がつけば、答えを計算することができます。 第二点は、筆算の足し算を使うのになぜ正しいのかを理解している必要は無いということです (知っていることに越したことは無いですが)。 手順さえ正しく従えば、正しい答えを得ることができます。
関数型プログラミングの優れているのは、それが筆算の足し算のようになっていることです。 私たちは、正しく従えば正しく答えを得ることが保証されているレシピをいくつか持っています。 私たちは、これをプログラムを計算すると言います。 これは、プログラミングに創造性が欠けると言っているわけではありません。しかし、問題の構造をよく理解するのがチャレンジであって、それができたらすぐにレシピを使うことができます。 コードそのものは興味深い部分では無いのです。
私たちは Scala を使って関数型プログラミングを教えますが、Scala そのものを教えるわけではありません。 Scala は今需要がある言語です。 Scala プログラマーは様々な産業において比較的簡単に職を探すことができ、それは Scala を習うための重要な動機となります。 Scala の人気の理由の 1つとして Scala がオブジェクト指向プログラミングという古いプログラミングの方法と関数型プログラミングの 2つをまたいでいる言語だということが挙げられます。 多くのコードがオブジェクト指向スタイルで書かれていて、そのスタイルに慣れ親しんだプログラマーも多くいます。 Scala は、オブジェクト指向プログラミングから関数型プログラミングへ緩やかに移行する方法を与えてくれます。 しかし、これは Scala が大きな言語であることも意味し、オブジェクト指向の部分と関数型の部分の相互作用には分かりづらいこともあります。 私たちは、関数型プログラミングの方がオブジェクト指向プログラミングよりもずっと効果的で、新しいプログラマーが同時にオブジェクト指向のテクニックを教えて混乱させる必要は無いと思っています。 それは、後からでも遅くはないと思います。 そのため、この本では完全に Scala の関数型プログラミングの部分だけを使うことにします。
私たちは、関数型プログラミングと Scala を探検するにあたって、楽しんでもらえるを願ってコンピューター・グラフィックスという方法を選びました。 Scala には多くの入門書がありますが、多くの例題はビジネスや数学に関するものです。 例えば、とても人気な Coursera コースの初めの練習問題は、指示関数を用いて集合を実装するというものです。 あなたがそういうコンセプトに直接取り組みたいタイプの人ならば、そういうコンテンツは既にいっぱいあると思います。 私たちは別のグループを対象としようと思いました。数学はちょっと苦手だけども、ビジュアル・アートなら興味があるかなという人たちです。 正直に言っておくと、この本にも数学は出てきますが、そのコンセプトが何故必要なのかを動機づけ、ヴィジュアル化することで、怖さを軽減できたことを願っています。
この本は Scala を使いこなせるようになるための基礎となるメンタルモデルを提供しますが、自立できるための Scala の全てをここでカバーできるわけではありません。 更に Scala を習うためには、他の Scala の教科書の中から良いものを選んで進むことをお勧めします。 拙著の Essential Scala も検討してみてください。
練習問題を一人で解いているならば、Gitter chat room に参加して分からないことを聞いたり、この本の感想をシェアしてみてください。
Creative Scala のテキスト、及び練習問題で使われるお絵かきライブラリ Doodle は全てオープンソースです。 コードは私たちの GitHub アカウントにて公開しています。 英語版にコントリビュートしたい方は Gitter もしくは email で連絡してください。
ダウンロードしてくれてありがとう。これから creative プログラミングを始めましょう!
—Dave と Noel
Early access 版に関する注意
これは Creative Scala のベータ版です。 本文内や練習問題にタイポやその他の間違いがあるかもしれません。
日本語版に関する間違いの報告は eed3si9n/creative-scala の issues を使ってお願いします。
英語版に関する間違いの報告は Gitter chat room もしくは email でお願いします:
- Dave Gurnell (dave@underscore.io)
- Noel Welsh (noel@underscore.io)
謝辞
Creative Scala (英語版) は Dave Gurnell と Noel Welsh によって書かれました。Richard Dallaway さん、Jonathan Ferguson さん、そして Underscore のみんなが何度も校正を行ってくれたことを感謝します。
間違いを指摘してくれたり、この本を改善するための意見をくださった多くの方々にもこの場をかりて感謝したいと思います: Neil Moore さん; Kelley Robinson さん、Julie Pitt さん、その他の ScalaBridge オーガナイザー; d43 さん; Matt Kohl さん; Alexa Kovachevich さんなど ScalaBridge その他のイベントで Creative Scala を読み進めた生徒の方々; その他直接コメントや意見をくれた多くの素晴らしい Scala コミュニティーの面々。Bridgewater 社、特に Lauren Cipicchio さんが、もしすると知らずに Creative Scala 第二版の初期開発に投資していただき、初期の生徒を提供していただいたことをここで感謝したいと思います。
最後に Creative Scala がプログラミング言語理論とコンピューターサイエンス教育にたずさわる多くの研究者の成果のおかであることをここに書きたいと思います。特に私たちが強調したいのは:
- PLT research group の著作、特に Matthew Flatt、Matthias Felleisen、Robert Bruce Findler、Shriram Krishnamurthi 共著の “How to Design Programs”、そして
- Mark Guzdial、Dianna Xu らによって率先されたプログラミング入門に関する「creative coding」というアプローチです。
1 始めてみよう
まず最初のステップは Creative Scala の作業に必要なソフトウェアをインストールすることです。ここでは 2つの道のりを解説します:
- テキストエディタとターミナルを使う方法。プログラミングを一切やったこと無い人にはこの方法をお勧めします。
- IntelliJ IDEA を使う方法。IDE を使うのに慣れている人やターミナルを使うのが不安な人にはこの方法をお勧めします。
もしも経験を積んだデベロッパーで自分の好みのセットアップがある場合はそのままそれを使って、必要に応じて以下の手順を調整して使ってください。
何を言っているのかさっぱりという人は、この章でこれから説明していくので読み進めてください。
1.1 ターミナルとテキストエディタのインストール
この節では、プログラミングが初めてという人のために私たちがお勧めする、ターミナルとテキストエディタを使った Creative Scala のセットアップ方法を解説します。 インストールするものは:
- JVM
- Git
- テキストエディタ、1つ
- Creative Scala のテンプレートプロジェクト
1.1.1 macOS
ターミナルを開く。(ツールバー右側の虫めがねアイコンに “terminal” と打ち込んでください。)
Java をインストールします。 ターミナルに以下を打ってください:
java
もしもこれが実行されれば Java はインストール済みです。 インストールされていなければ、Java をインストールする画面が表示されます。
Homebrew をインストールします。 以下をターミナルにペーストしてください:
/usr/bin/ruby -e "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/install)"
Homebrew を使って git
をインストールします。 ターミナルに以下を打ち込んでください:
brew install git
次にテキストエディタ Atom をインストールします。 ターミナルに以下を打ち込んでください:
brew install Caskroom/cask/atom
Atom 内で Scala サポートをインストールしてください: Settings > Install > language-scala
これで Git を使って Creative Scala の作業をするための sbt プロジェクトを取得することができます。 以下を打ち込んでください:
git clone https://github.com/underscoreio/creative-scala-template.git
作業を共有する
代替のセットアップとして、先に Creative Scala template プロジェクトを fork して、それをあなたのコンピュータに clone するという方法があります。 これは、あなたが自分の作業結果を他の人とシェアしたい場合のセットアップで、例えばリモートのインストラクターと Creative Scala を受講している場合や、自分の作業を他の人にも見て欲しい場合に使うことができます。
このセットアップではまず Creative Scala template を fork します。 そしてあなたの fork を clone します。 この代替セットアップ方法に関してはこの章の後ほどの GitHub の節で解説します。
今作られたディレクトリに変えて、sbt を実行してみましょう。
cd creative-scala-template
./sbt.sh
sbt が起動したはずです。 sbt 内で console
と打ち込んでみてください。 最後に、以下を打ち込んでください:
Example.image.draw
3つの円の画像が現れるはずです!
ここまでできれば、Creative Scala の作業をするのに必要なソフトウェアは全てインストールされました。
最後のステップは Atom を起動して、src/main/scala
内にある Example.scala
を開くことです。
1.1.2 Windows
Java をダウンロードしてインストールします。 “JDK” (Java development kit) で検索してください。 Oracle のサイトが出てくるはずです。 ライセンスを承諾して、JDK をダウンロードしてください。 ダウンロードが完了したら、インストーラーを実行してください。
Atom をダウンロードしてインストールします。 https://atom.io/
へ行って、Atom の Windows版をダウンロードしてください。 ダウンロードが完了したら、インストーラーを実行してください。
Git をダウンロードしてインストールします。 https://git-scm.com/
へ行って、Git の Windows版をダウンロードしてください。 ダウンロードが完了したら、インストーラーを実行してください。 最後に Git を開くオプションが出てくるはずなので、それを選択してください。 コマンドプロンプト画面が開きます。 以下を打ち込んでください:
git clone https://github.com/underscoreio/creative-scala-template.git
作業を共有する
代替のセットアップとして、先に Creative Scala template プロジェクトを fork して、それをあなたのコンピュータに clone するという方法があります。 これは、あなたが自分の作業結果を他の人とシェアしたい場合のセットアップで、例えばリモートのインストラクターと Creative Scala を受講している場合や、自分の作業を他の人にも見て欲しい場合に使うことができます。
このセットアップではまず Creative Scala template を fork します。 そしてあなたの fork を clone します。 この代替セットアップ方法に関してはこの章の後ほどの GitHub の節で解説します。
コマンドプロンプトを開きます。 画面左下の Windows アイコンをクリックしてください。 検索ボックスに「cmd」と入力して、プログラムが出てきたらそれを実行してください。 開いたウィンドウ内で以下を打ち込んでください:
cd creative-scala-template
さっきダウンロードした Create Scala template プロジェクトのディレクトリに変わるはずです。 以下を打ち込んで sbt を起動してください:
sbt.bat
sbt 内で console
と打ち込んでみてください。 最後に、以下を打ち込んでください:
Example.image.draw
3つの円の画像が現れるはずです!
ここまでできれば、Creative Scala の作業をするのに必要なソフトウェアは全てインストールされました。
最後のステップは Atom を起動して、src/main/scala
内にある Example.scala
を開くことです。
1.1.3 Linux
macOS の手順に従って、Homebrew の代わりに使っているディストリビューションのパッケージマネジャーを使ってください。
1.2 IntelliJ
IntelliJ は、Scala や他のプログラミング言語のための統合開発環境 (IDE) です。これはいくつものプログラミングツールを 1つのアプリケーションにまとめたもので、Visual Studio や Eclipse など他の IDE に慣れている人にお勧めします。
まずは IntelliJ Scala Bundle をダウンロードしてください。
IntelliJ Scala Bundle を起動すると、Welcome to IntelliJ IDEA というダイアログが出てきて、Create New Project、Import Project、Open、Check out from Version Control という選択肢が表示されます。 「Check out from Version Control」を選択して、URL に https://github.com/underscoreio/creative-scala-template.git
と入力して、「Clone」を選びます。 「Would you like to open it?」と聞かれたら「Yes」を選びます。
オプション画面が出てきたら「Use sbt shell for build and import」選択します。
画面左下の sbt shell に console
と打ち込んでください。 (busy) >
と書かれたプロンプトに以下を打ち込んでください:
Example.image.draw
3つの円の画像が現れるはずです!
:q
と打ち込んで console
を終了します。
1.3 予備知識
この節では、これから私たちが使うツールの予備知識を解説します。 もしもあなたが経験を積んだデベロッパーであれば既に常識だと思うので読み飛ばしてください。 もしもそうじゃなければ、これから使うソフトウェアについての背景を知ることで役に立てばいいと思います。
1.3.1 ターミナル
コンピューターの黎明期には、今では一般的なユーザー・インターフェイスとなった、グラフィカルなウィンドウ、マウスで制御されるカーソル、そしてコンピューターの直接操作ということそのものが存在しませんでした。 その代りに、ユーザーは端末 (ターミナル) という装置にコマンドを打ち込んでコンピューターを操作しました。 直接操作の方が多くの場面において優れていますが、ターミナルを用いたコマンドライン操作の方が便利なこともあります。 例えば、data
で始まるファイル名のファイルがどれだけの容量を占めているかを調べたいとするとき Linux や macOS ならば以下のコマンドを実行することができます:
du -hs data*
これは 3つの要素に分解することができます:
du
コマンドはディスク使用状況 (disk usage) を意味し、- フラグ
-hs
は人が読みやすいまとめ (human readable summary) を意味し、 - パターン
data*
は、ファイル名がdata
で始まる全てのファイルを意味します。
これを直接操作系のインターフェイスを用いて行うのはより時間のかかる作業となるでしょう。
コマンドラインのほうが学習するのが難しいですが、代わりに非常に強力なツールを得ることができます。 私たちが使うターミナルの用法は限られているものなので、上の例が怖いなと思っても心配しないでください!
1.3.2 テキストエディタ
あなたは多分ワードプロセッサーで文章を書いたことがあると思います。 ワードプロセッサーはテキストを書いて、(最近見ることが減ってきた) 印刷されたページでのフォーマットの制御を行うことができます。 ワードプロセッサーは、文章の作成に便利なスペルチェッカーや目次の生成などといった強力なコマンドを持ちます。
テキストエディタはコードを書くためのワードプロセッサーです。 ワードプロセッサーがテキストの視覚的なプレゼンテーションにこだわるように、テキストエディタはプログラミングに特化した多くの機能を持ちます。 強力な検索置換機能、そしてプロジェクト内の多くの異なるファイル間へ素早くジャンプできるための機能などが典型的な例です。
テキストエディタは端末が使われていた時代にさかのぼるため、驚くことに当時から使われ続けているツールがいくつかあります。 古来から続いており、今も現役で活躍している 2大エディターとして Emacs と Vim があります。 私は Emacs を約20年使い続けているため、Emacs が全ての存在しうるテキストエディターの中で最も偉大なものであり、Vim ユーザーは悪趣味と劣等なツールに呪われてしまった脳みそが筋肉の人たちであることを本能的に分かっています。 Vim ユーザーは私のことを同じように思っているでしょう。
Vim と Emac のユーザーが一丸となることがあるとすれば、それは今流行りの Sublime Text や Atom といったテキストエディタは人類文明の没落を招いているということです。 しかしながら、初めてのテキストエディタとしては Atom をお勧めします。 Vim と Emacs の両方共現在広く使われているユーザーインターフェイスが確立する前に作られてたため、使いこなすのにかなり癖があるからです。
1.3.3 コンパイラ
私たちがテキストエディタで書くコードは、そのままではコンピュータが実行することができません。 コンパイラはコードをコンピュータが実行できる形式に翻訳します。 その翻訳を行う途中で、いくつかコードの検査も行います。 この検査が通らない場合はコードはコンパイルされずに、コンパイラは代わりにエラー・メッセージを表示します。 コンパイラが何をチェックすることができて、何ができないのかはまた後ほど見ていきます。
コンパイラがコンピュータが実行できる形に翻訳すると上で言いましたが、Scala に関して言うと実はこれは完全な真実ではありません。 コンパイラが出力するのはバイトコードと呼ばれるもので、Java Virtual Machine (JVM) という別のプログラムがこのコードを実行します1。
1.3.4 総合開発環境 (IDE)
総合開発環境 (IDE) はテキストエディタ、コンパイラ、その他のプログラミング用のツールを一つのプログラムにまとめたものです。 IDE に絶対的な信用をおいている人たちもいれば、ターミナルとテキストエディタを好む人もいます。 プログラミングに初めての人へ私たちがお勧めするのはターミナルとテキストエディタを使う方法です。 IDE に慣れているならば、現在 Scala 開発に最も適している IDE は IntelliJ IDEA です。
1.3.5 バージョン管理
バージョン管理は私たちが使うツールの 1つです。 バージョン管理システムは、グループ化された複数のファイルに対する全ての変更を記録するためのプログラムです。 プロジェクトにおいて、複数の人が同時に作業できるようにするのは非常に便利ですが、そのときにバージョン管理を使うことで間違ってお互いの変更が上書きされないことを保証します。 Creative Scala を行うにあたってバージョン管理はさして重要なことではありませんが、早めにバージョン管理に触れておくのは良いことだと思います。
私たちが使うバージョン管理は Git です。 これは強力ですが、複雑なものです。 幸い今回はあまり Git に関して習う必要はありません。 私たちの Git の用例の多くは、Git に保存したソフトウェアを共有するための GitHub というウェブサイト経由で行うのがほとんどです。 Creative Scala で使われるソフトウェアは GitHub で共有されています。
1.4 GitHub
Creative Scala の作業をするのに必要なコードを全てセットアップした[テンプレート]を用意しました。 このテンプレートは、コードを共有するためのウェブサイトである GitHub に保存されています。
このテンプレートをあなたのコンピュータにコピーすることができ、Git はこれを clone と呼んでいます。 しかし、この方法ではあなたが行う変更を GitHub へ保存し直して、他の人が見れるようにはなりません。
もしもあなたが行う変更を他の人と共有したい場合は、テンプレートプロジェクトを自分の GitHub へコピーする必要があります。 Git はこれを fork と呼んでいます。 GitHub 上でまずレポジトリを fork して、あなたの fork を手元に clone してください。 この方法を行えば、自分の変更点を GitHub のあなたの fork へ保存し直すことができます。
このプロセスを始めるには、GitHub アカウントを作成する必要があります。アカウントを持っていない人は作ってください。
アカウントができたら、ブラウザ上からテンプレートプロジェクトへ行きます。 上の右側に “Fork” と書かれたボタンがあります。 このボタンを押して、自分のテンプレートプロジェクトを作成します。 テンプレートの自分の fork を表示するウェブページに飛ばされるはずです。 このリポジトリの名前を覚えておいてください。GitHub のユーザー名が yourname
さんだとすると、yourname/creative-scala-template
みたいな感じになると思います。
あなたの fork を clone するには、以下のコマンドの yourname
の部分を GitHub ユーザー名に置き換えて実行するだけです。
git clone git@github.com:yourname/creative-scala-template.git
これで、あなたが行う変更を GitHub 上のあなたの fork へ送ることができるようになりました。 Git を使ってこれを行うのは少し複雑です。 何らかの変更を行ったあと以下を実行する必要があります:
add
Git のインデックスと呼ばれるものに変更を追加するcommit
変更を記録するpush
変更を fork へ送信する
以下は、コマンドラインからこれを実行する一例です。
git add
git commit -m "Explain here what you did"
git push
GitHub は、GitHub Desktop という Git を使うための無料のグラフィカルなツールを作っています。 Git を始めたばかりのときはこれが一番分かりやすい方法でしょう。
2 式、値、型
Scala のプログラムは式、値、型という 3つの基礎要素から成り立っています。この節では、これらの概念を考察します。
非常にシンプルな式の一例です:
1 + 2
式は Scala コードの断片です。式はテキストエディタに書くこともあれば、紙に書いたり、壁に書くこともできます。
式は作文に似ています。作文が世界に作用を持つためには誰かが読む必要があるのと同様 (ということは読者が書かれた言語を理解している必要もあります)、式が作用を持つにはコンピュータが式を実行する必要があります。式を実行した結果が値です。値は、コンピューターのメモリの中に生きていて、それは作文を読んだ結果が読者の頭の中に生きているのと同様です。式を値に変換するプロセスを指して、式を評価すると言ったり、実行すると言ったりもします。
console に式を書いて「Enter」(もしくは 「Return」) を押すことで即時に式を評価することができます。今すぐ試してみてください。
1 + 2
// res1: Int = 3
console は、式を評価した値と式の型を返します。
1 + 2
という式は 3
という値に評価されます。私たちはこのページに数字の 3 と書くことができますが、真の値はコンピューターのメモリに格納されたものです。この場合、これは 2の補数で表された 32ビット整数となります。「2の補数で表された 32ビット整数」の意味は重要なものではありません。このページや console に書かれた数字ではなく、3
という値のコンピューターの表現こそが真の値であることを強調するために書いたものです。
このパズルの最後のピースは型です。プログラムを実行せずに決定できるものを型と言います。式 1 + 2
は Int
型を持ち、これはこの式が評価する値を整数だと解釈するべきことを意味します。これは、この式の結果を使って他の式を書くことができるけども、その式が整数に合った演算である必要があることを意味します。例えば、加算、減算、乗算、除算などが可能ですが、整数を小文字に変換することはできません。
型は多くの場合において、私たちが値 (コンピューターのメモリにある「あれ」) をどう理解するべきかを教えてくれます。整数として理解するべきなのか、マウスの現在位置を表す点のストリームとして理解するべきでしょうか? それは型が教えてくれます。私たちは型を、実行時に表現を持たないものを含む他のことにも使うことができます。これらの用法はここで深入りするには少し難易度が高いですが、型が値に対応すると思ってしまうのは間違いです。Scala では型はコンパイル時のみに存在します。任意の値があるとき、その値を生成した式の型の表現は実行時にはありません。
Scala プログラムが実行する前に、それはコンパイルされる必要があります。コンパイルは、プログラムのつじつまが合うかの検査を行います。例えば、(1 + 2)
は構文的に正しいけども、(1 + 2
は正しくありません。プログラムは、型検査も通過する必要があります。型検査は、行おうとしている演算が今ある型に対して正しいかの検査です。1 + 2
は型検査を通りますが (整数の加算をしています)、1.toUpperCase
は通りません (数字に大文字も小文字もありません)。
コンパイルに成功したプログラムだけを実行することができます。コンパイルは、作文の文法のようなものと考えることができます。例えば、 「F$Rf fjrmn;l df.fd」は英文法的に間違っています。文字の並びは単語を形成していません。「dog fly a here no」という文は妥当な単語から成るけども、その並びは英文法を違反します。これは Scala が型検査を行うのと似ていると思います。
コードがコンパイルされる時のことをコンパイル時、コードが実行される時のことを実行時と言います。
2.1 リテラル式
Scala の様々な式を探検してみましょう。初めは、最もシンプルな式であるリテラルです。 リテラル式の具体例です:
3
// res0: Int = 3
リテラルは「それ自身」に評価されます。式の書き方と console が値を表示する方法は一緒です。ただし、値の書かれた表現とコンピューターのメモリ内の実際の表現には違いがあることを思い出してください。
Scala には多くの異なる形のリテラルがあります。私たちは既に Int
リテラルを見ました。浮動小数点数という別の型があり、これは別のリテラル構文で表されます。これは、コンピューターの実数に対する近似値に対応します。具体例を見てみましょう:
0.1
// res1: Double = 0.1
見ての通り、この型は Double
と呼ばれます。
数字はいいとして、テキストはどうするのでしょうか。Scala の String
型は文字の列を表します。文字リテラルはダブルクォートで内容を囲んで書きます。
"To be fond of dancing was a certain step towards falling in love."
// res2: String = To be fond of dancing was a certain step towards falling in love.
数行にまたがった文字列を書きたいことがあります。これは、以下のようにトリプルダブルクォートを使って書きます。
"""
A new, a vast, and a powerful language is developed for the future use of analysis,
in which to wield its truths so that these may become of more speedy and accurate
practical application for the purposes of mankind than the means hitherto in our
possession have rendered possible.
-- Ada Lovelace, the world's first programmer
"""
// res3: String =
// "
// A new, a vast, and a powerful language is developed for the future use of analysis,
// in which to wield its truths so that these may become of more speedy and accurate
// practical application for the purposes of mankind than the means hitherto in our
// possession have rendered possible.
//
// -- Ada Lovelace, the world's first programmer
// "
String
は文字の列です。文字それぞれにも Char
という型があって、それはシングルクォートを使って書かれます。
'a'
// res4: Char = a
最後に、イギリスの論理学者 George Boole にちなんで名付けられた Boolean
型のリテラル表現を見ていきましょう。気取った名前ですが、値が true
か false
であるかというだけの意味で、ブールリテラルはそのまま以下のように書かれます。
true
// res5: Boolean = true
false
// res6: Boolean = false
リテラル式を使って値を作ることができるようになりましたが、何らかの方法で値と関わりを持つことができなければ何もできません。これまでに 1 + 2
というような複合式は見てきました。次の節ではオブジェクトとメソッドを習って、このような式やもっと面白い式が動作しているのかを理解します。
2.2 値はオブジェクトである
Scala において全ての値はオブジェクトです。オブジェクトは、データとそのデータに関する演算をグループ化したものです。例えば、2 はオブジェクトです。このデータは整数の 2 で、演算は慣れ親しんだ +、- などといったものです。オブジェクトの持つ演算をオブジェクトのメソッドと呼びます。
2.2.1 メソッド呼び出し
メソッドを呼び出すことでオブジェクトと関わりを持つことができます。例えば、toUpperCase
メソッドを呼び出すことで String
値を大文字にしたものを得ることができます。
"Titan!".toUpperCase
// res0: String = TITAN!
メソッドの中にはパラメータ (引数と呼ばれることもあります) を受け取って、メソッドがどう動くかを制御できるものもあります。例えば take
メソッドは String
値からいくつかの文字を取り出します。それが何文字なのかを take
にパラメータを渡して指定する必要があります。
"Gilgamesh went abroad in the world".take(3)
// res1: String = Gil
"Gilgamesh went abroad in the world".take(9)
// res2: String = Gilgamesh
メソッド呼び出しは式の一つであり、オブジェクトへと評価されます。そのため、メソッド呼び出しを連鎖させて、より複雑なプログラムを書くことができます:
"Titan!".toUpperCase.toLowerCase
// res3: String = titan!
メソッド呼び出しの構文
メソッド呼び出しの構文は
anExpression.methodName(param1, ...)
または
anExpression.methodName
で、
anExpression
は (オブジェクトに評価される) 任意の式でmethodName
はメソッド名で- 省略可能な
param1, ...
は 1つもしくは複数の式で、メソッドのパラメータとして評価されます。
2.2.2 演算子
全ての値はオブジェクトで、メソッドは object.methodName(parameter)
という構文で呼び出すと言いました。だとすると、1 + 2
という式はどのように説明したらいいでしょうか?
Scala において、a.b(c)
と書ける式は a b c
と書くことができます。そのため、以下の式は等価です:
1 + 2
// res4: Int = 3
1.+(2)
// res5: Int = 3
1つ目のメソッド呼び出しの書き方は、演算子スタイルと呼ばれます。
演算子中置記法
a.b(c)
と書ける任意の Scala の式は a b c
と書くことができます。
a b c d e
は、a.b(c).d(e)
と等価であり a.b(c, d, e)
ではないことに注意してください。
2.3 型
より複雑な式が書けるようになった所で、型に関してもう少し話すことができます。
型の用例の 1つとして存在しないメソッドの呼び出しを防止するというものがあります。コンパイラが式を値に評価するとき、式の型はどのメソッドが存在するのかをコンパイラに教えてくれます。存在しないメソッドを呼び出そうとしてもそれはコンパイルされません。以下に簡単な具体例を使って説明します。
"Brontë" / "Austen"
// <console>:13: error: value / is not a member of String
// "Brontë" / "Austen"
// ^
1.take(2)
// <console>:13: error: value take is not a member of Int
// 1.take(2)
// ^
本当に、式の型がどのメソッドを呼び出せるのかを決定します。これはより複雑な式に対してメソッドを呼び出すことで実験することができます。
(1 + 3).take(1)
// <console>:13: error: value take is not a member of Int
// (1 + 3).take(1)
// ^
この型検査の処理は、メソッドのパラメータにも適用されます。
1.min("zero")
// <console>:13: error: type mismatch;
// found : String("zero")
// required: Int
// 1.min("zero")
// ^
型は式の属性で (以前にも話した通り) コンパイル時に存在します。そのため、実行時に式を評価した結果がエラーとなっても、式の型は決定することができます。例えば、Int
をゼロで割ると実行時エラーが発生します。
1 / 0
// java.lang.ArithmeticException: / by zero
// ... 43 elided
式 1 / 0
はそれでも型を持ち、以下のようにして console で表示させることができます。
:type 1 / 0
// Int
実行時に失敗するサブ式を含んだ複合式を書くこともできます。
(2 + (1 / 0) + 3)
// java.lang.ArithmeticException: / by zero
// ... 43 elided
そして、この式も型を持ちます。
:type (2 + (1 / 0) + 3)
// Int
2.4 練習問題
2.4.0.1 算数
整数リテラル、加算、減算の全てを使って 42 に評価される式を書いてみよう。
この練習問題は Scala のコードを書くのに慣れるためのものです。色々な解答が可能ですが、1つの例として以下のように書けます。
1 + 43 - 2
// res0: Int = 42
2.4.0.2 文字列の追加
++
メソッドを使って 2つの文字列を連結してみよう (文字列の追加とも言います)。適当な式を普通のメソッド呼び出しスタイルと演算子スタイルそれぞれを使って書いてみよう。
こんな感じでいいと思います。
"It is a truth ".++("universally acknowledged")
// res1: String = It is a truth universally acknowledged
"It is a truth " ++ "universally acknowledged"
// res2: String = It is a truth universally acknowledged
2.4.0.3 優先順位
数学では演算子には優先順位があることを習いました。例えば、1 + 2 * 3
という式があるとき、乗算を先に行ってから加算を行います。Scala でもこのルールが当てはまるでしょうか?
console で色々実験してみると、Scala も標準的な演算子の優先順位に従うことが分かると思います。以下にこれを示す例を挙げます。
1 + 2 * 3
// res3: Int = 7
1 + (2 * 3)
// res4: Int = 7
(1 + 2) * 3
// res5: Int = 9
2.4.0.4 型と値
以下のうち、コンパイルに失敗する式はどれでしょうか? もしくはコンパイルできるけれども、実行に失敗する式はどれでしょうか? コンパイルにも実行にも成功する式の評価値の型は、それぞれ何になるでしょうか?
1 + 2
"3".toInt
"Electric blue".toInt
"Electric blue".take(1)
"Electric blue".take("blue")
1 + ("Moonage daydream".indexOf("N"))
1 / 1 + ("Moonage daydream".indexOf("N"))
1 / (1 + ("Moonage daydream".indexOf("N")))
1 + 2
// res14: Int = 3
この式は Int
型を持ち、3
と評価されます。
"3".toInt
// res15: Int = 3
この式は Int
型を持ち、3
と評価されます。
"Electric blue".toInt
// java.lang.NumberFormatException: For input string: "Electric blue"
// at java.lang.NumberFormatException.forInputString(NumberFormatException.java:65)
// at java.lang.Integer.parseInt(Integer.java:580)
// at java.lang.Integer.parseInt(Integer.java:615)
// at scala.collection.immutable.StringLike.toInt(StringLike.scala:301)
// at scala.collection.immutable.StringLike.toInt$(StringLike.scala:301)
// at scala.collection.immutable.StringOps.toInt(StringOps.scala:29)
// ... 43 elided
この式は Int
型を持ちますが、実行時に失敗します。
"Electric blue".take(1)
この式は String
型を持ち、"E"
と評価されます。
"Electric blue".take("blue")
// <console>:13: error: type mismatch;
// found : String("blue")
// required: Int
// "Electric blue".take("blue")
// ^
この式はコンパイル時に失敗するため、型を持ちません。
1 + ("Moonage daydream".indexOf("N"))
// res19: Int = 0
この式は Int
型を持ち、0
と評価されます。
1 / 1 + ("Moonage daydream".indexOf("N"))
// res20: Int = 0
この式は Int
型を持ち、演算子の優先順位により (1 / 1) + -1
と評価され、0
となります。
1 / (1 + ("Moonage daydream".indexOf("N")))
この式は Int
型を持ちますが、ゼロによる除算のため実行時に失敗します。
2.4.0.5 浮動小数点数の弱点
Double を紹介したとき、それは実数の近似値だと言いました。これは何故だと思いますか? ⅓ や π といった数を表すことを考えてみましょう。それらの数を 10進数で表すとしたらいくら容量が必要でしょうか?
Double
が近似値であるのは、コンピューターの有限なメモリーに収める必要があるからです。1つの Double
は、64ビットの容量を取り、これは多くの桁数を保持するのに十分ですが、π のように無限に展開する数を格納することはできません。
⅓ という数も 10進数では無限に展開します。Double は 2進数で格納されているため、10進数なら有限の桁数で表せる数でも 2進数では有限の表現を持たない場合があります。実は、0.1 はそのような数の一例です。
一般的に、浮動小数点数が実数と同じように振る舞うと期待すると思わぬ所でひどい目に合わされます。Creative Scala を使うには事足りますが、浮動小数点数を使って帳簿管理ソフトを書いてはいけません!
2.4.0.6 式の先にあるもの
今の計算モデルは、式 (プログラムのテキスト) とその型、そしてそれが評価されたときの値 (コンピューターのメモリ内に存在するもの) という 3つの要素を持ちます。これははたして十分なものでしょうか? このモデルを使って株式市場やコンピューターゲームを書くことができるでしょうか? このモデルを拡張する方法を考えてみてください。(これは自由回答の質問です。)
現行のモデルを拡張するいくつかの方法を考えることができます。
役に立つプログラムを書くには作用 (effect)、つまりコンピューターのメモリを超えた世界に関する何らかの変更を引き起こす能力を必要とします。例えば、画面に何か表示したり、音を出したり、他のコンピューターにメッセージを送信したりなどの作用があります。console は値を画面に表示することで自動的に何らかの作用を引き起こしてると言えます。より役に立つプログラムを書くには、それ以上の作用が必要になります。
また、私たちは今の所独自のオブジェクトやメソッドを定義したり、プログラムの中で値を再利用するすべを持ちません。例えば、プログラム中で誰かの名前を使いたいとすると、その名前を何度も繰り返して書く必要があります。抽象化の方法が必要で、これからその方法を見ていきます。
3 絵を使った演算
これまで数、文字列、その他の簡単なオブジェクトを使った演算をみてきました。これらは特に面白いものではありません。ここからは絵を使った計算に注目して、後ほどアニメーションをみていきます。絵を使うことは、より直接的にクリエイティブな機会を与えてくれます。自分たちのプログラムからリアルなアウトプットが出てくる感覚は他の方法では得られないことです。
私たちはグラフィックスを作るのに Doodle というライブラリを使います。この章では Dooble の初歩を習います。
Doodle 内の sbt console を使って練習問題を実行すると普通に動作すると思います。そうじゃない場合で Dooble を使うときは、コードに始めに以下の import 文を書く必要があります。
import doodle.core._
import doodle.core.Image._
import doodle.syntax._
import doodle.jvm.Java2DFrame._
import doodle.backend.StandardInterpreter._
3.1 イメージ
まずは以前のように console を使って、簡単な形を描いてみましょう。
Image.circle(10)
// res0: doodle.core.Image = Circle(10.0)
何が起こっているのでしょう? Image
はオブジェクトで、circle
はそのオブジェクトのメソッドです。私たちは circle
に 10
というパラメータを渡して、私たちが作る円の半径を指定します。結果の型に注目してください。Image
となっていますね。
Doodle 内で console を実行した場合は、これらのイメージを作るためのメソッドが使用可能な状態になっているので、circle(10)
とだけ書くこともできます。
circle(10)
// res1: doodle.core.Image = Circle(10.0)
この円を描画するためには、draw
メソッドを呼びます。
circle(10).draw
fig. 1 のようなウィンドウが表示されるはずです。
Doodle は、円、長方形、三角形といったいくつかの基本図形をサポートします。長方形を描いてみましょう。
rectangle(100, 50).draw
結果は fig. 2 のようになります。
最後に、fig. 3 のような三角形を描いてみましょう。
triangle(60, 40).draw
練習問題
堂々巡り
1、10、100 単位の幅の円を作ってみましょう。次に、それを描いてみよう!
この練習問題は Dooble が正しくインストールされているかの確認を行い、このライブラリを使うのに慣れてもらいます。 Doodle を使うときの注意点として、イメージの定義とイメージの描画が別であるということがあります。この点に関してはこの本を通して何回も出てきます。
以下のようなコードで円を作ることができます。
circle(1)
circle(10)
circle(100)
それぞれの円の draw
メソッドを呼ぶことで円を描画することができます。
circle(1).draw
circle(10).draw
circle(100).draw
私のアートのタイプ
円の型は何でしょうか? 長方形の場合は? 三角形の場合は?
console で確認できる通り、それらは全て Image
型を持ちます。
:type circle(10)
// doodle.core.Image
:type rectangle(10, 10)
// doodle.core.Image
:type triangle(10, 10)
// doodle.core.Image
私のアートのタイプじゃない
イメージの描画の型は何でしょうか? これは何を意味するのでしょう?
再び console でこの質問を聞いてみましょう。
:type circle(10).draw
// Unit
見ての通り、イメージの描画の型は Unit
です。Unit
は特筆すべき値を返さない式のための型です。これは draw
の場合に当てはまります。何故なら draw
は画面に何かを表示するために呼ばれるのであり、戻り値に使い道は無いからです。Unit
型を持つ値は 1つだけあります。これは unit値と呼ばれ、リテラル式 ()
として書かれます。
console は unit値をデフォルトでは表示しないことに注意してください。
()
console に型を聞くことで、そこに unit値があることを確認することができます。
:type ()
// Unit
3.2 レイアウト
ここまでで、基本図形のイメージの作り方を見てきました。レイアウトメソッドを使ってイメージを組み合わせることで、より複雑なイメージを作ることができます。以下のコードを試してみましょう。fig. 4 のように、円と長方形が隣り合わせになっているのが見えるはずです。
(circle(10) beside rectangle(10, 20)).draw
tbl. 1 は、イメージを組み合わせるために Doodle が提供するレイアウトメソッドです。それぞれ使ってみて、何をするのか見てみよう。
演算子 | 型 | 説明 | 使用例 |
---|---|---|---|
Image beside Image |
Image |
2つのイメージを横に並べる | circle(10) beside circle(20) |
Image above Image |
Image |
2つのイメージを縦に並べる | circle(10) above circle(20) |
Image below Image |
Image |
2つのイメージを縦に並べる | circle(10) below circle(20) |
Image on Image |
Image |
2つのイメージを中心で重ねる | circle(10) on circle(20) |
Image under Image |
Image |
2つのイメージを中心で重ねる | circle(10) under circle(20) |
練習問題
The Width of a Circle
これまで見てきたレイアウトメソッドと基礎図形のイメージを使って fig. 5 のような絵を描いてみよう。
これは 3つの小さな円を 1つの大きな円に重ねたものなので、コードではこのように書けます。
(circle(20) beside circle(20) beside circle(20)) on circle(60)
// res0: doodle.core.Image = On(Beside(Beside(Circle(20.0),Circle(20.0)),Circle(20.0)),Circle(60.0))
3.3 色
レイアウトの他に、Doodle を使って私たちのイメージに彩りを与えることができます。tbl. 2 で解説するメソッドを試して、結果がどうなるか見てください。
演算子 | 型 | 説明 | 使用例 |
---|---|---|---|
Image fillColor Color |
Image |
指定した色でイメージを塗りつ | ぶす circle(10) fillColor Color.red |
Image lineColor Color |
Image |
指定した色で線画を描く | circle(10) lineColor Color.blue |
Image lineWidth Int |
Image |
指定した筆幅で線画を描く | circle(10) lineWidth 3 |
Doodle では、色を作るための方法がいくつかあります。 最もシンプルな方法は CommonColors.scala で定義済みの色を使うことです。 tbl. 3 によく使われる色を挙げます。
色 | 型 | 使用例 |
---|---|---|
Color.red |
Color |
circle(10) fillColor Color.red |
Color.blue |
Color |
circle(10) fillColor Color.blue |
Color.green |
Color |
circle(10) fillColor Color.green |
Color.black |
Color |
circle(10) fillColor Color.black |
Color.white |
Color |
circle(10) fillColor Color.white |
Color.gray |
Color |
circle(10) fillColor Color.gray |
Color.brown |
Color |
circle(10) fillColor Color.brown |
練習問題
邪視の魔除け
邪視に対抗するための伝統的なお守りに似せた fig. 6 のようなイメージを作ってみよう。私は虹彩の部分は cornflowerBlue
、外側の部分は darkBlue
を使ったけども、独自に他の色も実験してみて!
これが私のお守りです:
((circle(10) fillColor Color.black) on
(circle(20) fillColor Color.cornflowerBlue) on
(circle(30) fillColor Color.white) on
(circle(50) fillColor Color.darkBlue))
// res0: doodle.core.Image = On(On(On(ContextTransform(doodle.core.Image$$Lambda$4768/1796673738@3dd50473,Circle(10.0)),ContextTransform(doodle.core.Image$$Lambda$4768/1796673738@5dff3aed,Circle(20.0))),ContextTransform(doodle.core.Image$$Lambda$4768/1796673738@6eff56b4,Circle(30.0))),ContextTransform(doodle.core.Image$$Lambda$4768/1796673738@18757234,Circle(50.0)))
3.4 色の作成
ここまでで、イメージの中で定義済みの色を使う方法を見てきました。独自の色を作りたいとしたらどうすればいいでしょう? この節では、独自の色を作ったり、既存の色を変換して新しいものにする方法を解説します。
3.4.1 RGB カラー
コンピューターは、異なる量の赤、緑、青成分を混ぜて幅広い色を再現することで色を取り扱います。この RGB モデルは色の加法混合の一種です。赤、緑、青の成分はそれぞれ 0 から 255 の値を持つことができます。3つ全ての成分が最大値の 255 であるときは、純粋な白を得ることができます。全ての成分が 0 のときは黒となります。
Color
オブジェクトの rgb
メソッドを使って独自の RGB カラーを作ることができます。このメソッドは、赤、緑、青成分の 3つのパラメータを取ります。これらは UnsignedByte
2 と呼ばれる 0 から 255 の数です。UnsignedByte
には Int
のようなリテラル式が無いため、Int
から UnsignedByte
へと変換する必要があります。これは、uByte
メソッドを使って行うことができます。Int
は UnsignedByte
よりも多くの値を取ることができるので、もしも数が UnsignedByte
で表現するには小さすぎたり大きすぎる場合は 0 から 255 の範囲で最も近い値に変換されます。具体例で説明します。
0.uByte
// res0: doodle.core.UnsignedByte = UnsignedByte(-128)
255.uByte
// res1: doodle.core.UnsignedByte = UnsignedByte(127)
128.uByte
// res2: doodle.core.UnsignedByte = UnsignedByte(0)
-100.uByte // 小さすぎるので、0 に変換する
// res3: doodle.core.UnsignedByte = UnsignedByte(-128)
1000.uByte // 大きすぎるので、255 に変換する
// res4: doodle.core.UnsignedByte = UnsignedByte(127)
(UnsignedByte
は Doodle の機能で、Scala が提供するものではないことに注意してください)
UnsignedByte
の作り方が分かったところで、RGB カラーを作ってみましょう。
Color.rgb(255.uByte, 255.uByte, 255.uByte) // White
Color.rgb(0.uByte, 0.uByte, 0.uByte) // Black
Color.rgb(255.uByte, 0.uByte, 0.uByte) // Red
3.4.2 HSL カラー
RGB カラー表現は取り扱いが簡単ではありません。色相、彩度、明度 (HSL) のフォーマットの方が、私たちが色を認知するのにより近い形でモデル化されています。この表現法では色は以下の成分を持ちます:
- 色相 (hue): 色相環上の位置を 0 から 360度の角度で表したもの。
- 彩度 (saturation): 単調なグレーから純粋な色までの色の強度を 0 から 1 までの数で表したもの。
- 明度 (lightness): 黒から純白までの明るさを 0 から 1 までの数で表したもの。
fig. 7 は色相と明度を変えることでどう色が変化するかを示し、fig. 8 は彩度の変化の影響を示します。
Color.hsl
メソッドを使って HSL 表現の色を作ることができます。このメソッドは、色相、彩度、明度をパラメータとして受け取ります。色相は Angle
で、degrees
メソッド (か radians
メソッド) を用いて Double
を Angle
へと変換することができます。
0.degrees
// res8: doodle.core.Angle = Angle(0.0)
180.degrees
// res9: doodle.core.Angle = Angle(3.141592653589793)
3.14.radians
// res10: doodle.core.Angle = Angle(3.14)
彩度と明度は 0.0 から 1.0 へと標準化されます。.normalized
メソッドを使って Double
を標準化された値へと変換します。
0.0.normalized
// res11: doodle.core.Normalized = Normalized(0.0)
1.0.normalized
// res12: doodle.core.Normalized = Normalized(1.0)
1.2.normalized // Too big, is clipped to 1.0
// res13: doodle.core.Normalized = Normalized(1.0)
-1.0.normalized // Too small, is clipped to 0.0
// res14: doodle.core.Normalized = Normalized(0.0)
これで HSL 表現を使って色を作ることができます。
Color.hsl(0.degrees, 0.8.normalized, 0.6.normalized) // A pastel red
この色を見るためには、絵の中で使ってみてください。例えば、fig. 9 を見てください。
3.4.3 色の操作
構図の効果は、実際に使われた色そのものだけではなく、色と色の間の関係よることが多いと思います。既存の色から新しい色を作ることができるメソッドがいくつかあります。特によく使われるのは:
spin
は色相をAngle
の量だけ回転します。saturate
とdesaturate
はそれぞれ彩度に対してNormalized
値を加算したり減算したりします。lighten
とdarken
はそれぞれ明度に対してNormalized
値を加算したり減算したりします。
具体例で解説すると
((circle(100) fillColor Color.red) beside
(circle(100) fillColor Color.red.spin(15.degrees)) beside
(circle(100) fillColor Color.red.spin(30.degrees))).lineWidth(5.0)
は fig. 10 を生成します。
次は似た例ですが、fig. 11 のように彩度と明度を操作します。
(((circle(20) fillColor (Color.red darken 0.2.normalized))
beside (circle(20) fillColor Color.red)
beside (circle(20) fillColor (Color.red lighten 0.2.normalized))) above
((rectangle(40,40) fillColor (Color.red desaturate 0.6.normalized))
beside (rectangle(40,40) fillColor (Color.red desaturate 0.3.normalized))
beside (rectangle(40,40) fillColor Color.red)))
3.4.4 透明度
アルファ値を与えることで、色に透明度を追加することができます。アルファ値 0.0 は完全な透明な色を表し、アルファ値 1.0 は完全に不透明な色を表します。Color.rgba
と Color.hsla
は、Normalized
のアルファ値を表す 4つめのパラメータを受け取ります。既にある色に alpha
メソッドを呼ぶことで異なる透明度を持つ新しい色を作ることができます。例えば、fig. 12 のようになります。
((circle(40) fillColor (Color.red.alpha(0.5.normalized))) beside
(circle(40) fillColor (Color.blue.alpha(0.5.normalized))) on
(circle(40) fillColor (Color.green.alpha(0.5.normalized))))
練習問題
類似色の三角形
3つの三角形を、三角に配置して、類似色で色付けしてみよう。類似色とは色相の近い色のことです。(ちょっと手のこんだ) 具体例 fig. 13 を見てください。
回答を console に書くにはちょっと長くなりすぎてきていますね。次にその対策を見ていきます。
((triangle(40, 40)
lineWidth 6.0
lineColor Color.darkSlateBlue
fillColor (Color.darkSlateBlue lighten 0.3.normalized saturate 0.2.normalized spin 10.degrees)) above
((triangle(40, 40)
lineWidth 6.0
lineColor (Color.darkSlateBlue spin (-30.degrees))
fillColor (Color.darkSlateBlue lighten 0.3.normalized saturate 0.2.normalized spin (-20.degrees))) beside
(triangle(40, 40)
lineWidth 6.0
lineColor (Color.darkSlateBlue spin (30.degrees))
fillColor (Color.darkSlateBlue lighten 0.3.normalized saturate 0.2.normalized spin (40.degrees)))))
// res19: doodle.core.Image = Above(ContextTransform(doodle.core.Image$$Lambda$6824/185365984@4bfaba7c,ContextTransform(doodle.core.Image$$Lambda$6826/1797463237@3e526236,ContextTransform(doodle.core.Image$$Lambda$6825/745609583@30274484,Triangle(40.0,40.0)))),Beside(ContextTransform(doodle.core.Image$$Lambda$6824/185365984@4391a371,ContextTransform(doodle.core.Image$$Lambda$6826/1797463237@5da6c825,ContextTransform(doodle.core.Image$$Lambda$6825/745609583@70c84275,Triangle(40.0,40.0)))),ContextTransform(doodle.core.Image$$Lambda$6824/185365984@7a413b8c,ContextTransform(doodle.core.Image$$Lambda$6826/1797463237@3485959e,ContextTransform(doodle.core.Image$$Lambda$6825/745609583@5f8994cb,Triangle(40.0,40.0))))))
3.5 練習問題
3.5.1 ターゲット
アーチェリー用のターゲットを、fig. 14 のように 3つの同心円で得点帯を表して描いてみよう。
ボーナス得点として、fig. 15 のようにターゲットを練習場に置けるようにスタンドを追加してください。
最もシンプルな解法は on
演算子を使って同心円を描くことです。
(circle(10) on circle(20) on circle(30))
ボーナス得点のスタンドは 2つの長方形を使って描きます。
(
circle(10) on
circle(20) on
circle(30) above
rectangle(6, 20) above
rectangle(20, 6)
)
3.5.2 ぶれない標的
ターゲットを赤と白で色付けしてみよう。(もし前問で追加した場合は) スタンドは茶色、芝生は緑に色付けしてみよう。 例としては fig. 16 を参照してください。
ここでのコツはカッコをうまく使って合成の順序を制御することです。 fillColor()
、lineColor()
、そして lineWidht()
メソッドは単一のイメージに適用され、イメージが正しい形から構成されているかを確認する必要があります。
(
( circle(10) fillColor Color.red ) on
( circle(20) fillColor Color.white ) on
( circle(30) fillColor Color.red lineWidth 2 ) above
( rectangle(6, 20) above rectangle(20, 6) fillColor Color.brown ) above
( rectangle(80, 25) lineWidth 0 fillColor Color.green )
)
4 大きなプログラムの書き方
console にプログラムを打ち込んでいくのが辛くなってきています。 この章では、より大きなプログラムを書くためのツールを解説します:
- プログラムをファイルに保存することで、何度も繰り返してコードを書き込む必要が無くなります。
- 値に名前を与えることで再利用できるようにします。
例題を Doodle の sbt console 内で実行した場合は、何もしなくても動作するはずです。そうじゃない場合は、以下の import 文を使って Doodle を使用可能な状態にする必要があります。
import doodle.core._
import doodle.core.Image._
import doodle.syntax._
import doodle.jvm.Java2DFrame._
import doodle.backend.StandardInterpreter._
4.1 console 内での作業
テキスト・エディタや IDE を使ってコードをファイルに保存できるようになりますが、Scala コンパイラが探せるように正しい場所に保存する必要があります。 Doodle テンプレートから作業している場合は、コードは src/main/scala/
ディレクトリに保存してください。
保存したコードを console から使うにはどうしたらいいでしょうか? console 内だけで動作する特別なコマンドがあって、それを使ってファイルに保存したコードを実行することができます。 このコマンドは :paste
3 と呼ばれています。:paste
に続けて実行したいファイル名を書きます。例えば、src/main/scala/Example.scala
というファイルに式
circle(100) fillColor Color.paleGoldenrod lineColor Color.indianRed
を保存したとすると、以下のように console に書くことでこのコードを実行することができます。
:paste src/main/scala/Example.scala
// res0: doodle.core.Image = ContextTransform(<function1>,ContextTransform(<function1>,Circle(100.0)))
上の例では res0
という名前が与えられたことに注目してください。あなたが console に打ち込んで追従しているとしたら、今まで console に何を書いたのかによって別の数になったかもしれません。res0.draw
(もしくは、あなたの console のための別の名前) を評価することでこのイメージを描画することができます。
4.1.1 console を使うためのコツ
console をより効率良く使うためのコツです:
上矢印キーを押すと console に最後に打ち込んだものが出てきます。長いファイル名を何度も打ち込まないでいいようになるので便利です! 上矢印キーを複数回押すことで console 内での履歴をさかのぼることができます。
Tab
キーを押すことで、console にコードの補完を行ってもらうことができますが、残念ながらファイル名を探すことはできないので、自分で書く必要があります。例えば、Stri
と書いてTab
を押すと、console は可能な補完例を表示します。Strin
と書けば、console がString
と補完できるようになります。
コードをファイルに保存し始めると、次回 sbt を起動したときにコンパイラーが私たちのコードを見て怒るのに気付くかもしれません。その対策方法は次の節で解説するので続きを読んでください。
4.2 console 外でのコーディング
これまで console で書いてきたコードは、console 外で実行すると問題が発生します。例えば、以下のコードを src/main/scala
内の Example.scala
に書いてください。
Image.circle(100) fillColor Color.paleGoldenrod lineColor Color.indianRed
次に、sbt を再起動して console に入ってみましょう。以下のようなエラーが表示されるはずです。
[error] src/main/scala/Example.scala:1: expected class or object definition
[error] circle(100) fillColor Color.paleGoldenrod lineColor Color.indianRed
[error] ^
[error] one error found
IDE を使っている場合も似たようなエラーが出てくるはずです。
問題は、このようになっています:
- Scala は、console が起動する前に全てのコードをコンパイルしようとします。
- ファイルに書かれるコードには、直接 console に書かれるコードには無い制約がいくつかあります。
そのため、これらの制約を知って、ファイルに書くコードの書き方を変える必要があります。
エラーメッセージにヒントが隠されています。expected class or object definition
(クラスまたはオブジェクトを期待する)。クラスが何かはまだ分かりませんが、オブジェクトのことは知っています – 全ての値はオブジェクトです。Scala では、全てのコードはオブジェクトかクラス内に書かれる必要があります。オブジェクトは、以下のように式をラッピングすることで定義できます。
object Example {
(circle(100) fillColor Color.paleGoldenrod lineColor Color.indianRed).draw
}
次は別の理由でコンパイルが通りません。以下のようなエラーが沢山出てきたと思います。
[error] doodle/shared/src/main/scala/doodle/examples/Example.scala:2: not found: value circle
[error] (circle(100) fillColor Color.paleGoldenrod lineColor Color.indianRed).draw
[error] ^
私たちが circle
という名前を使ったけども、この名前が何を指しているのか分からないと、コンパイラは言っています。 上のコード内の Color
でも同様の問題が発生します。 名前に関する詳しい解説は後ほど行います。 とりあえず今の所は import
文を書いて、これらの名前の値をどこから見つければいいのかを教えてあげましょう。 Color
という名前は doodle.core
というパッケージの中にあり、circle
という名前は doodle.core
内の Image
オブジェクトの中にあります。 doodle.core
内の全ての名前と Image
オブジェクト内の全ての名前を使うようにコンパイラに指示するには以下のように書きます。
import doodle.core._
import doodle.core.Image._
コードが完全に動作するためには、コンパイラは他にもいくつかの名前を探す必要があります。 それらは以下のように import します。
import doodle.syntax._
import doodle.jvm.Java2DFrame._
import doodle.backend.StandardInterpreter._
これらの import 文はファイルの一番上に書くので、コード全体はこのようになります:
import doodle.core._
import doodle.core.Image._
import doodle.syntax._
import doodle.jvm.Java2DFrame._
import doodle.backend.StandardInterpreter._
object Example {
(circle(100) fillColor Color.paleGoldenrod lineColor Color.indianRed).draw
}
これで問題なくコードがコンパイルするはずです。
これで、sbt 内の console に行った場合は、私たちのコードをさっき名付けた Example
という名前を使って参照することができます。
Example // draws the image
練習問題
まだ行っていなければ、上のコードを src/main/scala/Example.scala
というファイルに保存して、コードがコンパイルされて console からアクセスできることを確認してみよう。
4.3 名前
前の節では多くの新しい概念を紹介しました。 この節では、そのうちの 1つ「値に名前をつけること」をもう少し掘り下げて見てみましょう。
私たちは、名前を使って色んな物を参照します。 例えば「プロフェスール・エミル・ペロ」(Professeur Emile Perrot) はとても香りの強いバラの品種を指し、「チェリー・パフェ」(Cherry Parfait) は非常に病気に強いけどもほとんど香りがしない品種のことです。 話し言葉においてこの関係が正確にはどのようになっているのかに関して、人々は多くの考察を与えてきました。 プログラミング言語はより制限されているため、正確な定義を与えることができます: 名前は値を指します。 名前が値に束縛されている、もしくは名前がバインディングを導入するといった言い方をすることもあります。 値が名前を持つ場合は、値をそのまま書いて使うことができる所全てで、代わりに名前を使うことができます。 別の言い方をすると、名前は値が参照する値に評価されます。 ここから必然的に出てくる疑問があります: どうやって値に名前を与えるのでしょう? Scala ではいくつの方法があるので、それを見ていきましょう。
4.3.1 オブジェクトリテラル
オブジェクトリテラルの宣言の仕方は既に見ています。
object Example {
(circle(100) fillColor Color.paleGoldenrod lineColor Color.indianRed).draw
}
これは、今までに見てきた他のリテラル式同様にリテラル式ですが、この場合 Example
という名前のオブジェクトを作ります。 プログラムの中で Example
という名前を使うと、このオブジェクトに評価されます。
Example
// Example.type = Example$@76c39258
console 内で何回か試してみてください。 名前を使ったときの違いに気づいたでしょうか? Example
という名前を最初に使ったときは絵が描かれたけども、次回以降は何も起こらなかったことに気づいたかもしれません。 オブジェクトの名前を初めて使ったときにはオブジェクトの本文が評価されて、オブジェクトが作られます。 次回以降の名前の使用時にはオブジェクトが既に存在するので再評価されません。 この場合は、オブジェクトの内部の式が draw
メソッドを呼ぶため私たちはこの違いに気付くことができました。 もしこれを 1 + 1
(もしくは、draw
を抜いただけのもの) などに置き換えると違いは分からなくなります。 これに関しては後ほどの章で存分に解説します。
このオブジェクトの型は何なのか気になるかもしれません。 console に聞いてみましょう。
:type Example
// Example.type
Example
の型は Example.type
で、他に値を持たない固有の型です。
4.3.2 val
宣言
オブジェクトリテラルはオブジェクトの作成と名前の定義を同時に行います。 この 2つを分けて、既に存在する値に名前を与えられると便利です。 val
宣言を用いてそれを行うことができます。
val
を使うには
val <名前> = <値>
の <名前>
と <値>
をそれぞれ名前と値に評価されるような式をそれぞれ書きます。
具体例で解説します。
val one = 1
val anImage = Image.circle(100).fillColor(Color.red)
これら 2つの宣言は one
と anImage
という名前を定義します。 後からこれらの名前をコードの中で使って値を参照することができます。
one
// res0: Int = 1
anImage
// res1: doodle.core.Image = ContextTransform(doodle.core.Image$$Lambda$8479/247257415@46e720c5,Circle(100.0))
4.3.3 宣言
上の節で、宣言と定義という言い方をしました。 これらの用語が正確には何を意味するのかを理解して、object
と val
の違いをより深く見ていきましょう。
式については分かっています。 それらは、値に評価されるプログラムの一部です。 宣言や定義は、プログラムの別の部分で、それらは値に評価されません。 代わりに、それらは何かに名前を与えます。実は、Scala では値だけではなく型も宣言できますが、これに関してはここではあまり考察しません。 object
と val
は両方とも宣言です。
宣言と式が別になっていることの結果の 1つとして、以下のようなプログラム
val one = ( val aNumber = 1 )
// <console>:2: error: illegal start of simple expression
// val one = ( val aNumber = 1 )
// ^
は val aNumber = 1
が式ではなく、値に評価されないため書くことができません。
しかし、以下のようには書くことができます。
val aNumber = 1
// aNumber: Int = 1
val one = aNumber
// one: Int = 1
4.3.4 トップレベル
値に名前を与えるのに object
と val
宣言という別々の方法があるのに納得がいかないかもしれません。 名前を宣言するのに val
を使って、object
は名前を付けずにオブジェクトの作成を行えばいいんじゃないでしょうか? 名前を付けずにオブジェクトリテラルを宣言することはできるでしょうか?
Scala はそれを許しません。 例えば、以下のようには書くことができません。
object {}
// <console>:2: error: identifier expected but '{' found.
// object {}
// ^
オブジェクトリテラルは必ず名前を付ける必要があります。
Scala は、トップレベルと呼ばれるコードとその他のものを区別します。 トップレベルにあるコードは、それをラッピングするコードを一切外側に持ちません。 別の言い方をすると、それは object
に包むことなくファイルに直接書いてコンパイルすることができるものです。
式はトップレベルではないことを見ました。 val
もトップレベルではありません。 しかし、オブジェクトリテラルはトップレベルです。
この区別は、ちょっと面倒なものです。 他の言語にはこの区別が無いものもあります。 Scala の場合は、Scala が Java コードを実行させるための Java Virtual Machine (JVM) 上に構築されていることによります。 Java がトップレベルとその他のコードを区別するために、Scala も JVM 上で動作するために仕方がなく区別をする必要があります。 Scala の console はこのトップレベル区別を行わないため、Scala を習い始めたときに混乱しやすいポイントとなります (console に書かれたもの全てがオブジェクトにラッピングされたいると思ってください)。
オブジェクトリテラルはトップレベルであることが許されているけども、val
宣言は許されていないということは、オブジェクトリテラル内に val
を宣言できるということでしょうか? オブジェクトリテラル内に val
を宣言した場合、後からその名前を参照することができるでしょうか?
できます!
このようにして、オブジェクトリテラル内に val
を置くことができます:
object Example {
val hi = "Hi!"
}
これを後から参照するには、既に使ってきた .
構文を使います。
Example.hi
// res2: String = Hi!
hi
を単独で使うことはできないことに注意してください。
hi
// <console>:28: error: not found: value hi
// hi
// ^
Scala に対して、Example
オブジェクト内で定義された名前 hi
を参照したいと伝える必要があるからです。
4.3.5 スコープ
さっきの練習問題をやったとすると (やったよね?)、オブジェクト内で宣言された名前は、その名前を含んだオブジェクトも参照しないとオブジェクト外で使えないことを見ました。 具体例で解説すると、
object Example {
val hi = "Hi!"
}
// defined object Example
以下のようには書くことができません。
hi
// <console>:28: error: not found: value hi
// hi
// ^
Scala に対して Example
内の中で hi
を探す必要があると伝える必要があります。
Example.hi
// res5: String = Hi!
名前が修飾無しで使える所のことを名前が可視 (visible) 状態であるという言い方をして、名前が可視状態である所のことをそのスコープと言います。 そのため、この気取った新しい用語を使うと、「hi
は Example
外では不可視である」または「Example
外では hi
はスコープに無い」と言えます。
名前のスコープはどうやったら分かるでしょうか? ルールは簡単で、名前は宣言された位置から始まり、直近の外側の中括弧 ({
と }
) の終わりまで可視状態にあります。 上の例では、hi
は Example
の中括弧に囲まれているので、そこで可視状態にあります。 他では見えません。
オブジェクトリテラルをオブジェクトリテラルの中で宣言することができ、より細かなスコープの区別することができます。 具体例で解説すると、
object Example1 {
val hi = "Hi!"
object Example2 {
val hello = "Hello!"
}
}
hi
は Example2
内でもスコープ内です (Example2
は hi
の外側の中括弧内で定義されているため)。 しかし、hello
のスコープは Example2
だけに限定されているため、hi
のスコープよりも小さなものです。
もしスコープ内で既に宣言されている名前を再宣言するとどうなるでしょうか? これはシャドーイングと呼ばれています。 以下のコードでは、Example2
内の hi
は Example1
内の hi
を覆い隠します。
object Example1 {
val hi = "Hi!"
object Example2 {
val hi = "Hello!"
}
}
Scala はこれを許容しますが、コードがすごく分かりづらくなるので一般的には悪い考えです。
オブジェクトリテラルを使わなくても新しいスコープを作ることができます。 Scala は、中括弧を置くことでほぼ全ての場所でスコープを作ることができます。
例えば以下のように書けます。
object Example {
val good = "Good"
// Create a new scope
{
val morning = good ++ " morning"
val toYou = morning ++ " to you"
}
val day = good ++ " day, sir!"
}
morning
(と toYou
) は新しいスコープ内で宣言されています。(このスコープには名前が無いため) このスコープを外側から参照することはできないので、宣言されているスコープ外から morning
を参照することは不可能です。 残りのプログラムに知られたくない秘密があるときはこの方法を使って隠すことができます。
このような Scala での入れ子のスコープの振る舞いはレキシカルスコープと呼ばれます。 全ての言語がレキシカルスコープを持つわけではありません。 例えば、Ruby や Python はレキシカルスコープを持たず、JavaScript はやっと最近になって導入されました。 筆者の意見では、レキシカルスコープを持たない言語を設計するのは、グアテマラ産激辛トウガラシを大量に食べた後で手を洗わずにトイレに行くぐらい馬鹿げたことだと思います。
練習問題
名前とスコープが理解できているかをテストするために、以下のそれぞれの場合で answer
の値を求めてみましょう。
val a = 1
val b = 2
val answer = a + b
まずはシンプルな例から。answer
は 1 + 2
なので 3
です。
object One {
val a = 1
object Two {
val a = 3
val b = 2
}
object Answer {
val answer = a + Two.b
}
}
これもシンプルな例です。answer
は 1 + 2
なので 3
です。Two.a
は answer
が定義されている場所ではスコープ外です。
object One {
val a = 5
val b = 2
object Answer {
val a = 1
val answer = a + b
}
}
ここでは Answer.a
が One.a
を覆い隠すので、answer
は 1 + 2
で 3
となります。
object One {
val a = 1
val b = a + 1
val answer = a + b
}
これは完全に普通のコードです。b
の宣言の右辺項にある式 a + 1
は普通の式であるので、answer
は再び 3
となります。
object One {
val a = 1
object Two {
val b = 2
}
val answer = a + b
}
b
は answer
が宣言されている所では b
はスコープ外であるので、このコードはコンパイルを通りません。
object One {
val a = b - 1
val b = a + 1
val answer = a + b
}
引っ掛け問題です! このコードは動作しません。ここでは a
と b
がそれぞれに対して定義されているため循環依存となり、解決されません。
4.4 抽象化
前の節では名前について色々習いました。 気取ったプログラマー用語を使うと、名前は式を抽象化すると言えます。 これは、名前の定義することの本質を一言で言い表しているけども、ちょっと用語を解読してみよう。
抽象化とは、要らない詳細を取り除くという意味です。 例えば、数は抽象化の 1つです。 「1」という数の純粋な概念は自然界には存在しません。 それはいつも 1つのリンゴや 1冊の Creative Scala の本といった具合に 1つの物です。 算数を行うときに、数という概念は何を数えているのかという要らない詳細を抽象化して数だけを操作することができます。
同様に、名前は式を代理します。 式は値をどのように構築するかを教えてくれます。 その値に名前があれば、その値がどのように構築されるかは知らなくてもよくなります。 式は任意の複雑さを持つことができますが、名前を使うことでどれだけ複雑なのかを気にする必要が無くなります。 これが、名前は式を抽象化すると言ったときの意味です。 いつでも式が出てきたら、それは同じ値を持つ名前に置き換えることができます。
抽象化はコードを読み書きしやすくします。 fig. 17 のように箱の列を作る具体例を用いて説明しましょう。
この絵を作る単一の式を書くことは可能です。
(
Image.rectangle(40, 40).
lineWidth(5.0).
lineColor(Color.royalBlue.spin(30.degrees)).
fillColor(Color.royalBlue) beside
Image.rectangle(40, 40).
lineWidth(5.0).
lineColor(Color.royalBlue.spin(30.degrees)).
fillColor(Color.royalBlue) beside
Image.rectangle(40, 40).
lineWidth(5.0).
lineColor(Color.royalBlue.spin(30.degrees)).
fillColor(Color.royalBlue) beside
Image.rectangle(40, 40).
lineWidth(5.0).
lineColor(Color.royalBlue.spin(30.degrees)).
fillColor(Color.royalBlue) beside
Image.rectangle(40, 40).
lineWidth(5.0).
lineColor(Color.royalBlue.spin(30.degrees)).
fillColor(Color.royalBlue)
)
このコードの中に内在するシンプルなパターンがありますが、それが見づらくなっています。 初見で全ての長方形が同じものであることが分かるでしょうか? 基本となる箱のコードに名前を付けるという抽象化を行うことでコードの見通しが良くなります。
val box =
Image.rectangle(40, 40).
lineWidth(5.0).
lineColor(Color.royalBlue.spin(30.degrees)).
fillColor(Color.royalBlue)
box beside box beside box beside box beside box
これでどうやって箱が作られているのかと、絵の中で箱が 5回繰り返されているのが分かりやすくなりました。
練習問題
再びアーチェリー
前の章のアーチェリーのターゲットに戻ってみてみましょう。 fig. 18 を見てください。
前回イメージを作ったときは値に名前をつける方法を知らなかったので、1つの大きい式を書きました。 今回は、イメージの部品にそれぞれ名前をつけて、イメージがどのように構築されているのか他の人が分かりやすいようにしてください。 どの部分に名前を付けるべきで、他の部分は名前をつけるに足らないという判断は自分のセンスで行う必要があります。
私たちは、以下のようにターゲット、スタンド、地面を分けて名前を付けました。 これで最終的なイメージがどう構築されているのかの見通しが良くなったと思います。 私たちの考えでは、これ以上細かく部品に名前をつけても役に立たないと思いました。
val coloredTarget =
(
Image.circle(10).fillColor(Color.red) on
Image.circle(20).fillColor(Color.white) on
Image.circle(30).fillColor(Color.red)
)
val stand =
Image.rectangle(6, 20) above Image.rectangle(20, 6).fillColor(Color.brown)
val ground =
Image.rectangle(80, 25).lineWidth(0).fillColor(Color.green)
val image = coloredTarget above stand above ground
一丁先を行く
より実践的な名前の使い方として、fig. 19 のような町並みの 1シーンを作ってみよう。 イメージの部品に名前を付けることによってかなりの繰り返しを省けるはずです。
これが私たちの解答です。 見ての通り、風景を小さな部品に分けることで比較的小さなコードに収めることができました。
val roof = Image.triangle(50, 30) fillColor Color.brown
val frontDoor =
(Image.rectangle(50, 15) fillColor Color.red) above (
(Image.rectangle(10, 25) fillColor Color.black) on
(Image.rectangle(50, 25) fillColor Color.red)
)
val house = roof above frontDoor
val tree =
(
(Image.circle(25) fillColor Color.green) above
(Image.rectangle(10, 20) fillColor Color.brown)
)
val streetSegment =
(
(Image.rectangle(30, 3) fillColor Color.yellow) beside
(Image.rectangle(15, 3) fillColor Color.black) above
(Image.rectangle(45, 7) fillColor Color.black)
)
val street = streetSegment beside streetSegment beside streetSegment
val houseAndGarden =
(house beside tree) above street
val image = (
houseAndGarden beside
houseAndGarden beside
houseAndGarden
) lineWidth 0
4.5 パッケージとインポート
私たちのコードをコンパイルできるように変えたとき多くのimport 文を追加する必要がありました。 この節ではその解説を行います。
ある名前が別の名前を覆い隠すことができることは前に見ました。 これはプログラムが大きくなると、1つのプログラムの別々の部分が同じ名前を別の用途に使おうとして問題を引き起こす可能性があります。 スコープを作って外からの名前を隠すことはできますが、トップレベルで定義された名前の対策をする必要があります。
同様の問題が自然言語でも発生します。 例えば、あなたの弟と友達の両方が「ズィギー」という名前だとすると、その名前を使った時どっちを指しているのかを補足する必要があります。 文脈によっては明らかかもしれませんが、友達の場合は「ズィギー S」、弟の場合は「ズィギー」と呼び分ける必要があるかもしれません。
Scala では、名前を整理するのにパッケージを用います。 パッケージはトップレベルで定義される名前のためのスコープを作ります。 同一のパッケージ内のトップレベルの名前は全て同じスコープ内で定義されます。 別のスコープ内にあるパッケージの名前を持ってくるにはインポートを行う必要があります。
パッケージを作るのは簡単で、以下のように
package <名前>
ファイルの一番上に書いて、<name>
を自分のパッケージ名に置き換えます。
パッケージ内で定義された名前を使うには import
文を使って、パッケージ名を指定した後、全ての名前なら _
、いくつかの名前だけならその名前を書きます。
具体例で解説します。
console 内ではパッケージを定義することはできません。 以下のコードを動作させるためには、example
パッケージ内のコードをファイルに置いてコンパイルする必要があります。
まずは、パッケージ内でいくつかの名前を定義してみましょう。
package example
object One {
val one = 1
}
object Two {
val two = 2
}
object Three {
val three = 3
}
次に、これらの名前をスコープ内に持ち込むにはインポートを行います。 1つの名前だけインポートすることができます。
import example.One
One.one
One
と Two
の両方をインポートする場合。
import example.{One, Two}
One.one + Two.two
もしくは、example
内の全ての名前をインポートする場合。
import example._
One.one + Two.two + Three.three
Scala では、スコープを定義することができる色んなものをインポートすることができ、これにはオブジェクトを含みます。 以下のコードは one
をスコープにインポートします。
import example.One._
one
4.5.1 パッケージの整理
パッケージはトップレベルの名前が衝突するのを防ぐけども、パッケージ名同士の衝突はどうしたらいいでしょうか? 一般的には、パッケージは階層的に整理することで衝突を回避します。 例えば、Doodle では core
パッケージは doodle
パッケージ内に定義されます。
import doodle.core._
上のような import
文を使うとき、これはパッケージ doodle
内のパッケージ core
が欲しくて、core
と呼ばれているかもしれない他のパッケージでは無いことを明示します。
5 置き換えモデルによる評価
私たちのプログラムが何を行っているのかを理解するためには Scala の式がどのように評価されるのかというメンタルモデルが必要となります。 これまでの所は、くだけたモデルで何とかやってきました。 この節では、置き換えモデルによる評価を理解してもう少し形式化されたモデルにしていきます。 プログラミングの多くのこと同様に、気取った用語を使っていますがシンプルな概念です。 この場合、多分高校の数学で習ったことのある置き換えの話で、同じ考え方を新しい文脈に持ってきたものです。
例題を Doodle の sbt console 内で実行した場合は、何もしなくても動作するはずです。そうじゃない場合は、以下の import 文を使って Doodle を使用可能な状態にする必要があります。
import doodle.core._
import doodle.core.Image._
import doodle.syntax._
import doodle.jvm.Java2DFrame._
import doodle.backend.StandardInterpreter._
5.1 置き換え
置き換えは、式を見たら、それが評価される値で置き換えることができるというものです。具体例で説明すると、
1 + 1
を見たら、これは 2
で置き換えることができます。 そのため、
(1 + 1) + (1 + 1)
のような複合式が出てきたら 1 + 1
を 2
に置き換えて
2 + 2
となり、それは 4
に評価されます。
これは、高校の数学で式を簡易化するときに行ったのと同様の論理的思考です。 当然計算機科学ではこの過程を指す気取った用語があります。 置き換えの他に、これを式を簡約すると言ったり、等式推論 (equational reasoning) と言ったりします。
置き換えは私たちのプログラムを筋道立てて考える方法を与えてくれます。別の言い方とすると、「何が起こっているのかを正確に知ることができる」ということです。 今まで見てきた全ての式に置き換えを適用することができます。 ここではイメージよりも数や文字列を使った例題の方が分かりやすいので、前の章で見た例題に戻ります:
1 + ("Moonage daydream".indexOf("N"))
前の例は少し大ざっぱでしたが、ここではもう少し正確にコンピューターが何を行っているかのステップを解説しましょう。 コンピューターを真似ていると思ってください。
+
を含む式は 2つのサブ式である 1
と ("Moonage daydream".indexOf("N"))
から構成されます。 まず左か、右かのどちらを先に評価するかを決める必要があります。 ここでは、適当に右のサブ式を選ぶことにします (この選択に関してはまた後ほど)。
サブ式である ("Moonage daydream".indexOf("N"))
もまた 2つのサブ式 "Moonage daydream"
と "N"
から構成されます。 リテラル式自身は値では無いのでこれらも評価する必要があることを思い出して、再び右側から評価するとします。
リテラルである "N"
は、値の "N"
へ評価されます。 混乱を避けるために、この説明文中だけの約束事として、値の方は |"N"|
と書くことにしましょう。(この ||
を含む式をコピー&ペーストしても、コンパイルできませんので注意してください。) これで、最初のステップにより 1つの式をその値に置き換えることができます。
1 + ("Moonage daydream".indexOf(|"N"|))
次に、サブ式の左辺側を評価して、リテラル式 "Moonage daydream"
をその値である |"Moonage daydream"|
に置き換えることができます。 これで以下のようになります:
1 + (|"Moonage daydream"|.indexOf(|"N"|))
これで (|"Moonage daydream"|.indexOf(|"N"|))
という式全体を評価できるようになり、これは |-1|
に評価されます (ここでも縦棒を使って整数値とリテラル式を区別しています)。 再び置き換えを使って以下を得ます:
1 + |-1|
次に左辺のリテラル 1
を評価して |1|
を得ます。 置き換えを行って、以下を得ます:
|1| + |-1|
これで式全体を評価できるようになり、以下を得ます:
|0|
Scala に式全体を評価してもらって検算しましょう。
1 + ("Moonage daydream".indexOf("N"))
// res4: Int = 0
正解です!
ここまでを見て、いくつか気づいたことがあると思います:
- コンピューターのように厳密に置き換えを行うには多くのステップを伴います。
- 暗算で行った評価でも正しい答えを得ることができたかもしれません。
- 一見適当に右から左に行った評価でも正しい答を得ることができました。
たまたま Scala が行っている置き換え順を選ぶことができたのか (違いますが、まだこれは調査していません)、どの順で評価しても関係無いのでしょうか? 最初の足し算の例のように、正しい答えの得られる近道があるのはどの場合でしょうか? これらの質問を後ほど考察しますが、まずは名前がある場合に置き換えがどうなるかを見ていきましょう。
5.1.1 名前
名前の置き換えルールは、名前が参照する値で置き換えることです。 このルールは既にそれとなく使ってきたものですが、ここで形式化します。
具体例で解説すると、
val name = "Ada"
name ++ " " ++ "Lovelace"
このコードに置き換えを適用して
"Ada" ++ " " ++ "Lovelace"
を得ることができ、これは
"Ada Lovelace"
に評価されます。
これで置き換えプロセスの中で名前をより形式的に取り扱うことができるようになりました。 例えば、最初に例に戻ると
1 + 1
この式に名前を与えることができます:
val two = 1 + 1
以下のような複合式があるとき
(1 + 1) + (1 + 1)
置き換えによって 1 + 1
を two
で置き換えて以下を得ることができます:
two + two
この式を計算したとき
1 + ("Moonage daydream".indexOf("N"))
私たちはサブ式に分解してから、それぞれを評価して置き換えました。 言葉を使ったため、これはかなり難解なものになってしまいました。 val
宣言を使うことで、これはよりコンパクトかる分かりやすく書き直すことができます。 以下は同じ式を部品に分解したものです。
val a = 1
val b = "Moonage daydream"
val c = "N"
val d = b.indexOf(c)
val e = a + d
ここで (現在では適当に) 上から下の順に評価が行われると定義した場合、異なる評価順を試して結果に影響が出るか実験することができます。
例えば、
val c = "N"
val b = "Moonage daydream"
val a = 1
val d = b.indexOf(c)
val e = a + d
は以前と同じ結果となります。 しかし、
val e = a + d
val a = 1
val b = "Moonage daydream"
val c = "N"
val d = b.indexOf(c)
は e
が a
と d
に依存して、上から下の順序では a
と d
が評価されていないためうまくいきません。 これは試すのも少し馬鹿げていると思うかもしれません。最終的に評価しようとしている式は e
であり、a
と d
は e
のサブ式であるため、当然サブ式は式の前に評価される必要があります。
5.2 評価順序
評価順序の話をする準備が整いました。 評価順なんて関係あるのかと思うかもしれません。 これまで見た例だと、式をサブ式の前に評価してはいけないという問題以外では評価順は関係無いように見えます。
これらの問題の考察を行うには新しい概念を導入する必要があります。 ここまではほぼ全て純粋な式のみを取り扱ってきました。 これらは自由な順序で置き換えしても問題の無い式のことです4。
非純粋な式は評価順に影響を受けるものです。 これまでに 1つ非純粋な式を見ていて、それは draw
メソッドです。
Image.circle(100).draw
Image.rectangle(100, 50).draw
と
Image.rectangle(100, 50).draw
Image.circle(100).draw
を評価したとき、イメージを含むウィンドウが異なる順番で現れます。 特に面白みの無い違いですが、確かに違いではあります。
非純粋な式の特徴はそれらの評価が私たちに見える形の変化を引き起こすことです。 例えば、draw
を評価するとイメージが表示されます。 これらの観測可能な変化を副作用もしくは作用と呼びます。 副作用を含むプログラムは、自由に置き換えを行うことができません。 しかし、副作用を使って評価順序を調査することができます。 それを行う道具は println
メソッドです。
println
メソッドはテキストを console に表示して (副作用)、Unit値に評価されます。 以下が具体例です:
println("Hello!")
// Hello!
println
の console に表示するという副作用は評価順序を調べるのに便利なものです。 例えば、
println("A")
// A
println("B")
// B
println("C")
// C
を実行した結果は式が上から下へと評価することを示します。 println
を使ってさらに調査してみましょう。
練習問題
println は置き換えができない
純粋なプログラムはどの式でも名前を与えてその式が出てきた所を名前で置き換えることができます。 具体例で示すと、
(2 + 2) + (2 + 2)
を置き換えて、以下のように書くことができ、
val a = (2 + 2)
a + a
プログラムの結果は変わりません。
非純粋な式の 1例として println
を使って、この置き換えがうまくいかないこと、そのため副作用とも言われる非純粋な式が置き換えを壊すことを示してみよう。
以下はこれを示すシンプルな例です。 以下の 2つのプログラムが異なることを観測することができます。
println("Happy birthday to you!")
// Happy birthday to you!
println("Happy birthday to you!")
// Happy birthday to you!
println("Happy birthday to you!")
// Happy birthday to you!
val a = println("Happy birthday to you!")
// Happy birthday to you!
// a: Unit = ()
a
a
a
つまり、副作用があるときは自由に置き換えを使うことができないため、評価順序を気にする必要があると言えます。
狂気のメソッド
スコープを紹介したときにブロック式も見ましたが、そのときはその名前では呼びませんでした。 ブロックは中括弧 ({}
) を使って作ることができます。それは中括弧内全ての式を評価します。ブロック内の最後の式の結果がブロック式の結果となります。
// 3 に評価される
{
val one = 1
val two = 2
one + two
}
// res13: Int = 3
ブロック式を使って、何か役に立つ値に評価されるブロックの中に println
を入れることで、メソッドのパラメータの評価順を調査することができます。
例えば、Image.rectangle
や Color.hsl
とブロック式を使って Scala がメソッドパラメータを特定の順序で評価しているのか、そうだとしたらどの順序なのかを調べてみましょう。
セミコロン (;
) で分けて書くことでブロックをよりコンパクトに 1行で書くことができることに注意してください。 これは普通はお行儀の良い方法ではありませんが、このような実験には役立つかもしれません。 以下が例となります。
// Evaluates to three
{ val one = 1; val two = 2; one + two }
// res15: Int = 3
以下のコードは、メソッドのパラメータが左から右へと評価されていることを示します。
Color.hsl(
{
println("a")
0.degrees
},
{
println("b")
1.normalized
},
{
println("c")
1.normalized
}
)
// a
// b
// c
// res16: doodle.core.Color = HSLA(Angle(0.0),Normalized(1.0),Normalized(1.0),Normalized(1.0))
これをよりコンパクトに書くとこうなります
Color.hsl({ println("a"); 0.degrees },
{ println("b"); 1.normalized },
{ println("c"); 1.normalized })
// a
// b
// c
// res17: doodle.core.Color = HSLA(Angle(0.0),Normalized(1.0),Normalized(1.0),Normalized(1.0))
ラストオーダー
Scala はどのような順序で式の評価を行っているでしょうか? 満足がいくまで必要な実験を行って答を探してみましょう。 Scala は、全ての式において一貫性のあるルールを適用していると仮定することができます。 異なる式に対する特別な場合はありません。
式は上から下へ評価され、メソッドのパラメータは左から右へと評価されることは既に見ました。 一般的な式が左から右へと評価されることをチェックしてみましょう。 これは以下のように比較的簡単に証明できます。
{ println("a"); 1 } + { println("b"); 2 } + { println("c"); 3}
// a
// b
// c
// res18: Int = 6
結果として、Scala の式は、上から下へ、左から右へと評価されていることが分かりました。
5.3 局所推論
副作用があるときには評価順序が大切であることを見てきました。 例えば以下のような副作用のある式があるとき、
disableWarheads() // 弾頭を無効化
launchTheMissles() // ミサイル発射
式が確かに上から下へと評価されて、ミサイルを発射する前に弾頭が無効化されていることを保証したいと思います。
作用はプログラムが世界に対して変化をもたらすことなので、全ての役に立つプログラムは何らかの作用を持ちます。 その作用はプログラムが終了した後に何らかの表示を行うことだけかもしれませんが、作用であることには違いありません。 副作用を最小限にするのは関数型プログラミングにおける重要なゴールの 1つなので、もう少しこの事に関してみてみましょう。
置き換えは非常に分かりやすいものです。 評価の順序が関係無ければ、今見ているコードの意味を他のコードが勝手に変えることが無いことを意味します。 1 + 1
は、他にどんなコードがプログラムに含まれていようとも 2
ですが、launchTheMissles()
の作用は弾頭を既に無効化したかしないかに依存します。
結果として、純粋なコードは単独でも理解できることを意味します。 他のコードが意味を変えることが無いので、コードの一部だけを取り出して残りは無視することができます。 一方、非純粋なコードの意味はそれまで評価されて全てのコードに依存します。 この特性は局所推論 (local reasoning) と呼ばれます。 純粋なコードはこの特性を持ち、非純粋なコードはそれを持ちません。
プログラムが大きくなるにつれて全ての詳細を頭の中に入れておくのがどんどん辛くなっていきます。 私たちの頭の大きさは固定されている量なので、唯一の解法は抽象化を導入することです。 抽象化が無関係な詳細を取り除くということを覚えているでしょうか。 純粋なコードは残りのコードの全てが無関係な詳細であると言っているので究極の抽象化であると言えるでしょう。 この大きなプログラムを分かりやすくさせるという能力は、関数型プログラマーをワクワクさせる特性の 1つです。 関数型プログラムは作用を避けるという意味ではありません。全ての有用なプログラムは作用を持ちます。 関数型プログラムが目指すのは、作用をうまく制御することでコードの大部分をシンプルな置き換えモデルを使って推論できるようにすることです。
5.3.1 意味の意味
ここまでコードの意味について考察するとき、「意味」をコードが評価される結果もしくはそれが実行する副作用という意味で使ってきました。
置き換えでは、プログラムの意味はそれが評価されたものだと全く同じです。 そのため、同じ結果に評価される 2つのプログラムは等価です。 これは、副作用が置き換えを壊す理由です。置き換えモデルは副作用という考えを持たないので、作用に違いのある 2つのプログラムを見分けることができません。
プログラムは評価される結果以外でも異なることがあります。 例えば、同じ結果にたどり着くのに 1つのプログラムは別のものよりも長く時間がかかるかもしれません。 置き換えモデルはこれも区別しません。
置き換えは抽象化であり、値以外の全てのものを捨ててしまいます。 副作用、時間、メモリ使用量などは置き換えにとっては無関係なものですが、プログラムを書いたり実行したりする人にとってはそうではないかもしれません。 ここにトレードオフがあります。 より豊かなモデルを使ってこれらの詳細を捕捉することもできますが、取り扱いは難しくなります。 多くの人にとって多くの場合は、置き換えが非常にシンプルかつ役に立つものなので正しいトレードオフとなります。
6 メソッド
私たちは既にメソッドを使ってきました。メソッドを通じて私たちはオブジェクトと関わりを持つことができます。 この章では、独自のメソッドを書く方法を解説します。
名前は式を抽象化する方法を与えてくれます。 メソッドは式を抽象化して、一般化する方法を与えてくれます。 ここで一般化とは、関連する複数のもの (ここでは式) のグループを表現できる能力を指します。 メソッドは式のテンプレートを捉えて、その呼び手がメソッドのパラメータを渡すことでテンプレートの一部を補うことができます。
例題を Doodle の sbt console 内で実行した場合は、何もしなくても動作するはずです。そうじゃない場合は、以下の import 文を使って Doodle を使用可能な状態にする必要があります。
import doodle.core._
import doodle.core.Image._
import doodle.syntax._
import doodle.jvm.Java2DFrame._
import doodle.backend.StandardInterpreter._
6.1 メソッド
前に出てきた章の 1つの中で fig. 20 で示すイメージを以下のようなプログラムを使って作りました。
val box =
Image.rectangle(40, 40).
lineWidth(5.0).
lineColor(Color.royalBlue.spin(30.degrees)).
fillColor(Color.royalBlue)
box beside box beside box beside box beside box
ここで、箱の色を変えたいと思ったとします。 現状だと別の色のためにわざわざ式を書き直す必要があります。
val paleGoldenrod = {
val box =
Image.rectangle(40, 40).
lineWidth(5.0).
lineColor(Color.paleGoldenrod.spin(30.degrees)).
fillColor(Color.paleGoldenrod)
box beside box beside box beside box beside box
}
val lightSteelBlue = {
val box =
Image.rectangle(40, 40).
lineWidth(5.0).
lineColor(Color.lightSteelBlue.spin(30.degrees)).
fillColor(Color.lightSteelBlue)
box beside box beside box beside box beside box
}
val mistyRose = {
val box =
Image.rectangle(40, 40).
lineWidth(5.0).
lineColor(Color.mistyRose.spin(30.degrees)).
fillColor(Color.mistyRose)
box beside box beside box beside box beside box
}
これはつかれます。 それぞれの式は少ししか違いがありません。 大まかなパターンをとらえて、色違いだけを表すことができれば嬉しいです。 メソッドを宣言することでまさにそれを実現することができます。
def boxes(color: Color): Image = {
val box =
Image.rectangle(40, 40).
lineWidth(5.0).
lineColor(color.spin(30.degrees)).
fillColor(color)
box beside box beside box beside box beside box
}
// 色違いの箱を作る
boxes(Color.paleGoldenrod)
boxes(Color.lightSteelBlue)
boxes(Color.mistyRose)
自分で試してみて、全部書き下した場合とメソッドを使った場合で同じ結果を得られるかみてみましょう。
メソッドの宣言の例を 1つみたので、メソッドの構文の解説をする必要があります。続いて、メソッドの書き方、メソッド呼び出しのセマンティクス、どう置き換えするのかを見ていきましょう。
6.2 メソッド構文
私たちは既にメソッド宣言の 1例を見ています。
def boxes(color: Color): Image = {
val box =
Image.rectangle(40, 40).
lineWidth(5.0).
lineColor(color.spin(30.degrees)).
fillColor(color)
box beside box beside box beside box beside box
}
これをモデルとしてメソッド宣言の構文を理解していきましょう。 最初の部分はキーワード def
です。 キーワードは Scala コンパイラにとって特別な意味を持つ単語で、この場合メソッドを宣言するという意味を持ちます。 これまでに object
と val
というキーワードも見てきました。
def
の直後にはメソッドの名前が続き、この場合 boxes
で、これは val
や object
がそれぞれ宣言するものの名前が直後に続くのに似ています。 val
宣言同様に、メソッド宣言はトップレベル宣言ではないので、ファイルに書く場合は object
宣言 (もしくはその他のトップレベル宣言) にラッピングされている必要があります。
次に、括弧 (()
) で定義されるメソッドパラメータが続きます。 このメソッドパラメータは、呼び出す人がメソッドが評価する式に差し込むことができる部分です。 メソッドパラメータを宣言するとき、名前と型を与える必要があります。 コロン (:
) を使って名前と型を分けます。 ここまでは型を宣言する必要はありませんでした。 ほとんどの場合、Scala は型推論という仕組みを使って型を自動的に計算してくれます。 しかし型推論はメソッドパラメータの型は推論できないため、私たちが与える必要があります。
メソッドパラメータの次に戻り値の型が来ます。 戻り型はメソッド呼ばれたときに評価される値の型です。 パラメータ型と違って Scala は戻り型を推論することができますが、自分で書くのが良い作法なので Creative Scala では戻り型を書くことにします。
最後に、メソッドが呼ばれたときに結果として返す値を計算するための式の本文が来ます。 本文は、boxes
のようにブロック式のときもあれば、単一の式のときもあります。
メソッド宣言の構文
メソッド宣言の構文は
def methodName(param1: Param1Type, ...): ResultType =
bodyExpression
で
methodName
がメソッド名、- 省略可能な
param1 : Param1Type, ...
は 1つもしくはそれ以上のパラメータ名とパラメータ型の対、 - 省略可能な
ResultType
はメソッドを呼んだ結果得られる値の型で、 bodyExpression
はメソッドを呼んだ結果を計算するために評価される式。
練習問題
簡単な例題でメソッドの宣言を練習してみましょう。
2乗
Int
の引数を受け取り、その引数の 2乗の Int
を返す squire
というメソッドを書いてみよう。(数の 2乗は自身を掛けることで得られるよ)
解答は
def square(x: Int): Int =
x * x
手順を追って解にたどり着くことができます。
名前 (square
) パラメータの型、そして戻り型 (Int
) が与えられています。 ここから、以下のようなメソッドの骨組みを書くことができます。
def square(x: Int): Int =
???
パラメータの名前として x
を選びました。 これは、適当な選択です。 特に意味のある名前が見つからないときは 1文字の x
、v
、i
といった名前がよく出てきます。
ちなみに、これは既に妥当なコードです。 コンソールに入力してみてください。 このように宣言した場合、square
を呼ぶとどんな結果となるでしょう?
次に、本文を完成させる必要があります。 2乗は数を自身で掛け算することだと言われているので、???
を x * x
で置き換えます。 これは単一の式なので中括弧で囲む必要はありません。
ハーフ
Double
の引数を受け取って、その引数を半分にした Double
を返す halve
というメソッドを書いてみよう。
def halve(x: Double): Double =
x / 2.0
square
で見たのと同じ手順でこの解が得られるはず。
6.3 メソッドのセマンティクス
メソッドの宣言の仕方が分かったので、セマンティクスを見ていきましょう。 置き換えモデルを使った場合、メソッド呼び出しはどう理解すればいいでしょう?
メソッド呼び出しはそれが評価する値へと置き換えることができると分かっています。 しかし、この値を導き出すためにはもう少しきめ細かいモデルを必要とします。 モデルを以下のように拡張します: メソッド呼び出しを見ると、新しいブロックを作り、そのブロック内ではパラメータをそれぞれに対応するメソッド呼び出し引数へと束縛してメソッド本体を置き換えます。
これで普通どおり置き換えを適用できるようになりました。
簡単な例を見てみましょう。以下のメソッドがあるとき
def square(x: Int): Int =
x * x
このメソッド呼び出しは
square(2)
ブロックを導入して
{
square(2)
}
パラメータ x
を 2
に束縛して、
{
val x = 2
square(2)
}
メソッド本文を置き換えることで展開することができます。
{
val x = 2
x * x
}
これで通常の置き換えを行い、以下を得ることができます。
{
2 * 2
}
そしてこうなります。
{
4
}
前にも見ましたが、置き換えは複雑ですが、各ステップの一つ一つは特に難しいものではないことが分かります。
練習問題
前回置き換えを見たときは評価の順序に多くの時間をさきました。 上の説明でメソッドの引数が本文よりも先に評価されることを決めました。 他にも可能な選択肢があります。 例えば、メソッドの引数が必要になった時点で評価することも可能です。 もしメソッドがパラメータの 1つを使わなかった場合は無駄を省くことができるかもしれません。 古い友だちでる println
を使って、Scala においてメソッドのパラメータがいつ評価されているかを調査してみましょう。
以下のプログラムはパラメータがメソッドの本文よりも先に評価されることを証明します。
def example(a: Int, b: Int): Int = {
println("In the method body!")
a + b
}
// example: (a: Int, b: Int)Int
example({ println("a"); 1 }, { println("b"); 2 })
// a
// b
// In the method body!
// res6: Int = 3
前述のもう一つの代替方法を採用しているプログラミング言語もあって、その代表として Haskell があり、これは遅延 (lazy) もしくは非正格 (non-strict) 評価と呼ばれます。
6.4 まとめ
この章では、簡単な独自メソッドの書き方と、メソッド呼び出しを置き換えモデルを使って理解する方法を学びました。
メソッドは名前同様に式を抽象化するもので、かつ関連するグループの式を一つの名前を使って一般化するものであることをみました。
いくつかの面白いメソッドを書きましたが、それでもコードの重複が残っています。次の章では、構造的再帰を使って自然数を一般化する方法をみていきます。
7 構造的再帰
この章では、構造的計算における最初のメジャーなパターンである自然数の構造的再帰をみていきます。ちょっと大げさな感じなので分解してみましょう。
- パターンとは、多くの異なる場面で役立つコードの書き方を意味します。この本の色々な場面で構造的再帰が出てきます。
- 自然数とは 0、1、2、それ以上の整数を指します。
- 再帰とは、何かが自分自身を参照することを意味します。構造的再帰は、処理しているデータの構造に沿った再帰という意味です。もしデータが再帰的 (自身を参照している) ならば構造的再帰も自分自身を参照します。これが何を意味をするのかはこれからより詳しくみていきます。
例題を Doodle の sbt console 内で実行した場合は、何もしなくても動作するはずです。そうじゃない場合は、以下の import 文を使って Doodle を使用可能な状態にする必要があります。
import doodle.core._
import doodle.core.Image._
import doodle.syntax._
import doodle.jvm.Java2DFrame._
import doodle.backend.StandardInterpreter._
7.1 箱の線
まずは、fig. 21 のように箱を一列並べて描いた例から始めましょう。
まずは手始めに 1つの箱を定義しましょう。
val aBox = Image.rectangle(20, 20).fillColor(Color.royalBlue)
// aBox: doodle.core.Image = ContextTransform(doodle.core.Image$$Lambda$12080/520014866@ea65276,Rectangle(20.0,20.0))
1つの箱を一列に並べた場合はこうなります。
val oneBox = aBox
// oneBox: doodle.core.Image = ContextTransform(doodle.core.Image$$Lambda$12080/520014866@ea65276,Rectangle(20.0,20.0))
2つの箱を隣同士に並べた場合も簡単です。
val twoBoxes = aBox beside oneBox
// twoBoxes: doodle.core.Image = Beside(ContextTransform(doodle.core.Image$$Lambda$12080/520014866@ea65276,Rectangle(20.0,20.0)),ContextTransform(doodle.core.Image$$Lambda$12080/520014866@ea65276,Rectangle(20.0,20.0)))
3つの場合も似ています。
val threeBoxes = aBox beside twoBoxes
// threeBoxes: doodle.core.Image = Beside(ContextTransform(doodle.core.Image$$Lambda$12080/520014866@ea65276,Rectangle(20.0,20.0)),Beside(ContextTransform(doodle.core.Image$$Lambda$12080/520014866@ea65276,Rectangle(20.0,20.0)),ContextTransform(doodle.core.Image$$Lambda$12080/520014866@ea65276,Rectangle(20.0,20.0))))
以下、いくつの箱でも作ろうと思えば作ることができます。
これらのイメージを作るのに奇妙な方法だと思ったかもしれません。 例えば、何故以下のように書かないのかと思わなかったでしょうか?
val threeBoxes = aBox beside aBox beside aBox
// threeBoxes: doodle.core.Image = Beside(Beside(ContextTransform(doodle.core.Image$$Lambda$12080/520014866@ea65276,Rectangle(20.0,20.0)),ContextTransform(doodle.core.Image$$Lambda$12080/520014866@ea65276,Rectangle(20.0,20.0))),ContextTransform(doodle.core.Image$$Lambda$12080/520014866@ea65276,Rectangle(20.0,20.0)))
これらの 2つの定義は等価です。 ここでは先にあるイメージを元に後続のイメージを作ることで扱っている構造を強調して、これが構造的再帰への前段階となっています。
このような方法でイメージを書くのは疲れる作業です。 私たちがやりたいのはコンピューターに何らかの方法で描きたい箱の数を伝えることです。 よりテクニカルに言うと、上の式を抽象化したいと言うことができます。 前の章で、メソッドは式を抽象化すると習ったので、この問題を解くのにメソッドを使ってみましょう。
まずはいつも通り、定義したいメソッドの骨組みである、メソッドに入っていくものとメソッドが評価するものから始めましょう。 この場合、私たちは欲しい箱の数として Int
の count
を提供して、Image
を得ます。
def boxes(count: Int): Image =
???
// boxes: (count: Int)doodle.core.Image
次に新しいこととして、構造的再帰が来ます。 上で threeBoxes
が twoBoxes
を使って、twoBoxes
が box
を使って定義できることに気づきました。 box
も、以下のように箱が無い状態を元に定義することができます:
val oneBox = aBox beside Image.empty
// oneBox: doodle.core.Image = Beside(ContextTransform(doodle.core.Image$$Lambda$12080/520014866@ea65276,Rectangle(20.0,20.0)),Empty)
ここでは、Image.empty
を使って箱が無い状態を表します。
boxes
メソッドを既に実装したと想像してください。 もしも正しく実装されたならば、boxes
は以下の性質を常に保つという事ができるでしょう:
boxes(0) == Image.empty
boxes(1) == aBox beside boxes(0)
boxes(2) == aBox beside boxes(1)
boxes(3) == aBox beside boxes(2)
最後の 3つの性質は全て同じ一般的な形をしています。 boxes(n) == aBox beside boxes(n - 1)
という 1つの性質を使って、それら全ておよび n > 0
全ての場合を記述することができます。
これで、2つの性質だけが残りました。
boxes(0) == Image.empty
boxes(n) == aBox beside boxes(n-1)
これらの 2つの性質は boxes
の振る舞いを完全に定義します。 実際、これらの性質をコードに変換するだけで boxes
を実装することができます。
boxes
の完全な実装は
def boxes(count: Int): Image =
count match {
case 0 => Image.empty
case n => aBox beside boxes(n-1)
}
// boxes: (count: Int)doodle.core.Image
試してみて、どのような結果が得られるか確かめてください! この実装は上に書いた性質よりほんの少しだけ冗長ですが、これが私たち最初の自然数の構造的再帰です。
ここで 2つの疑問に答える必要があります。 まず、この match
式はどのように動くのでしょう? このようなメソッドを自分で作れるようになるための原理はあるのでしょうか? 一つづつ疑問に答えます。
練習問題: 積み上げられた箱
match
式の詳細に入る前でも boxes
を改造して fig. 22 のようなイメージを作ることができるはずです。
ここでは match
の構文に慣れるために、boxes
をコピー・ペーストするのでは無く、練習のために手で書き出してみましょう。
boxes
の beside
を above
に変えるだけです。
def stackedBoxes(count: Int): Image =
count match {
case 0 => Image.empty
case n => aBox beside stackedBoxes(n-1)
}
// stackedBoxes: (count: Int)doodle.core.Image
7.2 match 式
前節では match
式をみました。
count match {
case 0 => Image.empty
case n => aBox beside boxes(n-1)
}
この新しい種類の式をどう理解して、自分でも書けるようになるでしょうか? 分解してみましょう。
まず最初に言っておくべきなのは、match
が式であることで、そのためこれは値へと評価されます。 そうじゃなければ boxes
メソッドはうまくいきません。
それが何に評価するのかを理解するには、もう少し詳細が必要です。 match
式は一般的に、以下のような形を持ちます:
<anExpression> match {
case <pattern1> => <expression1>
case <pattern2> => <expression2>
case <pattern3> => <expression3>
...
}
<anExpression>
(上記の具体例だと count
) は、私たちがマッチをする値を評価するのに使われる式です。 パターン <pattern1>
などのパターンはこの値に対してマッチングされます。 これまで 2種類のパターンをみました。
- リテラル (例えば
case 0
) は、リテラル式が評価される値と厳密にマッチし、 - ワイルドカード (例えば
case n
) は、何にでもマッチして右辺の式で使えるバインディングを導入します。
最後に、右辺の式 <expression1>
などは、今まで書いてきたのと同じただの式です。 match
式全体は最初にマッチしたパターンの右辺式の値へと評価されます。 そのため、boxes(0)
を呼ぶと両方のパターンとマッチしますが (ワイルドカードは何にでもマッチするため)、リテラルパターンが最初に来るため評価されるのは Image.empty
の方です。
全ての可能な場合をチェックする match
式は、網羅的マッチ (exhaustive match) と呼ばれます。 count
が 0以上であると前提を置けば、boxes
の match
は網羅的です。
まずは match
式に慣れてください。続いて自然数に対する構造的再帰の説明をする前に自然数の構造を見ていきます。
練習問題
結果を予想する
以下の式の評価値を予想して理由を考えることで match を理解しているか確かめてみましょう。
"abcd" match {
case "bcde" => 0
case "cdef" => 1
case "abcd" => 2
}
1 match {
case 0 => "zero"
case 1 => "one"
case 1 => "two"
}
1 match {
case n => n + 1
case 1 => 1000
}
1 match {
case a => a
case b => b + 1
case c => c * 2
}
第1の例は 2
に評価されます。それは、候補の中でパターン "abcd"
が唯一リテラル式 "abcd"
にマッチするものだからです。
第2の例は "one"
に評価されます。最初にマッチした case が評価に使われるからです。
第3の例は 2
に評価されます。case n
は何にでもマッチするワイルドカードパターンを定義するからです。
最後の例は 1
に評価されます。最初にマッチした case が評価に使われるからです。
マッチ無し
match
式のどのパターンにもマッチしなかった場合はどうなるのでしょうか? まずは結果を予想してみて、次にマッチに失敗する match
式を書いてみて正しく予想できたか実験してみましょう。 (現時点では特定の振る舞いをする論理的な理由は見当たらないので、常識の範囲内でどんな予想でもいいです)
私は 3つの常識的な可能性を思いつきましたが、あなたは他のアイディアがあるかもしれません。
- 式は、
Image.empty
のような何らかのデフォルト値へと評価することができるかもしれません。(どうやって Scala は正しいデフォルト値を選べばいいでしょうか?) - Scala コンパイラはそのようなコードを禁止するべきです。
match
式は実行時に失敗します。
マッチしない match
式の例です。
2 match {
case 0 => "zero"
case 1 => "one"
}
// scala.MatchError: 2 (of class java.lang.Integer)
// ... 43 elided
正しい答は最後の 2つのどちらかで、コンパイルに失敗するか実行時に失敗します。 この例では、実行時の失敗となります。 正確な答はどう Scala が設定されているかによります (私たちは Scala に網羅的では無い match
式を拒否するように指示することができますが、それはデフォルトのふるまいではありません)。
7.3 自然数
自然数は 0以上の整数です。つまり、0, 1, 2, 3… の数です。(自然数を 0 ではなく 1 から始めて定義する人もいますが、私たちの目的としてはどちらの定義を使っても大差は無いのでここでは 0 から始まるものと前提を置きます)
自然数の面白い特性として、再帰的に定義できることが挙げられます。つまり、自然数はそれら自身を使って定義することが可能です。このような巡回した定義は無意味な結果になると一見思うかもしれません。これは基底ケース (base case) を定義に含んで再帰を停止させることで回避します。具体的な定義は:
自然数 n
は
- 0 もしくは
- 1 +
m
、ただしm
は自然数である。
0
の場合が基底ケースで、その他の場合が自然数 n
を別の自然数 m
で定義するので再帰的になっています。m
は常に n
よりも小さく、基底ケースが自然数の最小値なので、この定義は全ての自然数を定義します。
任意の自然数があるとき (例えば 3) 上記の定義を使って分解していくことができます:
3 = 1 + 2 = 1 + (1 + 1) = 1 + (1 + (1 + 0))
再帰ルールを使って等式を可能な限り展開していきました。最後に基底ケースを使って再帰を停止します。
7.4 構造的再帰
構造的再帰に進みます。自然数の構造的再帰パターンは 2つのものを与えてくれます:
- 自然数を処理するための再利用可能なコードの骨組み、そして
- この骨組みを使って全ての自然数の処理を実装することができる保証
boxes
を以下のように書いたのを思い出してください。
def boxes(count: Int): Image =
count match {
case 0 => Image.empty
case n => aBox beside boxes(n-1)
}
// boxes: (count: Int)doodle.core.Image
boxes
を作ったときはこのパターンが何の前触れも無く出てきました。 今見るとこのパターンは自然数の定義に直接沿っていることが分かります。 自然数の再帰的定義を思い出してください: 自然数 n
は
- 0 もしくは
- 1 +
m
、ただしm
は自然数である。
match
式のパターンはこの定義にマッチします。
count match {
case 0 => ???
case n => ???
}
という式は count
を、count
が 0 の場合と、それ以外の自然数 n
の場合 (その場合 1 + m
) の 2つのケースにおいてチェックしていることを意味します。
match
式の右辺はそれぞれのケースにおいて何をするかを指示します。0
の場合は Image.empty
です。n
の場合は、aBox beside boxes(n-1)
です。
ここが重要な点です。 右辺の構造が、マッチする自然数の構造と同様になっていることに気づいたでしょうか。 基底ケースの 0
にマッチする場合は、私たちの結果も基底ケースの Image.empty
です。再帰的ケースの n
にマッチする場合は、右辺の構造も自然数の定義の再帰的ケースの構造に対応します。 定義は n
は 1 + m
だと言っています。 右辺では私たちは 1 を aBox
に置き換えて、+ を beside
に置き換えて、定義も再帰する所では私たちは再帰的に boxes
を m
(n-1
となります) で呼び出します。
def boxes(count: Int): Image =
count match {
case 0 => Image.empty
case n => aBox beside boxes(n-1)
}
// boxes: (count: Int)doodle.core.Image
繰り返しますが、match
式の左辺は自然数の定義と完全に一致します。右辺も定義と一致しますが、自然数の代わりにイメージと置き換えられています。ゼロに相当するイメージは Image.empty
です。1 + m
に相当するイメージは aBox beside boxes(m)
です。
この汎用パターンは自然数を何か別の型へと変換したい全ての場合において適用することができます。 私たちは常に match
式を持ちます。 私たちは常に基底ケースと再帰ケースに対応する 2つのパターンを持ちます。 右辺も常に 1
、+
そして n-1
を特定の結果に合わせた基底ケースと再帰ケースを持ちます。
自然数の構造的再帰パターン
自然数の構造的再帰の大まかなパターンは
def name(count: Int): Result =
count match {
case 0 => resultBase
case n => resultUnit add name(n-1)
}
で、Result
、resultBase
、resultUnit
そして add
は解いている問題に特定のものです。 自然数の構造的再帰パターンを実装するためには
- 私たちが書いているメソッドが自然数を入力として受け取ることに気づき
- 結果の型を考えて
- 結果のための基底、単位 (unit)、そして加算をどうするべきか決める必要があります。
このシンプルですが強力なツールを使って他に何ができるか探検する準備が整いました。
7.4 証明とプログラム
数学を勉強したことがあれば、帰納法を用いた証明を見たことがあるでしょう。 帰納法による証明の大まかなパターンは自然数の構造的再帰の大まかなパターンとよく似ています。 これは偶然ではなく、この 2つには深い関係があります。 私たちは自然数の構造的再帰を帰納法による証明の 1つだとみなすことができます。 構造的再帰の骨組みを使うことで全ての自然数の変換を書くことができるという主張は、暗黙的に私たちが使っている数学的基礎に裏付けられています。 この 2つのつながりを使って私たちのコードの性質を証明することもできます。構造的回帰は暗黙的に何らかの性質の証明を定義します。
この証明とプログラムのつながりはカリー=ハワード同型対応 (Curry-Howard Isomorphism) と呼ばれます。
練習問題
クロス
最初の練習問題はクロスのイメージを生成する cross
という関数を作ることです。 fig. 23 は 4つのクロスのイメージを表し、それぞれ cross
を 0
から 3
と共に呼び出した場合に対応します。
メソッドの骨組みは
def cross(count: Int): Image =
???
// cross: (count: Int)doodle.core.Image
cross
の本文にはどのようなパターンを使うことができるでしょう? パターンを書き出してみよう。
自然数の構造的再帰ですね。以下のようになるはずです。
def cross(count: Int): Image =
count match {
case 0 => <resultBase>
case n => <resultUnit> <add> cross(n-1)
}
どのパターンを使うかが分かったので、プログラムの特定の部分を埋めていく必要があります:
- 基底ケース、そして
- 単位および加法演算。
ヒント: fig. 23 を使って上の要素を探すことができます。
絵から、基底ケースが 1つの円であることが分かります。
絵の後続の要素は絵の上、下、右、左に円を追加しています。そのため、私たちの単位はベースと同じく 1つの円ですが、加法演算子は今までに見たようなただの beside
や above
では無く、unit beside (unit above cross(n-1) above unit) beside unit
となります。
cross
の実装を完成させなさい。
解答例です。
def cross(count: Int): Image = {
val unit = Image.circle(20)
count match {
case 0 => unit
case n => unit beside (unit above cross(n-1) above unit) beside unit
}
}
チェス盤
クロスの練習問題では私たちが作ろうとしているものの再帰的構造を見つけるのが難しいところだと分かりました。それが見えれば、構造的再帰パターンを埋めていくのは単純作業です。
この練習問題と次で、再帰構造を見つける練習をしましょう。 この練習問題のミッションはチェス盤の再帰構造を見つけて、チェス盤を描くメソッドを実装することです。 メソッドの骨組みは
def chessboard(count: Int): Image =
???
count
に 0
から 2
を渡してチェス盤を描いた場合の例を fig. 24 に示しました。 ヒント: count
がチェス盤の幅ではなく、原子的な「チェス盤の単位」を返すことに注目してください。
chessboard
を実装してみよう。
chessboard
は自然数の構造的再帰なので、このパターンの骨組みはすぐに書くことができます。
def chessboard(count: Int): Image =
count match {
case 0 => resultBase
case n => resultUnit add chessboard(n-1)
}
前にもやったように、結果に対応する基底、単位、加法演算を決める必要があります。 fig. 24 でチェス盤の連続を示すことで私たちはヒントをあげました。 そこから、基底が 2x2 のチェス盤であることが分かります。
val blackSquare = Image.rectangle(30, 30) fillColor Color.black
val redSquare = Image.rectangle(30, 30) fillColor Color.red
val base =
(redSquare beside blackSquare) above (blackSquare beside redSquare)
次に、単位と加法演算を求めます。 単位は再帰呼出し chessboard(n-1)
によって得られる値です。 加法演算は (unit beside unit) above (unit beside unit)
です。
これらを組み合わせるとこうなります。
def chessboard(count: Int): Image = {
val blackSquare = Image.rectangle(30, 30) fillColor Color.black
val redSquare = Image.rectangle(30, 30) fillColor Color.red
val base =
(redSquare beside blackSquare) above (blackSquare beside redSquare)
count match {
case 0 => base
case n =>
val unit = chessboard(n-1)
(unit beside unit) above (unit beside unit)
}
}
以前にプログラミング経験のある人はチェス盤を 2つの入れ子のループを使って作ることを思いついたかもしれません。 ここでは私たちは小さいチェス盤から大きいチェス盤へと合成するという別の方法を取っています。 このように問題を別の方法で分解することをしっかり理解することは関数型プログラミングが上手になるための重要なステップとなります。
シェルピンスキーの三角
fig. 25 に示したシェルピンスキーの三角は有名なフラクタルです。(fig. 25 はシェルピンクスキーの三角ですが)
一見複雑に見えますが、構造を分解して自然数の構造的再帰を使って生成することができます。 以下の骨組みを使ってメソッドを実装してみましょう。
def sierpinski(count: Int): Image =
???
// sierpinski: (count: Int)doodle.core.Image
今回はヒント無しです。 今までに見たことを使えばできるはずです。
鍵となるステップは、シェルピンスキーの三角の基底が triangle above (triangle beside triangle)
であると気づくことです。 それに気付けば、コードの構造は chessboard
と全く同じものです。 以下が私たちの実装です。
def sierpinski(count: Int): Image = {
val triangle = Image.triangle(10, 10) lineColor Color.magenta
count match {
case 0 => triangle above (triangle beside triangle)
case n =>
val unit = sierpinski(n-1)
unit above (unit beside unit)
}
}
// sierpinski: (count: Int)doodle.core.Image
7.5 再帰に関する論理的な考察
自然数の構造的再帰の達人となりました。 ここで置き換えモデルへと戻ってこれが新しいツールである再帰と使えるか見てみましょう。
置き換えは、式の値を分かっている値と置き換えることができると言っていることを思い出してください。 メソッド呼び出しの場合は、メソッドの本文をパラメータを適当に名前を変えることで置き換えることができました。
再帰の最初の例は、以下のように書かれた boxes
でした。
val aBox = Image.rectangle(20, 20).fillColor(Color.royalBlue)
def boxes(count: Int): Image =
count match {
case 0 => Image.empty
case n => aBox beside boxes(n-1)
}
boxes(3)
に対して置き換えを使うことで何が得られるか見てみましょう。
まず最初の置き換えは
boxes(3)
// Substitute body of `boxes`
3 match {
case 0 => Image.empty
case n => aBox beside boxes(n-1)
}
となります。match
式の評価と置き換え方を知っているので、以下が得られます。
3 match {
case 0 => Image.empty
case n => aBox beside boxes(n-1)
}
// Substitute right-hand side expression of `case n`
aBox beside boxes(2)
次に boxes(2)
を置き換えて以下を得られます。
aBox beside boxes(2)
// Substitute body of boxes
aBox beside {
2 match {
case 0 => Image.empty
case n => aBox beside boxes(n-1)
}
}
// Substitute right-hand side expression of `case n`
aBox beside {
aBox beside boxes(1)
}
この過程を何度か繰り返すことで以下を得られます。
aBox beside {
aBox beside {
1 match {
case 0 => Image.empty
case n => aBox beside boxes(n-1)
}
}
}
// Substitute right-hand side expression of `case n`
aBox beside {
aBox beside {
aBox beside boxes(0)
}
}
// Substitute body of boxes
aBox beside {
aBox beside {
aBox beside {
0 match {
case 0 => Image.empty
case n => aBox beside boxes(n-1)
}
}
}
}
// Substitute right-hand side expression of `case 0`
aBox beside {
aBox beside {
aBox beside {
Image.empty
}
}
}
最後の結果を単純化したもの
aBox beside aBox beside aBox beside Image.empty
はまさに私たちが期待するものと同じものです。 そのため、置き換えは再帰に関しても論理的に考察することができると言うことができます。 これは素晴らしいことです! しかし、置き換えは書き出さなければかなり複雑でついていくのが難しくなります。 再帰そのものは正しいと仮定して、各ステップでの何を導入しているかだけを考えるのが再帰について考察する実践的な方法です。
例えば、boxes
を論理的に考察する場合、
def boxes(count: Int): Image =
count match {
case 0 => Image.empty
case n => aBox beside boxes(n-1)
}
調べるだけで基底ケースが正しいことが分かります。 再帰ケースを見たときに、boxes(n-1)
は正しいと仮定します。 そして、「再帰ケースで行っていることは正しくて、再帰そのものが正しいでしょうか?」と問います。 答はイエスです。再帰の boxes(n-1)
が n-1
個の箱を一列に作ったとき、もう1つの箱を頭に並べるのは正しいことだからです。 この方法を使った論理思考は置き換えを使ったときよりも非常に簡潔でかつ構造的再帰を使っているならば正しいことが保証されています。
練習問題
以下は少し現実味に欠ける構造的再帰の例です。 これらのメソッドが、言っていることを実際に行うかを実行せずに確認しましょう。
// 自然数が与えられたとき、その数を返します。
// Examples:
// identity(0) == 0
// identity(3) == 3
def identity(n: Int): Int =
n match {
case 0 => 0
case n => 1 + identity(n-1)
}
もちろんです! 基底ケースは率直なものです。 再帰ケースを見るときは、identify(n-1)
が n-1
の identity (n-1
そのものです) を返すことを仮定します。その場合、n
の identity は 1 + identity(n-1)
です。
// 自然数が与えられたとき、その倍を返します。
// Examples:
// double(0) == 0
// double(3) == 6
def double(n: Int): Int =
n match {
case 0 => 0
case n => 2 * double(n-1)
}
これはダメです! このメソッドは 2つの異なる形で壊れています。 まず第一に、再帰ケースで掛け算を行っているため、いずれはゼロの基底ケースで掛け算する必要があり、全体の結果もゼロとなります。
これを修正するために、1
のケースを追加を試みることはできます (そして構造的再帰の骨組みがどうしてうまくいかなったのかを疑問に思うかもしれません)。
def double(n: Int): Int =
n match {
case 0 => 0
case 1 => 1
case n => 2 * double(n-1)
}
しかし、これも正しい結果を返しません! 再帰ケースで間違ったことを行ってます。掛ける代わりに足し算を行うべきです。
代数の復習をしてみましょう:
2(n-1 + 1) == 2(n-1) + 2
そのため、double(n-1)
が 2(n-1)
ならば、私たちは 2 を掛けるのでは無く、2 を足すべきです。 正しいメソッドは
def double(n: Int): Int =
n match {
case 0 => 0
case n => 2 + double(n-1)
}
です。
7.6 補助パラメータ
ここまでで自然数の構造的再帰を使ったいくつもの面白いプログラムを見てきました。 このセクションでは補助パラメータを使ってより複雑なプログラムを書くことを可能とする拡張をみていきます。 補助パラメータは再帰呼出しに他の情報を渡すための追加のパラメータのことです。
例えば、fig. 26 で示す線に沿って並ぶ次々と大きくなっていく箱のような絵を作ることを考えます。
どうやってこのイメージを作ることができるでしょう?
自然数の構造的再帰であることは分かっているので、骨組みはすぐに書くことができます。
def growingBoxes(count: Int): Image =
count match {
case 0 => base
case n => unit add growingBoxes(n-1)
}
boxes
で色々やったことを活かすことでもう少し書くことができます。
def growingBoxes(count: Int): Image =
count match {
case 0 => Image.empty
case n => Image.rectangle(???,???) beside growingBoxes(n-1)
}
ここでつまずくのは、右に行くに従って箱のサイズを大きくする方法です。
トリッキーな方法としては、再帰ケースの順序を逆にして箱のサイズを n
についての関数にすることです。コードはこうなります。
def growingBoxes(count: Int): Image =
count match {
case 0 => Image.empty
case n => growingBoxes(n-1) beside Image.rectangle(n*10, n*10)
}
// growingBoxes: (count: Int)doodle.core.Image
補助パラメーターを使った解法に読み進める前にじっくり時間をかけて何故この方法でうまくいくのかを理解してください。
補助パラメーターを使った場合は、単に growingBoxes
に現在の箱の大きさを指定するもう1つのパラメーターを追加するだけです。 再帰するときにこのサイズを変更します。 コードはこうなります。
def growingBoxes(count: Int, size: Int): Image =
count match {
case 0 => Image.empty
case n => Image.rectangle(size, size) beside growingBoxes(n-1, size + 10)
}
// growingBoxes: (count: Int, size: Int)doodle.core.Image
補助パラメータ法は 2つの利点があります。まず 1つの再帰から次で何が変わるのかだけを考えればいい (この場合、箱が大きくなる) のと、呼び出し側でこのパラメータを変更することができることです (例えば、最初の箱を大きくしたり小さくしたり)。
補助パラメータ法をみた所で、練習してみましょう。
箱のグラデーション
この練習問題では、fig. 27 のような絵を描きます。 箱の線の描き方は分かっています。 ここでの課題は各ステップで色を変えることです。
ヒント: 各再帰で塗る色を spin
することができます。
解答を実装する 2通りの方法があります。 補助パラメーター法は gradientBoxes
にパラメーターを追加して Color
を構造的再帰に渡してまわる方法です。
def gradientBoxes(n: Int, color: Color): Image =
n match {
case 0 => Image.empty
case n => aBox.fillColor(color) beside gradientBoxes(n-1, color.spin(15.degrees))
}
// gradientBoxes: (n: Int, color: doodle.core.Color)doodle.core.Image
growingBoxes
の例で行ったように塗るための色を n
に関する関数にすることもできます。
def gradientBoxes(n: Int): Image =
n match {
case 0 => Image.empty
case n => aBox.fillColor(Color.royalBlue.spin((15*n).degrees)) beside gradientBoxes(n-1)
}
// gradientBoxes: (n: Int)doodle.core.Image
同心円
バリエーションとして、fig. 28 のような同心円を描いてみましょう。ここでは、色ではなく各ステップでサイズを変えています。その他はパターンとしては同じようになるはずです。実装してみてください。
これはほとんど growingBoxes
と同じものです。
def concentricCircles(count: Int, size: Int): Image =
count match {
case 0 => Image.empty
case n => Image.circle(size) on concentricCircles(n-1, size + 5)
}
// concentricCircles: (count: Int, size: Int)doodle.core.Image
もう一度、気持ちを込めて
今度は両方のテクニックを組み合わせて各ステップでサイズと色を変更して、fig. 29 で得られるような絵を描いてみましょう。 自分の好みになるまで色々実験してみてください。
これが私たちの解法で、コードの繰り返しを減らすために問題を再利用可能なパーツへと分けてみました。 これでもまだ多くの繰り返しがありますが、それらを減らすためのツールは後ほど見ていきます。
def circle(size: Int, color: Color): Image =
Image.circle(size).lineWidth(3.0).lineColor(color)
// circle: (size: Int, color: doodle.core.Color)doodle.core.Image
def fadeCircles(n: Int, size: Int, color: Color): Image =
n match {
case 0 => Image.empty
case n => circle(size, color) on fadeCircles(n-1, size+7, color.fadeOutBy(0.05.normalized))
}
// fadeCircles: (n: Int, size: Int, color: doodle.core.Color)doodle.core.Image
def gradientCircles(n: Int, size: Int, color: Color): Image =
n match {
case 0 => Image.empty
case n => circle(size, color) on gradientCircles(n-1, size+7, color.spin(15.degrees))
}
// gradientCircles: (n: Int, size: Int, color: doodle.core.Color)doodle.core.Image
def image: Image =
fadeCircles(20, 50, Color.red) beside gradientCircles(20, 50, Color.royalBlue)
// image: doodle.core.Image
7.7 ネストしたメソッド
メソッドは宣言です。 メソッドの本文内には別の宣言や式を含むことができます。 そのため、メソッド宣言は他のメソッド宣言を含むことができます。
なぜこれが役に立つのかを見るために、以前に書いたメソッドをもう一度見てみましょう:
def cross(count: Int): Image = {
val unit = Image.circle(20)
count match {
case 0 => unit
case n => unit beside (unit above cross(n-1) above unit) beside unit
}
}
// cross: (count: Int)doodle.core.Image
unit
は cross
メソッドの中で宣言されています。 そのため、unit
の宣言は cross
の本文内だけにスコープ付けされています。 他の宣言を間違ってシャドーイングしないように宣言のスコープを必要最小限に制限するのはお行儀が良いことです。 しかし、ここで cross
の実行時の振る舞いを考察すると、少し嬉しくない特性が見つかるはずです。
ここれは置き換えモデルを使って cross(1)
を展開します。
cross(1)
// Expands to
{
val unit = Image.circle(20)
1 match {
case 0 => unit
case n => unit beside (unit above cross(n-1) above unit) beside unit
}
}
// Expands to
{
val unit = Image.circle(20)
unit beside (unit above cross(0) above unit) beside unit
}
// Expands to
{
val unit = Image.circle(20)
unit beside (unit above
{
val unit = Image.circle(20)
0 match {
case 0 => unit
case n => unit beside (unit above cross(n-1) above unit) beside unit
}
}
above unit) beside unit
}
// Expands to
{
val unit = Image.circle(20)
unit beside (unit above
{
val unit = Image.circle(20)
unit
}
above unit) beside unit
}
このような巨大な展開を書き出した理由は再帰するたびに unit
を作り直していることを示すためです。 これは unit
が作られるたびに何かを println することでも証明できます。
def cross(count: Int): Image = {
val unit = {
println("Creating unit")
Image.circle(20)
}
count match {
case 0 => unit
case n => unit beside (unit above cross(n-1) above unit) beside unit
}
}
// cross: (count: Int)doodle.core.Image
cross(1)
// Creating unit
// Creating unit
// res0: doodle.core.Image = Beside(Beside(Circle(20.0),Above(Above(Circle(20.0),Circle(20.0)),Circle(20.0))),Circle(20.0))
unit
は非常に小さいのであまり大したことないですが、多くのメモリや時間を取る処理をしている可能性もあり、不必要に繰り返すのは嬉しくないことです。
これは、unit
を cross
の外に出すことで解決できます。
val unit = {
println("Creating unit")
Image.circle(20)
}
// Creating unit
// unit: doodle.core.Image = Circle(20.0)
def cross(count: Int): Image = {
count match {
case 0 => unit
case n => unit beside (unit above cross(n-1) above unit) beside unit
}
}
// cross: (count: Int)doodle.core.Image
cross(1)
// res1: doodle.core.Image = Beside(Beside(Circle(20.0),Above(Above(Circle(20.0),Circle(20.0)),Circle(20.0))),Circle(20.0))
これは、unit
は必要以上に大きいスコープを持つことになるので、それも嬉しくないです。 ネストされたメソッド、別名内部メソッドを使うとより良く書くことができます。
def cross(count: Int): Image = {
val unit = {
println("Creating unit")
Image.circle(20)
}
def loop(count: Int): Image = {
count match {
case 0 => unit
case n => unit beside (unit above loop(n-1) above unit) beside unit
}
}
loop(count)
}
// cross: (count: Int)doodle.core.Image
cross(1)
// Creating unit
// res2: doodle.core.Image = Beside(Beside(Circle(20.0),Above(Above(Circle(20.0),Circle(20.0)),Circle(20.0))),Circle(20.0))
これは、スコープを最小にしつつ unit
を一度だけ作るという求めている振る舞いとなります。 内部メソッド loop
は以前通り構造的再帰を行います。 cross
内で忘れずに呼ぶ必要があります。 私は、このような内部メソッドはループを行っていることを示すために通常 loop
や iter
(iterate の略) と名付けています。
このテクニックは既に見てきたことの小さなバリエーションですが、いくつかの練習問題を行ってパターンを習得したか確認してみましょう。
練習問題
チェス盤
chessboard
をネストしたメソッドを使って書き換えて、blackSquare
、redSquare
そして base
が chessboard
が呼ばれたときに一度だけ作られるようにしましょう。
def chessboard(count: Int): Image = {
val blackSquare = Image.rectangle(30, 30) fillColor Color.black
val redSquare = Image.rectangle(30, 30) fillColor Color.red
val base =
(redSquare beside blackSquare) above (blackSquare beside redSquare)
count match {
case 0 => base
case n =>
val unit = cross(n-1)
(unit beside unit) above (unit beside unit)
}
}
// chessboard: (count: Int)doodle.core.Image
これが私たちが行った方法です。boxes
で使ったのと全く同じパターンです。
def chessboard(count: Int): Image = {
val blackSquare = Image.rectangle(30, 30) fillColor Color.black
val redSquare = Image.rectangle(30, 30) fillColor Color.red
val base =
(redSquare beside blackSquare) above (blackSquare beside redSquare)
def loop(count: Int): Image =
count match {
case 0 => base
case n =>
val unit = loop(n-1)
(unit beside unit) above (unit beside unit)
}
loop(count)
}
// chessboard: (count: Int)doodle.core.Image
賢く箱に入れる
以下の boxes
を書き直して、aBox
が boxes
内のみのスコープに入り、かつ boxes
が呼ばれたときに一度だけ作られるようにしましょう。
val aBox = Image.rectangle(20, 20).fillColor(Color.royalBlue)
def boxes(count: Int): Image =
count match {
case 0 => Image.empty
case n => aBox beside boxes(n-1)
}
これは2段階に分けて解きます。まずは、aBox
を boxes
に取り込みます。
def boxes(count: Int): Image = {
val aBox = Image.rectangle(20, 20).fillColor(Color.royalBlue)
count match {
case 0 => Image.empty
case n => aBox beside boxes(n-1)
}
}
次に、内部メソッドを使って再帰のたびに aBox
が作られるのを防ぎます。
def boxes(count: Int): Image = {
val aBox = Image.rectangle(20, 20).fillColor(Color.royalBlue)
def loop(count: Int): Image =
count match {
case 0 => Image.empty
case n => aBox beside loop(n-1)
}
loop(count)
}
7.8 結論
この章では、自然数の構造的再帰という初めての構造的な大きなパターンを見ました。 イメージを生成する多くの例を見ましたが、このパターンは自然数を (別の自然数を含む) 何かへと変換する全ての場面で使うことができます。
このパターンや構造的再帰の別のバリエーションは、この本の色々な所でこれからも出てきます。
8 園芸と高階関数
この章では、花の描き方と第1級値としての関数の使い方を習います。
私たちは、プログラムが値を取り扱うことは分かっていますが、全ての値が第1級 (first-class) ではありません。第1級値はメソッドのパラメータとして渡したり、メソッド呼び出しの結果として返せるものを指します。
もしも私たちが関数を別の関数の引数として渡したら
- 渡された関数は第1級値として使われており、また
- 関数パラメーターを受け取った関数は高階関数 (higher-order function) と呼ばれます。
この用語は特に重要なものではありませんが、他の書物でも見かけると思うので (多少おぼろげでも) 知っておくと役に立つと思います。 最初は何のことか分からないかもしれませんが、例を見ていくうちに分かるようになります。
これまでは関数とメソッドという用語を特に区別せずに使ってきました。 Scala では、これら 2つの用語は関連はしますが、別々の意味を持つことを見ていきます。
背景の説明はここまでにして、
- Scala での関数の作り方
- 第1級関数を使ってプログラムを構造化する方法
を見ていきましょう。これを動機づける例として fig. 30 にような花を描く例を使います。
例題を Doodle の sbt console 内で実行した場合は、何もしなくても動作するはずです。そうじゃない場合は、以下の import 文を使って Doodle を使用可能な状態にする必要があります。
import doodle.core._
import doodle.core.Image._
import doodle.syntax._
import doodle.jvm.Java2DFrame._
import doodle.backend.StandardInterpreter._
8.1 パラメトリック曲線
今の所、円や長方形といった基本的な形しか作ることができません。 目標である花の形を作るにはより細かいコントロールを必要とします。 数学のツールであるパラメトリック方程式もしくはパラメトリック曲線と呼ばれているものを使うことにします。
パラメトリック方程式はいくつかの入力 (「パラメトリック」という通りパラメーターを用います) から空間内の場所である点を得られる関数のことです。 例えば、円のパラメトリック方程式は Angle
から点に対するものです。
def parametricCircle(angle: Angle): Point =
???
これらの点を小さなドットや他の形でプロットすることで私たちが描こうとする大きな形を作ることができます。
fig. 31 では、円のパラメトリック関数によって生成された点で小さな円を描いた場合の例です。 左から右にかけて 90度、45度、22.5度おきに点を描いています。 より多くの点を描くことで、形の輪郭である大きな円がハッキリとしてくることが分かります。
パラメトリック曲線を作るには Dooble が点をどう表すのかを習い、イメージを空間の特定の点にレイアウトして、高校以来忘れていた幾何学を復習する必要があります。
8.2 点
Doodle では、Point
型を用いて 2次元内の場所を表します。これは 2つの等価な表し方があって、
- デカルト座標と呼ばれる x と y 座標を使った方法と
- 極座標と呼ばれる角度と、その角度における原点からの距離 (半径) を使って表すことができます。
私たちは Point.cartesian
を使ってデカルト座標で点を作るか、Point.polar
を使って極座標で点を作ることができます。以下の表は Point
の主なメソッドを示します。
演算 | 型 | 説明 | 例 |
---|---|---|---|
Point.cartesian(Double, Double) |
Point |
デカルト座標を使って Point を作る。 |
Point.cartesian(1.0, 1.0) |
Point.polar(Double, Angle) Point(Double, Angle) |
Point |
極座標を使って Point を作る。 |
Point.polar(1.0, 90.degrees) |
Point.zero |
Point |
原点での Point を作る (x と y ともにゼロ) |
Point.zero |
Point.x |
Double |
Point の x 座標を得る。 |
Point.zero.x |
Point.y |
Double |
Point の y 座標を得る。 |
Point.zero.y |
Point.r |
Double |
Point の半径を得る。 |
Point.zero.r |
Point.angle |
Angle |
Point の角度を得る。 |
Point.zero.angle |
8.3 柔軟なレイアウト
Image
を特定の点に置くことはできるでしょうか? これまでの所は、イメージを on
、beside
、above
のみを使って配置してきました。 さらに at
メソッドというツールを使った、より柔軟なレイアウトが必要になります。 正方形の 4隅で円を描く例です:
val dot = Image.circle(5).lineWidth(3).lineColor(Color.crimson)
val squareDots =
dot.at(0, 0).
on(dot.at(0, 100)).
on(dot.at(100, 100)).
on(dot.at(100, 0))
これは fig. 32 で示したイメージを作ります。
at
レイアウトをどう使うのか、また何故ドットを on
で積み上げる必要があるのかを理解するためには Doodle がレイアウトをどのように行っているのかを理解する必要があります。
Doodle において全ての Image
は原点 (origin) を持ちます。 ほとんどのイメージの場合はこれはイメージの中央にありますが、そうである必要はありません。 Doodle が複合 Image
をレイアウトするときは、原点を合わせています。 例えば、複数の Image
を above
を使ってレイアウトする場合、それらの原点は垂直に並び、複合 Image
の原点も原点を結んだ線の中点となります。 fig. 33 には、beside
を使ったレイアウトにおいて原点 (赤い円) がどう並ぶのかを示した例があります。 そして、on
を使った場合は原点は積み上げられていくため、事実上複数のイメージが同じ原点を持つことになります。
at
を使うことで、Image
をその原点から移動させることができます。 ここで見ていく例では、全ての要素が同じ原点を共有してほしいので、at
でイメージを動かした後で on
を使ってイメージを組み合わせます。
at
を呼ぶしには 2通りの方法があります:
dot.at(100, 100)
といった風に x と y のオフセットを渡します。dot.at(Vec(100, 100))
のように、オフセットを含んだVec
(ベクトル) を渡します。
toVec
を使って Point
を Vec
へと変換することができます。
Point.cartesian(1.0, 1.0).toVec
// res0: doodle.core.Vec = Vec(1.0,1.0)
8.4 幾何学
もう一つの基礎となるのは、幾何学を用いた点の位置づけです。 ある点が原点から r
の距離、角度 a
の位置にあるとき、x と y 座標はそれぞれ (a.cos) * r
、(a.sin) * r
となります。 もしくは、極座標を使ってそれを表します!
val polar = Point.polar(1.0, 45.degrees)
// polar: doodle.core.Point = Polar(1.0,Angle(0.7853981633974483))
val cartesian = Point.cartesian((45.degrees.cos) * 1.0, (45.degrees.sin) * 1.0)
// cartesian: doodle.core.Point = Cartesian(0.7071067811865476,0.7071067811865475)
// これらは同じです
polar.toCartesian == cartesian
// res2: Boolean = true
cartesian.toPolar == polar
// res3: Boolean = true
8.5 合わせて一緒に
これらを組み合わせてパラメトリックな円を作ることができます。 デカルト座標を使った半径 200 のパラメトリックな円のコードは以下のようになります:
def parametricCircle(angle: Angle): Point =
Point.cartesian(angle.cos * 200, angle.sin * 200)
極座標の場合はシンプルにこんなふうになります:
def parametricCircle(angle: Angle): Point =
Point.polar(200, angle)
そして円上の点を均一にサンプリングします。イメージを作るには、それらの点上に何か (例えば三角形) を描画します。
def sample(start: Angle, samples: Int): Image = {
// Angle.one is one complete turn. I.e. 360 degrees
val step = Angle.one / samples
val dot = triangle(10, 10)
def loop(count: Int): Image = {
val angle = step * count
count match {
case 0 => Image.empty
case n =>
dot.at(parametricCircle(angle).toVec) on loop(n - 1)
}
}
loop(samples)
}
これは、多分もう慣れ親しんだ構造的再帰となっています。
これを描くと、多くの三角形が円状に並んでいるのが見えるようになります。 例えば fig. 34 は sample(0.degrees, 72)
の結果を示しています。
8.5.1 花
花を作るための次のステップは、円よりも面白い形を使うことです。これは parametricCircle
をより面白い方程式に変えることを意味します。 例えば、以下の rose
を見てみましょう。 これは、最大半径が 200 のバラの曲線です。 角度に掛ける値 (下では 7
) を変えることで別の形を得ることができます。
// Parametric equation for rose with k = 7
def rose(angle: Angle) =
Point.polar((angle * 7).cos * 200, angle)
高密度でサンプリングされた例を fig. 35 に示します。
sample
を変えて parametricCircle
の代わりに rose
を呼び出すことも可能ですが、それは少し満足がいきません。 異なるパラメトリック方程式を使って実験してみたいとしたらどうでしょうか? 点を作るメソッド (つまりパラメトリック方程式) を sample
へのパラメータとして渡せれば便利です。 これはできるでしょうか? そのためには、以下ができる必要があります:
- メソッドパラメータとしてのメソッドの型を書くことができる
- メソッドの呼び出し (例えば
rose(0.degrees)
) とメソッドそのものへの参照を区別できる
まずは 2つ目の問題から見ていきましょう。メソッドを呼び出さずに参照するとエラーとなります。
rose
// <console>:29: error: missing argument list for method rose
// Unapplied methods are only converted to functions when a function type is expected.
// You can make this conversion explicit by writing `rose _` or `rose(_)` instead of `rose`.
// rose
// ^
便利なことに、何をすれば良いのかはエラーメッセージが教えてくれていて、ここでやっと関数を紹介することができます。
8.6 関数
前の節でのエラーメッセージが言っているように、全てのメソッドは _
を使って関数に変換することができ、その関数には同じパラメータを渡すことができます。
// Parametric equation for rose with k = 7
def rose(angle: Angle) =
Point.cartesian((angle * 7).cos * angle.cos, (angle * 7).cos * angle.sin)
rose _
// res1: doodle.core.Angle => doodle.core.Point = $$Lambda$19618/2004056292@4223048
(rose _)(0.degrees)
// res2: doodle.core.Point = Cartesian(1.0,0.0)
関数はだいたいメソッドと同じものですが、関数は第1級値として使うことができます:
- 関数を他のメソッドや関数への引数として渡すことができます
- メソッドや関数から戻り値として返すことができます
val
を使って名前を付けることができます
val roseFn = rose _
// roseFn: doodle.core.Angle => doodle.core.Point = $$Lambda$19651/38035097@116d059e
roseFn(0.degrees)
// res3: doodle.core.Point = Cartesian(1.0,0.0)
8.6.1 関数型
メソッドに関数を渡すためには、(パラメータを宣言するときには型を宣言する必要があるため) それらの型を書ける必要があります。
関数の型は (A, B) => C
のように書き、このとき A
と B
はパラメータの型で、C
は結果の型です。 このパターンは引数を取らない関数から、任意の引数の関数まで一般化することができます。
上の例では、f
は 2つの Int
をパラメータとして受け取り、Int
を返す関数である必要があります。これは、(Int, Int) => Int
と書くことができます。
関数型宣言の構文
関数型を宣言するには、以下のように書きます:
(A, B, ...) => C
ここで
A, B, ...
は入力パラメータの型でC
は結果の型
関数が 1つのパラメータのみ受け取る場合は括弧は省略することができます:
A => B
8.6.2 関数リテラル
関数にはリテラル構文があります。 例えば、以下は入力値に 42
を加算する関数です。
(x: Int) => x + 42
// res4: Int => Int = $$Lambda$19655/2135072360@49df8d35
関数に引数を適用するのは通常通りに書くことができます。
val add42 = (x: Int) => x + 42
// add42: Int => Int = $$Lambda$19656/39381750@599e9559
add42(0)
// res5: Int = 42
関数リテラルの構文
関数リテラルの宣言構文は:
(parameter: type, ...) => 式
- 省略可能な
parameter
は、関数パラメータの名前 type
は関数パラメータの型式
は関数の結果を決定する
8.6.3 オブジェクトとしての関数
Scala はオブジェクト指向言語なので、全ての第1級値はオブジェクトです。 そのため関数はメソッドを持つことができ、それらを使って関数合成を行うことができます:
val addTen = (a: Int) => a + 10
// addTen: Int => Int = $$Lambda$19665/1133842630@72f20e0
val double = (a: Int) => a * 2
// double: Int => Int = $$Lambda$19666/1985361549@739c528
val combined = addTen andThen double // this composes the two functions
// combined: Int => Int = scala.Function1$$Lambda$4502/1858136768@4c51fa83
combined(5)
// res6: Int = 30
練習問題
関数型
上で定義された roseFn
の型はなんでしょうか? この型は何を意味するでしょう?
型は Angle => Point
です。これは roseFn
が、Angle
型の単一のパラメータを受け取り、Point
型の値を返すことを意味します。別の言い方をすると、roseFn
は Angle
を Point
へと変換します。
関数リテラル
roseFn
を関数リテラルとして書いてみましょう。
val roseFn = (angle: Angle) =>
Point.cartesian((angle * 7).cos * angle.cos, (angle * 7).cos * angle.sin)
// roseFn: doodle.core.Angle => doodle.core.Point = $$Lambda$19670/241182642@71b67ef4
8.7 高階メソッドと関数
関数は何の役に立つでしょうか? 私たちは既にメソッドを使って再利用可能なコードの断片をパッケージ化して名前を付けることができます。 コードを値として扱うことで他に得られる利点は何でしょう? 先ほど
- 関数を他の関数やメソッドへパラメータとして渡すこと
- 戻り値として関数を返すメソッドを定義できること
とは言いましたが、実際にこの機能は使っていません。これらを見ていきましょう。
具体例として、同心円の例題のパターンを考えます:
def concentricCircles(count: Int, size: Int): Image =
count match {
case 0 => Image.empty
case n => Image.circle(size) on concentricCircles(n-1, size + 5)
}
このパターンは、Image.circle
を別の形に差し替えることで多くの異なるイメージを作ることが可能です。 しかし、Image.circle
の置き換え提供するたびに私たちは concentricCircles
の新しい定義を必要とします。
Image.circle
の代替となるものをパラメータとして渡すことができれば、concentricCircles
を完全に一般化することができます。 円を描くだけでは無いので、concentricShapes
と名前を変えて、適当な大きさの形を描くのを singleShape
に任せることにします。
def concentricShapes(count: Int, singleShape: Int => Image): Image =
count match {
case 0 => Image.empty
case n => singleShape(n) on concentricShapes(n-1, singleShape)
}
これで、同じ concentricShapes
の定義を再利用してただの円、色々な色の四角形、異なる透明度の円などを描くことができます。 適当な定義の singleShape
を渡すだけです:
// Passing a function literal directly:
val blackCircles: Image =
concentricShapes(10, (n: Int) => Image.circle(50 + 5*n))
// Converting a method to a function:
def redCircle(n: Int): Image =
Image.circle(50 + 5*n) lineColor Color.red
val redCircles: Image =
concentricShapes(10, redCircle _)
練習問題
色と形
以下のコードから始めて、色と形の関数を書いて fig. 36 で示したイメージを作ってみましょう。
def concentricShapes(count: Int, singleShape: Int => Image): Image =
count match {
case 0 => Image.empty
case n => singleShape(n) on concentricShapes(n-1, singleShape)
}
この concentricShapes
メソッドは以前の練習問題の concentricCircles
メソッド同様のものです。 主な違いは、singleShape
の定義をパラメータとして渡すことです。
この問題について少し考えてみましょう。 私たちは、2つのことをする必要があります:
3つの目標となるイメージに対してそれぞれ適当な
singleShape
の定義を書くことconcentricShapes
にそれぞれ適当なsingleShape
を渡して3回呼び出して、その結果をbeside
を使って並べる
singleShape
パラメータの定義を注意して見てみましょう。 このパラメータの型は Int => Image
なので、これは Int
パラメータを受け取って Image
を返す関数です。 この型のメソッドは以下のように宣言できます:
def outlinedCircle(n: Int) =
Image.circle(n * 10)
このメソッドを関数へと変換して、concentricShapes
へ渡して黒い輪郭の同心円を描くことができます:
concentricShapes(10, outlinedCircle _)
これは fig. 37 で示したものを生成します。
練習問題の残りは、この関数をコピーして、名前を変えて、期待される色と形の組み合わせを得られるように改造することです:
def circleOrSquare(n: Int) =
if(n % 2 == 0) Image.rectangle(n*20, n*20) else Image.circle(n*10)
(concentricShapes(10, outlinedCircle) beside concentricShapes(10, circleOrSquare))
fig. 38 の出力を見てください。
ボーナスポイントとして、上の形の例を作れるようになったら、リファクタリングして 色のための関数と形を生成する関数という 2つのベースとなる関数を書いてみましょう。 これらの関数は以下のようにコンビネーターを使って組み合わせて、コンビネーターの結果を concentricShapes
へ引数として渡しましょう。
def colored(shape: Int => Image, color: Int => Color): Int => Image =
(n: Int) => ???
最もシンプルな解答は、singleShapes
を以下のように定義することです:
def concentricShapes(count: Int, singleShape: Int => Image): Image =
count match {
case 0 => Image.empty
case n => singleShape(n) on concentricShapes(n-1, singleShape)
}
def rainbowCircle(n: Int) = {
val color = Color.blue desaturate 0.5.normalized spin (n * 30).degrees
val shape = Image.circle(50 + n*12)
shape lineWidth 10 lineColor color
}
def fadingTriangle(n: Int) = {
val color = Color.blue fadeOut (1 - n / 20.0).normalized
val shape = Image.triangle(100 + n*24, 100 + n*24)
shape lineWidth 10 lineColor color
}
def rainbowSquare(n: Int) = {
val color = Color.blue desaturate 0.5.normalized spin (n * 30).degrees
val shape = Image.rectangle(100 + n*24, 100 + n*24)
shape lineWidth 10 lineColor color
}
val answer =
(concentricShapes(10, rainbowCircle) beside
concentricShapes(10, fadingTriangle) beside
concentricShapes(10, rainbowSquare))
しかし、重複したコードがあります。 特に、rainbowCircle
と rainbowTriangle
は同じ定義の color
を用います。 lineWidth(10)
と lineColor(color)
も繰り返し呼び出されていますが、重複を避けることができます。 ボーナス解答は、これらを単独の関数へと抽出して、colored
コンビネーターを使って組み合わせます:
def concentricShapes(count: Int, singleShape: Int => Image): Image =
count match {
case 0 => Image.empty
case n => singleShape(n) on concentricShapes(n-1, singleShape)
}
// concentricShapes: (count: Int, singleShape: Int => doodle.core.Image)doodle.core.Image
def colored(shape: Int => Image, color: Int => Color): Int => Image =
(n: Int) =>
shape(n) lineWidth 10 lineColor color(n)
// colored: (shape: Int => doodle.core.Image, color: Int => doodle.core.Color)Int => doodle.core.Image
def fading(n: Int): Color =
Color.blue fadeOut (1 - n / 20.0).normalized
// fading: (n: Int)doodle.core.Color
def spinning(n: Int): Color =
Color.blue desaturate 0.5.normalized spin (n * 30).degrees
// spinning: (n: Int)doodle.core.Color
def size(n: Int): Double =
50 + 12 * n
// size: (n: Int)Double
def circle(n: Int): Image =
Circle(size(n))
// circle: (n: Int)doodle.core.Image
def square(n: Int): Image =
Image.rectangle(2*size(n), 2*size(n))
// square: (n: Int)doodle.core.Image
def triangle(n: Int): Image =
Image.triangle(2*size(n), 2*size(n))
// triangle: (n: Int)doodle.core.Image
val answer =
(concentricShapes(10, colored(circle, spinning)) beside
concentricShapes(10, colored(triangle, fading)) beside
concentricShapes(10, colored(square, spinning)))
// answer: doodle.core.Image = Beside(Beside(On(ContextTransform(doodle.core.Image$$Lambda$8928/441125667@4bfba9b3,ContextTransform(doodle.core.Image$$Lambda$8949/545221680@704ce579,Circle(170.0))),On(ContextTransform(doodle.core.Image$$Lambda$8928/441125667@7fe2d73e,ContextTransform(doodle.core.Image$$Lambda$8949/545221680@45a06b87,Circle(158.0))),On(ContextTransform(doodle.core.Image$$Lambda$8928/441125667@5380e4b1,ContextTransform(doodle.core.Image$$Lambda$8949/545221680@4470b495,Circle(146.0))),On(ContextTransform(doodle.core.Image$$Lambda$8928/441125667@2d42c002,ContextTransform(doodle.core.Image$$Lambda$8949/545221680@6bf86a7e,Circle(134.0))),On(ContextTransform(doodle.core.Image$$Lambda$8928/441125667@3f89aedc,ContextTransform(doodle.core.Image$$Lambda$8949...
8.8 練習問題
関数の知識をいっぱい得られたので、花を描く問題へと戻ってみます。 今回は以前よりも多くのデザイン作業を行ってもらいます。
花を描くタスクを小さい関数へと分解して組み合わせるのが目標です。 個々の構成要素を関数へと分けることで、より広い創造的自由度が得られるようにしてください。
まずは、自分でこの作業を行ってみてください。詰まったら、以下に私たちが行った分解方法を参照してください。
8.8.1 構成要素
私たちは、花の描画の 2つの構成要素を同定しました:
- パラメトリック方程式
- 角度に対する構造的再帰
関数へと抽象化可能な他の構成要素は何でしょう? それらの型は何でしょうか? (これは、意図的に自由回答となっています。研究してください!)
パラメトリック曲線を描くとき、異なる曲線の半径を変えたいと思うでしょう。 これは関数へと抽象化できます。 この関数の型はどうあるべきしょうか? 最も明白な方法は (Point, Double) => Point
で、Double
パラメータが点をスケールする量とします。 しかし、これは初期値から変わらない Double
を渡しまわる必要があり、使いづらいものです。
より良い構造は、Double => (Point => Point)
の型を持つ関数を作ることです。 この関数は、スケール係数を受け取ります。 これは、スケール係数に基づいた Point
の変換関数を返します。 こうすることで、固定したスケール値を分けることができます。実装は以下のようになります
def scale(factor: Double): Point => Point =
(pt: Point) => {
Point.polar(pt.r * factor, pt.angle)
}
以前に、パラメトリック方程式を sample
から抽象化したいと言ったと思います。 これは、以下のように簡単に行うことができます。
def sample(start: Angle, samples: Int, location: Angle => Point): Image = {
// Angle.one is one complete turn. I.e. 360 degrees
val step = Angle.one / samples
val dot = triangle(10, 10)
def loop(count: Int): Image = {
val angle = step * count
count match {
case 0 => Image.empty
case n =>
dot.at(location(angle).toVec) on loop(n - 1)
}
}
loop(samples)
}
イメージで使うプリミティブ図形の選択 (dot
か Image.triangle
) を抽象化したいと思うかもしれません。 location
を Angle => Image
関数へと変えることでこれが可能となります。
def sample(start: Angle, samples: Int, location: Angle => Image): Image = {
// Angle.one is one complete turn. I.e. 360 degrees
val step = Angle.one / samples
def loop(count: Int): Image = {
val angle = step * count
count match {
case 0 => Image.empty
case n => location(angle) on loop(n - 1)
}
}
loop(samples)
}
構造的再帰のコードから問題に特定な部分を抽象化することができます。 今までは
def loop(count: Int): Image = {
val angle = step * count
count match {
case 0 => Image.empty
case n => location(angle) on loop(n - 1)
}
}
でしたが、基底ケース (Image.empty
) と問題に特定な再帰の部分 (location(angle) on loop(n - 1)
) を抽出することができます。基底ケースはただの Image
ですが、再帰の部分は (Angle, Image) => Image
型となります。最終結果は以下のようになります。
def sample(start: Angle, samples: Int, empty: Image, combine: (Angle, Image) => Image): Image = {
// Angle.one is one complete turn. I.e. 360 degrees
val step = Angle.one / samples
def loop(count: Int): Image = {
val angle = step * count
count match {
case 0 => empty
case n => combine(angle, loop(n - 1))
}
}
loop(samples)
}
これは、非常に抽象的な関数です。この抽象化に気づく人は多くないと私たちは思っていますが、この考え方に興味があれば、畳み込みやモノイドについて調べてみてください。
8.8.2 組み合わせ
構成要素の分解ができたら、次はそれらを組み合わせて面白い結果を作ることができます。やってみましょう。
これは回答の一例です。
def parametricCircle(angle: Angle): Point =
Point.cartesian(angle.cos, angle.sin)
def rose(angle: Angle): Point =
Point.cartesian((angle * 7).cos * angle.cos, (angle * 7).cos * angle.sin)
def scale(factor: Double): Point => Point =
(pt: Point) => {
Point.polar(pt.r * factor, pt.angle)
}
def sample(start: Angle, samples: Int, location: Angle => Point): Image = {
// Angle.one is one complete turn. I.e. 360 degrees
val step = Angle.one / samples
val dot = triangle(10, 10)
def loop(count: Int): Image = {
val angle = step * count
count match {
case 0 => Image.empty
case n => dot.at(location(angle).toVec) on loop(n - 1)
}
}
loop(samples)
}
def locate(scale: Point => Point, point: Angle => Point): Angle => Point =
(angle: Angle) => scale(point(angle))
// Rose on circle
val flower = {
sample(0.degrees, 200, locate(scale(200), rose _)) on
sample(0.degrees, 40, locate(scale(150), parametricCircle _))
}
8.8.3 実験
色々実験して、創造的な可能性を探ってみましょう!
fig. 30 を作った実装は Flowers.scala に置いてあります。あなたは、どのような花を描いたでしょうか? 教えてください! 私たちの email アドレスは noel@underscore.io
と dave@underscore.io
です。
9 形と列と星
In this chapter we will learn how to build our own shapes out of the primitve lines and curves that make up the triangles, rectangles, and circles we’ve used so far. In doing so we’ll learn how to represent sequences of data, and manipulate such sequences using higher-order functions that abstract over structural recursion. That’s quite a lot of jargon, but we hope you’ll see it’s not as difficult as it sounds!
例題を Doodle の sbt console 内で実行した場合は、何もしなくても動作するはずです。そうじゃない場合は、以下の import 文を使って Doodle を使用可能な状態にする必要があります。
import doodle.core._
import doodle.core.Image._
import doodle.syntax._
import doodle.jvm.Java2DFrame._
import doodle.backend.StandardInterpreter._
9.1 Paths
All shapes in Doodle are ultimately represented as paths. You can think of a path as giving a sequence of movements for an imaginary pen, starting from the local origin. Pen movements come in three varieties:
moving the pen to a point without drawing a line;
drawing a straight line from the current position to a point; and
drawing a Bezier curve from the current position to a point, with the shape of the curve determined by two control points.
Paths themselves come in two varieties:
- open paths, where the end of the path is not necessarily the starting point; and
- closed paths, that end where they begin—and if the path doesn’t end where it started a line will be inserted to make it so.
The picture in fig. 39 illustrates the components that can make up a path, and shows the difference between open and closed paths.
9.1.1 Creating Paths
Now we know about paths, how do we create them in Doodle? Here’s the code that created fig. ¿fig:pictures:open-closed-paths?.
import doodle.core.Point._
import doodle.core.PathElement._
val triangle =
List(
lineTo(cartesian(50, 100)),
lineTo(cartesian(100, 0)),
lineTo(cartesian(0, 0))
)
val curve =
List(curveTo(cartesian(50, 100), cartesian(100, 100), cartesian(150, 0)))
def style(image: Image): Image =
image.
lineWidth(6.0).
lineColor(Color.royalBlue).
fillColor(Color.skyBlue)
val openPaths =
style(openPath(triangle) beside openPath(curve))
val closedPaths =
style(closedPath(triangle) beside closedPath(curve))
val paths = openPaths above closedPaths
From this code we can see we create paths using the openPath
and closePath
methods on Image
, just as we create other shapes. A path is created from a List
of PathElement
. The different kinds of PathElement
are created by calling methods on the PathElement
object, as described in tbl. 4.
Method | Description | Example |
---|---|---|
moveTo(Point) |
Move the pen to Point without drawing. |
moveTo(cartesian(1, 1)) |
lineTo(Point) |
Draw a straight line to Point |
lineTo(cartesian(2, 2)) |
curveTo(Point, Point, Point) |
Draw a curve. The first two points specify control points and the last point is where the curve ends. | curveTo(cartesian(1,0), cartesian(0,1), cartesian(1,1)) |
Constructing a List
is straight-forward: we just call List
with the elements we want the list to contain. Here are some examples.
// List of Int
List(1, 2, 3)
// res1: List[Int] = List(1, 2, 3)
// List of Image
List(Image.circle(10), Image.circle(20), Image.circle(30))
// res3: List[doodle.core.Image] = List(Circle(10.0), Circle(20.0), Circle(30.0))
// List of Color
List(Color.paleGoldenrod, Color.paleGreen, Color.paleTurquoise)
// res5: List[doodle.core.Color] = List(RGBA(UnsignedByte(110),UnsignedByte(104),UnsignedByte(42),Normalized(1.0)), RGBA(UnsignedByte(24),UnsignedByte(123),UnsignedByte(24),Normalized(1.0)), RGBA(UnsignedByte(47),UnsignedByte(110),UnsignedByte(110),Normalized(1.0)))
Notice the type of a List
includes the type of the elements, written in square brackets. So the type of a list of integers is written List[Int]
and a list of PathElement
is written List[PathElement]
.
Exercises
Polygons
Create paths to define a triangle, square, and pentagon. Your image might look like fig. 40. Hint: you might find it easier to use polar coordinates to define the polygons.
Using polar coordinates makes it much simpler to define the location of the “corners” (vertices) of the polygons. Each vertex is located a fixed rotation from the previous vertex, and after we’ve marked all vertices we must have done a full rotation of the circle. This means, for example, that for a pentagon each vertex is (360 / 5) = 72 degrees from the previous one. If we start at 0 degrees, vertices are located at 0, 72, 144, 216, and 288 degrees. The distance from the origin is fixed in each case. We don’t have to draw a line between the final vertex and the start—by using a closed path this will be done for us.
Here’s our code to draw fig. 40, which uses this idea. In some cases we haven’t started the vertices at 0 degrees so we can rotate the shape we draw.
import doodle.core.Image._
import doodle.core.PathElement._
import doodle.core.Point._
import doodle.core.Color._
val triangle =
closedPath(List(
moveTo(polar(50, 0.degrees)),
lineTo(polar(50, 120.degrees)),
lineTo(polar(50, 240.degrees))
))
val square =
closedPath(List(
moveTo(polar(50, 45.degrees)),
lineTo(polar(50, 135.degrees)),
lineTo(polar(50, 225.degrees)),
lineTo(polar(50, 315.degrees))
))
val pentagon =
closedPath((List(
moveTo(polar(50, 72.degrees)),
lineTo(polar(50, 144.degrees)),
lineTo(polar(50, 216.degrees)),
lineTo(polar(50, 288.degrees)),
lineTo(polar(50, 360.degrees))
)))
val spacer =
rectangle(10, 100).noLine.noFill
def style(image: Image): Image =
image.lineWidth(6.0).lineColor(paleTurquoise).fillColor(turquoise)
val image =
style(triangle) beside spacer beside style(square) beside spacer beside style(pentagon)
Curves
Repeat the exercise above, but this time use curves instead of straight lines to create some interesting shapes. Our curvy polygons are shown in fig. 41. Hint: you’ll have an easier time if you generalise into a method your code for creating a curve.
The core of the exercise is to replace the lineTo
expressions with curveTo
. We can generalise curve creation into a method that takes the starting angle and the angle increment, and constructs control points at predetermined points along the rotation. This is what we did in the method curve
below, and it gives us consistent looking curves without having to manually repeat the calculations each time. Making this generalisation also makes it easier to play around with different control points to create different outcomes.
import doodle.core.Image._
import doodle.core.Point._
import doodle.core.PathElement._
import doodle.core.Color._
def curve(radius: Int, start: Angle, increment: Angle): PathElement = {
curveTo(
polar(radius * .8, start + (increment * .3)),
polar(radius * 1.2, start + (increment * .6)),
polar(radius, start + increment)
)
}
val triangle =
closedPath(List(
moveTo(polar(50, 0.degrees)),
curve(50, 0.degrees, 120.degrees),
curve(50, 120.degrees, 120.degrees),
curve(50, 240.degrees, 120.degrees)
))
val square =
closedPath(List(
moveTo(polar(50, 45.degrees)),
curve(50, 45.degrees, 90.degrees),
curve(50, 135.degrees, 90.degrees),
curve(50, 225.degrees, 90.degrees),
curve(50, 315.degrees, 90.degrees)
))
val pentagon =
closedPath((List(
moveTo(polar(50, 72.degrees)),
curve(50, 72.degrees, 72.degrees),
curve(50, 144.degrees, 72.degrees),
curve(50, 216.degrees, 72.degrees),
curve(50, 288.degrees, 72.degrees),
curve(50, 360.degrees, 72.degrees)
)))
val spacer =
rectangle(10, 100).noLine.noFill
def style(image: Image): Image =
image.lineWidth(6.0).lineColor(paleTurquoise).fillColor(turquoise)
val image = style(triangle) beside spacer beside style(square) beside spacer beside style(pentagon)
9.2 Working with Lists
At this point you might be thinking it would be nice to create a method to draw polygons rather than constructing each one by hand. There is clearly a repeating pattern to their construction and we would be able to generalise this pattern if we knew how to create a list of arbitrary size. It’s time we learned more about building and manipulating lists.
9.2.1 The Recursive Structure of Lists
You’ll recall when we introduced structural recursion over the natural numbers we said we could transform their recursive structure into any other recursive structure. We demonstrated this for concentric circles and a variety of other patterns.
Lists have a recursive structure, and one that is very similar to the structure of the natural numbers. A List
is
- the empty list
Nil
; or - a pair of an element
a
and aList
, writtena :: tail
, wheretail
is the rest of the list.
For example, we can write out the list List(1, 2, 3, 4)
in its “long” form as
1 :: 2 :: 3 :: 4 :: Nil
// res0: List[Int] = List(1, 2, 3, 4)
Notice the similarity to the natural numbers. Earlier we noted we can expand the structure of a natural number so we could write, say, 3
as 1 + 1 + 1 + 0
. If we replace +
with ::
and 0
with Nil
we get the List
1 :: 1 :: 1 :: Nil
.
What does this mean? It means we can easily transform a natural number into a List
using our familiar tool of structural recursion5. Here’s a very simple example, which given a number builds a list of that length containing the String
“Hi”.
def sayHi(length: Int): List[String] =
length match {
case 0 => Nil
case n => "Hi" :: sayHi(n - 1)
}
// sayHi: (length: Int)List[String]
sayHi(5)
// res1: List[String] = List(Hi, Hi, Hi, Hi, Hi)
The code here is transforming:
0
toNil
, for the base case; andn
(which, remember, we think of as1 + m
) to"Hi" :: sayHi(n - 1)
, transforming1
to"Hi"
,+
to::
, and recursing as usual onm
(which isn - 1
).
This recursive structure also means we can transform lists into other recursive structures, such a natural number, different lists, chessboards, and so on. Here we increment every element in a list—that is, transform a list to a list—using structural recursion.
def increment(list: List[Int]): List[Int] =
list match {
case Nil => Nil
case hd :: tl => (hd + 1) :: increment(tl)
}
// increment: (list: List[Int])List[Int]
increment(List(1, 2, 3))
// res2: List[Int] = List(2, 3, 4)
Here we sum the elements of a list of integers—that is, transform a list to a natural number—using structural recursion.
def sum(list: List[Int]): Int =
list match {
case Nil => 0
case hd :: tl => hd + sum(tl)
}
// sum: (list: List[Int])Int
sum(List(1, 2, 3))
// res3: Int = 6
Notice when we take a List
apart with pattern matching we use the same hd :: tl
syntax we use when we construct it. This is an intentional symmetry.
9.2.2 Type Variables
What about finding the length of a list? We know we can use our standard tool of structural recursion to write the method. Here’s the code to calculate the length of a List[Int]
.
def length(list: List[Int]): Int =
list match {
case Nil => 0
case hd :: tl => 1 + length(tl)
}
// length: (list: List[Int])Int
Note that we don’t do anything with the elements of the list—we don’t really care about their type. Using the same code skeleton can just as easily calculate the length of a List[Int]
as a List[HairyYak]
but we don’t currently know how to write down the type of a list where we don’t care about the type of the elements.
Scala lets us write methods that can work with any type, by using what is called a type variable. A type variable is written in square brackets like [A]
and comes after the method name and before the parameter list. A type variable can stand in for any specific type, and we can use it in the parameter list or result type to indicate some type that we don’t know up front. For example, here’s how we can write length
so it works with lists of any type.
def length[A](list: List[A]): Int =
list match {
case Nil => 0
case hd :: tl => 1 + length(tl)
}
// length: [A](list: List[A])Int
Structural Recursion over a List
A List
of elements of type A
is:
- the empty list
Nil
; or - an element
a
of typeA
and atail
of typeList[A]
:a :: tail
The structural recursion skeleton for transforming list
of type List[A]
to some type B
has shape
def doSomething[A,B](list: List[A]): B =
list match {
case Nil => ??? // Base case of type B here
case hd :: tl => f(hd, doSomething(tl))
}
where f
is a problem specific method combining hd
and result of the recursive call to produce something of type B
.
Exercises
Building Lists
In these exercises we get some experience constructing lists using structural recursion on the natural numbers.
Write a method called ones
that accepts an Int
n
and returns a List[Int]
with length n
and every element 1
. For example
ones(3)
// res4: List[Int] = List(1, 1, 1)
It’s structural recursion over the natural numbers!
def ones(n: Int): List[Int] =
n match {
case 0 => Nil
case n => 1 :: ones(n - 1)
}
// ones: (n: Int)List[Int]
ones(3)
// res5: List[Int] = List(1, 1, 1)
Write a method descending
that accepts an natural number n
and returns a List[Int]
containing the natural numbers from n
to 1
or the empty list if n
is zero. For example
descending(0)
// res6: List[Int] = List()
descending(3)
// res7: List[Int] = List(3, 2, 1)
Once more, we can employ structural recursion over the natural numbers.
def descending(n: Int): List[Int] =
n match {
case 0 => Nil
case n => n :: descending(n - 1)
}
// descending: (n: Int)List[Int]
descending(0)
// res8: List[Int] = List()
descending(3)
// res9: List[Int] = List(3, 2, 1)
Write a method ascending
that accepts a natural number n
and returns a List[Int]
containing the natural numbers from 1
to n
or the empty list if n
is zero.
ascending(0)
// res10: List[Int] = List()
ascending(3)
// res11: List[Int] = List(1, 2, 3)
It’s structural recursion over the natural numbers, but this time with an internal accumulator.
def ascending(n: Int): List[Int] = {
def iter(n: Int, counter: Int): List[Int] =
n match {
case 0 => Nil
case n => counter :: iter(n - 1, counter + 1)
}
iter(n, 1)
}
// ascending: (n: Int)List[Int]
ascending(0)
// res12: List[Int] = List()
ascending(3)
// res13: List[Int] = List(1, 2, 3)
Create a method fill
that accepts a natural number n
and an element a
of type A
and constructs a list of length n
where all elements are a
.
fill(3, "Hi")
// res14: List[String] = List(Hi, Hi, Hi)
fill(3, Color.blue)
// res15: List[doodle.core.Color] = List(RGBA(UnsignedByte(-128),UnsignedByte(-128),UnsignedByte(127),Normalized(1.0)), RGBA(UnsignedByte(-128),UnsignedByte(-128),UnsignedByte(127),Normalized(1.0)), RGBA(UnsignedByte(-128),UnsignedByte(-128),UnsignedByte(127),Normalized(1.0)))
In this exercise we’re asking you to use a type variable. Otherwise it is the same pattern as before.
def fill[A](n: Int, a: A): List[A] =
n match {
case 0 => Nil
case n => a :: fill(n-1, a)
}
// fill: [A](n: Int, a: A)List[A]
fill(3, "Hi")
// res16: List[String] = List(Hi, Hi, Hi)
fill(3, Color.blue)
// res17: List[doodle.core.Color] = List(RGBA(UnsignedByte(-128),UnsignedByte(-128),UnsignedByte(127),Normalized(1.0)), RGBA(UnsignedByte(-128),UnsignedByte(-128),UnsignedByte(127),Normalized(1.0)), RGBA(UnsignedByte(-128),UnsignedByte(-128),UnsignedByte(127),Normalized(1.0)))
Transforming Lists
In these exercises we practice the other side of list manipulation—transforming lists into elements of other types (and sometimes, into different lists).
Write a method double
that accepts a List[Int]
and returns a list with each element doubled.
double(List(1, 2, 3))
// res18: List[Int] = List(2, 4, 6)
double(List(4, 9, 16))
// res19: List[Int] = List(8, 18, 32)
This is a structural recursion over a list, building a list at each step. The destructuring of the input is mirrored by the construction of the output.
def double(list: List[Int]): List[Int] =
list match {
case Nil => Nil
case hd :: tl => (hd * 2) :: double(tl)
}
// double: (list: List[Int])List[Int]
double(List(1, 2, 3))
// res20: List[Int] = List(2, 4, 6)
double(List(4, 9, 16))
// res21: List[Int] = List(8, 18, 32)
Write a method product
that accepts a List[Int]
and calculates the product of all the elements.
product(Nil)
// res22: Int = 1
product(List(1,2,3))
// res23: Int = 6
This is a structural recursion over a list using the same pattern as sum
in the examples above.
def product(list: List[Int]): Int =
list match {
case Nil => 1
case hd :: tl => hd * product(tl)
}
// product: (list: List[Int])Int
product(Nil)
// res24: Int = 1
product(List(1,2,3))
// res25: Int = 6
Write a method contains
that accepts a List[A]
and an element of type A
and returns true if the list contains the element and false otherwise.
contains(List(1,2,3), 3)
// res26: Boolean = true
contains(List("one", "two", "three"), "four")
// res27: Boolean = false
Same pattern as before, but using a type variable to allow type of the elements to vary.
def contains[A](list: List[A], elt: A): Boolean =
list match {
case Nil => false
case hd :: tl => (hd == elt) || contains(tl, elt)
}
// contains: [A](list: List[A], elt: A)Boolean
contains(List(1,2,3), 3)
// res28: Boolean = true
contains(List("one", "two", "three"), "four")
// res29: Boolean = false
Write a method first
that accepts a List[A]
and an element of type A
and returns the first element of the list if it is not empty and otherwise returns the element of type A
passed as a aprameter.
first(Nil, 4)
// res30: Int = 4
first(List(1,2,3), 4)
// res31: Int = 1
This method is similar to contains
above, except we now use the type variable in the return type as well as in the parameter types.
def first[A](list: List[A], elt: A): A =
list match {
case Nil => elt
case hd :: tl => hd
}
// first: [A](list: List[A], elt: A)A
first(Nil, 4)
// res32: Int = 4
first(List(1,2,3), 4)
// res33: Int = 1
Challenge Exercise: Reverse
Write a method reverse
that accepts a List[A]
and reverses the list.
reverse(List(1, 2, 3))
// res34: List[Int] = List(3, 2, 1)
reverse(List("a", "b", "c"))
// res35: List[String] = List(c, b, a)
The trick here is to use an accumulator to hold the partially reversed list. If you managed to work this one out, congratulations—you really understand structural recursion well!
def reverse[A](list: List[A]): List[A] = {
def iter(list: List[A], reversed: List[A]): List[A] =
list match {
case Nil => reversed
case hd :: tl => iter(tl, hd :: reversed)
}
iter(list, Nil)
}
// reverse: [A](list: List[A])List[A]
reverse(List(1, 2, 3))
// res36: List[Int] = List(3, 2, 1)
reverse(List("a", "b", "c"))
// res37: List[String] = List(c, b, a)
Polygons!
At last, let’s return to our example of drawing polygons. Write a method polygon
that accepts the number of sides of the polygon and the starting rotation and produces a Image
representing the specified regular polygon. Hint: use an internal accumulator.
Use this utility to create an interesting picture combining polygons. Our rather unimaginative example is in fig. 42. We’re sure you can do better.
Here’s our code. Note how we factored the code into small components—though we could have taken the factoring further is we wanted to. (Can you see how? Hint: do we need to pass, say, start
to every call of makeColor
when it’s not changing?)
import Point._
import PathElement._
def polygon(sides: Int, size: Int, initialRotation: Angle): Image = {
def iter(n: Int, rotation: Angle): List[PathElement] =
n match {
case 0 =>
Nil
case n =>
LineTo(polar(size, rotation * n + initialRotation)) :: iter(n - 1, rotation)
}
closedPath(moveTo(polar(size, initialRotation)) :: iter(sides, 360.degrees / sides))
}
def style(img: Image): Image = {
img.
lineWidth(3.0).
lineColor(Color.mediumVioletRed).
fillColor(Color.paleVioletRed.fadeOut(0.5.normalized))
}
def makeShape(n: Int, increment: Int): Image =
polygon(n+2, n * increment, 0.degrees)
def makeColor(n: Int, spin: Angle, start: Color): Color =
start spin (spin * n)
val baseColor = Color.hsl(0.degrees, 0.7.normalized, 0.7.normalized)
def makeImage(n: Int): Image = {
n match {
case 0 =>
Image.empty
case n =>
val shape = makeShape(n, 10)
val color = makeColor(n, 30.degrees, baseColor)
makeImage(n-1) on (shape fillColor color)
}
}
val image = makeImage(15)
9.3 Transforming Sequences
We’ve seen that using structural recursion we can create and transform lists. This pattern is simple to use and to understand, but it requires we write the same skeleton time and again. In this section we’ll learn that we can replace structural recursion in some cases by using a method on List
called map
. We’ll also see that other useful datatypes provide this method and we can use it as a common interface for manipulating sequences.
9.3.1 Transforming the Elements of a List
In the previous section we say several examples where we transformed one list to another. For example, we incremented the elements of a list with the following code.
def increment(list: List[Int]): List[Int] =
list match {
case Nil => Nil
case hd :: tl => (hd + 1) :: tl
}
// increment: (list: List[Int])List[Int]
increment(List(1, 2, 3))
// res0: List[Int] = List(2, 2, 3)
In this example the structure of the list doesn’t change. If we start with three elements we end with three elements. We can abstract this pattern in a method called map
. If we have a list with elements of type A
, we pass map
a function of type A => B
and we get back a list with elements of type B
. For example, we can implement increment
using map
with the function x => x + 1
.
def increment(list: List[Int]): List[Int] =
list.map(x => x + 1)
// increment: (list: List[Int])List[Int]
increment(List(1, 2, 3))
// res1: List[Int] = List(2, 3, 4)
Each element is transformed by the function we pass to map
, in this case x => x + 1
. With map
we can transform the elements, but we cannot change the number of elements in the list.
We find a graphical notation helps with understanding map
. If we had some type Circle
we can draw a List[Circle]
as a box containing a circle, as shown in fig. 43.
Now we can draw an equation for map
as in fig. 44. If you prefer symbols instead of pictures, the picture is showing that List[Circle] map (Circle => Triangle) = List[Triangle]
. One feature of the graphical representation is it nicely illustrates that map
cannot create a new “box”, which represents the structure of the list—or more concretely the number of elements and their order.
The graphical drawing of map
exactly illustrates what holds at the type level for map
. If we prefer we can write it down symbolically:
List[A] map (A => B) = List[B]
The left hand side of the equation has the type of the list we map and the function we use to do the mapping. The right hand is the type of the result. We can perform algebra with this representation, substituting in concrete types from our program.
9.3.2 Transforming Sequences of Numbers
We have also written a lot of methods that transform a natural number to a list. We briefly discussed how we can represent a natural number as a list. 3
is equivalent to 1 + 1 + 1 + 0
, which in turn we could represent as List(1, 1, 1)
. So what? Well, it means we could write a lot of the methods that accepts natural numbers as methods that worked on lists.
For example, instead of
def fill[A](n: Int, a: A): List[A] =
n match {
case 0 => Nil
case n => a :: fill(n-1, a)
}
// fill: [A](n: Int, a: A)List[A]
fill(3, "Hi")
// res2: List[String] = List(Hi, Hi, Hi)
we could write
def fill[A](n: List[Int], a: A): List[A] =
n.map(x => a)
// fill: [A](n: List[Int], a: A)List[A]
fill(List(1, 1, 1), "Hi")
// res3: List[String] = List(Hi, Hi, Hi)
The implementation of this version of fill
is more convenient to write, but it is much less convenient for the user to call it with List(1, 1, ,1)
than just writing 3
.
If we want to work with sequences of numbers we are better off using Ranges
. We can create these using the until
method of Int
.
0 until 10
// res4: scala.collection.immutable.Range = Range 0 until 10
Ranges
have a by
method that allows us to change the step between consecutive elements of the range:
0 until 10 by 2
// res5: scala.collection.immutable.Range = Range 0 until 10 by 2
Ranges
also have a map
method just like List
(0 until 3) map (x => x + 1)
// res6: scala.collection.immutable.IndexedSeq[Int] = Vector(1, 2, 3)
You’ll notice the result has type IndexedSeq
and is implemented as a Vector
—two types we haven’t seen yet. We can treat an IndexedSeq
much like a List
, but for simplicity we can convert a Range
or an IndexedSeq
to a List
using the toList
method.
(0 until 7).toList
// res7: List[Int] = List(0, 1, 2, 3, 4, 5, 6)
(0 until 3).map(x => x + 1).toList
// res8: List[Int] = List(1, 2, 3)
With Ranges
in our toolbox we can write fill
as
def fill[A](n: Int, a: A): List[A] =
(0 until n).toList.map(x => a)
// fill: [A](n: Int, a: A)List[A]
fill(3, "Hi")
// res9: List[String] = List(Hi, Hi, Hi)
9.3.3 Ranges over Doubles
If we try to create a Range
over Double
we get an error.
0.0 to 10.0 by 1.0
// <console>:28: warning: method to in trait FractionalProxy is deprecated (since 2.12.6): use BigDecimal range instead
// 0.0 to 10.0 by 1.0
// ^
// error: No warnings can be incurred under -Xfatal-warnings.
There are two ways around this. We can use an equivalent Range
over Int
. In this case we could just write
0 to 10 by 1
We can use the .toInt
method to convert a Double
to an Int
if needed.
Alternatively we can write a Range
using BigDecimal
.
import scala.math.BigDecimal
BigDecimal(0.0) to 10.0 by 1.0
BigDecimal
has methods doubleValue
and intValue
to get Double
and Int
values respectively.
BigDecimal(10.0).doubleValue()
// res13: Double = 10.0
BigDecimal(10.0).intValue()
// res14: Int = 10
Exercises
Ranges, Lists, and map
Using our new tools, reimplement the following methods.
Write a method called ones
that accepts an Int
n
and returns a List[Int]
with length n
and every element is 1
. For example
ones(3)
// res15: List[Int] = List(1, 1, 1)
We can just map
over a Range
to achieve this.
def ones(n: Int): List[Int] =
(0 until n).toList.map(x => 1)
// ones: (n: Int)List[Int]
ones(3)
// res16: List[Int] = List(1, 1, 1)
Write a method descending
that accepts an natural number n
and returns a List[Int]
containing the natural numbers from n
to 1
or the empty list if n
is zero. For example
descending(0)
// res17: List[Int] = List()
descending(3)
// res18: List[Int] = List(3, 2, 1)
We can use a Range
but we have to set the step size or the range will be empty.
def descending(n: Int): List[Int] =
(n until 0 by -1).toList
// descending: (n: Int)List[Int]
descending(0)
// res19: List[Int] = List()
descending(3)
// res20: List[Int] = List(3, 2, 1)
Write a method ascending
that accepts a natural number n
and returns a List[Int]
containing the natural numbers from 1
to n
or the empty list if n
is zero.
ascending(0)
// res21: List[Int] = List()
ascending(3)
// res22: List[Int] = List(1, 2, 3)
Again we can use a Range
but we need to take care to start at 0
and increment the elements by 1
so we have the correct number of elements.
def ascending(n: Int): List[Int] =
(0 until n).toList.map(x => x + 1)
// ascending: (n: Int)List[Int]
ascending(0)
// res23: List[Int] = List()
ascending(3)
// res24: List[Int] = List(1, 2, 3)
Write a method double
that accepts a List[Int]
and returns a list with each element doubled.
double(List(1, 2, 3))
// res25: List[Int] = List(2, 4, 6)
double(List(4, 9, 16))
// res26: List[Int] = List(8, 18, 32)
This is a straightforward application of map
.
def double(list: List[Int]): List[Int] =
list map (x => x * 2)
// double: (list: List[Int])List[Int]
double(List(1, 2, 3))
// res27: List[Int] = List(2, 4, 6)
double(List(4, 9, 16))
// res28: List[Int] = List(8, 18, 32)
Polygons, Again!
Using our new tools, rewrite the polygon
method from the previous section.
Here’s one possible implementation. Much easier to read than the previous implementation!
def polygon(sides: Int, size: Int, initialRotation: Angle): Image = {
import Point._
import PathElement._
val step = (Angle.one / sides).toDegrees.toInt
val path =
(0 to 360 by step).toList.map{ deg =>
lineTo(polar(size, initialRotation + deg.degrees))
}
closedPath(moveTo(polar(size, initialRotation)) :: path)
}
Challenge Exercise: Beyond Map
Can we use map
to replace all uses of structural recursion? If not, can you characterise the problems that we can’t implement with map
but can implement with general structural recursion over lists?
We’ve seen many examples that we cannot implement with map
: the methods product
, sum
, find
, and more in the previous section cannot be implemented with map
.
In abstract, methods implemented with map obey the following equation:
List[A] map A => B = List[B]
If the result is not of type List[B]
we cannot implement it with map
. For example, methods like product
and sum
transform List[Int]
to Int
and so cannot be implemented with map
.
Map
transforms the elements of a list, but cannot change the number of elements in the result. Even if a method fits the equation for map
above it cannot be implemented with map
if it requires changing the number of elements in the list.
9.3.4 Tools with Ranges
We’ve seen the until
method to construct Ranges
, and the by
method to change the increment in a Range
. There is one more method that will be useful to know about: to
. This constructs a Range
like until
but the Range
includes the endpoint. Compare
1 until 5
// res29: scala.collection.immutable.Range = Range 1 until 5
1 to 5
// res30: scala.collection.immutable.Range.Inclusive = Range 1 to 5
In technical terms, the Range
constructed with until
is a half-open interval, while the range constructed with to
is an open interval.
Exercises
Using Open Intervals
Write a method ascending
that accepts a natural number n
and returns a List[Int]
containing the natural numbers from 1
to n
or the empty list if n
is zero. Hint: use to
ascending(0)
// res31: List[Int] = List()
ascending(3)
// res32: List[Int] = List(1, 2, 3)
Now that we now about to
this is trivial to implement.
def ascending(n: Int): List[Int] =
(1 to n).toList
// ascending: (n: Int)List[Int]
ascending(0)
// res33: List[Int] = List()
ascending(3)
// res34: List[Int] = List(1, 2, 3)
9.4 My God, It’s Full of Stars!
Let’s use our new tools to draw some stars. For the purpose of this exercise let’s assume that a star is a polygon with p
points. However, instead of connecting each point to its neighbours, we’ll connect them to the nth
point around the circumference.
For example, fig. 45 shows stars with p=11
and n=1 to 5
. n=1
produces a regular polygon while values of n
from 2
upwards produce stars with increasingly sharp points:
Write code to draw the diagram above. Start by writing a method to draw a star
given p
and n
:
def star(p: Int, n: Int, radius: Double): Image =
???
Hint: use the same technique we used for polygon
previously.
Here’s the star
method. We’ve renamed p
and n
to points
and skip
for clarity:
def star(sides: Int, skip: Int, radius: Double): Image = {
import Point._
import PathElement._
val rotation = 360.degrees * skip / sides
val start = moveTo(polar(radius, 0.degrees))
val elements = (1 until sides).toList map { index =>
val point = polar(radius, rotation * index)
lineTo(point)
}
closedPath(start :: elements) lineWidth 2
}
Using structural recursion and beside
write a method allBeside
with the signature
def allBeside(images: List[Image]): Image =
???
// allBeside: (images: List[doodle.core.Image])doodle.core.Image
We’ll use allBeside
to create the row of stars. To create the picture we only need to use values of skip
from 1
to sides/2
rounded down. For example:
allBeside(
(1 to 5).toList map { skip =>
star(11, skip, 100)
}
)