Help us understand the problem. What is going on with this article?

Ruby で Contracts を使って動的型チェック

More than 5 years have passed since last update.

[妄想] Rubyに欲しい文法 の記事を読んでいて、ここに書いてあるような動的型チェックであれば Contracts を使えばいいんじゃないのかな、と思いました。

Contracts とは

Contracts という gem は、メソッドにコードコントラクトを設定する機構を追加するものです。

コードコントラクトというのは、あるコード(主にメソッド)に対して「事前条件(pre condition)」や「事後条件(post condition)」などを追加するもので、C++ や .NET Framework ではよく使われている(ような気がする)ものです。

ただ、この Contracts gem においては、もっと単純に、「メソッドの引数と戻り値がある条件を満たしているかをチェックするもの」と考えて良いと思います。そこに条件を設定できるなら、当然型チェックだって可能ですし、想定している用途としてもそれが主であるようです。

大きな特徴はその記述方法で、以下のように書きます。

Contract Num, Num => Num
def add(a, b)
   a + b
end

普通想定する型チェックのやり方は、メソッド内でassertion的に書いて例外を投げるものになるかと思いますが、Contracts ではこのようにメソッドの外に書きます。

この書き方は結構イカしてると感じます。Rubyっぽいかどうかは置いといて、メソッドのドメインロジックと条件記述が分離しているので、今までのドメインロジックコードのあり方を変えることなくチェックを追加できます。

使い方

ちゃんとドキュメントがあって、もし英語が読めなくてもコード見れば大体分かる作りになっているので、それを読めばわかると思いますが、2,3 簡単な例を示します。

インストール方法

gem install contracts

Contracts を使いたいコード内で require して、モジュールを mix-in します。

require 'contracts'
include Contracts

...

引数の型チェック

例1

  • 2つの引数がいずれも Numeric であること
  • 戻り値が Numeric であること
Contract Num, Num => Num
def add(a, b)
   a + b
end

puts add(1, "foo")

これを実行すると例外が発生する。(コールバックを変更することで例外を投げる以外の対応にすることもできる)。

./contracts.rb:60:in `failure_callback': Contract violation: (RuntimeError)
    Expected: Contracts::Num,
    Actual: "foo"
    Value guarded in: Object::add
    With Contract: Contracts::Num, Contracts::Num
    At: foo.rb:6 

例2

  • 引数が String であること
  • 戻り値が nil であること
Contract String => nil
def hello(name)
  puts "hello, #{name}!"
end

例3

複数の条件のいずれか、という指定も可能

  • 引数が Fixnum か Float であること
  • 戻り値が Fixnum か Float であること
Contract Or[Fixnum, Float] => Or[Fixnum, Float]
def double(x)
  2 * x
end

その他

その他、配列やハッシュ、可変長引数に対する Contracts も用意されています。詳しくはドキュメントを参照してください。

組み込みの Contracts

以下が組み込みで用意されています。

Num, Pos, Neg, Any, None, Or, Xor, And, Not, RespondTo, Send, Exactly, ArrayOf, HashOf, Bool, Maybe

Contract の自作

クラスメソッドに valid? を持ったクラスや、インスタンスメソッドに valid? を持ったクラスを定義することで自作の Contract が作れます。

例えば、最初の例で示した組み込み Contract の Num は以下のように実装されています。

class Num
  def self.valid? val
    val.is_a? Numeric
  end
end

このように Contract を自作することで、先の記事にあった正規表現のチェックも可能です。

class StrRepresentHttp
  def self.valid? val
    /^https?:/.match val
  end
end

Contract StrRepresentHttp => String 
def foo(s)
  s
end

foo("https://...")
foo("http://...")
foo("ftp://...")

結果

./contracts.rb:132:in `failure_callback': Contract violation for argument 1 of 1: (ContractError)
    Expected: StrRepresentHttp,
    Actual: "ftp://..."
    Value guarded in: Object::foo
    With Contract: StrRepresentHttp => String
    At: aaa.rb:17

感想

Ruby でこうした型チェックを行うことに対しての是非を問う声は色々あるでしょうが、個人的にはやれることが増えるのは単純に歓迎です。こういうのがあるのは嬉しい。

5t111111
Engineering Manager @ KODANSHAtech
kodanshatech
現代ビジネス、FRIDAYデジタル、ブルーバックス、FRaU、ViVi、VOCEなど講談社のウェブメディアやデジタルコンテンツ開発を行っています。
https://kodansha.tech/ja
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away