flint>flint blog>2011年> 7月>14日>Ruby でも型チェック

Ruby でも型チェック

動的型付け (スクリプト) 言語では、データ型のチェックが実行時にしか行われないため、プログラムの妥当性検証・デバッグといった作業が困難になります。 例えば、Ruby でプログラムを書いていて、次のようなバグに悩まされたことのある人は多いのではないでしょうか。

  • Integer オブジェクトを参照しているべき変数が、他の型のオブジェクトを参照している。
  • そのオブジェクトが「いつ」「どこで」代入されたものなのか分からない。

この手のバグは、問題の発生 (不正な型の代入) と発覚 (エラーの発生) の位置が離れてしまうので、非常に厄介。 発生箇所を絞り込むのが難しいため、プログラムを広範囲に渡って見直すハメになります。

check_type の導入

こうしたバグへの対処を容易にするため、私は以下に示す check_type メソッドをプログラム全体で使用しています。

# 型チェック
def check_type(type, instance, nilable =false)

    if (instance.nil?)
        unless nilable
            raise ArgumentError::new("non-nil constraint vioration")
        end # nilable
    else
        unless instance.kind_of?(type)
            raise ArgumentError::new("type mismatch: #{instance.class} for #{type}")
        end # instance, type
    end # instance.nil?

    return instance
end

これを各メソッドの配置して、引数の型チェックを実施。 チェックをパスすれば、対象のインスタンスがそのまま戻り値として返されるので、initializeset~ 系のメソッドでは、そのままインスタンス変数に代入することができます。

# ブログエントリ
class Entry

    attr :id             # 識別子
    attr :title          # 表題
    attr :content        # 本文
    attr :time_register  # 登録日時

    # 初期化
    def initialize(id, title, content, time_register)
        @id            =check_type(Integer, id,            false)
        @title         =check_type(String,  title ,        false)
        @content       =check_type(String,  content ,      true)
        @time_register =check_type(Time,    time_register, false)
    end

    # [DB] 項目の単一選択
    def self.db_select_one(dbh, id)
        check_type(DBI::DatabaseHandle, dbh, false)
        check_type(Integer,             id,  false)

        row =dbh.select_one('SELECT * FROM entry WHERE id = ?;', id)

        return row ? db_gen(dbh, row) : nil
    end

    # [DB] 項目の生成
    def self.db_gen(dbh, r)
        check_type(DBI::DatabaseHandle, dbh, false)
        check_type(DBI::Row,            r,   false)

        id            =r[:id]
        title         =r[:title]
        content       =r[:content]
        time_register =dbi_dt2t(r[:time_register])

        return Entry::new(id, title, content, time_register)
    end

end # class Entry

論理型 (Boolean) への対応

check_type は「instancetype で指定された型ないしその派生型のオブジェクトかどうか」を検証するだけなので、共通の親クラスをもたない複数の型のインスタンスを参照し得るような変数には適用できません。 時に厄介なのは、true (TrueClass のインスタンス) / false (FalseClass クラス) のいずれかを受け入れるような引数を持つメソッドです。

例えば、String / Symbol / Regexp のいずれかを引数として渡せるようなメソッドを作ろうとする場合などは、メソッド名を変えることなどによって (ダックタイピングに慣れ切っている人は気持ち悪いと思うようですが)、回避策を講じることが可能です。 しかし、truefalse については「別の型」ではなく、同じ型の「別の値」として扱われるべきものなので、型違いの引数を取る場合と同じ対処をするのはかなり不自然です。 そこで私は、論理型の引数については、仕方なく、代入を伴う場合とそうでない場合についてそれぞれ以下のような記述で妥協し、自分を納得させることにしています。(涙)

# リンク
class Link

    attr :href      # 参照先 (URL)
    attr :text      # アンカテキスト
    attr :disabled  # 状態

    # 初期化
    def initialize(href, text, disabled =false)
        @href     =check_type(String, href, false)
        @anchor   =check_type(String, text, false)
        @disabled =disabled ? true : false
    end

    # 文字列 (HTML) 変換
    def to_s(escape =false)
#       check_type(Boolean, escape, false)

        return sprintf(
            '<a href="%s">%s</a>',
            escape_html(@href), (escape ? escape_html(@text) : @text))
    end

end # class Link

個人的には、そもそも true, falseBoolean::true, Boolean::false, のエイリアスとして定義すべきではなかったのかと思うのですが...。

成田 (型はセマンティクス!)
このエントリーをはてなブックマークに追加

コメント

投稿者
URI
メールアドレス
表題
本文