Scalaで後置ifを作る
後置if
Rubyでは後置if/unlessが多用されますよね。たぶん。
例えばこんな感じで。
# Rubyのコード puts('Hello') if 1 == 1 # Helloを表示して => nil puts('Hello') if 1 == 0 # 何もせず => nil
確かに"do [処理] if [条件]"のように英語のように読めるため可読性がいいので私も好きです。
(個人的にunlessはなぜか混乱してしまうのでキライです)
こんなんScalaで使えないかなーと思ってたんですけど
Scalaでは組み込み構文としては後置if/unlessはないんですねー…
Javaならここで潔く諦めるところですが、
Scalaであればその柔軟性の高さから自分で作れてしまいます!
言語を拡張するのではありません!ライブラリとして追加できるのです!
ということで作ってみましょう!
仕様を考える
こんな仕様を目指してみましょう。
- 構文
{型Aを返す式} if {Booleanを返す式} //=> 型A
- 後置ifは任意の式に対して使用できる
- '型Aを返す式'は副作用を持つこともあるので'Booleanを返す式'がtrueの場合にのみ評価する
- '型Aを返す式'に応じた型Aの値を返す
後置ifの実装を考える
構文から実装を考える
いきなりしょうもない問題。”if"という名前はScalaの予約語なので自由に使えません…
実は`if`のようにバッククオートで囲めば予約語だろうと記号だろうと使えるんですが、
呼び出し時もバッククオートで囲まなくてはいけないためブサイクです…
しょうが無いのでここは"when"と似たような名前を付けて妥協しましょう…(´・ω・`)
(when/unlessだと名前に対称性がないって!?でもwhenの逆って…?)
また構文を観察すると
- 型Aを返す式(以下A)
- when
- Booleanを返す式(以下B)
という3つの部分から構成されますね。
これは"A.when(B)"と捉え、「クラスAのメソッドwhenを引数Bを与えて呼び出す」
と考えられないでしょうか?しかも値を返すし。
しかもScalaはレシーバ、メソッド、1引数の場合ドットとカッコを省略できる
シンタックスシュガーがあるのでなんとも都合よく"A when B"と記述ができるのです!
「任意の式で使える」仕様から実装を考える
任意の式でメソッド"when"が使えるようにするには既存のクラスを拡張しなくてはいけません。
そんな時Scalaではimplicit conversion(暗黙的型変換)と言う機能が使えましたよね!
すべての型でimplicit conversionを使ってwhenメソッドが使えるようになればいいわけですね
「trueの場合のみ評価する」仕様から実装を考える
"A when B"という構文としてはAが先に記述されるので
単純にwhenのメソッド呼び出しをしようとするとAは先に評価されてしまいます。
Aを値として扱うのではなく関数として扱えば評価する/しないのコントロールが可能です。
とはいえ単純に関数として扱うなら"()=>{A}"のような構文となってしまい目指す構文とは離れてしまいます…
そこで利用側には意識させずに関数的に扱えるCall-By-Name(名前渡し)による遅延評価を使えます。
Call-By-Nameとして定義することでシームレスにAを評価前の関数として保持し、
Bがtrueの時に実行、falseの時には捨てればいいわけです。
「型Aを返す」仕様から実装を考える
せっかくScalaは強い静的型付け言語なのですからタイプセーフであるべきです。
ということで型Aの情報を失わないようにしましょう。
これを実現するには、ジェネリクスを使えばいいですね。
Call-By-Nameとジェネリクスを合わせればいいわけです。
戻り型はどうでしょう。
trueで評価された場合Aの評価結果を返せばいいですが、
falseの場合は評価されませんので返す値がありません。
単純にnullを返せばいいじゃないかと思われるかもしれませんが、
Scalaでnullを使用するのはタブーです。
なのでnullを型で表すOption型でラップしましょう。
trueの場合はSome(Aの結果)を返し、falseの場合はNoneを返せばいいわけです。
これでばっちりタイプセーフに。
実装のまとめ
仕様から考えた実装の仕方をまとめるとこうなります。
では実装
実装してみるとこんな感じでしょうか。
class PostIf[A](exp: => A) { // => A と宣言しているのでexpはCall-By-Name def when(cond: Boolean): Option[A] = if (cond) Some(exp) else None def unless(cond: Boolean): Option[A] = when(!cond) // condを逆にしただけ } object PostIf { implicit def exp2PostIf[A](exp: => A): PostIf[A] = new PostIf(exp) }
思っていたよりもコード数は少ないんじゃないかと思います。
これで後置ifが使えるようになります!
自家製後置ifを使ってみる
使ってみましょう!
import PostIf._ // 後置if 1 + 1 when 1 == 1 //=> Some(2) 1 + 1 when 1 == 0 //=> None // 後置unless 1 + 1 unless 1 == 1 //=> None 1 + 1 unless 1 == 0 //=> Some(2) // 副作用がある場合 println("hello") when 1 == 1 // helloを表示して => Some(()) println("hello") when 1 == 0 // 何もせず => None
まずはimplicit conversionを有効にするためにobject PostIfのimplicitなメソッドをimportする必要があります。
それ以降はだいたいRubyと同じですね。(たぶん)
コンパイラのお仕事
1 + 1 when 1 == 1
この式を例にコンパイラの流れを追って行きましょう
- Intクラスに対するwhenメソッドの呼び出しがありますが、whenメソッドはIntクラスには定義されておらず不正な呼び出しです。
- しかしここでエラーにはならずコンパイラはimplicit conversionでメソッド名が解決できないかスコープを探索します。
- その結果exp2PostIfメソッドを適用すればPostIfクラスへの変換でwhenメソッドが見つかり型の安全性を満たせるので、コンパイラは次のようにコードを変換します
new PostIf[Int](1 + 1).when(1 == 1)
実行時の流れ
new PostIf[Int](1 + 1).when(1 == 1)
コンパイラが加工したコードが実行されます。
まとめ
Scalaならば通常言語仕様を拡張しなければならないような
制御構文のようなふるまいもライブラリとして提供することが可能です!
言語に不満があれば自分で拡張してしまえばいいのです