前回説明した通り、五の巻「不動直線」ではまず図形の変換を分かりやすく理解するためのアプリケーションを作っていきます。今回2章では前回のプログラムを構造化していきます。
- 引用元:[第5回の宿題]
今回扱ったプログラム「行列エディタ.nako」について、改良できる点がないか探せ。また、プログラムをより分かりやすくできないか考察せよ。
実はいくつか改良の余地がある前回のプログラム。保守性が高く、再利用しやすいプログラムとは、一体どんなものなのでしょうか。
前回説明した通り、五の巻「不動直線」ではまず図形の変換を分かりやすく理解するためのアプリケーションを作っていきます。今回2章では前回のプログラムを構造化していきます。
今回扱ったプログラム「行列エディタ.nako」について、改良できる点がないか探せ。また、プログラムをより分かりやすくできないか考察せよ。
実はいくつか改良の余地がある前回のプログラム。保守性が高く、再利用しやすいプログラムとは、一体どんなものなのでしょうか。
最も単純で基本的なプログラムの「構造化」の一つが、処理の関数化を行うことです。ある一連の処理を関数化するということは、その処理に名前をつけ、別の場所から呼び出すということです。
関数化すると、その一連の処理の「意味」するところが分かりやすくなり、プログラムの保守性・分かりやすさが向上します。さらに、同じ処理を何度も書かなくても再利用できるため、生産的・効率的です。
粒度という言葉は、このときどれくらいの大きさで処理を構造化(関数化)していくかを表します。例えば宿題のプログラムは、既に中くらいの粒度での関数化がなされています。粒度が大きすぎると処理の意味まとまりが捉え辛くなり分かりにくいので、適当に処理を分割して粒度を小さくした方がいいです。しかし粒度が細かすぎても逆に、プログラミング効率が悪く意味も分かりづらくなってしまいます。
粒度を考えるにしても、まず重要なのは抽象的な処理から具体的な意味・概念を抽出することです。宿題のプログラムから、共通する処理や概念を探すと、あるものが見えてきます。
ここでしばらくご無沙汰している数学を登場させましょう。一次変換の親戚に当たるアフィン変換です。なんだか難しそうな名前がついていますが、中身は単純、一次変換と平行移動を合成したものがアフィン変換です。
より正確には、アフィン変換fを、一次変換を表す行列Aと、平行移動を表すベクトルbを用いて次の式(1)のように定義できます[*1]。式としては何も難しいことはなく、一次変換に尾ひれがついただけであることが分かります。
また、Aが引き起こす一次変換の逆変換が存在する(Aが正則である)ならば、アフィン変換fの逆変換も存在します。今回はそれも必要になるので、簡単ですが念のために式(2)に示しておきます。
なおAが正則な2次正方行列であるならば、その逆行列A^-1は次の式(3)のように求まります。
それでは、matrix.nakoにアフィン変換を追加してみましょう。ここでは簡単のために二次元の行列とベクトルのグループも新しく作ることにします。アフィン変換の逆変換に必要な行列式・逆行列の計算も、二次元ならば単純で済むからです[*2]。
# 取り込み用ライブラリ:
# 「行列」「ベクトル」「二次正方行列」「二次元ベクトル」グループ
■行列
・{配列}要素
・データ ←行列設定 →行列取得 デフォルト
・行列設定(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)における座標の変換(なでしこの座標系⇔見た目の座標系)が、まさにアフィン変換なのです。
線色は青色 行列窓の中心X,中心Yから中心X+ARR[0][0]*単位長,中心Y+ARR[1][0]*単位長へ線 線色は緑色 行列窓の中心X,中心Yから中心X+ARR[0][1]*単位長,中心Y+ARR[1][1]*単位長へ線
行列窓の中心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]。できる限り両者を共存させつつ、状況に応じた自分なりの妥協点を見出せるようになりましょう。
上で解説してきたこと以外にも、様々な点を改良しています。前回のプログラムと見比べて、どのように変化しているのかチェックしてみてください。
!『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。#3A00=1;A01=0のように変更した時イベントが連続して発生し、その中でベクトル図示関数が連続で呼び出されていた。#1と同様必要ない描画処理を回避するために、成分変更関数の中の#3-eでイベント変数#3-aを呼び出すようにすることで、描画が不要な時は#3-a,#3-cのように中身を空にしておき処理をスキップさせ、そのあと#3-b,#3-dのように再定義している。#4AのCOL列ベクトルとして設定する。#5行列窓上で見た目の座標系における点(X1,Y1)から(X2,Y2)へ線を引く処理を行う。(X2,Y2)のなでしこの座標系における値を基本X,基本Yにセットして、単位長を示す文字『1』を描画する際に利用している。今回はアフィン変換・構造化・最適化を扱いましたが、いずれもその考え方の一端に触れたに過ぎません。前回のプログラムは場合によっては処理落ちすることもあったので、比べて実行してみれば最適化の威力は感じられるかもしれませんが、構造化の利点は体感できないかもしれません。いずれにしてもこういったプログラミング手法は、プログラムを書かなければなかなか身につかないものです。構造化ならば粒度・分かりやすさ、最適化ならば無駄・実行速度をそれぞれ意識することで、段々と身に付いていくでしょう。
次回はやっと不動直線を扱います。一次変換によってある直線が不動であるとは一体どういうことなのでしょう。今回は宿題はお休みとするので、不動直線について予習してみるのもいいかも…。