これは PROGLOVE Advent Calendar 2019 の7日目の記事です。
はじめに
式のメタファーを利用することで、複雑なものを単純なものの組み合わせで表現できるようになります。それにより、複雑なものを作るときの考え方が変わります。まずは単純なものから作り始めよう、という思考になるのです。もし式のメタファーを使わなければ、そもそもスタート地点がわからないので作り始めることすらできないかもしれません。そうでないにしても、無理に作って複雑になりすぎてしまい、手に負えなくなって途中で作るのを諦めてしまうかもしれません。
考え方が変わるだけではありません。式のメタファーを使うことで、自然と関心の分離の恩恵も得られます。関心の分離の利点は色々ありますが、式のメタファーにより得られる最も大きな利点は「集中したいことに集中できる」ことです。具体的な例については記事後半で説明します。
「式のメタファー」という用語の初出
この用語を私が初めて目にしたのは『テスト駆動開発』という本です。そして、おそらくこの本が「式のメタファー」という用語が使われている唯一の本です。
著者は、メタファーがここまで強力だとは思わなかったと書いていますが、式のメタファーについての説明は「(2 + 3) * 5
のようなもの」という簡単なもので済ませています。どのように強力であるかについても文章としては明記されていません(本に書かれているコードを手元で実装することで強力さを体感することはできます)。
この記事では、式のメタファーとは何か、そしてどのように強力であるかについて詳しく説明していきます。
「式のメタファー」でいう式とは何か
式とは数式のことです。たとえば以下のようなものです。
0.1 + 2.3 - 4.5 \times 6.7 \div 8.9
上の数式は2つのもので構成されています。$+$ や $-$ などの演算記号と、$0.1$ や $2.3$ などの数です。「式のメタファー」でいう式をイメージする際は、この2種類のもので構成された数式を頭の中に思い浮かべればよいです。
演算記号と数には様々な種類がありますが、「式のメタファー」でいう式を理解する上では、種類の数を少なくしてしまったほうが楽です。ということで、ここでは式を次のように定義します。
「式は $0$ 以上の整数と2つの記号 $+$ $\times$で構成される」
つまり、$2 + 3 \times 5$ や $7$ は式ですが、$-21$ や $2.7 + 4.9$ や $57 - 91$ は式ではありません。式でない理由は、前から「$0$ 未満の整数がある」「小数を含む」「記号 $-$ が含まれている」です。
このように式を定義することで嬉しい性質が生まれます。それは、「式を評価した結果もまた式である」という性質です。
式を評価した結果もまた式である
ここでいう評価とは、実際に計算することに相当します。たとえば $2 + 3 \times 5$ という式を評価した結果は $17$ です。そして $17$ は式です。つまり「$2 + 3 \times 5$ という式を評価した結果は式である」といえます。実は先ほどの式の定義に則っている限り、式を評価した結果は必ず式となります。どれだけ複雑に式を構成してもです。
しかし、式を次のように定義したとします。
「式は $0$ 以上の整数と3つの記号 $+$ $\times$ $-$ で構成される」
記号 $-$ を追加しました。この定義の場合、式を評価した結果が式になるとは限りません。たとえば $1 - 4$ という式を考えたとき、式を評価した結果は $-3$ になります。$-3$ は $0$ 以上の整数ではないため式ではありません。
「式のメタファー」でいう式は、「式を評価した結果もまた式である」という条件を満たします。つまり、記号 $-$ が含まれている後者の定義は、「式のメタファー」でいう式ではありません。
まとめ
「式のメタファー」でいう式とは、「式を評価した結果もまた式である」という条件を満たした数式のことです。この条件を満たすために、数式に登場する演算記号と数をうまく定義する必要があります。
式のメタファーをどのように使うか
式とは何かについて説明しましたが、どのように活用していくか、いまいち感覚が掴めなかったかもしれません。ということで、式のメタファーを実際にどのような使い方をするかについて見ていきます。
式のメタファーの使い道には、たとえば次のようなものがあります。
- Webクローラ
- パーサ
- 3DCG上のオブジェクト
それぞれについて詳しく説明していきます。
Webクローラ
Webクローラ(以下クローラ)とは、Webサイトを巡回するプログラムのことです。
詳しく説明します。人がWebサイトを巡回するときのことをイメージするとわかりやすいです。まず何かに関する情報を知りたいとき、Googleに検索ワードを入力して画面内のボタンを押し、検索結果画面に遷移します。そして検索結果画面から目的に合ったサイトのリンクをクリックし、そのサイトに遷移します。更にまたそのサイト内の別のリンクをクリックして…というように、「URL生成 → サイトアクセス → ページ内情報取得」という流れを繰り返します。これを人の代わりに行うプログラムのことをクローラと呼びます。
このクローラは、式のメタファーを当てはめることができます。ひとつのWebページに特化したクローラを複数作成し、それとはまた別に、複数のクローラを連結する機能を実装したとします。「連結してできるものもまたクローラ」というルールを守ることで、複数のクローラを気軽に連結することができます。
例えば、以下の2つのクローラを実装した場合を考えます。
- クローラ
A
: トップページのURL生成 → サイトアクセス → 商品カテゴリID取得 - クローラ
B
: 商品カテゴリIDからカテゴリ内商品一覧ページのURLを生成 → サイトアクセス → 商品ID取得
この2つのクローラを連結する機能 +
があれば、トップページのURLから商品IDを取得するクローラは、ただ A + B
と書くだけで作ることができます。
ここに、更に次のクローラを先ほどのクローラの後ろに連結したくなったとします。
- クローラ
C
: 商品IDから商品詳細ページのURLを生成 → サイトアクセス → 商品名の取得
A + B
もまたクローラであるため、ただ A + B + C
と書くだけで実装が終わります。この例の場合、+
は式でいう演算記号に相当し、A
B
C
は式でいう数に相当します。
+
という記号は適切ではないかもしれません。数式で使われる+
は交換法則を満たしますが、今回の例は交換法則を満たしません。よって>>
といった記号を用いたほうがよいかもしれません。今回は、式のメタファーを使っていることを明確にするために+
を使いました。
パーサ
パーサとは、ある一定の規則で並んだ文字列を解析するプログラムのことです。
これについても詳しく説明します。クローラのときと同じく人に例えます。パソコンのディスプレイに $1 + 2 + 3$ という文字列が表示されているとき、人はまずこの文字列の意味を解釈しようとします。そして「あ、これは $1$ と $2$ を足して、それに $3$ を足すことだな」と文字列の意味を理解します。
パーサは、この例の「$1 + 2 + 3$ という文字列を解釈して意味を理解する」に相当するプログラムのことです。つまり、ただの文字列から「意味」に変換するプログラムだといえます。意味というと抽象的でイメージしづらいですが、現実的には「意味」の形式はある程度決まっており、それは木構造です。
つまりパーサとは、文字列を解析して木構造に変換するプログラムのことです。
このパーサもまた、式のメタファーに当てはめることができます。以下の3つのパーサがある場合を考えます。
- パーサ
A
: 変数名を解析 - パーサ
B
:=
記号を解析 - パーサ
C
: 整数を解析
もしパーサを連結する +
が定義されていれば、"variable=12345"
という文字列を解析するパーサは A + B + C
と書くだけで作ることができます。
=
の両側に0個以上の空白文字を許容したくなるかもしれません。そういったときは、0個以上の空白文字を解析するパーサ D
を新たに作り、先ほどのパーサを A + D + B + D + C
と書き換えるだけでいいです。
3DCG上のオブジェクト
今回は、以下のようなサイコロを作ることを考えます。
このような複雑なオブジェクトを作り、更にそれを回転させるのは容易でないように思えます。しかし、式のメタファーを使うことで想像よりも簡単に作ることができます。
サイコロが単純なオブジェクトの組み合わせで構成されていると考えます。具体的には、以下のオブジェクトで構成されていると考えます。
- オブジェクト
A
: 6個のサイコロの面 - オブジェクト
B
: 12個の、隣り合う2つのサイコロの面の境界にある滑らかな面 - オブジェクト
C
: 8個の角
すると、サイコロは A + B + C
で実装することができます。
ここで、オブジェクトは任意に移動、回転が行えるものとします。
先ほどはこれらを「単純なオブジェクト」と表現しましたが、まだ複雑です。特にオブジェクト A
には「目」があるため、そのまま実装するのは難しそうです。そこで式のメタファーの「式を評価した結果もまた式である」という性質を利用します。具体的には、オブジェクト A
を更に分割します。
その結果、以下のポリゴンを組み合わせれば作れることがわかります。
- オブジェクト
D
: 半球体 - オブジェクト
E
: 丸い穴の開いた正方形 - オブジェクト
F
: 長方形
これらのオブジェクトをそれぞれ何個か作り、適切に移動、回転して組み合わせることでオブジェクト A
が作れます。
ここまで単純にすることで、ようやく実装できる気がしてきます。3Dの基礎が身に付いていれば長方形は簡単に作れますし、オブジェクト D
E
も、長方形に比べれば難易度は上がりますが、それほど難しくありません。
これらのポリゴンを作れば、あとはそれらを移動、回転させて組み合わせることで、ひとつの面が作れます。たとえばサイコロの1の面は、次のようなコードになります(F1
~ F8
は、ポリゴン F
を移動、回転させたポリゴンです)。
D + E + F1 + F2 + F3 + F4 + F5 + F6 + F7 + F8
同様にして他のサイコロの面を作っていきます。これらのオブジェクトを A1
~ A6
とし、適切に移動、回転させたオブジェクトを A1'
~ A6'
とすると、オブジェクト A
は A1' + A2' + A3' + A4' + A5' + A6'
で実装できます。
振り返り
3つの具体例を挙げました。式のメタファーの利点を振り返っていきます。
単純なものの実装に集中できる
「3DCG上のオブジェクト」の例の中で、「半球体や丸い円の開いた正方形を作ることはそれほど難しくない」といいました。しかし、実装する際には三角関数の知識が必要とされる上、どのように三角形を配置していくか(3DCG上のオブジェクトのすべては、何百何千個の三角形で構成されています)について考えなければなりません。この問題を解くためには、しっかりと考える必要があります。
式のメタファーを用いることで、思考に集中できます。
単純なものをどのようにして移動、回転させるかという別の問題がありますが、単純なものを作る際は、この問題について考える必要はありません。なぜなら、そうしてできた単純なものは結局はオブジェクトであり、移動や回転が行えることが保障されているからです。単純なものを作った後で考えればいいのです。
これが、記事冒頭部分で述べた関心の分離の利点です。
パーサの例でも同じです。たとえばプログラミング言語のパーサを作るとなると途方もないですが、複雑なパーサは単純なパーサの組み合わせで構成できることが保障されているため、まず単純なパーサを作ればいいことが分かります。たとえば数を読み取るパーサであったり、"Hello, World\n"
などの文字列を解析するパーサであったりです。そして関心の分離により、それらの単純なパーサを作ることに集中できます。
複雑なものを作ることに抵抗がなくなる
プログラミング言語のパーサを作ったり、サイコロなどの複雑な3DCG上のオブジェクトを作ったりすることへの抵抗がなくなります。なぜなら、単純なものを組み合わせる機能をうまく実装すれば、あとは単純なものを作ってそれらをパズルのように組み立てていくだけで複雑なものが出来上がるからです。
注意点
式のメタファーを利用する際にはいくつか注意点があります。
式のメタファーは結局はメタファー
「式のメタファー」でいう式は $2 + 3 \times 5$ のようなもの、と最初のほうで説明しました。これだけ見ると、評価の対象は2つに固定されているように思えます。しかし、2つに固定されているわけではありません。評価の対象が3つ以上になることもありますし、1つの場合もあります。
たとえば「1文字以上の英数字を解析するパーサ」を作る際、「パーサを1回以上繰り返し適用する機能」があれば便利です。なぜなら、その機能を「英数字1文字を解析するパーサ」に適用すれば、目的のパーサが作れるからです。この機能は、2つではなく1つのパーサが対象です。
この機能を式のメタファーに無理やり当てはめるとすれば、階乗のようなものでしょうか(階乗は $4!$ のように表記し、$ 4 \times 3 \times 2 \times 1$ を表します)。しかしそれは正確ではありません。なぜなら、階乗は有限で、先ほどの機能は無限だからです。
式のメタファーと実際の設計の間に違いがあることを認識すべきです。
「式を評価した結果もまた式である」を厳守する
このルールを守ることが大切です。それを実感するために、評価した結果が式でなくなる場合を考えます。
別のものになれば、元のものでは使えた様々な機能が使えなくなります。
3DCG上のオブジェクトの例だと、単純なオブジェクトを組み合わせて作ったサイコロはオブジェクトでなくなるため、移動や回転が行えません。よって、gif画像のような回転を行うためには、新たに「サイコロに特化した回転」の機能を実装する必要があります。
サイコロはオブジェクトに似ているため、サイコロを回転させるアルゴリズムは、オブジェクトを回転させるアルゴリズムに似てきます。ということで、非常に似通った、しかし細部が異なる2つのコードが出来上がります。これは精神衛生上よくありません。なぜなら、一方にバグが含まれていると、もう一方にもバグが含まれている可能性が高く、そのときの修正コストが2倍になるからです。
これらの理由により、「式を評価した結果もまた式である」というルールは厳守する必要があります。
まとめ
ただひとつのルール「式を評価した結果もまた式である」を守ることで、式のメタファーを利用することによる様々な利点が得られます。利点とは、設計の際の思考パターンの変化と関心の分離です。特に思考パターンの変化によって得られる恩恵が大きいです。単純なものを作ることが着実な一歩となるからです。
式のメタファーの考え方を取り入れて、自分の思い描く世界を構築していきましょう!
あとがき
パーサは A + D + B + D + C
で実装できると書きましたが、現実としてはそれほど単純ではありません。たとえばパーサ A
で得た文字列は意味を持ちますが、パーサ D
で得た文字列は意味を持ちません。それはただの空白文字の連続だからです。パーサによって「パーサの結果」を変えたいことがあるのです。Haskell の Parsec は、この問題を見事に解決しています。
モナドを使うことで「今文字列の何文字目を解析しているか」という情報を巧みに隠し、パーサの結果を Haskell の強固な型システムに守られた状態で利用することができます。そういった状態でパーサを構築していくのは非常に心地良いです。正直なところ、僕自身まだ Parsec についてしっかりと理解できていません。この心地良さを様々な言語(自然言語含む)で表現しようとしてきましたが、ことごとく失敗しています。おそらくモナドというものを今一度しっかりと理解する必要があるのだと思います。最近は Discord で Haskell のことが好きな人たちの会話を目にしている影響か、もう一度 Haskell についてしっかりと勉強したいという意欲がわき始めています。
そもそも、私が式のメタファーの思想を最初に知ったのは Parsec です。当時は、パーサをこんなに簡単に組み合わせられることに驚きました。しかも新たな文法を覚える必要はなく、それはただ Haskell の機能によって実現されていました。
ということで、Parsec を使うことで、式のメタファーを体感することができます。そして、式のメタファーの考え方は、言語を問わずに使うことができます(たとえばこちらがJavaScriptでの実装例です。サイコロを作るコードがあります)。演算子のオーバーロードができないなどの細かな違いはありますが、考え方は取り入れることができます。そのため、Parsec を、そして Haskell を勉強することは、決して無駄にはならないと思います。
Parsec を使ったときの驚きと感動を皆さんと共有したいので、ぜひ使ってほしいです。