前回ははじめてな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 を導入したので試験パターンを入れてみました。
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 によるプロジェクト新規作成