1. 引数と戻り値のチェック

NeuronCheckのもっとも基本的な使い方は、メソッドの引数、戻り値が期待したものであるかどうかをチェックすることです。
これは一般的に「型チェック」と呼ばれるものですが、NeuronCheckでは型チェック以上の検証(値が指定範囲に収まっているかどうかのチェックなど)も行うことができます。

require 'neuroncheck'

class Foo
  extend NeuronCheck

  ndecl {
    args String, Numeric, respondable(:each), ['flash', 'none']
    returns :self
  }
  def foo_method(text, rate, targets, type)
    # Main Process...

    return self
  end
end

ndecl ブロックは、その直後に定義されるメソッド(上記のdef foo_method)について、引数や戻り値の型を宣言します
そして、メソッドの実行時に、実際に渡された引数や戻り値が宣言内容と一致しているかどうかのチェックを実行します。

最初に ndecl を実行する前に、クラスやモジュールの定義内で extend NeuronCheck を実行しておかなくてはならないことに注意してください。

上記コード例のように宣言することで、foo_methodの呼び出し時に次のようなチェックを行うことができます。

  • 引数textが文字列 (String) であること
  • 引数rateが数値であること(NumericのサブクラスであるInteger, Float, Rationalなども受け付ける)
  • 引数targetseachメソッドが定義されていること (配列、Hash、Rangeなど)
  • 引数typeが ‘flash’ か ‘none’ のいずれかの値であること
  • 戻り値がselfであること

実際にスクリプト内で、foo_methodの呼び出し時に不正な引数を渡した場合、NeuronCheckは下記のようなエラーを表示します。

inst = Foo.new
inst.foo_method('Value', 0.8, 'flash')
#=> エラーなし

inst.foo_method('Value', '0.8', 'flash')
#=> 下記の例外が発生する
#   script.rb:20:in `<main>': 2nd argument `rate' of `Foo#foo_method' must be Numeric, but was "0.8" (NeuronCheckError)
#             got: "0.8"
#       signature: Foo#foo_method(value:String, rate:Numeric, targets:respondable(:each), type:["flash", "none"]) -> self
#     declared at: script.rb:7:in `block in <class:Foo>'

おそらくNeuronCheckを使わない場合でも、メイン処理の中のどこかのタイミングでエラーは発生するでしょう。
ですが、NeuronCheckを使うことでより早い段階で、説明的なエラー表示が行われることになり、よりエラーの発生原因が分かりやすくなることが期待できます。


なお、チェックに使える値には、最初のコード例以外にもさまざまなものがあります。例えば……

args [String, nil]      #=> 引数が、文字列かnilであることをチェック
args [true, false]      #=> 引数が、trueかfalseであることをチェック
args /ruby/             #=> 引数が、「ruby」という単語を含む文字列であることをチェック
args 0..100             #=> 引数が、0から100までの間の数値であることをチェック

args any                #=> 引数はどんな値でもよい
args except(nil)        #=> nil以外ならどの値でもよい

args array_of(String)          #=> 引数が、文字列だけを内容として含む配列であることをチェック
args hash_of(Symbol, Integer)  #=> 引数が、Symbolをキー、数値を値として含むHashであることをチェック
                               #   たとえば {:foo => 1, :bar => 2} のようなHashを受け付ける

全ての使用可能なチェックを確認したい場合は、別文書「引数と戻り値のチェック」を参照してください。

NeuronCheckのプラグイン機構を利用することで、ユーザーが独自のキーワードを追加することも可能です(例えば、booleanキーワードを追加して、true/falseの検証を簡単に行えるようにするなど)。詳細は独自キーワードの追加を参照してください。


また、可変数の引数や、キーワード引数に対してチェックを行うことも可能です。

class Blog
  extend NeuronCheck

  ndecl {
    args String, Symbol, [true, false]
  }
  def post_article(title, *tags, public: false)
  end
end

