Creative Scala (日本語版)

Dave Gurnell, Noel Welsh 著 Eugene Yokota 訳

May 2018

序文

Creative Scala は、Scala 歴ゼロのデベロッパー向けに書かれています。 関数型プログラミングを楽しく学べることを目指しました。 あなたが他のプログラミング言語の初歩は少しかじったことがあることを前提としますが、Scala やその他の関数型言語には慣れ親しんでいないことを想定しています。

この本の 3つの目標は:

  1. 関数型プログラミングを紹介して、あなたがプログラムを計算したり筋道を立てて推論できるようになることで、他の関数型プログラミングに関する入門書に進めるようになること。
  2. Scala を使って自分の興味のあることに探検していくのに十分な Scala を教えること。
  3. これらを 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 でお願いします:

謝辞

Creative Scala (英語版) は Dave GurnellNoel 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 がプログラミング言語理論とコンピューターサイエンス教育にたずさわる多くの研究者の成果のおかであることをここに書きたいと思います。特に私たちが強調したいのは:

1 始めてみよう

まず最初のステップは Creative Scala の作業に必要なソフトウェアをインストールすることです。ここでは 2つの道のりを解説します:

  1. テキストエディタとターミナルを使う方法。プログラミングを一切やったこと無い人にはこの方法をお勧めします
  2. IntelliJ IDEA を使う方法。IDE を使うのに慣れている人やターミナルを使うのが不安な人にはこの方法をお勧めします。

もしも経験を積んだデベロッパーで自分の好みのセットアップがある場合はそのままそれを使って、必要に応じて以下の手順を調整して使ってください。

何を言っているのかさっぱりという人は、この章でこれから説明していくので読み進めてください。

1.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つの要素に分解することができます:

これを直接操作系のインターフェイスを用いて行うのはより時間のかかる作業となるでしょう。

コマンドラインのほうが学習するのが難しいですが、代わりに非常に強力なツールを得ることができます。 私たちが使うターミナルの用法は限られているものなので、上の例が怖いなと思っても心配しないでください!

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 を使ってこれを行うのは少し複雑です。 何らかの変更を行ったあと以下を実行する必要があります:

以下は、コマンドラインからこれを実行する一例です。

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 + 2Int 型を持ち、これはこの式が評価する値を整数だと解釈するべきことを意味します。これは、この式の結果を使って他の式を書くことができるけども、その式が整数に合った演算である必要があることを意味します。例えば、加算、減算、乗算、除算などが可能ですが、整数を小文字に変換することはできません。

型は多くの場合において、私たちが値 (コンピューターのメモリにある「あれ」) をどう理解するべきかを教えてくれます。整数として理解するべきなのか、マウスの現在位置を表す点のストリームとして理解するべきでしょうか? それは型が教えてくれます。私たちは型を、実行時に表現を持たないものを含む他のことにも使うことができます。これらの用法はここで深入りするには少し難易度が高いですが、型が値に対応すると思ってしまうのは間違いです。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 型のリテラル表現を見ていきましょう。気取った名前ですが、値が truefalse であるかというだけの意味で、ブールリテラルはそのまま以下のように書かれます。

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 はそのオブジェクトのメソッドです。私たちは circle10 というパラメータを渡して、私たちが作る円の半径を指定します。結果の型に注目してください。Image となっていますね。

Doodle 内で console を実行した場合は、これらのイメージを作るためのメソッドが使用可能な状態になっているので、circle(10) とだけ書くこともできます。

circle(10)
// res1: doodle.core.Image = Circle(10.0)

この円を描画するためには、draw メソッドを呼びます。

circle(10).draw

fig. 1 のようなウィンドウが表示されるはずです。

Figure 1: A circle

Figure 1: A circle

Doodle は、円、長方形、三角形といったいくつかの基本図形をサポートします。長方形を描いてみましょう。

rectangle(100, 50).draw

結果は fig. 2 のようになります。

Figure 2: A rectangle

Figure 2: A rectangle

最後に、fig. 3 のような三角形を描いてみましょう。

triangle(60, 40).draw
Figure 3: A triangle

Figure 3: A triangle

練習問題

堂々巡り

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
Figure 4: A circle beside a rectangle

Figure 4: A circle beside a rectangle

tbl. 1 は、イメージを組み合わせるために Doodle が提供するレイアウトメソッドです。それぞれ使ってみて、何をするのか見てみよう。

Table 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 のような絵を描いてみよう。

Figure 5: The width of a circle

Figure 5: The width of a circle

これは 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 で解説するメソッドを試して、結果がどうなるか見てください。

Table 2: Doodle でイメージに色を付けるためのいくつかのメソッド
演算子 説明 使用例
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 によく使われる色を挙げます。

Table 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 を使ったけども、独自に他の色も実験してみて!

Figure 6: 邪視退散!

Figure 6: 邪視退散!

これが私のお守りです:

((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つのパラメータを取ります。これらは UnsignedByte2 と呼ばれる 0 から 255 の数です。UnsignedByte には Int のようなリテラル式が無いため、Int から UnsignedByte へと変換する必要があります。これは、uByte メソッドを使って行うことができます。IntUnsignedByte よりも多くの値を取ることができるので、もしも数が 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) のフォーマットの方が、私たちが色を認知するのにより近い形でモデル化されています。この表現法では色は以下の成分を持ちます:

fig. 7 は色相と明度を変えることでどう色が変化するかを示し、fig. 8 は彩度の変化の影響を示します。

Figure 7: 彩度を 1 に固定して、色相 (角度) と明度 (中心からの距離) の変化を示した色相環。

Figure 7: 彩度を 1 に固定して、色相 (角度) と明度 (中心からの距離) の変化を示した色相環。

Figure 8: 色相と明度を固定することで彩度の変化が色に与える影響を示した勾配。左端は彩度が 0 で右端が彩度が 1 となっている。

Figure 8: 色相と明度を固定することで彩度の変化が色に与える影響を示した勾配。左端は彩度が 0 で右端が彩度が 1 となっている。

Color.hsl メソッドを使って HSL 表現の色を作ることができます。このメソッドは、色相、彩度、明度をパラメータとして受け取ります。色相は Angle で、degrees メソッド (か radians メソッド) を用いて DoubleAngle へと変換することができます。

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 を見てください。

Figure 9: パステルレッドを用いた三角形のレンダリング

Figure 9: パステルレッドを用いた三角形のレンダリング

3.4.3 色の操作

構図の効果は、実際に使われた色そのものだけではなく、色と色の間の関係よることが多いと思います。既存の色から新しい色を作ることができるメソッドがいくつかあります。特によく使われるのは:

具体例で解説すると

((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 を生成します。

Figure 10: Color.red から始まり、色相を 15度づつ回転させた 3つの円

Figure 10: Color.red から始まり、色相を 15度づつ回転させた 3つの円

次は似た例ですが、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)))
Figure 11: 上の 3つの円は明度の変化を示し、下の 3つの四角形は彩度の変化を示す

Figure 11: 上の 3つの円は明度の変化を示し、下の 3つの四角形は彩度の変化を示す

3.4.4 透明度

アルファ値を与えることで、色に透明度を追加することができます。アルファ値 0.0 は完全な透明な色を表し、アルファ値 1.0 は完全に不透明な色を表します。Color.rgbaColor.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))))
Figure 12: アルファ値 0.5 の円を使って透明度を表す

Figure 12: アルファ値 0.5 の円を使って透明度を表す

練習問題

類似色の三角形

3つの三角形を、三角に配置して、類似色で色付けしてみよう。類似色とは色相の近い色のことです。(ちょっと手のこんだ) 具体例 fig. 13 を見てください。

Figure 13: 類似色の三角形。色は darkSlateBlue のバリエーションとして選ばれています。

Figure 13: 類似色の三角形。色は darkSlateBlue のバリエーションとして選ばれています。

回答を 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つの同心円で得点帯を表して描いてみよう。

Figure 14: シンプルなアーチェリー用ターゲット

Figure 14: シンプルなアーチェリー用ターゲット

ボーナス得点として、fig. 15 のようにターゲットを練習場に置けるようにスタンドを追加してください。

Figure 15: スタンド付きのアーチェリー用ターゲット

Figure 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 を参照してください。

Figure 16: 色付けしたアーチェリーのターゲット

Figure 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 内だけで動作する特別なコマンドがあって、それを使ってファイルに保存したコードを実行することができます。 このコマンドは :paste3 と呼ばれています。: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 をより効率良く使うためのコツです:

コードをファイルに保存し始めると、次回 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 を使っている場合も似たようなエラーが出てくるはずです。

問題は、このようになっています:

そのため、これらの制約を知って、ファイルに書くコードの書き方を変える必要があります。

エラーメッセージにヒントが隠されています。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つの宣言は oneanImage という名前を定義します。 後からこれらの名前をコードの中で使って値を参照することができます。

one
// res0: Int = 1

anImage
// res1: doodle.core.Image = ContextTransform(doodle.core.Image$$Lambda$8479/247257415@46e720c5,Circle(100.0))

4.3.3 宣言

上の節で、宣言と定義という言い方をしました。 これらの用語が正確には何を意味するのかを理解して、objectval の違いをより深く見ていきましょう。

式については分かっています。 それらは、値に評価されるプログラムの一部です。 宣言定義は、プログラムの別の部分で、それらは値に評価されません。 代わりに、それらは何かに名前を与えます。実は、Scala では値だけではなく型も宣言できますが、これに関してはここではあまり考察しません。 objectval は両方とも宣言です。

宣言と式が別になっていることの結果の 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 トップレベル

値に名前を与えるのに objectval 宣言という別々の方法があるのに納得がいかないかもしれません。 名前を宣言するのに 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) 状態であるという言い方をして、名前が可視状態である所のことをそのスコープと言います。 そのため、この気取った新しい用語を使うと、「hiExample 外では不可視である」または「Example 外では hi はスコープに無い」と言えます。

名前のスコープはどうやったら分かるでしょうか? ルールは簡単で、名前は宣言された位置から始まり、直近の外側の中括弧 ({}) の終わりまで可視状態にあります。 上の例では、hiExample の中括弧に囲まれているので、そこで可視状態にあります。 他では見えません。

オブジェクトリテラルをオブジェクトリテラルの中で宣言することができ、より細かなスコープの区別することができます。 具体例で解説すると、

object Example1 {
  val hi = "Hi!"

  object Example2 {
    val hello = "Hello!"
  }
}

hiExample2 内でもスコープ内です (Example2hi の外側の中括弧内で定義されているため)。 しかし、hello のスコープは Example2 だけに限定されているため、hi のスコープよりも小さなものです。

もしスコープ内で既に宣言されている名前を再宣言するとどうなるでしょうか? これはシャドーイングと呼ばれています。 以下のコードでは、Example2 内の hiExample1 内の 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

まずはシンプルな例から。answer1 + 2 なので 3 です。

object One {
  val a = 1

  object Two {
    val a = 3
    val b = 2
  }

  object Answer {
    val answer = a + Two.b
  }
}

これもシンプルな例です。answer1 + 2 なので 3 です。Two.aanswer が定義されている場所ではスコープ外です。

object One {
  val a = 5
  val b = 2

  object Answer {
    val a = 1
    val answer = a + b
  }
}

ここでは Answer.aOne.a を覆い隠すので、answer1 + 23 となります。

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
}

banswer が宣言されている所では b はスコープ外であるので、このコードはコンパイルを通りません。

object One {
  val a = b - 1
  val b = a + 1

  val answer = a + b
}

引っ掛け問題です! このコードは動作しません。ここでは ab がそれぞれに対して定義されているため循環依存となり、解決されません。

4.4 抽象化

前の節では名前について色々習いました。 気取ったプログラマー用語を使うと、名前は式を抽象化すると言えます。 これは、名前の定義することの本質を一言で言い表しているけども、ちょっと用語を解読してみよう。

抽象化とは、要らない詳細を取り除くという意味です。 例えば、数は抽象化の 1つです。 「1」という数の純粋な概念は自然界には存在しません。 それはいつも 1つのリンゴや 1冊の Creative Scala の本といった具合に 1つの物です。 算数を行うときに、数という概念は何を数えているのかという要らない詳細を抽象化して数だけを操作することができます。

同様に、名前は式を代理します。 式は値をどのように構築するかを教えてくれます。 その値に名前があれば、その値がどのように構築されるかは知らなくてもよくなります。 式は任意の複雑さを持つことができますが、名前を使うことでどれだけ複雑なのかを気にする必要が無くなります。 これが、名前は式を抽象化すると言ったときの意味です。 いつでも式が出てきたら、それは同じ値を持つ名前に置き換えることができます。

