Rubyで時々「数値の逆数くらいメソッドで取得したい」と思うことがある。メソッドがあれば、数式を書いて括弧で括ったりする手間が省ける。
(1 / num).abs
# ↓
num.inverse.abs
無ければ作ればいいだけだが、いい機会なのでrefinementsを初めて利用してみる。ついでにモジュラ逆数の計算方法を写経する。
(追記)せっかく作ったのでgem化した。refinements無しも添付したため、利用は選択可能。
https://rubygems.org/gems/numeric_inverse
コード
refinementsによるメソッド定義方法は簡単だった。
- 通常のパッチでは単に既存クラスを開く、つまり
class XXX; ...; end
と書く。(モジュールも可) - refinementsでは適当なモジュールを作成し、その中で
refine(XXX) do; ...; end
と書く(Module#refine
を呼び出す)。...
の部分は変わらない。
逆数を求めるメソッド #inverse
は以下のように実装する。
- 基本的には親クラスである
Numeric
に1/self
を計算するメソッドを追加するだけでいい-
Integer
,Rational
,Float
,Complex
など子クラスはこれを呼び出すことになる -
Integer#/
は整商を求めてしまうので、#/
でなく#quo
を明示的に使う方が良い
-
- せっかくなので、整数クラス
Integer
ではオーバーライドして工夫をしておく- 整数論では「モジュラ逆数」というものが存在するので、引数に法を与えたらそちらを計算できるようにする
- 通常の逆数も、わざわざ
super()
を呼んで割り算をせず、直接Rational
を作成してしまえばいい
- メソッド名が長いと使うときに結局面倒なので、別名
#inv
も作っておく
module NumericInverse
refine Numeric do
##
# Returns its inverse.
#
# @return [Numeric] inverse
#
def inverse
1.quo(self)
end
alias inv inverse
end
refine Integer do
##
# Returns its inverse.
#
# @overload inverse
# @return [Rational] inverse
# @overload inverse(m)
# @param m [Integer] modulus
# @return [Integer] modular multiplicative inverse
# @raise [ArgumentError] if +m+ is not coprime to +self+
#
def inverse(m = nil)
return Rational(1, self) if m.nil?
# extended Euclidean algorithm
a, b, u, v = self, m, 1, 0
until b.zero?
a, (t, b) = b, a.divmod(b)
u, v = v, u - t * v
end
# u is an inverse iff self.gcd(m) == 1
if a.abs != 1
raise ArgumentError,
"modulus is not coprime to the receiver"
end
u = -u if a < 0
u % m # 0 <= u < m || m < u <= 0
end
alias inv inverse
end
end
一応yardocを書いてみたが、ドキュメントはうまく生成されなかった。(yard 0.9.16)
利用
main.using
または Module#using
を呼び出すことで、以降それに応じた範囲内(ファイル内/モジュール定義内)で利用できるようになる。
# モジュールを定義しただけでは使えない
require './numeric_inverse'
p Rational(1227, 2018).inv #=> NoMethodError
# `#using` を呼び出すと、スコープ内で使えるようになる
using NumericInverse
p Rational(1227, 2018).inv #=> (2018/1227)
一緒に定義した Integer#inverse
もきちんと機能している。
require './numeric_inverse'
using NumericInverse
# 整数の普通の逆数
p 63.inv #=> (1/63)
p 63.inv * 63 #=> (1/1)
# モジュラ逆数
p 63.inv(100) #=> 27
p 63.inv(100) * 63 #=> 1701
p 63.inv(100) * 63 % 100 #=> 1
irbでのエラー対策
irbで上記コードを試すと、 using
のところでエラーが出る。
irb(main):001:0> require './numeric_inverse'
=> true
irb(main):002:0> using NumericInverse
RuntimeError: main.using is permitted only at toplevel
from (irb):2:in `using'
from (irb):2
from /usr/bin/irb:11:in `<main>'
これを避けるには、irb起動時に --context-mode=1
オプションを追加する。(3未満の数なら良いが、0だと例外バックトレースが長くなるとのこと。)
参考
- refinements
- モジュラ逆数