blog = Blog.new
blog.post_article("test article 1")                      #=> エラーにならない
blog.post_article("test article 1", :food, :technology)  #=> エラーにならない
blog.post_article("test article 1", public: true)        #=> エラーにならない

blog.post_article("test article 1", 'FOOD')              #=> tagsに文字列が渡されたためエラー
blog.post_article("test article 1", public: 1)           #=> publicにtrueでもfalseでもない値が渡されたためエラー

さらに、initializeメソッドや特異メソッド、attr_accessorattr_readerなどによるアクセサ定義に対しても、同様にチェックを行うことができます。

class Blog
  extend NeuronCheck

  ndecl {
    args String
    returns Blog
  }
  def self.load(path)
  end

  ndecl { val String }
  attr_accessor :title

  ndecl { args String }
  def initialize(name)
  end
end

なお、attr_accessorattr_readerなどに対するチェックを行うときのみ、argsではなくvalを使用する必要があることに注意してください。詳細は属性メソッド (attr_*) に対する宣言の項を参照してください。

2. チェックのオフ

NeuronCheckの大きな特徴のひとつは、プロダクション環境(本番環境)のパフォーマンスに与える影響がきわめて小さい ということです。
全てのチェック処理を任意のタイミングでオン/オフすることができるため、開発環境ではチェックによる恩恵を受けつつ、プロダクション環境ではチェック無しで最大の性能を出すことができます。

これはEiffelやD言語の仕様――リリースビルド時に成果物からチェック処理を取り除く――を元にしています。

チェック処理をオフにしたい場合は、コード内でNeuronCheck.disableを実行してください。

NeuronCheck.disable

NeuronCheck.disable を実行して以降は、各メソッドを呼び出してもNeuronCheckによるチェック処理は実行されないため、NeuronCheckがパフォーマンスに与える影響はほぼ0になります。

require 'neuroncheck'

class Blog
  extend NeuronCheck

  ndecl { args String }
  def initialize(title)
  end
end

blog = Blog.new(100) # => 数値を渡したためエラーになる

NeuronCheck.disable
blog = Blog.new(100) # => 全てのチェック処理が無効化されているため、エラーにならない

なお、NeuronCheck.disable はブロック付きで呼び出すこともでき、その場合はブロック内でのみNeuronCheckが無効化されます。

NeuronCheck.disable do
  blog = Blog.new(100) # => 全てのチェック処理が無効化されているため、エラーにならない
end

実際のアプリケーションでは、事前に設定された値を見て、適宜NeuronCheck.disableを実行する必要があるでしょう。

たとえばSinatraを使ったWebアプリであれば、下記のような書き方をすることで、production環境の場合のみNeuronCheckを無効化することができます。

configure :production do
  NeuronCheck.disable
end

3. さまざまな書き方

NeuronCheckでは、できるだけストレス無くチェック処理を記述できるようにするために、何通りかの書き方(別記法)をサポートしています。

まず、ndeclメソッドにはndeclare, ncheck, nsig, ntypesigという名前の別名(エイリアス)が定義されているため、好みに合わせて好きなメソッド名を使用することができます。

# 下記の記述はすべて同じ効果
ndecl { ... }
ndeclare { ... }
ncheck { ... }
nsig { ... }
ntypesig { ... }

さらにRuby 2.1以降であれば、Refinement機能を利用し、using NeuronCheckSyntaxを実行することで、より簡潔な書き方ができるようになります。

たとえば、通常では下記のような記述を行いますが……

require 'neuroncheck'

class Converter
  # Converterクラス内でNeuronCheckを使用可能にする
  extend NeuronCheck

  # NeuronCheckのチェック宣言部
  ndecl {
    args String, respondable(:each), [Numeric, nil]
    returns :self
  }

  # 実際のメソッド定義。呼び出し時に上で宣言したチェックが行われる
  def convert(text, keywords, threshold = nil)
    # (メイン処理)
  end
end