抽象化はコードを読み書きしやすくします。 fig. 17 のように箱の列を作る具体例を用いて説明しましょう。

Figure 17: ロイヤルブルーで塗った 5つの箱

Figure 17: ロイヤルブルーで塗った 5つの箱

この絵を作る単一の式を書くことは可能です。

(
  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 を見てください。

Figure 18: アーチェリーのターゲット

Figure 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シーンを作ってみよう。 イメージの部品に名前を付けることによってかなりの繰り返しを省けるはずです。

Figure 19: 町並みの風景

Figure 19: 町並みの風景

これが私たちの解答です。 見ての通り、風景を小さな部品に分けることで比較的小さなコードに収めることができました。

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

OneTwo の両方をインポートする場合。

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 + 12 に置き換えて

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 + 1two で置き換えて以下を得ることができます:

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)

ead に依存して、上から下の順序では ad が評価されていないためうまくいきません。 これは試すのも少し馬鹿げていると思うかもしれません。最終的に評価しようとしている式は e であり、ade のサブ式であるため、当然サブ式は式の前に評価される必要があります。

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.rectangleColor.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 で示すイメージを以下のようなプログラムを使って作りました。

Figure 20: Five boxes filled with Royal Blue

Figure 20: Five boxes filled with Royal Blue

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 コンパイラにとって特別な意味を持つ単語で、この場合メソッドを宣言するという意味を持ちます。 これまでに objectval というキーワードも見てきました。

def の直後にはメソッドの名前が続き、この場合 boxes で、これは valobject がそれぞれ宣言するものの名前が直後に続くのに似ています。 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文字の xvi といった名前がよく出てきます。

ちなみに、これは既に妥当なコードです。 コンソールに入力してみてください。 このように宣言した場合、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)
}

パラメータ x2 に束縛して、

{
  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 構造的再帰

この章では、構造的計算における最初のメジャーなパターンである自然数の構造的再帰をみていきます。ちょっと大げさな感じなので分解してみましょう。

例題を 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 のように箱を一列並べて描いた例から始めましょう。

Figure 21: ロイヤルブルーで塗った 5つの箱

Figure 21: ロイヤルブルーで塗った 5つの箱

まずは手始めに 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つの定義は等価です。 ここでは先にあるイメージを元に後続のイメージを作ることで扱っている構造を強調して、これが構造的再帰への前段階となっています。

このような方法でイメージを書くのは疲れる作業です。 私たちがやりたいのはコンピューターに何らかの方法で描きたい箱の数を伝えることです。 よりテクニカルに言うと、上の式を抽象化したいと言うことができます。 前の章で、メソッドは式を抽象化すると習ったので、この問題を解くのにメソッドを使ってみましょう。

まずはいつも通り、定義したいメソッドの骨組みである、メソッドに入っていくものとメソッドが評価するものから始めましょう。 この場合、私たちは欲しい箱の数として Intcount を提供して、Image を得ます。

def boxes(count: Int): Image =
  ???
// boxes: (count: Int)doodle.core.Image

次に新しいこととして、構造的再帰が来ます。 上で threeBoxestwoBoxes を使って、twoBoxesbox を使って定義できることに気づきました。 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 は以下の性質を常に保つという事ができるでしょう:

最後の 3つの性質は全て同じ一般的な形をしています。 boxes(n) == aBox beside boxes(n - 1) という 1つの性質を使って、それら全ておよび n > 0 全ての場合を記述することができます。

これで、2つの性質だけが残りました。

これらの 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 をコピー・ペーストするのでは無く、練習のために手で書き出してみましょう。

Figure 22: ロイヤルブルーで塗った 3つの積み上げられた箱

Figure 22: ロイヤルブルーで塗った 3つの積み上げられた箱

boxesbesideabove に変えるだけです。

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種類のパターンをみました。

最後に、右辺の式 <expression1> などは、今まで書いてきたのと同じただの式です。 match 式全体は最初にマッチしたパターンの右辺式の値へと評価されます。 そのため、boxes(0) を呼ぶと両方のパターンとマッチしますが (ワイルドカードは何にでもマッチするため)、リテラルパターンが最初に来るため評価されるのは Image.empty の方です。

全ての可能な場合をチェックする match 式は、網羅的マッチ (exhaustive match) と呼ばれます。 count が 0以上であると前提を置けば、boxesmatch は網羅的です。

まずは 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 の場合が基底ケースで、その他の場合が自然数 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

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 にマッチする場合は、右辺の構造も自然数の定義の再帰的ケースの構造に対応します。 定義は n1 + m だと言っています。 右辺では私たちは 1 を aBox に置き換えて、+ を beside に置き換えて、定義も再帰する所では私たちは再帰的に boxesm (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)
  }

で、ResultresultBaseresultUnit そして add は解いている問題に特定のものです。 自然数の構造的再帰パターンを実装するためには

  • 私たちが書いているメソッドが自然数を入力として受け取ることに気づき
  • 結果の型を考えて
  • 結果のための基底、単位 (unit)、そして加算をどうするべきか決める必要があります。

このシンプルですが強力なツールを使って他に何ができるか探検する準備が整いました。

7.4 証明とプログラム

数学を勉強したことがあれば、帰納法を用いた証明を見たことがあるでしょう。 帰納法による証明の大まかなパターンは自然数の構造的再帰の大まかなパターンとよく似ています。 これは偶然ではなく、この 2つには深い関係があります。 私たちは自然数の構造的再帰を帰納法による証明の 1つだとみなすことができます。 構造的再帰の骨組みを使うことで全ての自然数の変換を書くことができるという主張は、暗黙的に私たちが使っている数学的基礎に裏付けられています。 この 2つのつながりを使って私たちのコードの性質を証明することもできます。構造的回帰は暗黙的に何らかの性質の証明を定義します。

この証明とプログラムのつながりはカリー=ハワード同型対応 (Curry-Howard Isomorphism) と呼ばれます。

練習問題

クロス

最初の練習問題はクロスのイメージを生成する cross という関数を作ることです。 fig. 23 は 4つのクロスのイメージを表し、それぞれ cross0 から 3 と共に呼び出した場合に対応します。

メソッドの骨組みは

def cross(count: Int): Image =
  ???
// cross: (count: Int)doodle.core.Image
Figure 23: 0 から 3 の count により生成されたクロス

Figure 23: 0 から 3 の count により生成されたクロス

cross の本文にはどのようなパターンを使うことができるでしょう? パターンを書き出してみよう。

自然数の構造的再帰ですね。以下のようになるはずです。

def cross(count: Int): Image =
  count match {
    case 0 => <resultBase>
    case n => <resultUnit> <add> cross(n-1)
  }

どのパターンを使うかが分かったので、プログラムの特定の部分を埋めていく必要があります:

ヒント: fig. 23 を使って上の要素を探すことができます。

絵から、基底ケースが 1つの円であることが分かります。

絵の後続の要素は絵の上、下、右、左に円を追加しています。そのため、私たちの単位はベースと同じく 1つの円ですが、加法演算子は今までに見たようなただの besideabove では無く、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 =
  ???

count0 から 2 を渡してチェス盤を描いた場合の例を fig. 24 に示しました。 ヒント: count がチェス盤の幅ではなく、原子的な「チェス盤の単位」を返すことに注目してください。

Figure 24: 0 から 2 の count により生成されたチェス盤

Figure 24: 0 から 2 の 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 はシェルピンクスキーの三角ですが)

Figure 25: シェルピンスキーの三角

Figure 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 で示す線に沿って並ぶ次々と大きくなっていく箱のような絵を作ることを考えます。

Figure 26: 再帰のたびにサイズが大きくなる箱

Figure 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 することができます。

Figure 27: ロイヤルブルーから始まって徐々に変化する色で塗られた 5つの箱

Figure 27: ロイヤルブルーから始まって徐々に変化する色で塗られた 5つの箱

解答を実装する 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 のような同心円を描いてみましょう。ここでは、色ではなく各ステップでサイズを変えています。その他はパターンとしては同じようになるはずです。実装してみてください。

Figure 28: ロイヤルブルー色の同心円

Figure 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 で得られるような絵を描いてみましょう。 自分の好みになるまで色々実験してみてください。

Figure 29: 面白いカラーバリエーションの同心円

Figure 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

unitcross メソッドの中で宣言されています。 そのため、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 は非常に小さいのであまり大したことないですが、多くのメモリや時間を取る処理をしている可能性もあり、不必要に繰り返すのは嬉しくないことです。

これは、unitcross の外に出すことで解決できます。

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 内で忘れずに呼ぶ必要があります。 私は、このような内部メソッドはループを行っていることを示すために通常 loopiter (iterate の略) と名付けています。

このテクニックは既に見てきたことの小さなバリエーションですが、いくつかの練習問題を行ってパターンを習得したか確認してみましょう。

練習問題

チェス盤

chessboard をネストしたメソッドを使って書き換えて、blackSquareredSquare そして basechessboard が呼ばれたときに一度だけ作られるようにしましょう。

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 を書き直して、aBoxboxes 内のみのスコープに入り、かつ 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段階に分けて解きます。まずは、aBoxboxes に取り込みます。

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級値はメソッドのパラメータとして渡したり、メソッド呼び出しの結果として返せるものを指します。

もしも私たちが関数を別の関数の引数として渡したら

この用語は特に重要なものではありませんが、他の書物でも見かけると思うので (多少おぼろげでも) 知っておくと役に立つと思います。 最初は何のことか分からないかもしれませんが、例を見ていくうちに分かるようになります。

これまでは関数メソッドという用語を特に区別せずに使ってきました。 Scala では、これら 2つの用語は関連はしますが、別々の意味を持つことを見ていきます。

背景の説明はここまでにして、

を見ていきましょう。これを動機づける例として fig. 30 にような花を描く例を使います。

Figure 30: この章で出てくるテクニックを使って作られた花

Figure 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度おきに点を描いています。 より多くの点を描くことで、形の輪郭である大きな円がハッキリとしてくることが分かります。

Figure 31: 左から右にかけて 90度、45度、22.5度おきに点を描画したパラメトリックな円

Figure 31: 左から右にかけて 90度、45度、22.5度おきに点を描画したパラメトリックな円

パラメトリック曲線を作るには Dooble が点をどう表すのかを習い、イメージを空間の特定の点にレイアウトして、高校以来忘れていた幾何学を復習する必要があります。

8.2

Doodle では、Point 型を用いて 2次元内の場所を表します。これは 2つの等価な表し方があって、

私たちは 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 を特定の点に置くことはできるでしょうか? これまでの所は、イメージを onbesideabove のみを使って配置してきました。 さらに 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 で示したイメージを作ります。

Figure 32: at レイアウトを使って 4つのドットを正方形の 4隅に配置したもの

Figure 32: at レイアウトを使って 4つのドットを正方形の 4隅に配置したもの

at レイアウトをどう使うのか、また何故ドットを on で積み上げる必要があるのかを理解するためには Doodle がレイアウトをどのように行っているのかを理解する必要があります。

Doodle において全ての Image原点 (origin) を持ちます。 ほとんどのイメージの場合はこれはイメージの中央にありますが、そうである必要はありません。 Doodle が複合 Image をレイアウトするときは、原点を合わせています。 例えば、複数の Imageabove を使ってレイアウトする場合、それらの原点は垂直に並び、複合 Image の原点も原点を結んだ線の中点となります。 fig. 33 には、beside を使ったレイアウトにおいて原点 (赤い円) がどう並ぶのかを示した例があります。 そして、on を使った場合は原点は積み上げられていくため、事実上複数のイメージが同じ原点を持つことになります。

Figure 33: 水平レイアウト (beside) を使った例で、原点が並ぶことを示す。

Figure 33: 水平レイアウト (beside) を使った例で、原点が並ぶことを示す。

at を使うことで、Image をその原点から移動させることができます。 ここで見ていく例では、全ての要素が同じ原点を共有してほしいので、at でイメージを動かした後で on を使ってイメージを組み合わせます。

at を呼ぶしには 2通りの方法があります:

toVec を使って PointVec へと変換することができます。

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) の結果を示しています。

Figure 34: sample を使って円状に配置された多くの三角形

Figure 34: sample を使って円状に配置された多くの三角形

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 に示します。

Figure 35: バラ曲線の例

Figure 35: バラ曲線の例

sample を変えて parametricCircle の代わりに rose を呼び出すことも可能ですが、それは少し満足がいきません。 異なるパラメトリック方程式を使って実験してみたいとしたらどうでしょうか? 点を作るメソッド (つまりパラメトリック方程式) を sample へのパラメータとして渡せれば便利です。 これはできるでしょうか? そのためには、以下ができる必要があります:

