Escape from the Memories Planet

思い出星職人をめざし、かつ、とどまらないブログ

自作行列簡約化ツール

この記事はUEC Advent Calendar 2018の12日目の記事です!

id:puman03君とともにNimを布教しようとたくらむnamniumと申します。m(_ _)m

今回は勉学に役立つものを書こう(作ろう)と思い、行列簡約化ツールを作成しました!

↓こちらから利用できます!

行列 簡約化

↓全体のソースコードはこちら

anotherhollow1125/kanyaku_matrix

コンセプト

すでにネット上にはいくつか転がっていたので、少しこだわりを持って作成いたしました。

  • enmtパパ曰く「除算の結果、0に近い数なのか0なのかの判定が大変となるため、簡約化するツールを作成するのは難しい」とのこと
  • ↑ 基礎プヨ第9回の「有理数クラス」のアイデアを拝借して、分数で値を保存した

  • なるべく表示を大きくしスマホでも扱えるようにした

  • 車輪の再開発にあたるので、自分が現在勉強している言語であるNimでコードを作成した
  • NimをJavaScriptにトランスパイルし、HTMLに埋め込むことでみんなに使ってもらえるようにした

技術解説・ソースコード

ちょっとずつNimの布教を兼ねて技術的説明をしていこうと思います!何かの役に立てば幸いですw

分数で数値を保持する

ソースコードの頭では必要なモジュール(strutilssequtils)と(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の面白い機能がこちら。RubyPythonにもありますが、演算子を自前で設定できます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

ijを現在操作中の基本的な行と列の位置を記憶しておく変数とし、変数kで行を変えながら簡約化を行う、という形になっています。

Nimでは返り値を表す特別な変数resultを使うことで、returnを省略できたりします。繰り返しと組み合わせて使うとすっきりしてとてもうれしいものです。

またRubyPythonでもできるスワップ(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


  1. 要出典。テンプレートの機能など僕も使いこなせていないNimの機能はまだまだたくさんあるので、この流れが正しいと断言はできないです。

  2. 演算子には様々な記号と組み合わせが使えるので、やろうと思えば面白い記述を作成することも可能です。

  3. 一部JSを利用しております。

  4. 後から知ったのですが、このシンタックスシュガーを UFCS (Unified Function Call Syntax) というらしいです。