Entry

動的型付け言語と型安全の話

2009年11月18日

先日,「qune: 空気読む Perl 空気読まない Java」というエントリを書いたんですけれど,そこで次のようなことを書いたのでした。

型は書かないだけでそれなりに意識しなくちゃいけないわけで,そこら辺が文脈を読むというか,空気を読むというか,そういうお作法と重なるところがあるんじゃないかと思ったり。

qune: 空気読む Perl 空気読まない Java

で,どういうわけだか,この話を読んだ知人から「具体例を示さんとわからん」といったご指摘を頂戴。少し凝った話なんですけれど,具体例を挙げてみます。

OOP にはデザインパターンつのがあって,その中に Strategy というパターンがあります。どういうもんかというと,C++ で書くとこういうもん(あまり厳密ではないけれど)。

#include <iostream>

class Calculator {
protected:
    Calculator() throw () {}
public:
    virtual ~Calculator() throw () {}
public:
    virtual int calc(int rhs, int lhs) throw () = 0;
};

class Add : public Calculator {
public:
    Add() throw () {}
    virtual ~Add() throw () {}
public:
    virtual int calc(int rhs, int lhs) throw () {
        return lhs + rhs;
    }
};

class Subtract : public Calculator {
public:
    Subtract() throw () {}
    virtual ~Subtract() throw () {}
public:
    virtual int calc(int rhs, int lhs) throw () {
        return lhs - rhs;
    }
};

// 似てるけど継承してない
class Multiply {
public:
    Multiply() throw () {}
    virtual ~Multiply() throw () {}
public:
    virtual int calc(int rhs, int lhs) throw () {
        return lhs * rhs;
    }
};

class Context {
public:
    Context(Calculator* calculator) throw ()
         : calculator_(calculator), rhs_(10), lhs_(25) {}
    virtual ~Context() throw () {}
public:
    void execute() {
        std::cout << calculator_->calc(rhs_, lhs_) << std::endl;
    }
private:
    Calculator* calculator_;
    int rhs_;
    int lhs_;
};

////////////////////////////////////////////////////////////////////
int
main(int argc, char* argv[]) {
    Add addSt;
    Subtract subSt;
    Multiply mulSt;

    Context addCtxt(&addSt);   // OK
    Context subCtxt(&subSt);   // OK
//  Context mulCtxt(&mulSt);   // NG type mismatch
//  Context badCtxt(1);        // NG compile error

    addCtxt.execute();         // OK
    subCtxt.execute();         // OK

    return 0;
}

要するに,アルゴリズムを表すクラス(Calculator)の派生クラスにアルゴリズムの実体を実装しておくことで,派生クラスを任意に生成したり切り替えたりすることができる,というもんです。コードをよく見てもらうと分かるんですけれど,Context クラスの execute() メソッドが,Calculator クラスの純粋仮想関数(calc())を実行しています。

これのどこが嬉しいのかというと,Context クラスがどの派生クラスを実行しているのか,意識しなくていいところです。計算の中身と計算を実行するメソッドが分離できるので,後から新しい演算を簡単に追加できるし,状況に応じて計算内容を動的に変更することもできます。つまるところ,アルゴリズムをポリモフィックに実装することができるわけです。

で,もうひとつ重要なこと。C++ の場合,ポリモフィックといっても,際限なくすべてのオブジェクトを受け入れるわけじゃありません。Context クラスは Calculator 型で受けているので,このクラスを継承した派生クラスじゃないといけません。この点で型の縛りがあるわけで,「受ける計算オブジェクトが必ず calc() メソッドを実装していること」が(静的に)保証されているわけ。上の例で言うと,calc() メソッドを実装していても,Calculator クラスを継承したクラスを渡さないと Context オブジェクトは動かない(コンパイルエラーになる)し,calc() メソッドを実装できない整数を渡すのも,コンパイルが通らない。

一方,動的な型付けの場合はどうでしょう。Ruby で同じようなことをすると,こんな感じでしょうか。

class Add
  def calc(rhs, lhs)
    lhs + rhs
  end
end

class Subtract
  def calc(rhs, lhs)
    lhs - rhs
  end
end

class Context
  def initialize(strategy)
    @strategy = strategy
    @rhs = 10
    @lhs = 25
  end
  # Context interface
  def execute()
    puts @strategy.calc(@rhs, @lhs)
  end
end

#############################################
add_ctxt = Context.new(Add.new())      # OK
sub_ctxt = Context.new(Subtract.new()) # OK
bad_ctxt = Context.new(1)              # OK!!

add_ctxt.execute()   # OK
sub_ctxt.execute()   # OK
# bad_ctxt.execute() # NG 'undefined method'

型を付けて渡すことができないので,とりあえず Context クラスにはなんでも渡せてしまいます。これはとりあえず,これでいい(よくもないんだが)。困るのはこの後で,実際に実行した後で,undefined method のエラーが出てプログラムが止まってしまうところです。渡した時点でインタプリタは,それが不正な受け渡しなのか,正しい受け渡しなのかを判断することができずに,矛盾があるところにいたって初めて気がつくわけです(上記コードで 1 を渡した場合が該当する)。先の引用で,Java の ClassCastException を批判した文章を紹介したけれども,結局この例外は実行時例外として同じような位置づけにあるんだと思います。

また,型付きで渡せないということは,たまたま calc() というメソッドを実装したオブジェクトを間違って渡した場合も,否応なく実行されてしまうことでもあります。Ruby の例で Calculator クラスを作って,各計算クラスの基底クラスにしなかったのは,そうしても意味がないからです。引数の型を保証する場合は,型にまつわるメタ関数を呼ぶ必要があるけれども,そうしてしまったら出来の悪い型付け言語と同じになってしまいます。

この場合,Context クラスの利用者(クライアント)は,execute() メソッドの実装まで踏み込まないと,オブジェクトがそのクラスで利用可能か分かりません。カプセル化されているようでされていない。どのメソッドがインターフェイスになって,Context クラスと各計算クラスが通信するのか,ぱっと見分からないわけです。この場合は,単純にその場で実装しているから,まだ分かりやすいけれども,Context#execute() 内でさらに別のオブジェクトを呼び出したり作ったりするようなことをすると,簡単に何を渡しているのか分からなくなります。

おそらく,動的な型付け言語の立場からすると,ここら辺が「常識」で解決できることなんだと思います。で,常識で解決できるかどうかはともかく,それを踏まえてプログラミングできることを,前回は「空気読める」と表現したのでした。ま,もともと,動的型付け言語に Strategy パターンはいらないという話もありそうだけれど。

結局,どちらを使っても,プログラマは型について少なからず踏まえている必要があるわけで,要するに,どっちもどっち,といった感じ。

Trackback
Trackback URL:
Ads
About
Search This Site
Ads
Categories
Recent Entries
Log Archive
Syndicate This Site
Info.
クリエイティブ・コモンズ・ライセンス
Movable Type 3.36
Valid XHTML 1.1!
Valid CSS!
ブログタイムズ

© 2003-2012 AIAN