読者です 読者をやめる 読者になる 読者になる

if 構文を葬りたいでござる。

Scala 設計

仕事でコード書いていて思うんですけども、「if 構文はもう新しいプログラミング言語には要らん」と思うんですよ。

(この記事では「if って言ったって言語によって文法が云々...」っていうツッコミをスルーするために Scala を例にして説明しますが、Scala の深い知識は不要です。)

if というのは非常にシンプルな構文です。

def func( v: Int ) {
    if( v > 0 ) {
        println( v );
    }
}

■ if はプログラムを 2 つに分ける。
if 構文は、条件式の真偽に応じてフローを分けます。

def func( v: Int ) = {
    if( v == 0 ) {
        println( "zero" );
    } else if( v > 0 ) {
        println( "plus" );
    } else {
        println( "minus" );
    }
}

フローを分けるというのは 今は亡き フローチャートによる表現がいちばんわかりやすいように思います。

このように、if はフローを 2 つにしか分けません。3 つ以上に分けたいときは、まず最初に true/false で 2 つに分けてその片方をさらに別の条件で 2 つに分けて... を繰り返すことで実現します。

if 構文は、フローだけで無くスコープをも分けます。

def func( v: Int ) = {
                        // v が使える。
    if( v > 0 ) {       // v が使える。
        val a = v + 1;  // v, a が使える。
        println( a );   // v, a が使える。
    } else {
        val b = v + 2;  // v, b が使える。
        println( b );   // v, b が使える。
    }
                        // v が使える。
}

if ブロックの外側と内側では利用できる名前の集合が異なります。つまり if は『条件によって「プログラムができること」を変化させられるような機能』であるってことで、それは分岐という目的にも合ってるように思います。


■ 分岐はデザインパターンである。

上で書いた通り if は、
 ・任意の条件に従って次に進むべき道を実行時に選択し
 ・その分岐の前後の環境を分断する
という、典型的な設計テクニックを構文に落とし込んだものです。すなわち if 分岐はデザインパターンです。

しかし、どう考えたって n 個に分けられる構文に統一する方が便利です。*1

def func( v: Int ) {
    v match {
         case 0                     => println( "zero" );
         case plus  if( plus  > 0 ) => println( "plus" );
         case minus if( minus < 0 ) => println( "minus" );
    }
}

なぜなら、if は 2 つにしか分岐できないけれど、将来 3 つに分岐しないといけなくなるかもしれないからです。この「将来もっと増えるかもしれないから◯◯する」という説明はプログラマだったら耳にタコができるくらい聞かされてきたはずです。


■ いかにして分岐するか
話のポイントを「if 構文」から「実行時の条件に応じた分岐」*2に広げると、分岐するために他にもいくつかの方法が用意されていることが分かります。


1.match
さっきもちらっと出しましたが分岐といえば match です。*3

def func( v: Int ) {
    v match {
         case 0 => println( "zero" );
         case _ => println( "other" );
    }
}

 v が 0 のときは "zero"、v がそれ以外のときは "other" と表示します。if と match の決定的な違いは、分岐条件の評価のために v が必要であることです。match は分岐条件の評価のためのオブジェクトが無いと分岐先の全てへは到達し得ない一方で、if は、どこへ分岐するかを決める条件の全てで共通のオブジェクトを必要としません。Scala の match は多機能ですが、これ以外の性質は全て if でも実現できるように思います。


2.override

class AbstractObj { def func(); }
class ObjA extends AbstractObj { override def func() = println( "ObjA" ); }
class ObjB extends AbstractObj { override def func() = println( "ObjB" ); }

def main()
{
    def call_func( obj: AbstractObj ) = obj.func(); // ここで分岐
    
    call_func( new ObjA );
    call_func( new ObjB );
}

 抽象化されたプログラムの設計では「分岐のパターンが何通りあり得るのか」が決定できない事があります。そういうとき、override を使って分岐させることがあります。この方法には
 ・分岐条件が「変数 obj が何者なのか」ということによってのみ決定づけられる。
 ・分岐した後の処理もまた「変数 obj が何者なのか」ということによってのみ決定づけられる。
という性質があります。この性質は「どこへ分岐するのか」「分岐した後で何をするのか」を決定づける責任から call_func を完全に解放することができます。一方 if は、これを限定的に実現することができます。


3.try 〜 catch

def func( v: Int ) {
    try {
        throw_some_exception( v );
        println( "Hello" );
    } catch {
        case _: ExceptionA => println( "A" );
        case _: ExceptionB => println( "B" );
    }
}

 throw_some_exception( v ) が ExceptionA を throw したときは "A" を、ExceptionB を throw したときは "B" を表示します。catch 節は match と非常に似ています。が、なぜ統合されていないかというと、ExceptionC が throw されたときの挙動が異なるためです。ExceptionC が throw されたとき、catch はいずれの case にも入らずに func の呼び出し上位にある catch に「どこへ分岐するのか」の判断を委譲します。つまり catch は、発生した例外に対してすべきことを限定的に選択しつつそれ以外の場合にどうするのかという判断を放棄します。
 そして try もまた分岐構文です。throw_some_exception( v ) が何らかの例外を throw したとき、それがどんな例外であっても "Hello" の表示は行われません。プログラムを 2 つにしか分けられないという点で、try と if は似ています。

■ if なぞ要らぬ
 ここまで紹介してきた通り、if は他のあらゆる分岐構文との機能重複を起こしています。
 新しいプログラミング言語オブジェクト指向・関数型・ジェネリクス云々でパラダイムがひしめいている上、読み書きしやすいようにとオーバーロード・糖衣構文・型推論などの機能が搭載されていて言語仕様はガチガチになっています。同じような機能が複数あるとき「書く人が好きな方法を選べる」というのは利点のようにも見えますが、「読む人が考慮すべきコトが増える」という欠点でもあることを考慮すべきです。その欠点を緩和するために if 構文なるものを消し去るというのは手っ取り早い方法のように思います。

 貴重なリプをいただきました。

 これは確かにその通りで、if が持つ評価順序に関する性質は今のプログラミング言語の利用者に広く活用されています。しかしその性質は、クラス設計やパターンマッチ、アルゴリズムの選択といったもうちょっと粒度の大きい単位のプログラム構成要素群と比較すると、重要度は下がるように思います。例えば if を使えばこういうプログラムが手軽に書けます。

def func() {
    if( open_file()              // must close
   && aquire_resouce() )         // must release
    {
        do_something();  
    } else if( boot_device() ) { // must shutdown
        do_something2();         // throws SomeException
    } else {
        do_something3();
    }
}

このような if 構文の評価順を巧みに使ったコードは、アンチパターンということにしてしまっても構わないように思うのです。

 で、結局何が言いたかったっていうと、
 「典型的なデザパタの一種である分岐の実装手段がありすぎてごちゃごちゃするので、包括的な方法を用意してリプレースしたいな... って考えてたらとりあえず if 構文は要らんような気がしてきた!
ってことでした。

おしまい。*4

*1:ソースコード中で使っている if はガード式としての if ですのでここは目をつぶってください

*2:コンパイルタイムに静的に決定する分岐を含むと話が大きくなりすぎるのでやめときます。

*3: match は、C++, Java でいうところの switch に相当しますがそれらに機能を足した強化版です。

*4:追記:if が多用される理由の一つに「そもそも短く書ける」ってのもありますね。