まずは 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 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 のように書き、このとき AB はパラメータの型で、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 型の値を返すことを意味します。別の言い方をすると、roseFnAnglePoint へと変換します。

関数リテラル

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 で示したイメージを作ってみましょう。

Figure 36: 色と形

Figure 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つのことをする必要があります:

  1. 3つの目標となるイメージに対してそれぞれ適当な singleShape の定義を書くこと

  2. concentricShapes にそれぞれ適当な singleShape を渡して3回呼び出して、その結果を beside を使って並べる

singleShape パラメータの定義を注意して見てみましょう。 このパラメータの型は Int => Image なので、これは Int パラメータを受け取って Image を返す関数です。 この型のメソッドは以下のように宣言できます:

def outlinedCircle(n: Int) =
  Image.circle(n * 10)

このメソッドを関数へと変換して、concentricShapes へ渡して黒い輪郭の同心円を描くことができます:

concentricShapes(10, outlinedCircle _)

これは fig. 37 で示したものを生成します。

Figure 37: 多くの円の輪郭

Figure 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 の出力を見てください。

Figure 38: 多くの円と正方形の輪郭

Figure 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))

しかし、重複したコードがあります。 特に、rainbowCirclerainbowTriangle は同じ定義の 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)
}

イメージで使うプリミティブ図形の選択 (dotImage.triangle) を抽象化したいと思うかもしれません。 locationAngle => 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.iodave@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:

Paths themselves come in two varieties:

The picture in fig. 39 illustrates the components that can make up a path, and shows the difference between open and closed paths.

Figure 39: The same paths draw as open (top) and closed (bottom) paths. Notice how the open triangle is not properly joined at the bottom left, and the closed curve inserts a straight line to close the shape.

Figure 39: The same paths draw as open (top) and closed (bottom) paths. Notice how the open triangle is not properly joined at the bottom left, and the closed curve inserts a straight line to close the shape.

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.

Table 4: How to create the three different types of PathElement.
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.

Figure 40: A triangle, square, and pentagon, defined using paths.

Figure 40: A triangle, square, and pentagon, defined using paths.

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.

Figure 41: A curvy triangle, square, and polygon, defined using paths.

Figure 41: A curvy triangle, square, and polygon, defined using paths.

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

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:

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 type A and a tail of type List[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.

Figure 42: Concentric polygons with pastel gradient fill.

Figure 42: Concentric polygons with pastel gradient fill.

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.

Figure 43: A List[Circle] representing by a circle within a box

Figure 43: A List[Circle] representing by a circle within a box

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.

Figure 44: map shown graphically

Figure 44: map shown graphically

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:

Figure 45: Stars with p=11 and n=1 to 5

Figure 45: Stars with p=11 and n=1 to 5

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)
  }
)

When you’ve finished your row of stars, try constructing a larger image from different values of p and n. There is an example in fig. 46. Hint: You will need to create a method allAbove similar to allBeside.

Figure 46: Stars with p=3 to 33 by 2 and n=1 to p/2

Figure 46: Stars with p=3 to 33 by 2 and n=1 to p/2

To create the image in fig. ¿fig:sequences:stars2? we started by creating a method to style a star.

def style(img: Image, hue: Angle): Image = {
  img.
    lineColor(Color.hsl(hue, 1.normalized, .25.normalized)).
    fillColor(Color.hsl(hue, 1.normalized, .75.normalized))
}

We then created allAbove, which you will notice is very similar to allBeside (wouldn’t it be nice if we could abstract this pattern?)

def allAbove(imgs: List[Image]): Image =
  imgs match {
    case Nil => Image.empty
    case hd :: tl => hd above allAbove(tl)
  }

The updated scene then becomes:

allAbove((3 to 33 by 2).toList map { sides =>
  allBeside((1 to sides/2).toList map { skip =>
    style(star(sides, skip, 20), 360.degrees * skip / sides)
  })
})

10 タートル代数と代数的データ型

In this chapter we explore a new way of creating paths—turtle graphics—and learn some new ways of manipulating lists and functions.

例題を Doodle の sbt console 内で実行した場合は、何もしなくても動作するはずです。そうじゃない場合は、以下の import 文を使って Doodle を使用可能な状態にする必要があります。

import doodle.core._
import doodle.core.Image._
import doodle.syntax._
import doodle.jvm.Java2DFrame._
import doodle.backend.StandardInterpreter._

10.1 Turtle Graphics

So far our paths have used an absolute coordinate system. For example, if we wanted to draw a square we’d use code like

import doodle.core.PathElement._

val path = 
  Image.openPath(
    List(moveTo(10,10), lineTo(-10,10), lineTo(-10,-10), lineTo(10, -10), lineTo(10, 10))
  )

It’s often easier to define paths in terms of relative coordinates, specifying how far we move forward or turn relative to our current location. This is how a turtle graphics system works. Here’s an example.

import doodle.turtle._
import doodle.turtle.Instruction._

// Create a list of instructions for the turtle
val instructions: List[Instruction] = 
  List(forward(10), turn(90.degrees), 
       forward(10), turn(90.degrees), 
       forward(10), turn(90.degrees), 
       forward(10))

// Ask the turtle to draw these instructions, creating an Image
val path: Image = Turtle.draw(instructions)

So where’s the turtle in all this? This model was developed in the 60s by Seymour Papert in the programming language Logo. The original Logo could control a robot that drew on paper with a pen. This robot was called a turtle, due to its rounded shape, and way of programming this robot became known as turtle graphics.

Using turtle graphics and another concept, known as an L-system, we can create images that mimic nature such as the plant in fig. 47.

Figure 47: A plant generated using turtle graphics and an L-system.

Figure 47: A plant generated using turtle graphics and an L-system.

10.2 Controlling the Turtle

Let’s look over the turtle graphics API, and use it to draw a few different images.

10.2.1 Instructions

We control the turtle by giving it instructions. These instructions are defined as methods on the object doodle.turtle.Instruction (similarly to the methods on doodle.core.Image that create images).

We can import the methods and then create instructions.

import doodle.turtle._
import doodle.turtle.Instruction._

forward(10)
// res0: doodle.turtle.Instruction = Forward(10.0)

turn(5.degrees)
// res1: doodle.turtle.Instruction = Turn(Angle(0.08726646259971647))

This doesn’t do anything useful unless we assemble these commands into an image. To do so, we create a list of instructions and then ask the turtle (doodle.turtle.Turtle to be exact) to draw them to an Image.

val instructions = 
  List(forward(10), turn(90.degrees), 
       forward(10), turn(90.degrees), 
       forward(10), turn(90.degrees), 
       forward(10))

val path = Turtle.draw(instructions)

This creates a path—an Image—which we can then draw in the usual way. This gives the output shown in fig. 48. This is not a very exciting image, but we can change color, line width, and so on to create more interesting results.

Figure 48: A square created via the turtle graphics system.

Figure 48: A square created via the turtle graphics system.

The complete list of turtle instructions in given in tbl. 5

Table 5: The instructions understood by the turtle.
Instruction Description Example
forward(distance) Move forward the given distance, specified as a Double. forward(100.0)
turn(angle) Turn the given angle (an Angle) from the current heading. turn(10.degrees)
branch(instruction, ...) Save the current position and heading, draw the given instructions , and then return to the saved position to draw the rest of the instructions. branch(turn(10.degrees), forward(10))
noop Do nothing! noop

Exercises

Polygons

In the previous chapter we wrote a method to create a polygon. Reimplement this method using turtle graphics instead. The method header should be something like

def polygon(sides: Int, sideLength: Double): Image =
 ???

You’ll have to do a bit of geometry to work out the correct turn angle, but as that’s half the fun we won’t spoil it for you.

Here’s our solution. It’s a structural recursion over the natural numbers. The turn angle is exactly the same as the rotation angle used to create polygons in polar coordinates in the previous chapter, though the derivation is quite different.

def polygon(sides: Int, sideLength: Double): Image = {
  val rotation = Angle.one / sides
  def iter(n: Int): List[Instruction] =
    n match {
      case 0 => Nil
      case n => turn(rotation) :: forward(sideLength) :: iter(n-1)
    }

  Turtle.draw(iter(sides))
}

10.2.1.1 The Square Spiral

The square spiral is shown in fig. 49. Write a method to create square spirals using turtle graphics.

This task requires a bit more design work than we usually ask of you. You’ll have to work out how the square spiral is constructed (hint: it starts at the center) and then create a method to draw one.

Figure 49: The square spiral!

Figure 49: The square spiral!

The key insights to draw the square spiral are realising:

Once we have this understood this, the structure is basically the same as drawing a polyon. Here’s our solution.

def squareSpiral(steps: Int, distance: Double, angle: Angle, increment: Double): Image = {
  def iter(n: Int, distance: Double): List[Instruction] = {
    n match {
      case 0 => Nil
      case n => forward(distance) :: turn(angle) :: iter(steps-1, distance + increment)
    }
  }

  Turtle.draw(iter(steps, distance))
}
// squareSpiral: (steps: Int, distance: Double, angle: doodle.core.Angle, increment: Double)doodle.core.Image

Turtles vs Polar Coordinates

We can create polygons in polar coordinates using a Range and map as shown below.

import doodle.core.Point._

def polygon(sides: Int, size: Int): Image = {
  val rotation = Angle.one / sides
  val elts =
    (1 to sides).toList.map { i =>
      PathElement.lineTo(polar(size, rotation * i))
    }
  closedPath(PathElement.moveTo(polar(size, Angle.zero)) :: elts)
}

We cannot so easily write the same method to generate turtle instructions using a Range and map. Why is this? What abstraction are we missing?

Each side of the polygon requires two turtle instructions: a forward and a turn. Thus drawing a pentagon requires ten instructions, and in general n sides requires 2n instructions. Using map we cannot change the number of elements in a list. Therefore mapping 1 to n, as we did int the code above, won’t work. We could map over 1 to (n*2), and on, say, odd numbers move forward and on even numbers turn, but this is rather inelegant. It seems it would be simpler if we had an abstraction like map that allowed us to change the number of elements in the list as well as transform the individual elements.

10.3 Branching Structures

In addition to the standard imports given at the start of the chapter, in this section we’re assuming the following:

import doodle.turtle._
import doodle.turtle.Instruction._

Using the branch turtle instruction we can explore some shapes that are difficult to create without it. The branch instruction takes a List[Instruction]. It saves the current state of the turtle (it’s location and heading), draws the given instructions, and returns the turtle to the saved state.

Consider the code below, which creates the image in fig. 50. This is easy to draw with a branching turtle, but quite involved to create with just a path.

val y = Turtle.draw(List(
          forward(100),
          branch(turn(45.degrees), forward(100)),
          branch(turn(-45.degrees), forward(100)))
        )
// y: doodle.core.Image = OpenPath(List(MoveTo(Cartesian(0.0,0.0)), MoveTo(Cartesian(0.0,0.0)), LineTo(Cartesian(100.0,0.0)), LineTo(Cartesian(170.71067811865476,70.71067811865474)), MoveTo(Cartesian(100.0,0.0)), LineTo(Cartesian(170.71067811865476,-70.71067811865474)), MoveTo(Cartesian(100.0,0.0))))
Figure 50: An image that is easy to create with a branching turtle, and comparatively difficult to create without.

Figure 50: An image that is easy to create with a branching turtle, and comparatively difficult to create without.

Using branching we can model some forms of biological growth, producing, for example, images of plants as in fig. 47. One particular model is known as an L-system. An L-system has consists of two parts:

A specific example of this process is shown in fig. 51. The figure on the left hand side is the seed. The rewrite rules are:

Figure 51: Modelling the growth of a plant using rewrite rules.

Figure 51: Modelling the growth of a plant using rewrite rules.

Concretely, we can write these rules as a transformation on Instruction assuming that we use NoOp to represent a bud.

val stepSize = 10
// stepSize: Int = 10

def rule(i: Instruction): List[Instruction] =
  i match {
    case Forward(_) => List(forward(stepSize), forward(stepSize))
    case NoOp => 
      List(branch(turn(45.degrees), forward(stepSize), noop), 
           branch(turn(-45.degrees), forward(stepSize), noop))
    case other => List(other)
  }
// rule: (i: doodle.turtle.Instruction)List[doodle.turtle.Instruction]

Note how we used pattern matching on Instruction, like we have on the other algebraic data types—natural numbers and List—we’ve seen so far. By importing doodle.turtle.Instruction._ we can access all the patterns for Instruction, which are

As a function, rule has type Instruction => List[Instruction], as we’re potentially transforming each instruction into several instructions (as we do in the case of Forward). Now how can we actually apply this rule to a List[Instruction] to create a List[Instruction] (for example, applying it to List[noop])? Can we use map?