using NeuronCheckSyntaxを使用すると、このように書くことができます。

require 'neuroncheck'
using NeuronCheckSyntax # Refinementの使用

class Converter
  decl String, respondable(:each), [Numeric, nil] => :self
  def convert(text, keywords, threshold = nil)
    # (メイン処理)
  end
end

この機能を使うと、1行で複数の引数・戻り値チェックを記述することができ、また 各クラス内でのextend NeuronCheckが不要になります。

この機能についての詳細は、NeuronCheckSyntaxによる簡易記法を参照してください。

4. 事前条件、事後条件のチェック

NeuronCheckは、契約プログラミングで提唱された事前条件と事後条件のチェックも実行することができます。
この機能を使用すると、引数や戻り値に対してより詳細なチェックを行ったり、インスタンスの状態に関するチェックを行うことが可能です。

事前条件や事後条件のチェックを行うためには、ndeclによる宣言部の中で、下記のように記述してください。

ndecl {
  precond do
    ... # 事前条件
  end

  postcond do |ret|
    ... # 事後条件
  end
}

「事前条件 (precondition)」は、メソッドの呼び出し前にチェックされ、引数やインスタンス変数の値が条件に合っているかどうかを判定します。
「事後条件 (postcondition)」は、メソッドの呼び出しが完了した後に呼び出され、戻り値やインスタンス変数の値が条件に合っているかどうかを判定します。

この事前条件と事後条件を使って、実際にチェック処理を行うコード例を下記に示します。

require 'neuroncheck'

class Foo
  extend NeuronCheck

  def initialize
    @file_loaded = false
  end

  ndecl {
    args String

    # 事前条件
    precond do
      # 引数「name」の文字数が10を超えていないかどうかを判定。10を超えていればエラー
      #  (引数チェックをクリアしているため、Stringであることは保証されている)
      assert{ name.length <= 10 }

      # インスタンス変数「@file_loaded」がtrueであるかどうかを判定。falseやnilであればエラー
      assert{ @file_loaded }
    end

    # 事後条件
    postcond do |ret|
      # 戻り値のチェックが必要な場合はここに書く
    end
  }
  def foo_method(name)
    # メイン処理
  end
end

inst1 = Foo.new
inst1.foo_method('therubyracer')  # 引数nameが10文字を超えているためエラーとなる
inst1.foo_method('rubyracer')     # 引数nameは10文字以内に収まっているが、この時点で@file_loadedがfalseのため、やはりエラーとなる

なお、この条件チェックの中では、通常はインスタンスの持つメソッドやアクセサを実行できないということに注意してください。
また、インスタンス変数への代入も無視されます。

  precond do
    self.method1            # インスタンスメソッドは実行できない (NoMethodErrorとなる)

    @file_loaded = true     # エラーとはならないが、実際にはインスタンス変数には値が代入されない
  end

NeuronCheckがインスタンスメソッドやアクセサの実行を禁止しているのは、実行したときにインスタンスに自明でない副作用を及ぼす可能性があるからです。(get_fooメソッドを実行することによって、インスタンス変数 @foo の値が初期化されるとか)
これは NeuronCheckをオンにしたときとオフにしたときで動作が違う可能性がある ということを意味します。

どうしてもインスタンスの持つメソッドやアクセサを呼び出したい場合は、allow_instance_method オプションを使用してください。

precond(allow_instance_method: true) do
  method1            # 正しく実行できる
end

ただし、事前条件や事後条件の中でアクセサやインスタンスメソッドを呼びたい場合には、副作用が起こらないことを注意深く確認してください。

なお、事前条件や事後条件の途中でチェック処理を打ち切りたい場合は、returnbreakではなくnextを使用してください。

precond do
  break # 例外 LocalJumpError が発生する (Ruby言語の制限)

  next  # OK
end

5. 属性メソッド (attr_*) に対する宣言

