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

平方根オブジェクトを実装する

More than 1 year has passed since last update.

普段主にRailsでサーバ開発をしているエンジニア(会社員)の@sasurai_usagi3です. よろしくお願いします.
突然ですが、$(\sqrt{2})^2$っていくつでしょうか? 正解は$2$ですね.
これをRubyに計算してもらおうとすると次のような式になります.

Math.sqrt(2) ** 2

ところがこれを実行すると2.0000000000000004(mac os Mojave10.14.1, Ruby2.5.0)となってしまいます. 悲しいですね.
本来無理数であるはずの$\sqrt{2}$を有限小数として値を返したのちべき乗を行うために誤差が発生します.

まぁこれが問題あるのかって言われるとわからないところですが, どうせならIntegerの2を返してほしいところなので平方根を表すオブジェクトを作成しました. 誰かが作ってそうな気もしますが, RubyGems.org でsqrtが検索に引っかかってこなかったのでそういうことだと信じます.

実際に作成して公開したgem, ソースコードはこちらから
- https://rubygems.org/gems/sqrt
- https://github.com/sasurai-usagi3/sqrt
加算・減算は綺麗に実装できない気配がしたので保留で, 除算は力尽きたのでまだ実装してないです.

オブジェクトをどうするか考える

$2\sqrt{2}$のようなのも正確に表したいので, オブジェクトとして平方根内部の値と係数をインスタンス変数として持つオブジェクトを作成します.
よってコンストラクタは次のようになります.

def initialize(coefficient, value)
  @coefficient = coefficient
  @value = value
end

以下平方根オブジェクトのクラスをSqrtとします.

べき乗を計算するメソッドの実装

べき数が小数, 有理数や虚数だったりするケースはどうすればいいのかわからないので今回は諦めます. べき数は整数だけに絞ります.
注意しなければならないのはこのメソッドが返す値の型は整数だけでなく平方根オブジェクトの場合もありえるということです.
$(\sqrt{2})^2 = 2 (Integer)$ ですが $(\sqrt{2})^3 = 2\sqrt{2} (Sqrt Object)$になります.
まとめるとべき数が偶数であるならIntegerを奇数ならSqrtを返すようにしてあげれば良いということになります.
実装すると次のような感じです.

def **(n)
  n.even? ? (@coefficient ** n) * (@value ** n / 2) : Sqrt.new((@coefficient ** n) * (@value ** n / 2), @value)
end

**メソッドを実装しています. 四則演算やべき乗のようなメソッドも自分で実装・オーバライドできるのが, Rubyの特徴でしたね. あとポイントとしては, nがIntegerの時n / 2も小数切り捨てとなりIntegerになります.
even? メソッドは文字通りレシーバが偶数ならtrueを返すメソッドです. こういった役立つメソッドが多くてありがたいです. 逆に多すぎて覚えきれん

乗算を計算するメソッドの実装

真面目にやろうとすると結構面倒です. Sqrt クラス同士の実装はcoefficientvalue同士掛け合わせるだけなので大したことはありません. ただSqrt * IntegerInteger * SqrtというケースがありえるためFloatRationalまで考えると組み合わせが多く面倒です.
前者はSqrtクラスの*メソッドに実装しますが, 後者はIntegerクラスの*に実装します. Integerクラスの*メソッドが今まで通りの動作も失わないように注意して実装します.

まずSqrtクラスの*メソッドから実装します.

(Sqrtクラスの方)
def *(target)
  if n.class == Sqrt
    Sqrt.new(@coefficient * target.instance_variable_get(:@coefficient), @value * target.instance_variable_get(:@value))
  elsif n.class <= Numerical
    Sqrt.new(@coefficient * target, @value)
  end
end

インスタンス変数のゲッタ・セッタを生やしていないので, もう一方のSqrtクラスのインスタンスからインスタンス変数の値を取るのにinstance_variable_getを使っています. 内部の計算以外でインスタンス変数を参照する機会はないと思っているのでゲッタを生やしていない経緯があります.

instance_variable_getをいちいち書くのは面倒だけど, ゲッタをパブリックにしたくない時にはprotectedでメソッドを生やすのも手です. というかそっちの方が楽ですね, はい.

def initialize(coefficient, value)
  @coefficient = coefficient
  @value = value
end

protected

def coefficient
  @coefficient
end

def value
  @value
end

とすると

(Sqrtクラスの方)
def *(target)
  if n.class == Sqrt
    Sqrt.new(@coefficient * target.coefficient, @value * target.value)
  elsif n.class <= Numerical
    Sqrt.new(@coefficient * target, @value)
  end
end

とでき, かつ

x = Sqrt.new(1, 2)

x.coeficient # NoMethodError

にできます.

若干脱線しましたが, 続いてInteger側の実装です.

class Integer
  alias_method :multiply, :*

  def *(n)
    n.class == Sqrt ? Sqrt.new(self.multiply(n.instance_variable_get(:@coefficient)), n.instance_variable_get(:@value)) : self.multiply(n)
  end
end

これがベタな実装でしょうか. alias_methodを使うことで上書きされてしまう*への上書きされる前のメソッドへの参照を残すことができます.
エイリアスを生やさず*内部で*を呼ぶと当然新しく定義した方が呼び出されるので無限ループにハマります.
ちなみにgem側の実装ではモジュールが読み込まれた時に上書きを行いたかったのでModule#includedを用いて次のように実装しています.

class << self
  # モジュールが読み込まれたら実行
  def included(klass)
    # class宣言行う時の環境にアクセスできる
    Integer.class_eval do
      alias_method :multiply, :*

      def *(n)
        n.class == Sqrt ? Sqrt.new(self.multiply(n.instance_variable_get(:@coefficient)), n.instance_variable_get(:@value)) : self.multiply(n)
      end
    end
  end

同様にFloatRationalでも実行します.

これで平方根の計算もより正確に実行できるようになりました.

x1 = Sqrt.new(1, 2)
x2 = Sqrt.new(1, 3)

x1 ** 2 # => 2
(x1 * x2) ** 2 # => 6

ちなみにMath#sqrtを使った場合

x1 = Math.sqrt(2)
x2 = Math.sqrt(3)

x1 ** 2 # => 2.0000000000000004
(x1 * x2) ** 2 # => 6.000000000000001

苦労した割に差ほっとんどないな...

まとめ

今回の実行でより正確(誤差レベルの)な平方根の計算が実行できるようになりました. 加算減算どうするかな...

Why do not you register as a user and use Qiita more conveniently?
  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
Comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  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