In this case map is not the right solution, as the types tell us. Remember the type equation for map is

List[A] map (A => B) = List[B]

If - we have List[Instruction]; and - we map a function Instruction => List[Instruction]; then - we’ll get a List[List[Instruction]]

as we can see from the type equation.

Our turtle doesn’t know how to draw List[List[Instruction]] so this won’t work.

There is a method flatten on List, which will convert a List[List[A]] to List[A]. We could use a combination of map and flatten but we have a better solution. This pattern comes up enough—and in different contexts which we’ll see later—that there is a method just to handle it. The method is called flatMap.

The type equation for flatMap is

List[A] flatMap (A => List[B]) = List[B]

and this is illustrated graphically in fig. 52. We can see that flatMap has the right type to combine rule with List[Instruction] to create a rewritten List[Instruction].

Figure 52: The type equation for flatMap illustrated graphically.

Figure 52: The type equation for flatMap illustrated graphically.

When discussing map we said that it doesn’t allow us to change the number of elements in the List. Graphically, we can’t create a new “box” using map. With flatMap we can change the box, in the case lists meaning we can change the number of elements in the list.

Exercises

Double

Using flatMap, write a method double that transforms a List to a List where every element appears twice. For example

double(List(1, 2, 3))
// res0: List[Int] = List(1, 1, 2, 2, 3, 3)

double(List("do", "ray", "me"))
// res1: List[String] = List(do, do, ray, ray, me, me)

There are two points to this:

def double[A](in: List[A]): List[A] =
  in.flatMap { x => List(x, x) }

Or Nothing

Using flatMap, write a method nothing that transforms a List to the empty List. For example

nothing(List(1, 2, 3))
// res2: List[Int] = List()

nothing(List("do", "ray", "me"))
// res3: List[String] = List()

We could easily write this method as

def nothing[A](in: List[A]): List[A] =
  List() // or List.empty or Nil

but the point of this exercise is to get more familiarity with using flatMap. With flatMap we can write the method as

def nothing[A](in: List[A]): List[A] =
  in.flatMap { x => List.empty }

Rewriting the Rules

Write a method rewrite with signature

def rewrite(instructions: List[Instruction], 
            rule: Instruction => List[Instruction]): List[Instruction] =
  ???

This method should apply rule to rewrite every instruction in instructions, except for branches which you’ll need to handle specially. If you encounter a branch you should rewrite all the instructions inside the branch but leave the branch alone.

Note: You’ll need to pass a List[Instruction] to branch, while branch itself accepts zero or more instructions (so-called varargs). To convert the List[Instruction] into a form that branch will accept, follow the parameter with :_* like so

val instructions = List(turn(45.degrees), forward(10))
// instructions: List[doodle.turtle.Instruction] = List(Turn(Angle(0.7853981633974483)), Forward(10.0))

branch(instructions:_*)
// res4: doodle.turtle.Instruction.Branch = Branch(List(Turn(Angle(0.7853981633974483)), Forward(10.0)))

There are two parts to this:

The latter is an example of structural recursion, though a slighlty more complex pattern than we’ve seen before.

def rewrite(instructions: List[Instruction], rule: Instruction => List[Instruction]): List[Instruction] =
  instructions.flatMap { i =>
    i match {
      case Branch(i) =>
        List(branch(rewrite(i, rule):_*))
      case other =>
        rule(other)
    }
  }

Your Own L-System

We’re now ready to create a complete L-system. Using rewrite from above, create a method iterate with signature

def iterate(steps: Int,
            seed: List[Instruction], 
            rule: Instruction => List[Instruction]): List[Instruction] =
  ???
// iterate: (steps: Int, seed: List[doodle.turtle.Instruction], rule: doodle.turtle.Instruction => List[doodle.turtle.Instruction])List[doodle.turtle.Instruction]

This should recursively apply rule to seed for steps iterations.

This is just a simple structural recursion of the natural numbers, with all the hard work done by rewrite.

def iterate(steps: Int, 
            seed: List[Instruction], 
            rule: Instruction => List[Instruction]): List[Instruction] =
  steps match {
    case 0 => seed
    case n => iterate(n - 1, rewrite(seed, rule), rule)
  }

Plants and Other Creations

Create the pictures shown in fig. 53 and fig. ¿fig:turtles-koch-curve? using your L-system implementation.

Figure 53: Five iterations of the simple branching L-system.

Figure 53: Five iterations of the simple branching L-system.

Figure 54: Five iterations of the Koch curve, a fractal that is simple to create with an L-System.

Figure 54: Five iterations of the Koch curve, a fractal that is simple to create with an L-System.

10.4 Exercises

10.4.1 Flat Polygon

Using the Turtle methods, Range, and flatMap, rewrite your method to create a polygon. The signature of polygon is

def polygon(sides: Int, sideLength: Double): Image = 
  ???

Using flatMap we can make the code more compact than the explicit structural recursion we had to use before.

def polygon(sides: Int, sideLength: Double): Image = {
  val rotation = Angle.one / sides
  
  Turtle.draw((1 to sides).toList.flatMap { n =>
    List(turn(rotation), forward(sideLength))
  })
}

10.4.2 Flat Spiral

Using the Turtle methods, Range, and flatMap, rewrite your method to create the square spiral. The signature of squareSpiral is

def squareSpiral(steps: Int, distance: Double, angle: Angle, increment: Double): Image =
  ???

Again, the result is more compact than the previous implementation without flatMap. Isthis easier to read? I find it about the same. I belive comprehensibility is a function of familiarity, and we’re (hopefully) by now becoming familiar with flatMap.

def squareSpiral(steps: Int, distance: Double, angle: Angle, increment: Double): Image = {
  Turtle.draw((1 to steps).toList.flatMap { n =>
   List(forward(distance + (n * increment)), turn(angle)) 
  })
}

10.4.3 L-System Art

In this exercise we want you to use your creativity to construct a picture of a natural object using your L-system implementation. You’ve seen many examples already that you can use an inspriation.

11 ジェネレーティブアートの合成

In this chapter we’ll explore techniques from generative art, which will in turn allow us to explore key concepts for functional programming. We’ll see:

Figure 55: An example image generated using the techniques in this chapter

Figure 55: An example image generated using the techniques in this chapter

例題を Doodle の sbt console 内で実行した場合は、何もしなくても動作するはずです。そうじゃない場合は、以下の import 文を使って Doodle を使用可能な状態にする必要があります。

import doodle.core._
import doodle.core.Image._
import doodle.syntax._
import doodle.jvm.Java2DFrame._
import doodle.backend.StandardInterpreter._

11.1 Generative Art

Generative art means art where some part of the composition is determined by an autonomous process. Concretely, for us this will mean adding an element of randomness.

Let’s take a really simple example. We’ve learned previously how to create concentric circles.

def concentricCircles(n: Int): Image =
  n match {
    case 0 => circle(10)
    case n => concentricCircles(n-1) on circle(n * 10) 
  }

(We now know we could write this using a Range and a method like allOn.)

We also learned how we could make coloured circles, using a second parameter.

def concentricCircles(n: Int, color: Color): Image =
  n match {
    case 0 => circle(10) fillColor color
    case n => concentricCircles(n-1, color.spin(15.degrees)) on (circle(n * 10) fillColor color) 
  }

Pictures constructed in this way are nice, but they are a bit boring in their regularity. What if we wanted to make a random alteration to the hue of the color at each step?

Scala provides some methods that produce random numbers. One such method is math.random. Each time we call it we get a different Double between 0.0 and 1.06.

math.random
// res0: Double = 0.7633332536466686

math.random
// res1: Double = 0.8467732588039283

Given math.random we could produce a method that returns a random Angle like so.

def randomAngle: Angle = 
  math.random.turns
// randomAngle: doodle.core.Angle

randomAngle
// res2: doodle.core.Angle = Angle(5.818661772515933)

randomAngle
// res3: doodle.core.Angle = Angle(5.197974039590069)

Why might we not want to do this? What principle does this break?

Generating random numbers in this way breaks substitution. Remember substitution says wherever we see an expression we should be able to substitute the value it evaluates to without changing the meaning of the program. Concretely, this means

val result1 = randomAngle
// result1: doodle.core.Angle = Angle(6.056941728992091)

val result2 = randomAngle
// result2: doodle.core.Angle = Angle(1.125961644411296)

and

val result1 = randomAngle
// result1: doodle.core.Angle = Angle(1.807376084798579)

val result2 = result1
// result2: doodle.core.Angle = Angle(1.807376084798579)

should be the same program and clearly they are not.

What should we do? Suffer the slings and arrows of outrageous computational models, or take arms against a sea of side-effects, and by opposing end them! There’s really only one choice.

11.2 Randomness without Effect

The solution to our problem is to separate describing how we’ll use random numbers from the process of actually generating them. This sounds complicated, but it’s exactly what we’ve be doing with Image throughout this book. We

We do the same thing with Doodle’s Random type. To access this code we first need to import the doodle.random package.

import doodle.random._

Now we can create values that describe creating a random number

val randomDouble = Random.double
// randomDouble: doodle.random.Random[Double] = Free(...)

No random numbers are actually created until we call the run method.

randomDouble.run
// res0: Double = 0.8423558052926786

The type Random[Double] indicates we have something that will produce a random Double when we run it. Just like with Image and draw, substitution holds with Random up until the point we call run.

Table tbl. 6 shows some of the ways to construct Random values.

Table 6: Some of the methods to create Random values.
Method Description Example
Random.always(value) Creates a Random that always produces the given value. Random.always(10)
Random.double Creates a Random that generates a Double uniformly distributed between 0.0 and 1.0. Random.double
Random.int Creates a Random that generates an Int uniformly distributed across the entire range. Random.int
Random.natural(limit) Creates a Random that generates a Int uniformly distributed in the range greater than or equal to 0 and less than 1. Random.natural(10)
Random.oneOf(value, ...) Creates a Random that generates one of the given values with equal chance. Random.oneOf("A", "B", "C")

11.2.1 Composing Random

Now we’ve seen how to create a Random, how do we compose them into more interesting programs? For example, how could we turn a random Double into a random Angle? It might be tempting to call run every time we want to manipulate a random result, but this will break substitution and is exactly what we’re trying to avoid.

Remember when we talked about map in the previous chapter we said it transforms the elements but keeps the structure (number of elements) in the List. The same analogy applies to the map method on Random. It lets us transform the element of a Random—the value it produces when it is run—but doesn’t let us change the structure. Here the “structure” means introducing more randomness, or making a random choice.

We can create a random value and apply a deterministic transformation to it using map, but we can’t create a random value and them use that value as input to a process that creates another random value.

Here’s how we can create a random angle.

val randomAngle: Random[Angle] =
  Random.double.map(x => x.turns)

When we run RandomAngle we can generate randomly created Angle

randomAngle.run
// res1: doodle.core.Angle = Angle(0.13176413017842567)

randomAngle.run
// res2: doodle.core.Angle = Angle(0.9994744431192187)

Exercises

Random Colors

Given randomAngle above, create a method that accepts saturation and lightness and generates a random color. Your method should have the signature

def randomColor(s: Normalized, l: Normalized): Random[Color] =
  ???

This is a deterministic transformation of the output of randomAngle, so we can implement it using map.

def randomColor(s: Normalized, l: Normalized): Random[Color] =
  randomAngle map (hue => Color.hsl(hue, s, l))

Random Circles

Write a method that accepts a radius and a Random[Color], and produces a circle of the given radius and filled with the given random color. It should have the signature

def randomCircle(r: Double, color: Random[Color]): Random[Image] =
  ???

Once again this is a deterministic transformation of the random color, so we can use map.

def randomCircle(r: Double, color: Random[Color]): Random[Image] =
  color map (fill => Image.circle(r) fillColor fill)

11.3 Combining Random Values

In addition to the standard imports given at the start of the chapter, in this section we’re assuming the following:

import doodle.random._

So far we’ve seen how to represent functions generating random values using the Random type, and how to make deterministic transformations of a random value using map. In this section we’ll see how we can make a random (or stochastic, if you prefer fancier words) transformation of a random value using flatMap.

To motivate the problem lets try writing randomConcentricCircles, which will generate concentric circles with randomly chosen hue using the utility methods we developed in the previous section.

We start with the code to create concentric circles with deterministic colors and the utilities we developed previously.

def concentricCircles(count: Int, size: Int, color: Color): Image =
  count match {
    case 0 => Image.empty
    case n => 
      Image.circle(size).fillColor(color) on concentricCircles(n-1, size + 5, color.spin(15.degrees))
  }
  
