FizzBuzzをコンパイラに解いてもらう

@jooohn1234氏:じゃあ発表させていただきます。「ScalaのコンパイラにFizzBuzzを解いてもらう(Dottyもあるよ)」ということで、ちなみにDottyというのは、現行はScala2がメジャーバージョンなんですけど、Scala3にあたるやつです。Scala3の書き方も紹介しますという発表です。

私はエムスリーの冨岡と言います。

右下に可愛い女の子がいると思うんですけど、これは私の娘をイメージしていて、1歳半のつむぎちゃんという、スライド中にも何回か出てくるので「つむぎちゃんだな」と思って聞いていただければと思います。

(会場笑)

今日はFizzBuzzの話をするんですけど、Wikipediaを見てみると「コードが書けないプログラマ志願者を見分ける手法」と書いてあって、これにはもうつむぎちゃんもビックリで……。

(会場笑)

恐ろしいなと。やばいですね。「ちょっとFizzBuzz書けないとやばいな」となるんですけど、問題はどういう問題かと言うと、1〜nまでの数について以下のように出力しますと。3で割り切れる場合は"Fizz"で、5で割り切れるときは"Buzz"、3でも5でも割り切れる場合は"FizzBuzz"、その他の場合はその数を出力するみたいな感じです。

「もう難しすぎる〜」みたいな感じですよね。

(会場笑)

ちなみにNP困難は関係ないんですけど「まったくわからん」と。

つむぎちゃんも1歳半なのでまったくわからないです。

(会場笑)

ここでマリー・アントワネットという人の言葉を借りると「FizzBuzzを自力で解けないんだったら、コンパイラに解いてもらえばいいんじゃないか」と言っている。

ということで、わからないけどコンパイラに解いてもらえばいいんだということで解いていきます。

順を追っていくと、まずコンパイラに数を認識してもらう、コンパイラに数値演算をしてもらう、そして割り切れる判定をしてもらって条件分岐して繰り返しをしてFizzBuzzを解いてもらおう、という順番でやっていきます。

コンパイラに数を認識してもらう

まず「コンパイラに数を認識してもらおう」です。たぶんこの勉強会に来ている人は「そうか」みたいな感じだと思うんですけど、ペアノ数というやつがあって、0か、ある自然数のその次みたいな感じで自然数を定義できそうですね。自然数に0が含まれるかどうかみたいな話は諸説あるんですけど、含まれるとしてください。

これはみなさんもわかっているとします。これをやると何がうれしいかと言うと、コンパイラが0と1を見分けてくれると。

「= 〇〇」というScalaのやつがあるんですけど、(スライドを指して)これは左と右の型が一致しているかどうかを見ていて、0はSucc[Zero]、つまり1みたいなものですけど、「0は1である」みたいなことをやろうとすると「そうじゃないよ」とScalaのコンパイラに怒られるわけです。

それでコンパイラが数字を認識してくれるようになったと。整数型に変換する方法も別に用意しておきましょう。0から5ぐらいまで用意しておくと便利です。

コンパイラに数値計算をしてもらう

次は「コンパイラに数値計算をしてもらおう」ということで、ここからはおそらくみなさんScalaをやったことある人だったらわかると思うんですけど、そうじゃない人のために、implicitというScalaの不思議な機能について説明させていただきます。

implicitとは何かというと、implicitly[string]と2行目に書いてあるんですけど、これはこのスコープ内にあるStringのimplicitの値を持ってくるみたいなことをやっています。

上にimplicitのStringとして"I am implicit"と定義されていて「implicitなのはこれだ!」とScalaのコンパイラがいい感じに見つけてきてくれて、それを持ってきてくれるというのがimplicitの基本です。

implicitはけっこういろいろできて、これはちょっと難しくて複雑な例になるんですけど、(スライドを指して)これはimplicitのスコープ内にあるんですけど、スコープの中にまた別のimplicit aがあったらBox[A]もimplicitとして解決できるよ、みたいなことを書けます。

implicitly[Box[String]]みたいなのをやろうとすると、StringがimplicitであったらBoxStringも解決できるという感じなんです。Box(I am implicit.)と。

これは再帰的に解決してくれるので、[Box[Box[Box[String]]]]みたいなのをやるとBox(Box(Box(I am implicit.)))というかたちで再帰的に解決してくれます。これを使います。

それで、「コンパイラに数値計算をしてもらおう」という話なんですけど、Diffというのを考えます。これは何かというと、ちょっともうボイラープレートが多すぎてわけがわかんないんですけど、言いたいのは「何か-0」はそのまま「何か」ですよね。「a-0」は「a」ですよねと。

それと、何かをaだとすると「aの次-bの次」は「aーb」と同じですよね、みたいなことを言っていて、「aーb」がimplicitのスコープ内にあれば「aの次-bの次」も証明できるみたいな感じのことをやっています。

それで、こういうのをやるとScalaコンパイラがいろいろ数値計算をしてくれて、この3-2、Diff[_3,_2]で、「1は3−2である」みたいなことをimplicitに解決してくれというと、怒られません。でも「2が3−2です」みたいなことをやろうとすると怒られる、という感じで引き算ができるようになりました。

割り切れる判定をしてもらう

これの応用で「割り切れる判定をしてもらおう」という感じなんですけど、同じような感じで0は分母で割り切れるというのと、分子-分母は分母で割り切れるみたいなものがどちらかあれば割り切れるよね、と定義します。

そうすると「4は2で割り切れる」というのは怒られないですが、3は2で割り切れるというのは怒られる。こういうことができます。

次、コンパイラに条件分岐をしてもらおうと。Scalaの特殊な解決の優先順位みたいなのがあります。

