前回の宿題

前回説明した通り、五の巻「不動直線」ではまず図形の変換を分かりやすく理解するためのアプリケーションを作っていきます。今回2章では前回のプログラムを構造化していきます。

引用元:[第5回の宿題]
今回扱ったプログラム「行列エディタ.nako」について、改良できる点がないか探せ。また、プログラムをより分かりやすくできないか考察せよ。

実はいくつか改良の余地がある前回のプログラム。保守性が高く、再利用しやすいプログラムとは、一体どんなものなのでしょうか。

関数化

最も単純で基本的なプログラムの「構造化」の一つが、処理の関数化を行うことです。ある一連の処理を関数化するということは、その処理に名前をつけ、別の場所から呼び出すということです。

関数化すると、その一連の処理の「意味」するところが分かりやすくなり、プログラムの保守性分かりやすさが向上します。さらに、同じ処理を何度も書かなくても再利用できるため、生産的・効率的です。

粒度という言葉は、このときどれくらいの大きさで処理を構造化(関数化)していくかを表します。例えば宿題のプログラムは、既に中くらいの粒度での関数化がなされています。粒度が大きすぎると処理の意味まとまりが捉え辛くなり分かりにくいので、適当に処理を分割して粒度を小さくした方がいいです。しかし粒度が細かすぎても逆に、プログラミング効率が悪く意味も分かりづらくなってしまいます。

粒度を考えるにしても、まず重要なのは抽象的な処理から具体的な意味概念を抽出することです。宿題のプログラムから、共通する処理や概念を探すと、あるものが見えてきます。

アフィン変換

ここでしばらくご無沙汰している数学を登場させましょう。一次変換の親戚に当たるアフィン変換です。なんだか難しそうな名前がついていますが、中身は単純、一次変換平行移動を合成したものがアフィン変換です。

数学的定義

より正確には、アフィン変換fを、一次変換を表す行列Aと、平行移動を表すベクトルbを用いて次の式(1)のように定義できます[*1]。式としては何も難しいことはなく、一次変換に尾ひれがついただけであることが分かります。

f:x→Ax+b (1)

また、Aが引き起こす一次変換逆変換が存在する(Aが正則である)ならば、アフィン変換fの逆変換も存在します。今回はそれも必要になるので、簡単ですが念のために式(2)に示しておきます。

f^-1:x→A^-1(x-b) (2)

なおA正則な2次正方行列であるならば、その逆行列A^-1は次の式(3)のように求まります。

A^-1=[[a,b],[c,d]]^-1=|A|^-1*[[d,-b],[-c,a]] (3)

プログラム

それでは、matrix.nakoにアフィン変換を追加してみましょう。ここでは簡単のために二次元の行列ベクトルのグループも新しく作ることにします。アフィン変換逆変換に必要な行列式逆行列の計算も、二次元ならば単純で済むからです[*2]

matrix.nako
# 取り込み用ライブラリ:
# 「行列」「ベクトル」「二次正方行列」「二次元ベクトル」グループ

■行列
 ・{配列}要素
 ・データ ←行列設定 →行列取得 デフォルト
 ・行列設定(V)〜
  要素=VをCSV取得
 ・行列取得〜
  それは要素
 ・掛ける({行列}Mを)〜
  Iとは整数。Jとは整数
  ARRとは配列=M→要素
  返り値とは配列
  Iを0から(ARRの表行数-1)まで繰り返す
   Jを0から(要素の表列数-1)まで繰り返す
    ARR[I]を反復、返り値[I][J]=返り値[I][J]+対象*要素[回数-1][J]
  返り値を戻す

■ベクトル +行列
 ・一次変換({行列}Mで)〜
  要素=自身→掛ける(M)
 ・平行移動({ベクトル}Vだけ)〜
  ARRとは配列=V→要素
  Iを0から要素の要素数-1まで繰り返す
   要素[I][0]=要素[I][0]+ARR[I][0]
 ・アフィン変換({行列}Aと{ベクトル}bで)〜 # 式(1)
  一次変換(A)
  平行移動(b)

■二次正方行列 +行列
 ・行列式〜
  _=要素[0][0]*要素[1][1]-要素[0][1]*要素[1][0]
 ・逆行列〜 # 式(3)
  返り値とは配列。Δとは数値=行列式
  返り値[0][0] =  要素[1][1]/Δ
  返り値[0][1] = -要素[0][1]/Δ
  返り値[1][0] = -要素[1][0]/Δ
  返り値[1][1] =  要素[0][0]/Δ
  _=返り値

■二次元ベクトル +ベクトル
 ・X ←X設定 →X取得
 ・X設定(V)〜要素[0][0]=V
 ・X取得〜_=要素[0][0]
 ・Y ←Y設定 →Y取得
 ・Y設定(V)〜要素[1][0]=V
 ・Y取得〜_=要素[1][0]
 ・逆平行移動({ベクトル}Vだけ)〜
  minus_Vをベクトルとして作成
  minus_V→要素[0][0] = - V→要素[0][0]
  minus_V→要素[1][0] = - V→要素[1][0]
  平行移動(minus_V)
 ・逆一次変換({二次正方行列}Mで)〜
  inv_Mを行列として作成
  inv_M=M→逆行列
  要素=自身→掛ける(inv_M)
 ・逆アフィン変換({二次正方行列}Aと{ベクトル}bで)〜 # 式(2)
  逆平行移動(b)
  逆一次変換(A)