def randomAngle: Random[Angle] =
  Random.double.map(x => x.turns)

def randomColor(s: Normalized, l: Normalized): Random[Color] =
  randomAngle map (hue => Color.hsl(hue, s, l))
  
def randomCircle(r: Double, color: Random[Color]): Random[Image] =
  color map (fill => Image.circle(r) fillColor fill)

Let’s create a method skeleton for randomConcentricCircles.

def randomConcentricCircles(count: Int, size: Int): Random[Image] =
  ???

The important change here is we return a Random[Image] not an Image. We know this is a structural recursion over the natural numbers so we can fill out the body a bit.

def randomConcentricCircles(count: Int, size: Int): Random[Image] =
  count match {
    case 0 => ???
    case n => ???
  }

The base case will be Random.always(Image.empty), the direct of equivalent of Image.empty in the deterministic case.

def randomConcentricCircles(count: Int, size: Int): Random[Image] =
  count match {
    case 0 => Random.always(Image.empty)
    case n => ???
  }

What about the recursive case? We could try using

val randomPastel = randomColor(0.7.normalized, 0.7.normalized)

def randomConcentricCircles(count: Int, size: Int): Random[Image] =
  count match {
    case 0 => Image.empty
    case n => 
      randomCircle(size, randomPastel) on randomConcentricCircles(n-1, size + 5)
  }
// <console>:35: error: type mismatch;
//  found   : doodle.core.Image
//  required: doodle.random.Random[doodle.core.Image]
//     (which expands to)  cats.free.Free[doodle.random.RandomOp,doodle.core.Image]
//            case 0 => Image.empty
//                            ^
// <console>:37: error: value on is not a member of doodle.random.Random[doodle.core.Image]
//              randomCircle(size, randomPastel) on randomConcentricCircles(n-1, size + 5)
//                                               ^

but this does not compile. Both randomConcentricCircles and randomCircle evaluate to Random[Image]. There is no method on on Random[Image] so this code doesn’t work.

Since this is a transformation of two Random[Image] values, it seems like we need some kind of method that allows us to transform two Random[Image], not just one like we can do with map.
We might call this method map2 and we could imagine writing code like

randomCircle(size, randomPastel).map2(randomConcentricCircles(n-1, size + 5)){
  (circle, circles) => circle on circles
}

Presumably we’d also need map3, map4, and so on. Instead of these special cases we can use flatMap and map together.

randomCircle(size, randomPastel) flatMap { circle =>
  randomConcentricCircles(n-1, size + 5) map { circles =>
    circle on circles
  }
}

The complete code becomes

def randomConcentricCircles(count: Int, size: Int): Random[Image] =
  count match {
    case 0 => Random.always(Image.empty)
    case n => 
      randomCircle(size, randomPastel) flatMap { circle =>
        randomConcentricCircles(n-1, size + 5) map { circles =>
          circle on circles
        }
      }
  }

Example output is shown in fig. 56.

Figure 56: The output of one run of randomConcentricCircles(10, 10).run.draw

Figure 56: The output of one run of randomConcentricCircles(10, 10).run.draw

Let’s now look closer at this use of flatMap and map to understand how this works.

11.3.1 Type Algebra

The simplest way, in my opinion, to understand why this code works is to look at the types. The code in question is

randomCircle(size, randomPastel) flatMap { circle =>
  randomConcentricCircles(n-1, size + 5) map { circles =>
    circle on circles
  }
}

Starting from the inside, we have

{ circles =>
    circle on circles
}

which is a function with type

Image => Image

Wrapping this we have

randomConcentricCircles(n-1, size + 5) map { circles =>
    circle on circles
  }

We known randomConcentricCircles(n-1, size + 5) has type Random[Image]. Substituting in the Image => Image type we worked out above we get

Random[Image] map (Image => Image)

Now we can deal with the entire expression

randomCircle(size, randomPastel) flatMap { circle =>
  randomConcentricCircles(n-1, size + 5) map { circles =>
    circle on circles
  }
}

randomCircle(size, randomPastel) has type Random[Image]. Performing substitution again gets us a type equation for the entire expression.

Random[Inage] flatMap (Random[Image] map (Image => Image))

Now we can apply the type equations for map and flatMap that we saw earlier:

F[A] map (A => B) = F[B]
F[A] flatMap (A => F[B]) = F[B]

Working again from the inside out, we first use the type equation for map which simplifies the type expression to

Random[Inage] flatMap (Random[Image])

Now we can apply the equation for flatMap yielding just

Random[Image]

This tells us the result has the type we want. Notice that we’ve been performing substitution at the type level—the same technique we usually use at the value level.

Exercises

Don’t forget to import doodle.random._ when you attempt these exercises.

Randomness and Randomness

What is the difference between the output of programOne and programTwo below? Why do they differ?

def randomCircle(r: Double, color: Random[Color]): Random[Image] =
  color map (fill => Image.circle(r) fillColor fill)

def randomConcentricCircles(count: Int, size: Int): Random[Image] =
  count match {
    case 0 => Random.always(Image.empty)
    case n => 
      randomCircle(size, randomPastel) flatMap { circle =>
        randomConcentricCircles(n-1, size + 5) map { circles =>
          circle on circles
        }
      }
  }

val circles = randomConcentricCircles(5, 10)
val programOne = 
  circles flatMap { c1 => 
    circles flatMap { c2 => 
      circles map { c3 => 
        c1 beside c2 beside c3
      } 
    }
  }

val programTwo =
  circles map { c => c beside c beside c }

programOne displays three different circles in a row, while programTwo repeats the same circle three times. The value circles represents a program that generates an image of randomly colored concentric circles. Remember map represents a deterministic transform, so the output of programTwo must be the same same circle repeated thrice as we’re not introducing new random choices. In programOne we merge circle with itself three times. You might think that the output should be only one random image repeated three times, not three, but remember Random preserves substitution. We can write programOne equivalently as

val programOne = 
  randomConcentricCircles(5, 10) flatMap { c1 => 
    randomConcentricCircles(5, 10) flatMap { c2 => 
      randomConcentricCircles(5, 10) map { c3 => 
        c1 beside c2 beside c3
      } 
    }
  }
// programOne: cats.free.Free[doodle.random.RandomOp,doodle.core.Image] = Free(...)

which makes it clearer that we’re generating three different circles.

Colored Boxes

Let’s return to a problem from the beginning of the book: drawing colored boxes. This time we’re going to make the gradient a little more interesting, by making each color randomly chosen.

Recall the basic structural recursion for making a row of boxes

def rowOfBoxes(count: Int): Image =
  count match {
    case 0 => rectangle(20, 20)
    case n => rectangle(20, 20) beside rowOfBoxes(n-1)
  }
// rowOfBoxes: (count: Int)doodle.core.Image

Let’s alter this, like with did with concentric circles, to have each box filled with a random color. Hint: you might find it useful to reuse some of the utilities we created for randomConcentricCircles. Example output is shown in fig. 57.

Figure 57: Boxes filled with random colors.

Figure 57: Boxes filled with random colors.

This code uses exactly the same pattern as randomConcentricCircles.

val randomAngle: Random[Angle] =
  Random.double.map(x => x.turns)
// randomAngle: doodle.random.Random[doodle.core.Angle] = Free(...)

val randomColor: Random[Color] =
  randomAngle map (hue => Color.hsl(hue, 0.7.normalized, 0.7.normalized))
// randomColor: doodle.random.Random[doodle.core.Color] = Free(...)

def coloredRectangle(color: Color): Image =
  rectangle(20, 20) fillColor color
// coloredRectangle: (color: doodle.core.Color)doodle.core.Image

def randomColorBoxes(count: Int): Random[Image] =
  count match {
    case 0 => randomColor map { c => coloredRectangle(c) }
    case n =>
      val box = randomColor map { c => coloredRectangle(c) }
      val boxes = randomColorBoxes(n-1)
      box flatMap { b =>
        boxes map { bs => b beside bs }
      }
  }
// randomColorBoxes: (count: Int)doodle.random.Random[doodle.core.Image]

11.4 Exploring Random

So far we’ve seen only the very basics of using Random. In this section we’ll see more of its features, and use these features to create more interesting pictures.

In addition to the standard imports given at the start of the chapter, in this section we’re assuming the following:

import doodle.random._

11.4.1 Normal Distributions

Often when using random numbers in generative art we will choose specific distributions for the shape they provide. For example, fig. 58 shows a thousand random points generated using a uniform, normal (or Gaussian) distribution, and a squared normal distribution respectively.

Figure 58: Points distributed according to uniform, normal, and squared normal distributions

Figure 58: Points distributed according to uniform, normal, and squared normal distributions

As you can see, the normal distribution tends to generate more points nearer the center than the uniform distribution.

Doodle provides two methods to create normally distributed numbers, from which we can create many other distributions. A normal distribution is defined by two parameters, it’s mean, which specifies the center of the distribution, and it’s standard deviation, which determines the spread of the distribution. The corresponding methods in Doodle are

11.4.2 Structured Randomness

We’ve gone from very structured to very random pictures. It would be nice to find a middle ground that incorporates elements of randomness and structure. We can use flatMap to do this—with flatMap we can use one randomly generated value to create another Random value. This creates a dependency between values—the prior random value has an influence on the next one we generate.

For example, we can create a method that given a color randomly perturbs it.

def nextColor(color: Color): Random[Color] = {
  val spin = Random.normal(15.0, 10.0)
  spin map { s => color.spin(s.degrees) }
}

Using nextColor we can create a series of boxes with a gradient that is partly random and partly structured: the next color in the gradient is a random perturbation of the previous one.

def coloredRectangle(color: Color, size: Int): Image =
  rectangle(size, size).
    lineWidth(5.0).
    lineColor(color.spin(30.degrees)).
    fillColor(color)

def randomGradientBoxes(count: Int, color: Color): Random[Image] =
  count match {
    case 0 => Random.always(Image.empty)
    case n =>
      val box = coloredRectangle(color, 20)
      val boxes = nextColor(color) flatMap { c => randomGradientBoxes(n-1, c) }
      boxes map { b => box beside b }
  }

Example output is shown in fig. 59.

Figure 59: Boxes filled with gradient that is partly random.

Figure 59: Boxes filled with gradient that is partly random.

Exercises

Particle Systems

A particle system is a technique used in computer graphics to create large numbers of “particles” that move according to simple rules. In fig. 60 there is an example of a particle system simulating a fire and smoke. For the mathematically inclined, a particle system is basically a stochastic process or random walk.

Figure 60: A simulation of a smoky fire, generating using a particle system.

Figure 60: A simulation of a smoky fire, generating using a particle system.

In this exercise we’ll build a particle system, which will give you a flexible system to experiment with these ideas. We’ll start with a fixed system and then abstract it to create reusable components.

Here’s a sketch of how a particle system works. To draw a single particle we

A particle system is just a collection of a number of particles—20 particles over 20 steps in fig. 60.

In the above description we’ve broken down the components of a partcile system. Now we just need to implement them.

The starting position can be any Random[Point]. Create one now.

This will do. You can create a more complicated (and interesting) distribution over starting position if you want.

val start = Random.always(Point.zero)

Let’s implement a method step that will take a single step in particle system. It will have skeleton

def step(current: Point): Random[Point] =
  ???

We need to decide how we will modify the current point to create the next point. I suggest adding some random noise, and a constant “drift” that will ensure the points tend to move in a particular direction. For example, we could increment the x coordinate by 10, which will cause a drift towards the right of the screen, plus some normally distributed noise to the x and y coordinates.

I’ve chosen to use normally distributed noise that is the same in both directions. Changing the noise will change the shape of the result—it’s worth playing around with different settings.

def step(current: Point): Random[Point] = {
  val drift = Point(current.x + 10, current.y)
  val noise =
    Random.normal(0.0, 5.0) flatMap { x =>
      Random.normal(0.0, 5.0) map { y =>
        Vec(x, y)
      }
    }

  noise.map(vec => drift + vec)
}

Now that we can step a particle we need to connect a sequence of steps to get a walk. There is one wrinkle here: we want to draw the intermediate stages so we’re going to define two methods:

The skeletons are

def render(point: Point): Image =
  ???

def walk(steps: Int): Random[Image] =
  ???

The implementation of render can be whatever you fancy. In the implementation of walk, you will have to call step to get the next Point, and then call render to convert the point to something that can be draw. You will also want to have an accumulator of the Image so far. Hint: you might find it useful to define an auxiliary parameter for walk.

In my definition of render I’ve shown how we can use information from the point to modify the shape in an interesting way.

The definition of walk is a structural recursion over the natural numbers with an internal accumulator and the recursion going through flatMap.

