自作行列簡約化ツール
この記事はUEC Advent Calendar 2018の12日目の記事です!
id:puman03君とともにNimを布教しようとたくらむnamniumと申します。m(_ _)m
今回は勉学に役立つものを書こう(作ろう)と思い、行列簡約化ツールを作成しました!
↓こちらから利用できます!
↓全体のソースコードはこちら
anotherhollow1125/kanyaku_matrix
コンセプト
すでにネット上にはいくつか転がっていたので、少しこだわりを持って作成いたしました。
- enmtパパ曰く「除算の結果、0に近い数なのか0なのかの判定が大変となるため、簡約化するツールを作成するのは難しい」とのこと
なるべく表示を大きくしスマホでも扱えるようにした
- 車輪の再開発にあたるので、自分が現在勉強している言語であるNimでコードを作成した
- NimをJavaScriptにトランスパイルし、HTMLに埋め込むことでみんなに使ってもらえるようにした
技術解説・ソースコード
ちょっとずつNimの布教を兼ねて技術的説明をしていこうと思います!何かの役に立てば幸いですw
分数で数値を保持する
ソースコードの頭では必要なモジュール(strutils
とsequtils
)と型
(type
)を宣言しています。Nimにはオブジェクト指向の概念がなくクラスにメソッドを記述していくという考えはなく(OOP自体の考え方はあります)、型
だけが存在します!
型
にあった関数を定義する、またはオーバーライドしていく、というのがNimの基本的な流れとなります1。
大規模になってくるとつらいところもありますが、記述は確かにOOPより楽になるところが多いようにも感じます。
何より型があるのって結構いいですね、、コンパイル時に記述ミスが見つかるのはPythonista的には結構うれしかったりします。 C言語よりお手軽に扱えるコンパイル言語としてNimはうってつけです!
import strutils, sequtils type Ratio = ref object nume : int # 分子 deno : int # 分母 Matrix = seq[seq[Ratio]]
C言語で言う構造体として定義したRatio
(分数)型はnume
(Numerator, 分子)、deno
(denominator, 分母)の二つを持っています。またMatrix
型をここでRatio
型の要素を持つ二次元配列として定義しております。
proc gcd(a, b: int): int = var x = a y = b while true: if x > y: x = x mod y if x == 0: return y else: y = y mod x if y == 0: return x proc red(p: Ratio): Ratio = var n = p.nume d = p.deno g = gcd(n.abs, d.abs) return Ratio(nume: int(n/g), deno: int(d/g))
お次の上記のコードでは、分数を約分させています。約分の方法は基礎プヨの丸パクリですw 数弱なので説明は省きます()
Nimの残念なところともいえるのはmod演算に%
を使用できないことなんですよね...実装することもできそうですがとりあえずデフォルトのmod
を使用しています
Nimは静的型付け言語なのですが、上記の例を見ればわかるように、初期化時に値を指定すると型を省略できます!ですが途中で型を変更することはできません。
proc `$`(p: Ratio): string = if p.deno != 1: return "\"" & $p.nume & "/" & $p.deno & "\"" else: return "\"" & $p.nume & "\""
お次のNimの面白い機能がこちら。RubyやPythonにもありますが、演算子を自前で設定できます2!
Nimでは$
演算子はto string
という意味をもつという慣例がありますので、それに従ってここでRatio
型に対して関数を設定しております。ですからオーバーライドに当たる部分ともいえます。
proc ratioNew(a, b: int): Ratio = var n = a d = b # if d == 0: raise newException(ValueError, "0 division") if d == 0: return Ratio(nume: 1, deno: 0) if n == 0: return Ratio(nume: 0, deno: 1) if d < 0: n = -n d = -d result = Ratio(nume: n, deno: d) result = result.red proc `+`(p: Ratio, q: Ratio): Ratio = return ratioNew(p.nume*q.deno+p.deno*q.nume, p.deno*q.deno) proc `-`(p: Ratio, q: Ratio): Ratio = return ratioNew(p.nume*q.deno-p.deno*q.nume, p.deno*q.deno) proc `*`(p: Ratio, q: Ratio): Ratio = return ratioNew(p.nume*q.nume, p.deno*q.deno) proc `/`(p: Ratio, q: Ratio): Ratio = return ratioNew(p.nume*q.deno, p.deno*q.nume) proc parseRatio(s: string): Ratio = var sp = s.split("/") n, d = 1 n = sp[0].parseInt if sp.len == 2: d = sp[1].parseInt return ratioNew(n, d)
Ratio
型を初期化する関数とその四則演算、キャスト用関数になります。ここでも演算子を定義していますね。内容はほぼ基礎プヨのパクリですが()
整数のときには分母を1
にする、といった点をここで工夫を施しています
行列の簡約化
ようやく?行列の簡約化のコードです。文字列を行列に変換する関数と、そのあとに簡約化関数echelonize
を定義しています
proc parseMatrix(mat_s: seq[seq[cstring]]): Matrix = result = @[] for row_s in mat_s: var row: seq[Ratio] = @[] for elm in row_s: row.add(($elm).parseRatio) result.add(row) # 肝心の簡約化はここから proc echelonize(mat: Matrix): Matrix = result = mat let row_num = len(result) col_num = len(result[0]) var i, j = 0 while j < col_num and i < row_num: for k in i..<row_num: var a = result[k][j] if a.nume == 0: if k < row_num-1: continue else: j += 1 # 列カウンタのみ進める break result[k] = result[k].map(proc(r: Ratio): Ratio = r / a) for p in 0..<row_num: if p != k: var t = result[p][j] for q in j..<col_num: result[p][q] = result[p][q] - result[k][q] * t (result[i], result[k]) = (result[k], result[i]) i += 1 j += 1 break
i
、j
を現在操作中の基本的な行と列の位置を記憶しておく変数とし、変数k
で行を変えながら簡約化を行う、という形になっています。
Nimでは返り値を表す特別な変数result
を使うことで、return
を省略できたりします。繰り返しと組み合わせて使うとすっきりしてとてもうれしいものです。
またRubyやPythonでもできるスワップを(result[i], result[k]) = (result[k], result[i])
という形で行っております。tmp
なんていうゴミ変数を用意しなくて済むのはやっぱり重要です
proc main(mat_s: seq[seq[cstring]]): cstring {. exportc .} = var mat = mat_s.parseMatrix res = mat.echelonize return ($res).replace("@", "")
最後にHTML側3から実行する、文字列の二次配列を引数に取り、最後に結果を文字列として返す(返した文字列はJS側にてeval
で変換)関数main
を定義しております。
一見mat.echelonize
のような記述はオブジェクト指向に見えますがこれはシンタックスシュガー4で、実際はechelonize(mat)
を実行しています。まさに先ほど定義した関数です
この場面ではあまりこのシンタックスシュガーは役立ちそうにないですが、文字列などに対して"string".replace("s", "t")
みたいに書けるというのはやっぱありがたいですね。。
実はPythonのインスタンスメソッドはこのシンタックスシュガーに近い構造を持っていたりします。証拠に、以下のコードは等価です。
Hoge.func(hoge, val) hoge.func(val)
話がNimからそれてしまいましたが、要はこのシンタックスシュガーが可読性に結構効いてきているということです。う~ん、、深い!
終わりに
とても読みにくい文章で、さらに公開も遅くなって申し訳ありませんでした(^ ^;
記事は徹夜明けの脳みそで書いていて、ツール自体も徹夜で作成したので、ビシバシとバグを見つけて僕まで報告くださるととても喜びます() 実際に役立ててくれればもっと喜びます!
まだまだNimにはいっぱい面白い(時には頭にくるような)発見がありましたが、とりあえず今回はこれでしめたいと思います!
Nimに限らず様々な言語に触れてみるってやっぱり重要ですね、、Rustとかもいつかやってみたいなと思います。
明日は Cambaroides_JPさんの「通信制高校から大学進学を目指す人に向けた何か」だそうです。
電通大も通信制になってくれたら実家でぬくぬくしながら勉学に励めるんですけどねw まぁサークルとか楽しいので今のままで良いですが()
ここまで読んでいただきありがとうございました!m(_ _)m