LoginSignup
4
2

More than 1 year has passed since last update.

はじめてなElixir(4) 有理数モジュールをつくってみる

Last updated at Posted at 2018-09-17

前回ははじめてなElixir(3)で無限ストリームや文字列で遊んでみました。本当は並行プログラミングにいくつもりだったんですが、ちょっと長い寄り道です。

有理数(分数)の計算を実装してみる

Elixir の整数が bignum なのはうれしいですね。実装するほうは大変でしょう。こんな実用性を求める言語によく bignum を導入しましたね。

defmodule Factrial do
  def fact(0) do 1 end
  def fact(n) do n * fact(n-1) end
end

なんて作って

iex(2)> Factrial.fact(0)
1
iex(3)> Factrial.fact(10)
3628800
iex(4)> Factrial.fact(100)
93326215443944152681699238856266700490715968264381621468592963895217599993229915608941463976156518286253697920827223758251185210916864000000000000000000000000

なんて遊べるのは33年前に初めて大学で研究室に所属して、Sun2 (SunOS1.2) の Franz Lisp で遊んで以来の感動です。1000の階乗とか求めてコンソールの画面が数字で埋まるのを見て満足した記憶が蘇りました。

はてそうなると今度は有理数も扱って欲しくなるではないですか。

mix 使ってみました

Elixir本とか Elixir School とか
Elixir | Mix | プロジェクト管理 CLI ツール Mix によるプロジェクト新規作成
とか見まして、見よう見まねで。

ついでに git も初体験。構成管理って RCS と make でやってた(ことがある)人間なんで、github も clone はするけど自分で commit / push とかしたことなかったんですよ。何かと新しいことばかりで目眩がします。

プログラムはこんな感じで

ま、大した難しいプログラムじゃないです。まずはプリアンブルがでかくなりました。

コメントをしっかり書いてみる

あとでハマるかもしれませんがとにかく doc としてなんか書いておこうという模範的な態度ということで。

defmodule Rational do

  @moduledoc """
  A simple sample module for rational number calicuration.
  """

  @doc """
  - Types
  There is not new defined types.
  Tupples describe rational numbers here.
  The module might define a new rational type hiding numerator/denominator.

  - Functions
  -- add/2
     addition of two rational numbers
  -- sub/2
     subtraction of two rational numbers
  -- mul/2
     muliplication of two rational numbers
  -- div/2
     division of two rational numbers
  -- rof/1
     Reduction of Fraction of a rational number
  -- rcd/2
     Reduction to Common Denominator of a list of two rational numbers.
     Returns a list of two rational numbers whose denominators are the same.
  """

こんな書き方で良いのでしょうか。

プログラム本体の先頭

div って関数を作りたいんですが Kernel モジュールの関数名とぶつかるんです。それも Kernel.div って書かなくて div って書いただけで衝突する。なので、div と書いたときに Kernel.div を指さないように except: 付きで Kernel を import します。

  import Kernel, except: [div: 2]
  require Integer

あと、最大公約数関数 gcd を使いたいので Integer モジュールを require しときました。ちなみに gcd はあるのに lcm 関数はありません。大した難しくもないのになぜ省略する。

データ型

分数を {分子, 分母} の形で表現することにしました。

{1, 2} # これが 1/2 を表す。

もうちょっと勉強して defstruct を使ってみたいですが、今日のところはこれで勘弁。

約分関数と通分関数

分数の計算にはとにかく必要ですね。

  def rof({n, d}) do
    {Kernel.div(n, Integer.gcd(n, d)), Kernel.div(d, Integer.gcd(n, d))}
  end

約分関数 rof/1 は分子と分母をそれぞれ最大公約数で割ればおしまいです。

iex(1)> Rational.rof({2,4})
{1, 2}
iex(2)> Rational.rof({4,6})
{2, 3}

通分関数 rcd/1 はデータの受け渡しが複雑になります。2つの分数はリストで渡して、リストで返してもらうことにしました。

  def rcd([{n1, d1}, {n2, d2}]) do
    d = Kernel.div(d1 * d2, Integer.gcd(d1, d2))
    [{Kernel.div(n1 * d, d1), d}, {Kernel.div(n2 * d, d2), d}]
  end

先に分母を計算して、それから答えのリストを返します。

iex(1)> Rational.rcd([{1,2},{1,4}])
[{2, 4}, {1, 4}]
iex(2)> Rational.rcd([{1,2},{1,3}])
[{3, 6}, {2, 6}]
iex(3)> Rational.rcd([{1,2},{1,2}])
[{1, 2}, {1, 2}]
iex(4)> Rational.rcd([{4,12},{3,12}])
[{4, 12}, {3, 12}]
iex(5)> Rational.rcd([{4,12},{3,1}]) 
[{4, 12}, {36, 12}]
iex(8)> Rational.rcd([{1,3},{1,6}])                
[{2, 6}, {1, 6}]
iex(9)> Rational.rcd([{1,3},{1,6}]) |> Rational.rcd
[{2, 6}, {1, 6}]

最後の、一度 rcd で出た結果は何回やってももう同じです。こうしたくて rcd/2 じゃなくて rcd/1 にしました。

加減乗除の関数

あとは公式の通り加減乗除の関数を作ります。乗除算は簡単です。加減算は一旦通分してからの計算です。全部最後に約分してます。

  def add({n1, d1}, {n2, d2}) do
    [{x, c}, {y, c}] = rcd([{n1, d1}, {n2, d2}])
    rof({x + y, c})
  end

  def sub({n1, d1}, {n2, d2}) do
    [{x, c}, {y, c}] = rcd([{n1, d1}, {n2, d2}])
    rof({x - y, c})
  end

  def mul({n1, d1}, {n2, d2}) do
    rof({n1 * n2, d1 * d2})
  end

  def div({n1, d1}, {n2, d2}) do
    rof({n1 * d2, d1 * n2})
  end

テストを書いてみる

上の結果ですが、手でもやりましたが、今回は mix を導入したので試験パターンを入れてみました。

rational_test.exs
defmodule RationalTest do
  use ExUnit.Case
  doctest Rational

  test "simple add test" do
    assert Rational.add({1, 2}, {1, 3}) == {5, 6}
  end

  test "simple sub test" do
    assert Rational.sub({1, 2}, {1, 3}) == {1, 6}
  end

  test "simple mul test" do
    assert Rational.mul({1, 2}, {1, 3}) == {1, 6}
  end

  test "simple div test" do
    assert Rational.div({1, 2}, {1, 3}) == {3, 2}
  end

  test "simple rof test" do
    assert Rational.rof({4, 6}) == {2, 3}
  end

  test "simple rcd test" do
    assert Rational.rcd([{1, 3}, {1, 2}]) == [{2, 6}, {3, 6}]
  end
end

本来ならもっと沢山書くべきですね。特に今回は難しくないのでもっと書けばよかったな。

$ mix test
......

Finished in 0.05 seconds
6 tests, 0 failures

Randomized with seed 228152

となってテストはパスします。実はこれを出す前にボロボロと間違いを指摘されてました。

参考文献

Elixir 1.7.3
Elixir School
Elixir | Mix | プロジェクト管理 CLI ツール Mix によるプロジェクト新規作成

4
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
4
2