def render(point: Point): Image = {
  val length = (point - Point.zero).length
  val sides = (length / 20).toInt + 3
  val hue = (length / 200).turns
  val color = Color.hsl(hue, 0.7.normalized, 0.5.normalized)
  Image.
    star(sides, 5, 3, 0.degrees).
    noFill.
    lineColor(color).
    at(point.toVec)
}

def walk(steps: Int): Random[Image] = {
  def loop(count: Int, current: Point, image: Image): Random[Image] = {
    count match {
      case 0 => Random.always(image on render(current))
      case n =>
        val next = step(current)
        next flatMap { pt =>
          loop(count - 1, pt, image on render(current))
        }
    }
  }

  start flatMap { pt => loop(steps, pt, Image.empty) }
}

Now you should be able to call walk and render the result.

The final step is create a number of particles and render them all. Create a method particleSystem with skeleton

def particleSystem(particles: Int, steps: Int): Random[Image] =
  ???

that does just this.

Once again we have a structural recursion over the natural numbers. Unlike walk the recursion goes through map, not flatMap. This is because particleSystem adds no new random choices.

def particleSystem(particles: Int, steps: Int): Random[Image] = {
  particles match {
    case 0 => Random.always(Image.empty)
    case n => walk(steps) flatMap { img1 =>
      particleSystem(n-1, steps) map { img2 =>
        img1 on img2
      }
    }
  }
}

Now render the result, and tweak it till you have something you’re happy with. I’m not particulary happy with the result of my code. I think the stars are too bunched up, and the colors are not very interesting. To make a more interesting result I’d consider adding more noise and changing the start color and perhaps compressing the range of colors.

Random Abstractions

The implementation of particleSystem above hard-codes in a particular choice of particle system. To make it easier to experiment with we might like to abstract over the particular choice of walk and start. How do you think we could do this?

We could make walk start, and render parameters to particleSystem, and make start and render parameters to walk.

Implement this.

If we add parameters with the correct name and type the code changes required are minimal. This is like doing the opposite of substitution—lifting concrete representations out of our code and replacing them with method parameters.

def walk(
  steps: Int,
  start: Random[Point],
  render: Point => Image
): Random[Image] = {
  def loop(count: Int, current: Point, image: Image): Random[Image] = {
    count match {
      case 0 => Random.always(image on render(current))
      case n =>
        val next = step(current)
        next flatMap { pt =>
          loop(count - 1, pt, image on render(current))
        }
    }
  }

  start flatMap { pt => loop(steps, pt, Image.empty) }
}

def particleSystem(
  particles: Int,
  steps: Int,
  start: Random[Point],
  render: Point => Image,
  walk: (Int, Random[Point], Point => Image) => Random[Image]
): Random[Image] = {
  particles match {
    case 0 => Random.always(Image.empty)
    case n => walk(steps, start, render) flatMap { img1 =>
      particleSystem(n-1, steps, start, render, walk) map { img2 =>
        img1 on img2
      }
    }
  }
}

This code doesn’t make me happy. Most of the parameters to particleSystem are only needed to pass on to walk. These parameters don’t change is any way within the structural recursion that makes up the body of particleSystem. At this point we can apply our principle of substitution—we can replace a method call with the value it evaluates to—to remove walk and associated parameters from particleSystem.

def particleSystem(particles: Int, walk: Random[Image]): Random[Image] = {
  particles match {
    case 0 => Random.always(Image.empty)
    case n => walk flatMap { img1 =>
      particleSystem(n-1, walk) map { img2 =>
        img1 on img2
      }
    }
  }
}

If you’re used to programming in imperative languages this may seem mind-bending. Remember that we’ve gone to some lengths to ensure that working with random numbers obeys substitution, up to the point that run is called. The walk method doesn’t actually create a random walk. It instead describes how to create a random walk when that code is actually run. This separation between description and action means that substitution can be used. The description of how to perform a random walk can be used to create many different random walks.

11.5 For Comprehensions

In addition to the standard imports given at the start of the chapter, in this section we’re assuming the following:

import doodle.random._

Scala provides some special syntax, called a for comprehension, that makes it simpler to write long sequences of flatMap and map.

For example, the code for randomConcentricCircles has a call to flatMap and map.

def randomConcentricCircles(count: Int, size: Int): Random[Image] =
  count match {
    case 0 => Random.always(Image.empty)
    case n => 
      randomCircle(size, randomPastel) flatMap { circle =>
        randomConcentricCircles(n-1, size + 5) map { circles =>
          circle on circles
        }
      }
  }

This can be replaced with a for comprehension.

def randomConcentricCircles(count: Int, size: Int): Random[Image] =
  count match {
    case 0 => Random.always(Image.empty)
    case n => 
      for {
        circle  <- randomCircle(size, randomPastel) 
        circles <- randomConcentricCircles(n-1, size + 5)
      } yield circle on circles 
  }

The for comprehension is often easier to read than direct use of flatMap and map.

A general for comprehension

for {
  x <- a
  y <- b
  z <- c
} yield e

translates to:

a.flatMap(x => b.flatMap(y => c.map(z => e)))

Which is to say that every <-, except the last, turns into a flatMap, and the last <- becomes a map.

For comprehensions are translated by the compiler into uses of flatMap and map. There is no magic going on. It is just a different way of writing code that would use flatMap and map that avoids excessive nesting.

Note that the for comprehension syntax is more flexible than what we have presented here. For example, you can drop the yield keyword from a for comprehension and the code will still compile. It just won’t return a result. We’re not going to use any of these extensions in Creative Scala, however.

11.6 Exercises

11.6.1 Scatter Plots

In this exercise we’ll implement scatter plots as in fig. 58. Experiment with different distributions (trying creating your own distributions by transforming ones defined on Random).

There are three main components of a scatter plot:

We tackle each task in turn.

Start by writing a method makePoint that will accept a Random[Double] for the x and y coordinates of a point and return a Random[Point]. It should have the following skeleton:

def makePoint(x: Random[Double], y: Random[Double]): Random[Point] =
  ???

Use a for comprehension in your implementation.

This is a nice example of composition of Randoms.

def makePoint(x: Random[Double], y: Random[Double]): Random[Point] =
  for {
    theX <- x
    theY <- y
  } yield Point.cartesian(theX, theY)
// makePoint: (x: doodle.random.Random[Double], y: doodle.random.Random[Double])doodle.random.Random[doodle.core.Point]

Now create, say, a thousand random points using the techniques we learned in the previous chapter on lists and a random distribution of your choice. You should end up with a List[Random[Point]].

Something like the following should work.

val normal = Random.normal(50, 15)
val normal2D = makePoint(normal, normal)

val data = (1 to 1000).toList.map(_ => normal2D)

Let’s now transform our List[Random[Point]] into List[Random[Image]]. Do this in two steps: first write a method to convert a Point to an Image, then write code to convert List[Random[Point]] to List[Random[Image]].

We can convert a Point to an Image using a method point below. Note I’ve made each point on the scatterplot quite transparent—this makes it easier to see where a lot of points are grouped together.

def point(loc: Point): Image =
  circle(2).fillColor(Color.cadetBlue.alpha(0.3.normalized)).noLine.at(loc.toVec)

Converting between the lists is just a matter of calling map a few times.

val points = data.map(r => r.map(point _))

Now create a method that transforms a List[Random[Image]] to a Random[Image] by placing all the points on each other. This is the equivalent of the allOn method we’ve developed previously, but it now works with data wrapped in Random.

You might recognise this pattern. It’s what we used in allOn with the addition of flatMap, which is exactly what randomConcentricCircles (and many other examples) use.

def allOn(points: List[Random[Image]]): Random[Image] =
  points match {
    case Nil => Random.always(Image.empty)
    case img :: imgs => 
      for {
        i  <- img
        is <- allOn(imgs)
      } yield (i on is)
  }

Now put it all together to make a scatter plot.

This is just calling methods and using values we’ve already defined.

val plot = allOn(points)

11.6.2 Parametric Noise

In this exercise we will combine parametric equations, from a previous chapter, with randomness.

Let’s start by making a method perturb that adds random noise to a Point. The method should have skeleton

def perturb(point: Point): Random[Point] =
  ???

Choose whatever noise function you like.

Here’s our solution. We’ve already seen very similar code in the scatter plot.

def perturb(point: Point): Random[Point] =
  for {
    x <- Random.normal(0, 10)
    y <- Random.normal(0, 10)
  } yield Point.cartesian(point.x + x, point.y + y) 

Now create a parametric function, like we did in a previous chapter. You could use the rose function (the function we explored previously) or you could create one of your own devising. Here’s the definition of rose.

def rose(k: Int): Angle => Point =
  (angle: Angle) => {
    Point.cartesian((angle * k).cos * angle.cos, (angle * k).cos * angle.sin)
  }

We can combine our parametric function and perturb to create a method with type Angle => Random[Point]. You can write this easily using the andThen method on functions, or you can write this out the long way. Here’s a quick example of andThen showing how we write the fourth power in terms of the square.

val square = (x: Double) => x * x
val quartic = square andThen square

Writing this with andThen is nice and neat.

def perturbedRose(k: Int): Angle => Random[Point] =
  rose(k) andThen perturb

Now using allOn create a picture that combines randomnes and structure. Be as creative as you like, perhaps adding color, transparency, and other features to your image.