NeuronCheckは、attr, attr_accessor, attr_reader, attr_writerで定義される属性の取得/設定メソッドについても、チェック処理を行うことができます。

ただしこの場合、通常のメソッド定義とは異なり、args, returnsの代わりにval(もしくは別名must_be, value)を使います。

class Foo
  extend NeuronCheck

  ndecl { val String }
  attr_accessor :caption
end

foo_inst = Foo.new
foo_inst = 'CAPTIONです'  #=> 正常に実行される
foo_inst = 1 #=> エラーになる
# 下記の記述はすべて有効
ndecl { val String }
ndecl { value String }
ndecl { must_be String }

またこの場合は、複数の属性に対して、一度にチェックを適用することも可能です。

class Foo
  ndecl { val String }
  attr_accessor :caption1, :caption2  # => caption1とcaption2の両方を文字列型とする
end

NeuronCheckSyntaxによる短縮記法を使うと、下記のように書くことができます。

using NeuronCheckSyntax

class Foo
  ndecl String
  attr_accessor :caption1, :caption2
end

6. NeuronCheckのチェックをメソッド定義以外で実行する (単体での使用)

NeuronCheckは通常、メソッド定義に対してチェックを設定するためのライブラリですが、時にはメソッド定義を持たない処理のチェックを行いたい場合があります。
これはたとえば、Rakeのタスクで条件チェックを行いたいときや、Sinatraを使っていてルーティング処理の際にチェックを行いたい場合などがあります。

こうした場合のために、NeuronCheckは宣言なしでの単体チェックを行うためのメソッドcheckmatch?を備えており、引数や戻り値のチェックと同じ仕組みで、値1つが正しい値であるかどうかをチェックすることができます。

NeuronCheck.check(str){ String }     # strの値が文字列なら何もしない。それ以外の場合ばエラーを発生させる

NeuronCheck.match?(str){ String }    # 上記と同じだが、エラーを発生させず、代わりにtrueかfalseを返す

7. 継承した場合の動作

NeuronCheckを定義したクラスを継承した場合、そのメソッドのチェックはどうなるのでしょうか?

これについて、NeuronCheckは次のシンプルな原則に従って動きます。親クラスのチェックはすべて実行されます。
サブクラスで事前条件を定義したとしても、親クラスの事前条件は有効なままです。

たとえば、次のような継承関係を作り…

require 'neuroncheck'

class Class1
  extend NeuronCheck

  ndecl {
    args String # 引数チェック1

    precond do
      # 事前条件チェック1
    end

    postcond do |ret|
      # 事後条件チェック1
    end
  }

  def foo(str1)
    # (メイン処理1)
  end
end

class Class2 < Class1
  extend NeuronCheck

  ndecl {
    args String, String # 引数チェック2

    precond do
      # 事前条件チェック2
    end

    postcond do |ret|
      # 事後条件チェック2
    end
  }

  def foo(str1, str2)
    super(str1)

    # (メイン処理2)
  end
end

この状態で、Class2のインスタンスに対してfooメソッドを呼び出したとします。

inst = Class2.new
inst.foo("a", "b")

この場合、下記の順番で処理が実施されます。

  1. Class2のfooメソッドに宣言された、引数チェック2と事前条件チェック2が実行される(不正値であればエラー)
  2. Class2のfooメソッドが実行され、super(str1)を呼び出す
  3. Class1のfooメソッドに宣言された、引数チェック1と事前条件チェック1が実行される(不正値であればエラー)
  4. Class1のfooメソッドにあるメイン処理1が実行される
  5. 戻り値に対して、Class1のfooメソッドに宣言された、事後条件チェック1が実行される(不正値であればエラー)
  6. Class2のfooメソッドにあるメイン処理2が実行さっる
  7. 戻り値に対して、Class2のfooメソッドに宣言された、事後条件チェック2が実行される(不正値であればエラー)

この動作は、契約プログラミングの先祖であるEiffelやD言語とは異なる動作です。