implicitの[IsEven[_3]]の値を探したいなとなったときに、コンパニオンオブジェクトと言って、シングルトンオブジェクトというかこの型に関係あるオブジェクトみたいなものがあります。こいつの中にimplicitの定義が含まれているかどうかを見てくれるんですけど、その中でも優先順位があって、先に継承ツリーの近い順に解決してくれるんですね。

なので、これはHighPriorityを継承していて、そいつがLowPriorityを継承してるという感じになっているんですけど、こっち(HighPriority)を先に見つけようとしてくれます。見つからなかったらこっち(LowPriority)みたいな感じになっています。これを使って3と0がIsEvenかというのを判定できるようになっています。

FizzBuzzに当てはめると、3でも5でも割り切れるとか、3で割り切れる、5で割り切れるみたいなものをそれらしい優先順位でトレイトを継承していくと。

そういう感じでやると、FizzBuzzValueの1は1で、2は2で、3はFizzで、5はBuzzで、15はFizzBuzzで、FizzBuzzぽくなってきました。

そして、「コンパイラに繰り返してもらおう」というのが次です。これは、結局再帰もできるので繰り返しできるという話ですね。

すると、こんな感じになると。

もうみなさん天才なのでわかるかと思います(笑)。

(会場笑)

「コンパイラにFizzBuzzを解いてもらおう」ということで、これはプリントをするというのがうまく表現できなくて、もっと良い方法があったら教えてくださいという感じなんですけど。

このPrintFizzBuzzMessageというものの値が解決されたときに、その値を素直にPrintlnするというのを用意しておくということです。

それで、そいつを使って[Range[PrintFizzBuzzMessage, _1, _15]]みたいな感じで実行するとこういうふうになるやつがコンパイルされると。

「やったね」と。「ifとかforとかまったくわからないけどFizzBuzzができました!」となるんですけど、こんなややこしいのやりたくないですよね。

なので、Dotty、Scala3だとどれだけ簡単になるのかというのをここからやっていきます。

Dottyでやるとどうなるか

コンパイラに数を認識してもらおうという感じなんですけど、Dottyはliteral typeというものがあって、1とかを素直に認識してくれます。

しかも、Scala2.13にもliteral typeがあるんですけど、その次みたいなものはないんですね。これはDottyだとあります。

最初からSという……Sでいいのかという感じがするんですけど、Sというのがあって、下を見てみると、2はS[1]であるとやると怒られないですけど、2はS[3]であるとやると怒られる。しかも怒られ方も「Int(2)はInt(3)じゃない」みたいにいい感じにやってくれます。Scala3はすごい。

型から値を導出したいというときも、Scala2だとほとんどの場合で型クラスみたいな仕組みを使わないといけないと思うんですけど、constValueというのがあって、ほぼ型クラスに近いんですけど、そのままリテラルが返ってくる、みたいなことができます。

数値計算をするのはめちゃめちゃ簡単です。

Scalaはmatch typeという邪悪な機能があって、型をmatch式に当てはめられるみたいな感じで、「AからBを引きたい」と書こうとすると「Bが0だったらAですね」と。AがSuccの何かで中身もSuccの何かだったら、中身同士をマイナスするみたいなほぼ普通のプログラムじゃないかみたいなことが書けます。いい感じで動きますね。

ちなみに、これはでかい計算はできなくて、10,000-10,000みたいなことをしようとすると怒られます。

「コンパイラに割り切れる判定をしてもらおう」も、match typeできます。

なんでもできるので、なんでもできると。

(会場笑)

条件分岐もmatch typeでできます。

ちなみにこのへんはいい感じにできたなと思うんですけど、"Fizz" | "Buzz" | "FizzBuzz" | Intと書いて出てくる値を全部リテラル型に投れる何かに……しかもunionですね。Scala3でやっと出てきたunionというのがあって、こういうのを使うとかなりいい感じに書けます。

3で割った余りと5で割った余りが(両方とも)0だったら"FizzBuzz"と表示するという、そのまま普通のプログラムみたいに書けます。確かにそれらしく動きます。

「コンパイラに繰り返してもらおう」というのも、結局1の次が2でその次がFizzみたいにいろんな型がごちゃ混ぜになったやつをやるんですけど、これはいわゆるHList(Heterogeneous List)というのがあるんですけど、Scala3では標準で用意してくれていていい感じにやれます。

しかもTuple.Mapみたいな、それぞれのHListになんとかマップしたいやつみたいなものも書けます。すごい。

最終的にどうするかというところでPrintするところなんですけど、Scala3ではinlineというのがあって、コンパイル時にinline化をしてくれるやつですね。

それを使うといい感じに書けて、これはコンパイル結果でコンパイルしたやつです。

ちょっとノイズがあってPrintln1、2、Fizz、4、Buzzとなって、FizzBuzzの答えが上から順番に実行されるみたいなコードが生成されます。これはすごくないですか?

(会場笑)

まとめとしてScala2の場合、implicitの機能を使ってコンパイラにFizzBuzzを解いてもらったんですけど、なんかすごい難しくややこしい方法でできました。「コードが書けないプログラマ志願者」と言われずに済みそうですね。

ちょっとコンパイラの力を借りてそういうことができる。Dottyに関しては難しいことを考えなくてもできるので、簡単すぎておもしろくないという欠点があります。つぐみちゃんもちゃんとプログラマとして認められた。

改めてこれを言うと、もしみなさんの中でFizzBuzzが書けない「ちょっと難しすぎるなぁ」という人がいたら、コンパイラに解いてもらえばいいんじゃないかという発表でした。

ありがとうございます。

(会場拍手)