Ruby でメソッドを多重定義するためのライブラリをつくった

まあつくったのは結構前なんですが、つくりました。
Ruby は動的型付け言語なので、静的型付けのような型によるメソッドの多重定義は出来ないんですが、このライブラリによって静的型付けのようなメソッドの多重定義を実現する事ができます。

require "stitcher"

using Stitcher

class X
    # Fixnum オブジェクトのみ代入できる変数定義
    stitcher_accessor value: Fixnum
    
    # Fixnum のみ受け取る
    stitcher_require [Fixnum]
    def add a
        value + a
    end

    # String のみ受け取る
    stitcher_require [String]
    def add a
        value + a.length
    end
end

x = X.new

x.value = 42       # OK
# x.value = "homu"   # Error

p x.add -3         # => 39
p x.add "homu"       # => 46
# p x.add [1, 2, ] # Error

[gem-stitcher]

[インストール]

$ gem install stitcher

[これは何?]

これは、静的型付け言語のように定義する(定義した)メソッドに対して引数のシグネチャを設定するライブラリです。 シグネチャを指定することにより、静的型付け言語のようなメソッドの多重定義を行うことを目的としています。

[使い方]

stitcher は定義したメソッドか、次に定義するメソッドに対してシグネチャを設定することで使用します。

require "stitcher"

# using することで Module クラスに Stitcher の機能が mixin される
using Stitcher


class Counter
    attr_reader :value

    def initialize value
        @value = value
    end
    # 既存のメソッドに対して引数のシグネチャを設定する
    # #initialize メソッドに対して Fixnum クラスのインスタンスのみ受け取るようにする
    stitcher_register :initialize, [Fixnum]

    # stitcher_register を使用することで次に定義するメソッドに対しての
    # シグネチャを設定することもできる
    # stitcher_register の第二引数と stitcher_require の第一引数は同じである
    stitcher_require [Fixnum]
    # add メソッドは Fixnum のインスタンスのみ受け付ける
    def add value
        @value += value
    end

    # また、違うシグネチャを設定することにより複数のメソッドを定義する事ができる
    stitcher_require [String]
    def add str
        @value += str.to_i
    end
end

count = Counter.new 0     # OK
# count = Counter.new "" # Error: Fixnum クラスのインスタンスではない

count.add 10               # OK
count.add "12"               # OK
# count.add 0.42           # Error
p count.value
# => 22

このように、

  • .stitcher_register で既存のメソッドに対してシグネチャを設定する
  • .stitcher_require を利用して次に定義するメソッドに対してシグネチャを設定する

というに使い分けて利用します。

[呼び出されるメソッドの優先順位]

複数のメソッドにマッチした場合、後から定義したメソッドを優先して呼び出します。

require "stitcher"

using Stitcher

class X
    stitcher_require [Object]
    def func a
        "func{Object}"
    end

    stitcher_require [String]
    def func a
        "func{String}"
    end
end

x = X.new
p x.func 10
# => "func{Object}"
p x.func "homu"
# => "func{String}"

ですので、クラス拡張でも利用することが出来ます。

require "stitcher"

using Stitcher

class Array
    stitcher_require [String]
    def at str
        at str.to_i
    end
end

p [1, 2, 3].at "1"
# => 2
p [1, 2, 3].at 2
# => 3

[可変長引数]

Array#+@ で可変長引数になります。

require "stitcher"

using Stitcher


class X
    def initialize step
        @step = step
    end

    # String を1つ以上受け取る可変長引数
    stitcher_require +[String]
    def join *args
        args.join @step
    end
end

x = X.new  ", "
p x.join "homu", "mami", "mado"
# => "homu, mami, mado"

# p x.join # Error

0個以上の可変長引数を受け取りたい場合は stitcher_require [] と組み合わせてください。

[ブロック引数の有無をチェック]

[String] & Stitcher::Concepts.blockable でブロック引数を要求します。

require "stitcher"

using Stitcher

class X
    stitcher_require [String]
    def func str
        str + str
    end

    stitcher_require [String] & Stitcher::Concepts.blockable
    def func str
        yield str
    end
end

x = X.new

p x.func "homu"
# => "homuhomu"

p x.func("mami"){ |str| str + "homu" }
# => "mamihomu"

[アクセッサ]

stitcher_accessor でアクセッサを定義する事が出来ます。

require "stitcher"

using Stitcher

class Person
    # String のみ代入できる name と
    # Fixnum のみ代入できる age のアクセッサを定義
    stitcher_accessor name: String, age: Fixnum
end

homu = Person.new
homu.name = "homu"   # OK
# homu.name = 42       # Error

homu.age  = 14     # OK
homu.instance_eval { @age = "homu" } # OK @ 変数への代入は抑制できない

他にも書き込み専用であれば stitcher_writer が利用できます。

[クラスオブジェクト以外の利用]

基本的にはクラスオブジェクトのリストを使用しますが、内部ではシグネチャのチェックに #=== を利用しているので、各シグネチャには #=== が定義されているオブジェクトであれば何でも渡すことができます。 なので、ProcRegexp オブジェクトなどをシグネチャとして渡すこともできます。

class Http
    # Regexp オブジェクトを設定
    stitcher_require [/^https?/]
    def get url
        # ...
    end

    # Proc オブジェクトを設定
    stitcher_require [ proc { |hash| (Hash === hash && hash.key?(:url)) } ]
    def get hash
        get hash[:url]
    end
end

http = Http.new
http.get "http://docs.ruby-lang.org/"            # OK
http.get({ url: "http://docs.ruby-lang.org/" }) # OK

http.get "ftp://docs.ruby-lang.org/"         # Error
http.get({ uri: "http://docs.ruby-lang.org/" }) # Error

このあたりは case 文と同じ構想になります。
何か不具合等があれば Issues までおねがいします。