このように、グループミックスを用いることで拡張した新しいグループを作るのは第2回で見たとおりです。ほとんど式(1),(2),(3)の通りに定義しているので、難しいことはないでしょう。二次元ベクトルの第1,2行の要素にアクセスするメンバ変数X,Yセッターゲッターを用いて定義しているのは、実際に書くプログラムを見やすくするためで、これも構造化の一つです。

構造化と最適化

さて、このアフィン変換が前回のプログラムとどう関係しているのでしょうか。実は前回敢えてあまり解説しなかった部分(#3や#4)における座標の変換(なでしこの座標系見た目の座標系)が、まさにアフィン変換なのです。

引用元:[第5回の宿題の関数#4]
 線色は青色
 行列窓の中心X,中心Yから中心X+ARR[0][0]*単位長,中心Y+ARR[1][0]*単位長へ線
 線色は緑色
 行列窓の中心X,中心Yから中心X+ARR[0][1]*単位長,中心Y+ARR[1][1]*単位長へ線
引用元:[第5回の宿題の関数#5]
 行列窓の中心X-単位長,中心Yから中心X+単位長,中心Yへ線
 行列窓の中心X,中心Y-単位長から中心X,中心Y+単位長へ線
 行列窓の中心X+5,中心Y+単位長へ『1』を文字表示
 行列窓の中心X+単位長,中心Y+5へ『1』を文字表示

どの線命令も行列窓の中心X+A,中心Y+Bから中心X+C,中心Y+Dへ線という形の式をしています。実はこれらの描画時のX,Y座標の計算式では、縮尺を変える一次変換中心を移動する平行移動の2つの計算を一緒にやっています。つまり、アフィン変換になっているのです。どれも同じアフィン変換を用いているだけなのに、式がごちゃごちゃしていてパッと見どういう計算なのか分かりにくいですね。そこで共通している命令の部分を関数化して名前をつけ、更にアフィン変換を利用するようにしてみましょう。

このようにプログラムの抽象的な処理にできるだけ具体的な意味を与えていくことで、処理の流れや意味を把握しやすくするのが構造化で、高級言語的な人間よりの考え方です。その一方で、実際にコードを実行してくれるのはコンピュータなので、コンピュータが処理しやすいように無駄を省くことで処理をできるだけ高速化したいこともあるでしょう。このように実行速度を上げるためにプログラムの無駄を省いたりする工夫を、(速度面での)最適化と呼びます[*3]。無意味な同じ処理・計算を避けることは、回数の多い繰り返し処理描画処理をする場合に非常に有効です。

困ったことに、大抵の場合、構造化したプログラムは分かりやすい分コンピュータにとっては無駄が多く、ごくごく小さなオーダーとは言え計算量が増してしまいます。逆に、最適化されたソースコードは往々にして分かりにくい変数アルゴリズムを用いるために、分かりにいものになってしまいます。もちろん一概にそうと言い切れる訳ではありませんが、ある意味で構造化と最適化は対照的な手法かもしれません[*4]。できる限り両者を共存させつつ、状況に応じた自分なりの妥協点を見出せるようになりましょう。

プログラム

上で解説してきたこと以外にも、様々な点を改良しています。前回のプログラムと見比べて、どのように変化しているのかチェックしてみてください。

行列エディタ2.nako
!『matrix.nako』を取り込む

!中心X =125
!中心Y =200
!単位長=80

行列窓とはフォーム
これについて
 テキストは『変換行列』
 クライアントW=265
 クライアントH=310
 タグ=-1。ポケット=1。#1−a
 マウス押した時は
  押されたボタンで条件分岐
   『左』ならばタグ= 0
   『右』ならばタグ= 1
   違えば、  タグ=-1
 マウス移動した時は
  もしポケット&&(タグ≧0)ならば #1−b
   ポケット=0
   タグをマウスX,マウスYでベクトル設定。
   ポケット=1
 マウス離した時は
  タグ=-1

A00とはスピンエディタ
これについて
 変更した時は〜0,0をA00に成分変更
 親部品は行列窓。位置は『5,5』
A01とはスピンエディタ
これについて
 変更した時は〜0,1をA01に成分変更
 親部品は行列窓。位置はA00の右側
A10とはスピンエディタ
これについて
 変更した時は〜1,0をA10に成分変更
 親部品は行列窓。位置はA00の下側
A11とはスピンエディタ
これについて
 変更した時は〜1,1をA11に成分変更
 親部品は行列窓。位置はA10の右側

Aとは行列。#2−a
図示イベントとは変数。#3−a
A00=1;A01=0
A10=0;A11=1
図示イベントは〜ベクトル図示。#3−b

Bとは二次正方行列。#2−b
B="{単位長},0{~}0,{単位長}"

Cとは二次元ベクトル。#2−c
CのX=中心X
CのY=中心Y

ベクトル図示
行列窓を表示

#4
●ベクトル設定({整数}COLを{整数}X,{整数}Yで)
 Vを二次元ベクトルとして作成
 V→X=X;V→Y=Y
 V→逆アフィン変換(BとCで)
 もしCOLが0ならば
  図示イベントは〜。#3−c
  A00=V→X;A10=V→Y
  図示イベントは〜ベクトル図示。#3−d
 違えば、もしCOLが1ならば
  図示イベントは〜。
  A01=V→X;A11=V→Y
  図示イベントは〜ベクトル図示。
 ベクトル図示。

●ベクトル図示
 ARRとは配列=Aのデータ
 座標軸クリア。線太さは4。線色は青色。
 0,0からARR[0][0],ARR[1][0]へ座標線。線色は緑色
 0,0からARR[0][1],ARR[1][1]へ座標線
 行列窓の描画処理反映

#5
●座標線({数値}X1,{数値}Y1から{数値}X2,{数値}Y2へ)
 Pを二次元ベクトルとして作成
 P→X=X1;P→Y=Y1
 P→アフィン変換(BとCで)
 Qを二次元ベクトルとして作成
 Q→X=X2;Q→Y=Y2
 Q→アフィン変換(BとCで)
 行列窓のP→X,P→YからQ→X,Q→Yへ線
 Q→X,Q→Yへ移動

●座標軸クリア
 行列窓を画面クリア
 線色は黒色。線太さは1
 -1,0から1,0へ座標線
 行列窓の基本X+5,基本Yへ『1』を文字表示
 0,-1から0,1へ座標線
 行列窓の基本X,基本Y+5へ『1』を文字表示

●成分変更({整数}I,{整数}Jを{数値}Nに)
 Aの要素[I][J]=N
 図示イベント。#3−e

プログラム解説

以下に主な変更点とその解説を列挙します。

#1
速度・描画面での最適化#1-aでフラグ(メンバ変数ポケット)を初期化し、#1-bでフラグがオンの時のみ処理を実行するようにしている。マウス移動した時イベントは短時間に何度も発生するため、描画処理が間に合わない事態が発生してしまうが、このようにフラグ管理することで回避できる。
#2
#2-aが編集する対象の行列A。#2-bがアフィン変換の一次変換部分を表す行列B。#2-cがアフィン変換の平行移動部分を表すベクトルC。
#3
速度・描画面での最適化。もともとのプログラムではA00=1;A01=0のように変更した時イベントが連続して発生し、その中でベクトル図示関数が連続で呼び出されていた。#1と同様必要ない描画処理を回避するために、成分変更関数の中の#3-eでイベント変数#3-aを呼び出すようにすることで、描画が不要な時は#3-a,#3-cのように中身を空にしておき処理をスキップさせ、そのあと#3-b,#3-dのように再定義している。
#4
逆アフィン変換を用いて、なでしこの座標系における点(X,Y)を、見た目の座標系での値に変換し、行列ACOL列ベクトルとして設定する。
#5
関数化アフィン変換を用いて、行列窓上で見た目の座標系における点(X1,Y1)から(X2,Y2)へ線を引く処理を行う。(X2,Y2)のなでしこの座標系における値を基本X,基本Yにセットして、単位長を示す文字『1』を描画する際に利用している。

まとめ

今回はアフィン変換構造化最適化を扱いましたが、いずれもその考え方の一端に触れたに過ぎません。前回のプログラムは場合によっては処理落ちすることもあったので、比べて実行してみれば最適化の威力は感じられるかもしれませんが、構造化の利点は体感できないかもしれません。いずれにしてもこういったプログラミング手法は、プログラムを書かなければなかなか身につかないものです。構造化ならば粒度分かりやすさ最適化ならば無駄実行速度をそれぞれ意識することで、段々と身に付いていくでしょう。

次回はやっと不動直線を扱います。一次変換によってある直線が不動であるとは一体どういうことなのでしょう。今回は宿題はお休みとするので、不動直線について予習してみるのもいいかも…。

注釈

*1
もちろん一次写像の定義同様、アフィン写像もより一般的に定義することができます。その定義はここでは省略しますが、簡単に言うと平行を保存する性質で定義されます。
*2
一般のn次正方行列の場合、簡単に行列式の計算は、第4回の注釈で書いたように定義通りに求めるか、あるいは余因子展開による再帰的定義などを利用します。逆行列は、余因子行列の転置から求めるか、基本変形を利用します。
*3
単に最適化と言っても、状況によって何が最適であるかは変わります。例えば搭載メモリが少ないマシンで実行するために、時間がかかる代わり消費メモリが少ないアルゴリズムを採用する(つまりメモリ最適化する)ような場合が考えられます。新しいPCでそこまでメモリを気にすることはないので、基本的には速度最適化を考えればいいと思います。
*4
このように、何かを犠牲にもう片方の何かの面で利益があるような状況をトレードオフと呼びます。注釈[*3]のような場合なら、時間(速度)とメモリのトレードオフと言います。