Here’s the code we used to create [#fig:generative:volcano]. It’s quite a bit larger than code we’ve seen up to this point, but you should understand all the components this code is built from.

object ParametricNoise {
  def rose(k: Int): Angle => Point =
    (angle: Angle) => {
      Point.cartesian((angle * k).cos * angle.cos, (angle * k).cos * angle.sin)
    }

  def scale(factor: Double): Point => Point =
    (pt: Point) => {
      Point.polar(pt.r * factor, pt.angle)
    }

  def perturb(point: Point): Random[Point] =
    for {
      x <- Random.normal(0, 10)
      y <- Random.normal(0, 10)
    } yield Point.cartesian(point.x + x, point.y + y) 

  def smoke(r: Normalized): Random[Image] = {
    val alpha = Random.normal(0.5, 0.1) map (a => a.normalized)
    val hue = Random.double.map(h => (h * 0.1).turns)
    val saturation = Random.double.map(s => (s * 0.8).normalized)
    val lightness = Random.normal(0.4, 0.1) map (a => a.normalized)
    val color =
      for {
        h <- hue
        s <- saturation
        l <- lightness
        a <- alpha
      } yield Color.hsla(h, s, l, a)
    val c = Random.normal(5, 5) map (r => circle(r))
    
    for {
      circle <- c
      line   <- color
    } yield circle.lineColor(line).noFill
  }

  def point(
    position: Angle => Point,
    scale: Point => Point,
    perturb: Point => Random[Point],
    image: Normalized => Random[Image],
    rotation: Angle
  ): Angle => Random[Image] = {
    (angle: Angle) => {
      val pt = position(angle)
      val scaledPt = scale(pt)
      val perturbed = perturb(scaledPt)

      val r = pt.r.normalized
      val img = image(r)

      for {
        i  <- img
        pt <- perturbed
      } yield (i at pt.toVec.rotate(rotation))
    }
  }

  def iterate(step: Angle): (Angle => Random[Image]) => Random[Image] = {
    (point: Angle => Random[Image]) => {
      def iter(angle: Angle): Random[Image] = {
        if(angle > Angle.one)
          Random.always(Image.empty)
        else
          for {
            p  <- point(angle)
            ps <- iter(angle + step)
          } yield (p on ps)
      }

      iter(Angle.zero)
    }
  }

  val image: Random[Image] = {
    val pts =
      for(i <- 28 to 360 by 39) yield {
        iterate(1.degrees){
          point(
            rose(5),
            scale(i),
            perturb _,
            smoke _,
            i.degrees
          )
        }
      }
    val picture = pts.foldLeft(Random.always(Image.empty)){ (accum, img) =>
      for {
        a <- accum
        i <- img
      } yield (a on i)
    }
    val background = (rectangle(650, 650) fillColor Color.black)

    picture map { _ on background }
  }
}

12 自分の代数的データ型

In this chapter we’ll learn how to create our own algebraic data types, and bring together all the tools we’ve seen far.

So far in Creative Scala we’ve used (algebraic) data types provided by Scala or Doodle, such as List and Point. In this section we’ll learn how to create our own algebraic data types in Scala, opening up new possibilities for the type of programs we can write.

例題を Doodle の sbt console 内で実行した場合は、何もしなくても動作するはずです。そうじゃない場合は、以下の import 文を使って Doodle を使用可能な状態にする必要があります。

import doodle.core._
import doodle.core.Image._
import doodle.syntax._
import doodle.jvm.Java2DFrame._
import doodle.backend.StandardInterpreter._

12.1 Algebraic Data Types

We’ve used algebraic data types throughout Creative Scala, but we’ve been a bit informal in how we describe them. At this stage a bit more rigour is useful.

An algebraic data type is built from two components: - logical ors; and - logical ands.

The List data type is a great example of an algebraic data type, as it uses both patterns. A List is Nil or a pair (the logical or pattern) and a pair has a head and a tail (the logical and pattern). Point is another example. A Point is either Cartesian or Polar. A Cartesian has an x and y coordinate, while a Polar has a radius and an angle. Note it’s not necessary to use both patterns to be an algebraic data type.

Being functional programmers we naturally have some fancy words for the logical or and logical and patterns. They are: - a sum type for the logical or; and - a product type for the logical and.

The concept of an algebraic data type is not specific to Scala. Let’s get some practice working with the concept before we see how to write algebraic data types in Scala.

Exercises

Path Elements

The PathElement type, used to construct paths, is a simple algebraic data type. You’ve used PathElement quite a bit so far. How do you think PathElement is defined in terms of sum and product types?

A PathElement is a sum type, as it is: - a MoveTo; or - a LineTo; or - a CurveTo.

A MoveTo is a product type that holds a single point (where to move to).

A LineTo is a product type that holds a single point (the end point of the line).

A CurveTo is a product type that holds three points: two control points and the end point of the line.

Totally Turtles

The Instruction type we used to control the turtle is also an algebraic data type. How do you think Instruction is defined?

An Instruction is: - a Forward; or - a Turn; or - a Branch; or - a NoOp

Therefore Instruction is a sum type. Forward, Turn, and Branch are all product types.

A Forward holds a distance, which is a Double.

A Turn holds an angle, which is an Angle.

A Branch holds a List[Instruction]—therefore the Instruction type is defined in terms of itself, just like List.

A NoOp holds no data.

12.1.1 Defining Algebraic Data Types

No we understand how to model data with algebraic data types, let’s see how to define our own.

The pattern is this:

sealed abstract class A extends Product with Serializable
// defined class A

final case class B() extends A
// defined class B

final case class C() extends A
// defined class C

There is a lot boilerplate here, which we can basically ignore beyond accepting it’s stuff we have to write. However, if you’re interested in what it means (and possibly have some prior object-oriented programming experience).

Describe sealed etc. here.

To define PathElement we might start with

sealed abstract class PathElement extends Product with Serializable
// defined class PathElement
// warning: previously defined object PathElement is not a companion to class PathElement.
// Companions must be defined together; you may wish to use :paste mode for this.

final case class MoveTo() extends PathElement
// defined class MoveTo

final case class LineTo() extends PathElement
// defined class LineTo

final case class CurveTo() extends PathElement
// defined class CurveTo

The other half of the pattern is

final case class A(b: B, c: C)

Describe constructor parameters here.

Returning to PathElement, MoveTo and LineTo each have a point (the destination) and CurveTo has a destination point and two control points. So we could write.

sealed abstract class PathElement extends Product with Serializable
final case class MoveTo(to: Point) extends PathElement
final case class LineTo(to: Point) extends PathElement
final case class CurveTo(cp1: Point, cp2: Point, to: Point) extends PathElement

And this is essentially how PathElement is defined in Doodle.

Exercise

Define your own algebraic data type to represent Instruction.

We can directly turn the textual description into code using the patterns above.

sealed abstract class Instruction extends Product with Serializable
// defined class Instruction

final case class Forward(distance: Double) extends Instruction
// defined class Forward

final case class Turn(angle: Angle) extends Instruction
// defined class Turn

final case class Branch(instructions: List[Instruction]) extends Instruction
// defined class Branch

final case class NoOp() extends Instruction
// defined class NoOp

12.2 Build Your Own Turtle

Here’s the Instruction type we defined in the previous section.

sealed abstract class Instruction extends Product with Serializable
// defined class Instruction

final case class Forward(distance: Double) extends Instruction
// defined class Forward

final case class Turn(angle: Angle) extends Instruction
// defined class Turn

final case class Branch(instructions: List[Instruction]) extends Instruction
// defined class Branch

final case class NoOp() extends Instruction
// defined class NoOp

Now we’ve defined our own Instruction type, let’s go one further and create our own Turtle. To complete our turtle we need to implement draw. We can start with

object Turtle {
  def draw(instructions: List[Instruction]): Image =
    ???
}
// defined object Turtle

Instruction is an algebraic data type, so we know we can use structural recursion to process it. However to do so we need to also store the current state of the turtle: it’s location (a Vec) and heading (an Angle). Implement a type to hold this data.

This is a product type.

final case class TurtleState(at: Vec, heading: Angle)
// defined class TurtleState

When we process the instructions, we will turn them into a List[PathElement], which we can later wrap with an open path to create an Image. For each instruction, the conversion will be a function of the current turtle state and the instruction, and will returnan updated state and a List[PathElement].

Implement a method process to do this job with signature

def process(state: TurtleState, instruction: Instruction): (TurtleState, List[PathElement]) =
  ???
// process: (state: TurtleState, instruction: Instruction)(TurtleState, List[doodle.core.PathElement])

First implement this without branching instructions. We’ll return to branches in a moment.

The core pattern is a structural recursion but the details are a bit more intricate in this case than we’ve seen before. We need to both create the path elements and update the state.

def process(state: TurtleState, instruction: Instruction): (TurtleState, List[PathElement]) = {
  import PathElement._
  
  instruction match {
    case Forward(d) =>
      val nowAt = state.at + Vec.polar(d, state.heading)
      val element = lineTo(nowAt.toPoint)

      (state.copy(at = nowAt), List(element))
    case Turn(a) =>
      val nowHeading = state.heading + a

      (state.copy(heading = nowHeading), List())
    case Branch(i) =>
      // Ignoring for now
      (state, List())
    case NoOp() =>
      (state, List())
  }
}
// process: (state: TurtleState, instruction: Instruction)(TurtleState, List[doodle.core.PathElement])

Now using process write a structural recursion over List[Instruction] that converts the instructions to a List[PathElement]. Call this method iterate with signature

def iterate(state: TurtleState, instructions: List[Instruction]): List[PathElement] =
  ???
// iterate: (state: TurtleState, instructions: List[Instruction])List[doodle.core.PathElement]
def iterate(state: TurtleState, instructions: List[Instruction]): List[PathElement] =
  instructions match {
    case Nil => 
      Nil
    case i :: is =>
      val (newState, elements) = process(state, i)
      elements ++ iterate(newState, is)
  }
// iterate: (state: TurtleState, instructions: List[Instruction])List[doodle.core.PathElement]

Now add branching to process, using iterate as a utility.

def process(state: TurtleState, instruction: Instruction): (TurtleState, List[PathElement]) = {
  import PathElement._
  
  instruction match {
    case Forward(d) =>
      val nowAt = state.at + Vec.polar(d, state.heading)
      val element = lineTo(nowAt.toPoint)

      (state.copy(at = nowAt), List(element))
    case Turn(a) =>
      val nowHeading = state.heading + a

      (state.copy(heading = nowHeading), List())
    case Branch(is) =>
     val branchedElements = iterate(state, is)
     
     (state, moveTo(state.at.toPoint) :: branchedElements)
    case NoOp() =>
      (state, List())
  }
}
// process: (state: TurtleState, instruction: Instruction)(TurtleState, List[doodle.core.PathElement])

Now implement draw using iterate.

Here’s the complete turtle.

object Turtle {
  def draw(instructions: List[Instruction]): Image = {
    def iterate(state: TurtleState, instructions: List[Instruction]): List[PathElement] =
      instructions match {
        case Nil => 
          Nil
        case i :: is =>
          val (newState, elements) = process(state, i)
          elements ++ iterate(newState, is)
      }

    def process(state: TurtleState, instruction: Instruction): (TurtleState, List[PathElement]) = {
      import PathElement._
      
      instruction match {
        case Forward(d) =>
          val nowAt = state.at + Vec.polar(d, state.heading)
          val element = lineTo(nowAt.toPoint)
    
          (state.copy(at = nowAt), List(element))
        case Turn(a) =>
          val nowHeading = state.heading + a
    
          (state.copy(heading = nowHeading), List())
        case Branch(is) =>
         val branchedElements = iterate(state, is)
         
         (state, moveTo(state.at.toPoint) :: branchedElements)
        case NoOp() =>
          (state, List())
      }
    }
    
    openPath(iterate(TurtleState(Vec.zero, Angle.zero), instructions))
  }
}
// defined object Turtle

12.2.1 Extensions

Turtles that can make random choices can lead to more organic images. Can you implement this?

13 まとめ

In this text we have covered a handful of the essential functional programming tools available in Scala.

13.1 Representations and Interpreters

We started by writing expressions to create and compose images. Each program we wrote went through two distinct phases:

  1. Build an Image
  2. Call the draw method to display the image

This process demonstrates two important functional programming patterns: building intermediate representations of the result we want, and interpreting the representations to produce output.

13.2 Abstraction

Building an intermediate representation allows us to only model the aspects of the result that we consider important and abstract irrelevant details.

For example, Doodle directly represents the primitive shapes and geometric relationships in our drawings, without worrying about implementation details such as screen coordinates. This keeps our code clear and maintainable, and limits the number of “magic numbers” we need to write. For example, it is a lot easier to determine that this Doodle program produces a house:

def myImage: Image =
  Triangle(50, 50) above Rectangle(50, 50)
// myImage: Image = // ...

than this implementation in Java2D:

def drawImage(g: Graphics2D): Unit = {
  g.setStroke(new BasicStroke(1.0f))
  g.setPaint(new Color(0, 0, 0))
  val path = new Path2D.Double()
  path.moveTo(25, 0)
  path.lineTo(50, 50)
  path.lineTo(0, 50)
  path.lineTo(25, 0)
  path.closePath()
  g.draw(path)
  f.drawRect(50, 50, 50, 50)
}

It’s important to realise that all of the imperative Java2D code is still present in Doodle. The difference is we have hidden it away into the draw method. draw acts as interpreter for our Images, filling in all of the details about coordinates, paths, and graphics contexts that we don’t want to think about in our code.

Separating the immediate value and the interpreter also allows us to change how interpretation is performed. Doodle already comes with two interpreters, one of which draws in the Java2D framework while the other draws in the HTML canvas. You can image yet more interpreters to, for example, achieve artistic effects such as drawing images in a hand-drawn style.

13.3 Composition

In addition to making our programs clearer, the functional approach employed by Doodle allows us to compose images from other images. For example, we can re-use our house to draw a street:

val house = Triangle(50, 50) above Rectangle(50, 50)
// house: Image = // ...

val street = house beside house beside house
// street: Image = // ...

The Image and Color values we create are immutable so we can easily re-use a single house three times within the same image.

This approach allows us to break down a complex image into simpler parts that we then combine together to create the desired result.

Reusing immutable data, a technique called structure sharing, is the basis of many fast, memory efficient immutable data structures. The quintissential example in Doodle is the Sierpinski triangle where we re-used a single Triangle object to represent an image containing nearly 20,000 distinct coloured triangles.

13.4 Expression-Oriented Programming

Scala provides convenient syntax to simplify creating data structures in a functional manner. Constructs such as conditionals, loops, and blocks are expressions, allowing us to write short method bodies without declaring lots of intermediate variables. We quickly adopt a pattern of writing short methods whose main purpose is to return a value, so omitting the return keyword is also a useful shorthand.

13.5 Types are a Safety Net

Scala’s type system helps us by checking our code. Every expression has a type that is checked at compile time to see if it matches up with its surroundings. We can even define our own types with the explicit purpose of stopping ourselves from making mistakes.

A simple example of this is Doodle’s Angle type, which prevents us confusing numbers and angles, and degrees and radians:

90
// res0: Int = 90

90.degrees
// res1: doodle.core.Angle = Angle(1.5707963267948966)

90.radians
// res2: doodle.core.Angle = Angle(2.0354056994857643)

90.degrees + 90.radians
// res3: doodle.core.Angle = Angle(3.606202026280661)

90 + 90.degrees
// <console>:20: error: overloaded method value + with alternatives:
//   (x: Double)Double <and>
//   (x: Float)Float <and>
//   (x: Long)Long <and>
//   (x: Int)Int <and>
//   (x: Char)Int <and>
//   (x: Short)Int <and>
//   (x: Byte)Int <and>
//   (x: String)String
// cannot be applied to (doodle.core.Angle)
//              90 + 90.degrees
//                 ^

13.6 Functions as Values

We spent a lot of time writing methods to produce values. Methods let us abstract over parameters. For example, the method below abstracts over colours to produce different coloured dots:

def dot(color: Color): Image =
  Circle(10) lineWidth 0 fillColor color
// dot: Color => Image = // ...

Coming from object oriented languages, methods are nothing special. More interesting is Scala’s ability to turn methods into functions that can be passed around as values:

def spectrum(shape: Color => Image): Image =
  shape(Color.red) beside shape(Color.blue) beside shape(Color.green)
// spectrum: (Color => Image) => Image = // ...

spectrum(dot)
// res0: Image = // ...

We wrote a number of programs that used functions as values, but the quintissential example was the map method of List. In the Collections chapter we saw how map lets us transform sequences without allocating and pushing values onto intermediate buffers:

List(1, 2, 3).map(x => x * 2)
// res0: List[Int] = List(2, 4, 6)

Functions, and their first class status as values, are hugely important for writing simple, boilerplate-free code.

13.7 Final Words

The intention of this book has been to introduce you to the functional parts of Scala. These are what differentiate Scala from older commercial languages such as Java and C. However, this is only part of Scala’s story. Many modern languages support functional programming, including Ruby, Python, Javascript, and Clojure. How does Scala relate to these languages, and why would you want to choose it over the other available options?

Perhaps the most significant draw to Scala is its type system. This distinguishes Scala from popular languages such as Ruby, Python, Javascript, and Clojure, which are dynamically typed. Having static types in a language is undeniably a trade-off—writing code is slower because we have to satisfy the compiler at every stage. However, once our code compiles we gain confidence about its quality.

Another major draw is Scala’s blending of object-oriented and functional programming paradigms. We saw a little of this in the first chapter—every value is an object with methods, fields, and a class (its type). However, we haven’t created any of our own data types in this book. Creating types is synonymous with declaring classes, and Scala supports a full gamut of features such as classes, traits, interitance, and generics.

Finally, a major benefit of Scala is its compatibility with Java. In many ways Scala can be seen as a superset of Java, and interoperation between the two languages is quite straightforward. This opens up a world of Java libraries to our Scala applications, and allows flexibility when translating Java applications to Scala.

13.8 Next Steps

We hope you enjoyed Creative Scala and drawing diagrams with Doodle. If you would like to learn more about Scala, we recommend that you pick one of the many great books available on the language.

Our own book, Essential Scala, is available from our web site and continues Creative Scala’s approach of teaching Scala by discussing and demonstrating core design patterns and the benefits they offer.

If you want to challenge yourself, try drawing something more complex with Doodle and sharing it with us via Gitter. There are lots of things you can try—check the examples directory in the Doodle codebase for some suggestions:

Koch Triangle (Koch.scala)

Koch Triangle (Koch.scala)

Suburban Scene (Street.scala)

Suburban Scene (Street.scala)

Mandelbrot Fractal by Mat Moore (Mandelbrot.scala)

Mandelbrot Fractal by Mat Moore (Mandelbrot.scala)

14 構文のクイックレファレンス

14.1 リテラルと式

// リテラル:
123      // Int
123.0    // Double
"Hello!" // String
true     // Boolean

// 算数:
10 + 2   // Int + Int    = Int
10 + 2.0 // Int + Double = Double
10 / 2   // Int / Int    = Double

// Boolean 論理演算:
true && false // logical AND
true || false // logical OR
!true         // logical NOT

// String の連結:
"abc" + "def" // String
"abc" + 123   // auto-conversion from Int to String

// メソッド呼び出しと中置演算子:
1.+(2)    // method call style
1 + 2     // infix operator style
1 + 2 + 3 // equivalent to 1.+(2).+(3)

// 条件式:
if(booleanExpression) expressionA else expressionB

// ブロック:
{
  sideEffectExpression1
  sideEffectExpression2
  resultExpression
}

14.2 値とメソッド宣言

// 値の宣言構文:
val valueName: SomeType = resultExpression // 明示的な型を用いた宣言
val valueName = resultExpression           // 型推論を用いた宣言

// パラメータリストと明示的な結果型を持ったメソッド:
def methodName(argName: ArgType, argName: ArgType): ReturnType =
  resultExpression

// パラメータリストと推論された結果型を持ったメソッド:
def methodName(argName: ArgType, argName: ArgType) =
  resultExpression

// (ブロックを用いた) 複合式のメソッド:
def methodName(argName: ArgType, argName: ArgType): ReturnType = {
  sideEffectExpression1
  sideEffectExpression2
  resultExpression
}

// パラメータリストを持たないメソッド:
def methodName: ReturnType =
  resultExpression

// パラメータリストを持ったメソッドの呼び出し:
methodName(arg, arg)

// パラメータリストを持たないメソッドの呼び出し:
methodName

14.3 値としての関数

関数値は (argName: ArgType, ...) => resultExpression として書く:

val double = (num: Int) => num * 2
// double: Int => Int = <function1>

val sum = (a: Int, b: Int) => a + b
sum: (Int, Int) => Int = <function2>

複数行に渡る関数はブロック式を用いて書く:

val printAndDouble = (num: Int) => {
  println("The number was " + num)
  num * 2
}
// printAndDouble: Int => Int = <function1>

scala> printAndDouble(10)
// The number was 10
// res0: Int = 20

パラメータや戻り型として関数を使う場合は、関数の型を書く必要がある。 その構文は ArgType => ResultType もしくは (ArgType, ...) => ResultType:

def doTwice(value: Int, func: Int => Int): Int =
  func(func(value))
// doTwice: (value: Int, func: Int => Int)Int

doTwice(1, double)
// res0: Int = 4

関数値は通常の式として、他の式の一部としてそのまま書くことができる。

doTwice(1, (num: Int) => num * 10)
// res1: Int = 100

引数の型は、コンパイラが推論可能ならば省略することができる:

doTwice(1, num => num * 10)
// res2: Int = 100

14.4 Doodle レファレンス・ガイド

14.4.1 インポート文

// これらの import があれば大丈夫:
import doodle.core._
import doodle.syntax._

14.4.2 イメージの作成

// プリミティブ・イメージ (黒の輪郭、塗りつぶし無し):
val i: Image = Circle(radius)
val i: Image = Rectangle(width, height)
val i: Image = Triangle(width, height)

// 演算子構文で書かれた複合イメージ:
val i: Image = imageA beside imageB // 水平に隣接
val i: Image = imageA above  imageB // 垂直に隣接
val i: Image = imageA below  imageB // 垂直に隣接
val i: Image = imageA on     imageB // 重ね合わせ
val i: Image = imageA under  imageB // 重ね合わせ

// メソッド呼び出し構文で書かれた複合イメージ:
val i: Image = imageA.beside(imageB)
// etc...

14.4.3 イメージのスタイリング

// 演算子構文で書かれたイメージのスタイリング:
val i: Image = image fillColor color   // 新しい塗りつぶし (輪郭は変化なし)
val i: Image = image lineColor color   // 新しい輪郭の色 (塗りつぶしは変化なし)
val i: Image = image lineWidth integer // 新しい輪郭の幅 (塗りつぶしは変化なし)
val i: Image = image fillColor color lineColor otherColor // 新しい塗りつぶしと輪郭

// メソッド呼び出し構文で書かれたイメージのスタイリング:
val i: Image = imageA.fillColor(color)
val i: Image = imageA.fillColor(color).lineColor(otherColor)
// etc...

14.4.4

// 基本色:
val c: Color = Color.red                       // 定義済みの色
val c: Color = Color.rgb(255.uByte, 127.uByte, 0.uByte)          // RGB color
val c: Color = Color.rgba(255.uByte, 127.uByte, 0.uByte, 0.5.normalized)    // RGBA color
val c: Color = Color.hsl(15.degrees, 0.25.normalized, 0.5.normalized)       // HSL color
val c: Color = Color.hsla(15.degrees, 0.25.normalized, 0.5.normalized, 0.5.normalized) // HSLA color

// 演算子構文で書かれた色の変換/混合:
val c: Color = someColor spin       10.degrees     // 色相の変化
val c: Color = someColor lighten    0.1.normalized // 明度の変化
val c: Color = someColor darken     0.1.normalized // 明度の変化
val c: Color = someColor saturate   0.1.normalized // 彩度の変化
val c: Color = someColor desaturate 0.1.normalized // 彩度の変化
val c: Color = someColor fadeIn     0.1.normalized // 透明度の変化
val c: Color = someColor fadeOut    0.1.normalized // 透明度の変化

// メソッド呼び出し構文で書かれた色の変換/混合:
val c: Color = someColor.spin(10.degrees)
val c: Color = someColor.lighten(0.1.normalized)
// etc...

14.4.5 パス

// PathElements のリストからのパスの作成:
val i: Image = OpenPath(List(
  MoveTo(Vec(0, 0).toPoint),
  LineTo(Vec(10, 10).toPoint)
))

// PathElements の列からのパスの作成:
val i: Image = OpenPath(
  (0 until 360 by 30) map { i =>
    LineTo(Vec.polar(i.degrees, 100).toPoint)
  }
)

// 要素の種類:
val e1: PathElement = MoveTo(toVec.toPoint)                        // 線なし
val e2: PathElement = LineTo(toVec.toPoint)                        // 直線
val e3: PathElement = BezierCurveTo(cp1Vec.toPoint, cp2Vec.toPoint, toVec.toPoint) // 曲線

// 注意: 最初の要素が MoveTo じゃない場合は、MoveTo に変換される

14.4.6 Angle と Vec

val a: Angle = 30.degrees                // 度数による角度
val a: Angle = 1.5.radians               // ラジアンによる角度
val a: Angle = math.Pi.radians           // π ラジアン
val a: Angle = 1.turns                   // 回転数による角度

val v: Vec = Vec.zero                    // ゼロベクトル (0,0)
val v: Vec = Vec.unitX                   // x 単位ベクトル (1,0)
val v: Vec = Vec.unitY                   // y 単位ベクトル (0,1)

val v: Vec = Vec(3, 4)                   // デカルト座標からのベクトル
val v: Vec = Vec.polar(30.degrees, 5)    // 極座標からのベクトル
val v: Vec = Vec(2, 1) * 10              // 長さの乗算
val v: Vec = Vec(20, 10) / 10            // 長さの除算
val v: Vec = Vec(2, 1) + Vec(1, 3)       // ベクトルの加算
val v: Vec = Vec(5, 5) - Vec(2, 1)       // ベクトルの減算
val v: Vec = Vec(5, 5) rotate 45.degrees // 反時計回りの回転

val x: Double = Vec(3, 4).x              // x 座標
val y: Double = Vec(3, 4).y              // y 座標
val a: Angle  = Vec(3, 4).angle          // (1, 0) から反時計回り
val l: Double = Vec(3, 4).length         // 長さ

  1. これも完全な真実ではないです! 私たちは通常 Scala のコードは JVM 上で実行しますが、Scala は 3つの別なフォーマットにコンパイルすることができます。第一は、もっとも広く使われいる JVM バイトコード。Scala は、JavaScript という別の言語にコンパイルすることでウェブブラウザ上で実行することも可能です。最後に、Scala Native は Scala を JVM 無しでコンピュータが直接実行できるものにコンパイルします。

  2. 1バイトは 256通りの可能性を持つ値で、コンピューター内の 8ビットを用いて表現する。符号付きバイトは -128 から 127 の整数値を持ち、符号なしバイトは 0 から 255 の値を持つ。

  3. :load というコマンドもあって、これは :paste とは少し異なる動作をします。:load はファイル内の各行を別にコンパイルして実行するのに対して、:paste はファイル全体を一気にコンパイルします。そのため、微妙に違うセマンティクスを持ちます。:paste の方が console 外での Scala コードの振る舞いに近いので、私たちは :load の代わりに :paste を使います。

  4. これは完全には正しくありません。純粋な式においても評価の順序が影響を与えるコーナーケースがいくつかあります。ここではあまりその心配はしなくても大丈夫です。もし興味があるならば、面白いことなので「正格評価」と「遅延評価」に関して調べてみましょう。

  5. This connection goes deeper. We can abstract the idea of things that can be “added” into a concept called a monoid, and a list represents a particular type of monoid called the free monoid. We aren’t going to work with this abstraction in Creative Scala but you’re encouraged to explore on your own!

  6. These numbers are not truly random. The output is determined by a value known as the seed. If we know the seed we can perfectly predict all the result we’ll get from calling math.random. However, going the other way—that is, predicting the seed given a sequence of outputs—is very difficult. The numbers so generated are called pseudo-random numbers, because they are not truly random but nonetheless are very difficult to predict.