Scalaで後置ifを作る

ターゲット

  • 制御構文っぽいものを作ってみたいScala中級者
  • Scalaに後置ifないのかよm9(^Д^)プギャー」っていう他言語の人

後置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の逆って…?)


また構文を観察すると

  1. 型Aを返す式(以下A)
  2. when
  3. 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を返せばいいわけです。
これでばっちりタイプセーフに。

実装のまとめ

仕様から考えた実装の仕方をまとめるとこうなります。

  • ifはScala予約語なのでwhenとする
  • whenはメソッドとして扱う
  • whenメソッドの追加のためimplicit conversionを使う
  • trueの場合にのみ式を評価するためCall-By-Nameで式を渡す
  • 型安全のためAの型はジェネリクスで保持する
  • 型安全のため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

この式を例にコンパイラの流れを追って行きましょう

  1. Intクラスに対するwhenメソッドの呼び出しがありますが、whenメソッドはIntクラスには定義されておらず不正な呼び出しです。
  2. しかしここでエラーにはならずコンパイラはimplicit conversionでメソッド名が解決できないかスコープを探索します。
  3. その結果exp2PostIfメソッドを適用すればPostIfクラスへの変換でwhenメソッドが見つかり型の安全性を満たせるので、コンパイラは次のようにコードを変換します
new PostIf[Int](1 + 1).when(1 == 1)

コンパイラが変換する式をそのままコードに書いても普通に動きます。
ここまでの解釈をコンパイル時に行います。

実行時の流れ

new PostIf[Int](1 + 1).when(1 == 1)

コンパイラが加工したコードが実行されます。

  1. PostIfクラスをnewして引数として(1 + 1)という式(関数)を渡します
    • PostIfクラスのコンストラクタ引数はCall-By-Nameで定義してあるのでこの時点では(1 + 1)の式は評価されません
    • newされたPostIfインスタンスは(1 + 1)という式の参照(関数)を持っているに過ぎません(≠値を持っている)
  2. PostIfインスタンスのwhenメソッドが呼ばれます。whenの引数(1 == 1)は即時評価(≠遅延評価)なのでここで評価されてtrueとなります
  3. whenメソッドは引数がtrueの場合にCall-By-Nameで(1 + 1)を評価しますのでここで(1 + 1)は評価され2となります。
    • 引数がfalseの場合は式の評価を行わないまま参照が切れるためGCされます
  4. 結果の2はSome(2)へラップされ戻り値として呼び出し元へ戻ります。

まとめ

Scalaならば通常言語仕様を拡張しなければならないような
制御構文のようなふるまいもライブラリとして提供することが可能です!
言語に不満があれば自分で拡張してしまえばいいのです