付録B:2018年度・定期レポートへのコメント(その2)
※ 以下のプログラムは,Julia 1.1向けに書き直した.
ネタもと
2018年12月,および,2019年1月の月間レポートでは,題材自由のレポートを課した. うち,後者は,関数を定義することを必須の条件とした.
提出された学生レポートを少し改変して,以下に示す.
次の関数 dora1()
は,何かを描く関数の定義である.
function dora1()
ts = 0:pi/18:2pi
xs = 6cos.(ts)
ys = 6sin.(ts)
plt.plot(xs, ys)
ts = 0:pi/18:2pi
xs = 1.2 .+ 1.2 * cos.(ts)
ys = 2.5 .+ 1.5 * sin.(ts)
plt.plot(xs, ys, "k")
ts = 0:pi/18:2pi
xs = -1.2 .+ 1.2cos.(ts)
ys = 2.5 .+ 1.5sin.(ts)
plt.plot(xs, ys, "k")
ts = 0:pi/18:2pi
xs = 0.5cos.(ts)
ys = 1.1 .+ 0.5sin.(ts)
plt.plot(xs, ys, "r")
ts = 0:pi/18:2pi
xs = 5cos.(ts)
ys = -1.5 .+ 4.5sin.(ts)
plt.plot(xs, ys, "k")
ts = 0:pi/18:2pi
xs = 0.5 .+ 0.5cos.(ts)
ys = 2.4 .+ 0.5sin.(ts)
plt.plot(xs, ys, "k")
ts = 0:pi/18:2pi
xs = -0.5 .+ 0.5cos.(ts)
ys = 2.4 .+ 0.5sin.(ts)
plt.plot(xs, ys, "k")
xs = -2.3:0.5:2.3
plt.plot(xs, 1 / 2.5 * xs .^ 2 .- 4, "k", label = "s")
xs = -2:0.1:1
plt.plot(-4 .+ xs, -0.3 * xs, "k")
plt.plot(5 .+ xs, 0.3 * xs, "k")
xs = -2:0.1:1
plt.plot(-4 .+ xs, -2 .+ 0 * xs, "k")
plt.plot(5 .+ xs, -2 .+ 0 * xs, "k")
xs = -2:0.1:1
plt.plot(-4 .+ xs, -1.2 .+ -0.2 * xs, "k")
plt.plot(5 .+ xs, -1.2 .+ 0.2 * xs, "k")
xs = -28:0.1:-5
plt.plot(0 * xs, -5 .+ -0.2 * xs, "k")
end
dora1 (generic function with 1 method)
この関数を呼出して,実行結果を示す. 何かのキャラクターの顔が描かれた.
using PyPlot
plt.axes().set_aspect("equal")
dora1()
plt.xlim(-7, 7)
plt.ylim(-7, 7)
この関数では,平面図形を描く曲線を各々設計した苦労の跡が偲ばれる. 作者の意図通りに動作し,大変結構である.
しかし,この関数を後から振り返ったときに, 各行の意図をすぐに汲み取るのは難しいだろう. 一部の数値だけ異なるが,同じようなプログラム片が並ぶのも,読みにくくしている.
下請け関数を定義する
さて,この顔は,楕円,直線,放物線の3つの図形から成り立っている. これら3つの図形を描く関数を定義してみよう.
まず,$\left(x_c, y_c\right)$ を中心とし,$x$ 軸方向の広がりが $2a$ , $y$ 軸方向の広がりが $2b$ であるような楕円を描く関数を定義する.式で書くと $\left(\dfrac{x-x_{c}}{a}\right)^2 + \left(\dfrac{y-y_{c}}{b}\right)^2 = 1$ である.
関数の引数は,xc
, yc
, a
, b
と 色(またはスタイル)を示す文字列 c
である. 最後の引数 c
を省略したときは c="k"
(黒色)を既定値とする.
function draw_ellipse(xc, yc, a, b, c = "k")
ts = 0:pi/36:2pi
xs = xc .+ a * cos.(ts)
ys = yc .+ b * sin.(ts)
plt.plot(xs, ys, c)
end
draw_ellipse (generic function with 2 methods)
上の関数の動作を確認しよう.上に続けて
using PyPlot
plt.axes().set_aspect("equal")
draw_ellipse(1, 1, 2, 1, "g")
draw_ellipse(1, 1, 1, 2, "r")
plt.xlim(-2, 4)
plt.ylim(-2, 4)
plt.axhline(1, color = "k", lw = 0.5)
plt.axvline(1, color = "k", lw = 0.5)
次に,2つの点 $(x_1, y_1)$ と $(x_2, y_2)$ とを結ぶ直線を描く関数を定義しよう.
function draw_line(x1, y1, x2, y2, c = "k")
xs = [x1, x2]
ys = [y1, y2]
plt.plot(xs, ys, c)
end
draw_line (generic function with 2 methods)
上の関数の動作を確認しよう.上に続けて
draw_line(-1, -1, 3, 2, "b")
draw_line(-1, 3, 3, 0, "g")
plt.xlim(-2, 4)
plt.ylim(-2, 4)
plt.axhline(1, color = "k", lw = 0.5)
plt.axvline(1, color = "k", lw = 0.5)
最後に, 放物線(2次間数)$y=ax^2+bx+c$ を,区間 $\left[x_1, x_2\right]$ の範囲で描く関数を定義しよう.
function draw_para(a, b, c, x1, x2, color = "k")
xs = range(x1, x2, length = 50)
ys = a * xs .^ 2 .+ b * xs .+ c
plt.plot(xs, ys, color)
end
draw_para (generic function with 2 methods)
上の関数の動作を確認しよう.上に続けて
draw_para(1, 0, -1, -2, 2, "b")
draw_para(-1, 0, 1, -2, 2, "g")
plt.xlim(-3, 3)
plt.ylim(-3, 3)
plt.axhline(0, color = "k", lw = 0.5)
plt.axvline(0, color = "k", lw = 0.5)
元の関数を書き換える
これらの「下請け」関数を呼び出す形で,元の関数 dora1()
を書き直そう. 隣接する部分がまとまるように,行の順番を少し入れ替えて,コメントをつけた (元の描画順に意図があるなら,ご容赦願いたい).
function dora2()
# face
draw_ellipse(0, 0, 6, 6, "b")
# nose
draw_ellipse(0, 1.1, 0.5, 0.5, "r")
# gray line
draw_ellipse(0, -1.5, 5, 4.5)
# eyes
draw_ellipse(1.2, 2.5, 1.2, 1.5)
draw_ellipse(-1.2, 2.5, 1.2, 1.5)
draw_ellipse(0.5, 2.4, 0.5, 0.5)
draw_ellipse(-0.5, 2.4, 0.5, 0.5)
# beard
draw_line(-6, 0.6, -3, -0.3)
draw_line(6, 0.3, 3, -0.6)
draw_line(-6, -2, -3, -2)
draw_line(6, -2, 3, -2)
draw_line(-6, -0.8, -3, -1.4)
draw_line(6, -1.0, 3, -1.6)
# mouth
draw_line(0, 0.6, 0, -4)
draw_para(1 / 2.5, 0, -4, -2.3, 2.3)
end
dora2 (generic function with 1 method)
実行してみよう.
using PyPlot
plt.axes().set_aspect("equal")
dora2()
plt.xlim(-7, 7)
plt.ylim(-7, 7)
リファクタリング
関数 dora1()
を dora2()
に書き直したように, プログラムの内容を保ったまま,見通しをよくしたり,実行速度を改善する作業を, 「リファクタリング(refactoring)」という.
(プログラムも含めて)複数の「要素(component)」が協力して働いて, ある目的を達成するものを,「システム(system)」という.
システムを「実装する(implement)」手法として, 「ボトム・アップ的な手法(bottom-up approach)」 , 「トップ・ダウン的な手法(top-down approach)」が知られている.
ボトム・アップ的な手法は, 下位の要素を作成してから (上の例では,関数 draw_ellipse
や draw_line
などを定義してから), それらの組合せで上位の要素(システム)を作成する (上の例では関数 dora2()
を定義する)手法である.
トップ・ダウン的な手法は, 先に,上位の要素(システム)を決めてから(関数 dora2()
を定義してから), その下位の要素(部品)を作成する(関数 draw_ellipse
などを定義する)手法である.
プログラミングの初級段階では,ボトム・アップ的な手法が分かりやすいだろうが, システムの成り立ちに習熟するにつれて, トップ・ダウン的な手法が取れるようになるであろう.
システムの考え方では, システムを構成する「要素」を,入力と出力の対応関係だけが決まっていて, その中身は関知しない「ブラック・ボックス black box」とみなす. しかしながら,現実のシステムでは,「要素」の「中身」を無視することができず, 上位のシステムの性能にも影響が及ぶ.
そのような「要素」と「要素」の「界面(interface)」を上手に扱うことができる人こそ, 優れた「工学者(engineer)」といえる.
諸君が優れた工学者になることを願って,この文の著者は対応しているつもりである.