ここではJupyter Notebookでプログラミング言語Rubyを用いて演習を行います。少ない時間でプログラムとは何であるかを原理的に理解することと、文房具としてのプログラミングをある程度マスターすることを目的としています。本格的なプログラミングを学ぶためにはそれぞれのプログラミング言語の演習を履修することをお勧めします。
Jupyter Notebookとはプログラムとその実行結果(値(文字列含む)、グラフ、表形式)、文章(説明)を混在させて編集・表示を行えるツールです。Jupyterのデータの処理を行うプログラムはサーバ方式で動作し、それにwebブラウザから接続して使います(OSのメニュー等からJupyter Notebookを起動すれば自動的にブラウザが立ち上がってローカルに動作するJupyerのサーバに自動的に接続します)。教育環境のWindowsにプログラミング言語Pythonの処理系の一つであるAnacondaがインストールされており、それにはJupyterが含まれています。そこにRubyのkernel(後述)もインストールされていれば、そのままJupyterでRubyを使うことが可能です。ただしRubyの様々な拡張プログラムをインストールするgem
というコマンドが利用できないと以下の資料の一部のプログラムは動作しません。ただし拡張プログラムが既にインストールされていればその限りではありません。フリーソフトウェアであり、BYODのPCにも無料でインストール可能です。この場合には自分で拡張プログラムをインストールできるでしょう。
この資料の内容を試してみるには、以下の条件を満たしている必要があります。
条件を満たさない場合、一部あるいは全部を実行することができません。
Anacondaのインストール方法は別ページを参照してください。 Jupyter notebookを起動して右上にあるNewの選択肢にRubyが現れれば、IRubyはインストールされています。 BYODのPCの場合、インストールされていなければインストールすればよいです。 IRuby, Jupyter, OS名を検索すれば方法を知ることが可能です。
仮にIRubyがうまく動作していると仮定します。 daru, daru-plotly, rbplotlyについては、以下のそれぞれを利用する実行例を試せばインストール済みかどうかがわかります。 インストールされていないBYODのPCの場合、やはりインストールすれば試せます。 インストール方法についてはやはりそれらとJupyter, Ruby, OS名等をキーワードとして検索してみてください。
これについては別ページを参照してください。 Rubyを用いる場合のポイントは、新しくnotebookを作成する際にkernelとしてRubyを指定することです。 notebookを開いた後でKernelメニューからkernelを変更することも可能です。 そういったところにRubyが現れない場合、Jupyterが動作していても、Rubyのkernelが正しくインストールされていません。
このページを表示しながら、プログラムのコード部分(In [ ]:
の右側枠内の部分)を順にJupyterのセル(やはり枠内の部分。プログラムなのでCode属性にする)に手入力するかコピー&ペーストし、実行します。或いは授業中に提示する予定の、notebookのデータファイルをuploadすることで、直接実行することが可能になります。但し提出課題の部分は自分で入力するなり変更するなりする必要があります。
課題の回答を作成して動作確認したらコピー&ペーストによりプレーンテキストのファイルとして提出してください(次回以降も同様)。
ファイル名: 課題提出用ID-課題番号.rb (この拡張子はRubyで書かれたプログラムであることを表します)
ファイル名の例: e20000-kadai09.rb
右側の課題番号は毎回変わりますし、左側の課題提出用IDは各自異なります。 画像ファイル等の場合には拡張子の部分を適切なものにしてください。
下の「1+2」はこれで一つのプログラムです。これを実行する(評価する)にはマウスでクリックして下のセルを選択し、(存在する場合は)ウィンドウ上方の[>|]
のボタンをクリックするか、SHIFT+returnキーを押します。Out[..]:
3というように値3が表示されれば正常に動作しています。
1+2
次のセル中に加減乗除(+-*/)
による数式を書き、同様に実行してみてください。セルの中を書き直して再実行できます。
10+4*(3/4+100)+101
これはRuby(マニュアル)というプログラミング言語のプログラムとして対話的に実行しています。一応ウィンドウ右上に[Ruby 2.3.7]
などと表示されていることを確認してください。プログラムの実行の仕方としてバッチ式と対話式という方式があり、前者は(複数の時もありますが)一つの定まったプログラムにデータを与えて処理を行う方式です。後者はプログラムの処理系にプログラムの断片を入力しつつ、対話的に小さいプログラムの実行と結果の表示を繰り返し行う方式です。
対話的に実行する方がとっつきやすいので、この資料では対話的に実行する方法で解説を行います。ただし、Jupyterの場合セルごとに実行を行うため、実行の順序によって処理系(プログラムの実行を行うプログラム達。本体部分はかなりの部分が機械語でできていたりします。実行する対象のプログラムはRuby等の、人間が理解しやすい高級言語で書かれたものです。大きく分けてコンパイラ方式とインタプリタ方式があります)の状態が異なる場合があります。また、そもそも実行していないセルがあると、その内容に依存する他のプログラムの断片がうまく動作しない場合がありえますので注意が必要です。これについては後で説明します。
ここでRubyのversionを確認しておきます。
RUBY_VERSION # この資料ではversion 2.3.7を使って解説している。#から行末まではコメント。
Rubyの言語の(major) versionは1と2でかなり違う部分があり、この資料ではRuby 2について記述しています。 マニュアル等を参照する際にはversionが近いものを参照してください。
変数はほとんどのプログラミング言語において重要な概念です。変数(variable)とは、何らかのデータを格納する箱だと思ってください。箱には名前があり、それを変数名と言います。Ruby用のコーディング規約がいくつか知られており、
例えば変数名は通常英小文字として単語の区切りを「_
」(アンダースコア)とします。
後で出てくるように違う箱だが同じ名前が与えられる場合があり、その場合についての理解が必要です。
また、Rubyを含めて手続き型のプログラミング言語では変数の値をプログラムの実行途中で変更することができ、それが計算の状態変化を表します。このようにプログラムの実行途中で値が変化することが可能な変数をmutableな変数と言います。変数の箱に値を入れることを代入(assignment)と言い、Rubyの場合には=
を使って表します。結果的に左右が等しくなるとは限らず、数学での等式を表しているのではないことに注意してください。
変数には種類があり、どのような種類の変数が使えるかはプログラミング言語により変わります。
Rubyの場合には数え方にもよりますが、5種類の変数があります。
しかしまずここではそれらのうち大域変数(global variable)という、プログラム(ファイル内)のどこからも参照できる(例外があります)変数のみが出てきます
。大域変数についてはそれらの箱はプログラムの実行開始時(あるいはその変数が存在することがわかった最初の時点)からプログラムの実行終了時まで存在し続けます。
Rubyの場合には大域変数は$
で開始します。
以下、上から順番に各セルを実行してみてください。例えばx=式
、で式の値を計算し、結果をxに代入します。次
Rubyでは代入文が値を持地ます。
なお#から右側はコメントとして扱われ、この部分は実行されません。
$x=11 # 変数$xに11を**代入**する
$y=23
$z=$x*$y
$x
$z*$z
$x=12
$z=$x*$y
$z+=3 # $z に3を加える
$z*=2 # $zの値を2倍する
この例のように、変数には値を格納することが出来ます。その後の命令の列が全く同じでも、変数の値によって計算は変わってきます。上から順に実行した場合には最初$x
の値が11
であり、それに依存して決まる$z
の値は最初253
になります。しかしその後$x
の値が12
に変更されてから$z=$x*$y
という同じ命令によって$z
の値は276
という異なる値になります。変数は電卓のメモリと同じ用途で用いることが可能です。
上から順に実行した場合、最初の$z=$x*$y
で$z
の値は253
になりますが、その後下の方の命令を実行して$x
の値が異なる値に変化してから(下の方のではなく)最初と同じセルの$z=$x*$y
を実行すると、$z
の値は異なるものとなります。また、$x=11
や$x=12
を実行する前に$z=$x*$y
を実行するとxの値が定義されていないためにエラーになります。このようにセルの実行順序によってエラーが起きたり変数の値を含めて状態が変化することに注意してください。また、$z+=3
のように実行の度に$z
が増加するため実行回数が影響を与える場合もあります。上から順に実行すれば実行回数に関わらず正しく動くようなnotebookとするのが望ましい場合が多いです。
Rubyでは数値以外にも様々なデータを扱います。例えばデータの列であるリストや文字列などがあります。データの種類のことを一般にデータ型といいます。プログラムの実行前に式が表すデータの型が定まるようなプログラミング言語を静的型付けの言語と言います。それに対し、データ型が実行前に決まらず、実行時にチェックされる言語を動的型付けの言語と言います。Rubyは後者になります。
静的型付けの言語はプログラムの実行前に、ある種のプログラムのミス(BUG)がないことを自動的にチェックできます。特に強い型付けという種類に分類される言語では、実行前のチェックを通っていればデータ型に関するエラーが実行時に起きないことが保証されます。ただしプログラミングを学ぶ際にデータ型に関する理論をある程度学ぶ必要がありますし、十分広い枠ではありますが、一定の枠に嵌ったプログラムのみを書けることになります。Rubyは動的型付けの言語なのでデータ型のエラーの実行前のチェックは行われず、実行時に検出されます。
ここまではほとんど電卓と同じように計算を行ってきただけでした。しかし今から出てくる条件判断と繰り返しは、通常の電卓にはないものであり、計算機を有用な道具とするには必須のものです。Rubyの条件文は以下のような構文になります。 改行とインデント(字下げ)はRubyではそれほど意味を持ちません。 このようなプログラミング言語は他にも多数あります。 このような言語では改行や字下げを入れる理由は専ら人間にとっての読みやすさのためです。 したがって以下の例中の改行を取り除いて空白に置き換えても問題ありません。
構文:
if 条件式
文1 # 条件がtrueであればelseまでが実行される。
文2 # elseまでは続けて実行される。
.
.
else # ifに対応するelse
文a # 条件がfalseであればifの終わりのendまでが実行される。
文b
.
.
end
次の命令
通常、条件式の部分には等しいかどうか(==
,!=
)大小関係等(<
,>
,<=
,>=
)やそれらの組み合わせ(&&
,||
,!
、それぞれand,or,notの意味で組み合わせる)等、真偽値をとるような式を記述します。
なおelse
からend
の前までの部分はなくても構いません。
その場合は条件式がFalse
であった場合、条件文内では追加で何も実行されません。
またelse
後の文a
...の部分がまた別のif文になっている場合、インデントのレベルを深くしないための書き方(elsif
)があります。これに付いては後で出てきます。
次の2つの具体例では条件部分の真偽により式の値が変化します。==
は両辺が等しい時true
、等しくない時false
となります。
if 1 == 1 then
1
else
2
end
if 0 == 1 then
1
else
2
end
このようにRubyではif
文が値を持ちます。
そうではないプログラミング言語もあります(Pythonなど)。
前述のようにRubyでは改行が空白と同程度の意味しか持たないため、次のように書いても同じ意味になります。
ただしコメントの開始を表す#
は改行までがコメントになります。
if 1 == 1 then 1 else 2 end
次のpというのは引数の値を出力します。
p 1==1, 1==0, 1!=0, 1>2, true || false, true && true, !true # pは引数(ひきすう、argument)の値を出力する。
最後の結果の値は配列の形になっています。 配列については後の方で説明します。
printだと引数毎の改行が入りません。
print 1==1, 1==0, 1!=0, 1>2, true || false, true && true, !true # printは引数(ひきすう、argument)の値を出力する。
Rubyで繰り返し処理を記述する方法は多数あります。ここではそれらのうちの一つ、for
によるものを紹介します。
構文:
for 変数 in 範囲
文1 # 変数の値を範囲の中で変えてforの境界までの文が繰り返し実行される。
文2
.
.
end
次の命令 # for文の終了後に実行される。
なおif
文やfor
文は入れ籠にすることができます。
for i in 1..100 # 範囲1..100は1から100までを意味する。
print i,"," # end=' 'はiの出力後に改行ではなく空白を出力する指定
end
代入の際に、「,
」で区切って複数個の変数に同時に代入できます。これにより例えば変数の値の交換を簡単に記述できます。
x = 1
y = 10
x,y = y,x # 同時代入
p x,y
新しい函数を定義することが可能です。例えば二次方程式$ax^2+2bx+c=0$の解(の一つ)を求める函数quadratic
は以下のように定義できます。
include Math # Math.sqrtと書くのであれば必要ない。
def quadratic(a,b,c) # Rubyの函数名・変数名は通常英小文字と「_」の列とする
(-b + sqrt(b**2 - a*c))/a # 返り値。a**b はaのb乗
end
quadratic(1,1,1)
Rubyの函数定義は以下のようなものになります。
構文:
def 函数名(引数1,引数2,...)
式1
式2
.
.
return 式n # 必要に応じてreturnを使う。函数の実行を終えて式nの値を結果とする。
.
.
式
end
ここで引数1,...
は函数に渡されるデータが格納される変数で、仮引数といいます。
これらに対し、実際に函数に渡される具体的なデータを実引数といいます。
仮引数は局所変数(local variable)であり、それらの「箱」は函数の値を求める間にのみ一時的に用意され、函数定義内部でのみ利用可能な変数となります。
変数の有効範囲のことをその変数のスコープと言います。
函数定義のdefにより新しいスコープができます。
以下の例はスコープから外れているために変数が未定義となる例です。
def test1()
newvariable_fun = 1
end
test1()
newvariable_fun
局所変数と同じ名前の変数が函数定義の外側に既にある場合、 それらは函数内部からは基本的に参照できなくなります。 同じ名前の変数がある場合、あるいは函数呼び出しが何重にも重なった場合に同じ変数名の変数が出てくる場合があります。 それらは変数名が同じなだけで、「箱」としては別物であることに注意してください。
x = 0
def test2()
x = 1 # これは函数内部でのみ有効な局所変数。
end
test2()
x # 同じ変数名だが別の箱。
ここまでの知識で定義できる函数の例題を見てみます。
フィボナッチ数列とはfib(0)=0, fib(1)=1, fib(n)=fib(n-2)+fib(n-1)
として再帰的に定義される数列です。自然界のいろいろなところで現れることが知られています。
def fib(i)
x,y = 0,1
for i in 0..i-1
x,y = y,x+y
end
x
end
[fib(0),fib(1),fib(2),fib(3),fib(4),fib(5),fib(6)] # この式は値の配列を結果とする
def fact(i) # 階乗を計算する函数
y = 1
for i in 1..i
y *= i
end
y
end
[fact(0),fact(1),fact(2),fact(3),fact(4),fact(5),fact(6)]
fact(300) # 任意多倍長整数演算を行える。5000!等も問題なく計算できる。
ここでグラフを描く方法を書いておきます。レポートを作成する際などに大変便利な機能であり、定規や色鉛筆などを超える文房具としてプログラムを利用することになります。ただしこの資料の今の進度よりは相対的に難しい概念が出てくる部分があります。 Ruby用のグラフ描画のためのライブラリ(様々な用途のために作られた、各種プログラムで共通に使うためのプログラム集)はいろいろあります。 以下ではそれらのうちでdaru-plotlyを用いています。
require 'daru/plotly' # ライブラリを読み込む
include Daru::Plotly::Methods
r = *0..14 # *で範囲を配列に変換
fib = { x:r, y:r.map{|x| fib(x)}, type: :scatter, mode: :markers}
# 範囲の各要素のfibの値を計算して配列とする(map)。点で描画(scatter,markers)。
pl = Plotly::Plot.new(data: [fib])
pl.show
0,14を他の値に変えれば範囲が変わり、fibを他の函数に変えれば表示する函数が変わります。函数定義を他で行なってそれを実行し(読み込ませ)てから変更した上のプログラムを実行すればグラフが書き換わります。
ここでr.map{|x| fib(x)}
は範囲r
の各値に対し函数fib
を適用した結果の列を作ります。
{}
内の部分はブロックと呼ばれ、Rubyに特徴的な言語要素です。
これは本質的にはλ式(無名函数)という、
数学的な計算理論に起原があるもので、
最初LISP言語で採用され、最近では様々なプログラミング言語に導入されています。
Rubyではそう意識させずにわかりやく使える形式としています。
それとr
、他のデータから{...}
によってPlotly::Plot.new
の引数として許される形に変換しています。
Plotly::Plot.new
はそれらデータを平面にプロットします。
次の例はもう少し実用に近い例であり、どのように指定すれば表示がどうなるのかわかると思います。
require 'daru/plotly'
include Daru::Plotly::Methods
x = *1..14
fib = { x:x, y:x.map{|x| Math.log(fib(x))}, type: :scatter, mode: :markers, name: 'fib'}
fact = { x:x, y:x.map{|x| Math.log(fact(x))}, name: 'fact'}
pl = Plotly::Plot.new(data: [fact,fib],
layout: { title: 'fibonatti and factorial', xaxis: { title: 'x' }, yaxis: { title: 'log(y)' } } )
pl.show
グラフではなく表形式で表示してみます。 ここではRuby用のdaruというライブラリを利用しています。
require 'daru' # このセルは函数定義のセル達の後に実行する
r = 0..14
Daru::DataFrame.new("fib" => r.map{|x| fib(x)}, "fact" => r.map{|x| fact(x)}).transpose()
Jupyterに限らず、ある種のキーワードを補完する機能がついている場合があります。
Jupyterの場合には[TAB]
キーにより補完を行います。
これはRuby以外のkernelの場合にも大抵使える機能です。
Codeセルでキーワードを途中まで打ち込んで[TAB]
キーを押すとそれを先頭とするキーワード候補の選択メニューが表示され、選択できる状態になります。
# 次の行に例えば「r」を入力し、`[TAB]`キーを押すとrで始まるキーワード達の選択メニューが表示される。
r
候補が多数ある場合にはスクロールして選択します。
逆にもしも候補が一つもない場合には何も表示されません。
一つしかない場合にはその候補の残りの文字列が自動入力されます。
この機能を指して補完(completion)と呼びます。
長いキーワードを入力する場合に一意的になるまでキー入力後、
[TAB]
により補完するとキーストローク数が減り、
誤りも減らせます。
利用したRuby kernelの場合、函数名やメソッド名等の前後どちらかに「?」を入力して[SHIFT]+↩︎
を押すと、
それについての説明が下部に表示されるという、Jupyterでは利用できる場合が多い機能が使えないようでした。
字句解析上の取扱が異なるためかもしれません。
ここまでに学習した事柄を使って、引数として与えた2つの整数を含めてそれらの間のすべての整数の和を求める函数sum
をRubyで記述してコピー&ペーストしてメールにて提出せよ。ただし数値演算は和と差のみしか使ってはならない。課題の提出先は従来と同じ、課題提出用IDとkadai07という課題番号を用いること。
ファイル名の拡張子は.rbとする。
# e10234 kadai07 課題提出用IDを書き換え
#
def sum(x,y)
x+y # 正しいプログラムに書き換えること
end
[sum(-2,1), sum(1,-3), sum(10,13), sum(-100,0)] # 結果をリストに
# -2 -5 46 -5050 以下の結果は最初当然正しくない
これまでに、フィボナッチ数列や階乗計算のプログラムを例題として示して解説しました。それらは元々再帰的に定義されています。プログラミング言語によっては再帰的に定義された数列等を再帰的なプログラムで表現できる機能を備えています。 Rubyにもその機能はあり、例えば次のようにしてフィボナッチ数列や階乗計算を行うプログラムを記述することができます。
def fib_r(x)
if x == 0
0
elsif x == 1 # elifはelse ifのことで、if文の最初の条件を満たさない時に条件がチェックされる
1
else
fib_r(x - 1) + fib_r(x - 2) # 再帰呼び出し。
end
end
[fib_r(0), fib_r(1), fib_r(2), fib_r(3), fib_r(4), fib_r(5)]
def fact_r(x)
if x == 0
1
else
x*fact_r(x - 1) # 再帰呼び出し。
end
end
[fact_r(0), fact_r(1), fact_r(2), fact_r(3), fact_r(4), fact_r(5)]
このように、fib_r
やfact_r
自身の定義の中でまたそれらを呼び出すことを一般に再帰呼び出し(retursive call)と呼びます。これもプログラミングにおける重要な概念の一つです。前に例題とした再帰呼び出しによらないプログラムと比較してみてください。ただしfib
については、再帰呼び出しのfib_r
の方は非常に効率の悪いものとなっています(なぜでしょうか)が、同じ再帰呼び出しでも効率の良いプログラムを書くことも可能であることを申し添えておきます。
Rubyのような手続き型の言語(ここでは変数への代入文があり、計算状態を変化させる操作の列、即ち手続きとしてプログラムを記述する種類のプログラミング言語を手続き型としています。もちろんRubyで函数的にプログラムを書いてゆくことも可能でしょう)の場合には、再帰呼び出しのプログラムを書く際に、一般には「再帰呼び出しを行う」という操作を行うことと、その時の状態を意識する場合が多いかもしれません。しかしこれまでに出てきたようなフィボナッチ函数や階乗のような、いつ計算しても値は同じだし、計算の状態の変化を引き起こさない場合(つまり値を返す以外の状態の変化である副作用(side effect)がない場合)、即ち函数定義としてプログラムを与えている場合には、再帰的プログラムを書く時に宣言的に考えることが可能です。またこのように考えた方がプログラミングを学びたての場合には理解しやすい人もいますし、すでに手続き的な考え方に慣れている人にとってはある意味新鮮な別の考え方であると思います。再帰呼び出しを行う函数を定義するときの考え方を以下に記述します。
1.はfib_r
についてはfib_r(0),fib_r(1)
の場合、fact_r
についてはfact_r(0)
の場合が該当します。2.はfib_r(x - 1)
やfib_r(x - 2)
、fact_r(x - 1)
が正しい値を返すとしてプログラムを書いています。3.はこれらの場合、値が少ないフィボナッチ数や階乗の値を計算する方が、元のフィボナッチ数や階乗の値を計算するよりも簡単だと考えられます。4.について、2つの函数は負の数に対しては計算が止まりません。4があるため、正確には定義しているのは函数ではなく部分函数(partial function)であるということになります。
考え方の内3が一番曖昧で、どのような意味で簡単な引数で再帰呼び出しをするかは場合によって異なります。但し大抵の場合は直観的にわかる範囲の場合が殆どです。あらゆる場合に適用できるような、この部分の厳密な定式化はある意味不可能であることがわかっています。
但し以上の考え方は、あくまで副作用のない場合であり、次に出てくるタートルグラフィクスの再帰的な描画の手続きの場合、基本的にはやはり手続き的な考え方をすることになります。 手続き的に考える場合、仮引数などの局所変数は呼び出しごとに別物となります。
画面に図形を表示する演習を行うと興味を引かれる人が多いようであるという理由で、ここではタートルグラフィクスによる演習を行います。タートルグラフィクスとは、タートル(亀)の動きによりさまざまな図形を描くというグラフィクスの一方式です。いわゆるフラクタル図形等の描画を簡単に行うことができます。タートルグラフィクスは、それ自身を扱うのが容易で、しかもタートルグラフィクスの基本プログラムを記述するのも簡単なため、入門的なコースや説明でよく使われるものの一つです。
タートルグラフィクスにはRubyで記述したそれ用のライブラリを用いることにします。追加で小さいプログラムを書けば簡単に使えます。
タートルをRubyのようなオブジェクト指向言語で実現する場合、タートルをオブジェクトとし、マルチタートルグラフィクスとするのが自然です。ライブラリではそのように記述しています。
include Math # sin,cosなどをモジュール名なしで使えるようにする
require 'rbplotly' # requireはrubyのライブラリを読み込む
$turtles = [] # 描画タートル列の作成
def show tl # 新しいメソッドshowを定義している
l = tl.map{|t| t.traces}.flatten # タートル毎の軌跡の列を一つの列にする
x2 = l.map{|t| t[:x].max}.max # 各座標の最小大値を求める
x1 = l.map{|t| t[:x].min}.min
y2 = l.map{|t| t[:y].max}.max
y1 = l.map{|t| t[:y].min}.min
x0 = (x1 + x2)/2.0
y0 = (y1 + y2)/2.0
if x2 - x1 > y2 - y1
d = (x2 - x1)/2.0
l.push({ x: [x0, x0], y: [y0 - d, y0 + d], mode: :markers, marker: {width: 0, color: 'white'} })
else
d = (y2 - y1)/2.0
l.push({ x: [x0 - d, x0 + d], y: [y0, y0], mode: :markers, marker: {width: 0, color: 'white'} })
end
plot = Plotly::Plot.new(data: l)
plot.layout.width = 800
plot.layout.height = 800
plot.layout.xaxis = {autotick: true, zeroline: false}
plot.layout.yaxis = {autotick: true, zeroline: false}
plot.show
end
def clear tl # タートル達の軌跡のクリア
tl.each{|t| t.clear}
end
class Turtle # 亀クラスの定義
def initialize # Turtle.newを呼び出すとこのルーチンが呼ばれる
@speed = 1.0 # 亀の基本スピード
# @で始まる変数はオブジェクトのインスタンス変数
# インスタンス変数はオブジェクトごとに存在する
@theta = 0.0 # 亀の向き 0.0=上向き
@color = 'black' # 亀の足跡の色
@down = true # true=描画する
@traces = [] # トレース(軌跡)は最初は空
home # このように、他のメソッドを呼び出すこともできる
north
end
def home # ウィンドウ中央に亀を移動させる
@x = 0.0 # 原点
@y = 0.0
self # selfはメッセージを受信したオブジェクト自身
end
def north # 上を向かせる
@theta = 0.0
self
end
def forward t # 亀をt歩進め描画する
x1 = @x + t*@speed*sin(@theta/180.0*PI) # tはパラメータ(仮引数)である
y1 = @y + t*@speed*cos(@theta/180.0*PI) # tは局所変数でもある。x1,y1も局所変数
if @down
if !@traces.empty? && @traces.last[:line][:color] == @color && # 軌跡リストが空か、最後の色か座標が
@traces.last[:x].last == @x && @traces.last[:y].last == @y # 異なれば別の軌跡とする
@traces.last[:x].push x1 # 続きの場合
@traces.last[:y].push y1
else
@traces.push({ x: [@x,x1], y: [@y,y1], mode: :lines, line: {width: 1, color: @color} }) # 別の場合
end
end
@x = x1
@y = y1
self
end
def up # 筆を上げる
@down = false
self
end
def down # 筆を下げる
@down = true
self
end
def right r # 亀の向きを時計回りにr°回転させる
# このようにright(r)を略してright rなどと書ける場合がある
@theta += r # mod 360とした方がよいかもしれない
self
end
def left r
right -r
end
def color c # 亀の色を変える
@color = c # 'black', 'white', 'red', 'green', 'blue',
self # 'magenta', 'cyan', 'yellow'
end
def locate(x,y) # 亀を<x,y>に移動させる
@x = x.to_i
@y = y.to_i
self
end
def speed(s)
@speed = s
end
def traces
@traces
end
def clear
@traces = []
end
end
タートルグラフィクスによっていわゆるフラクタル曲線を簡単に描くことが可能になります。 なお、グラフィクスのプログラムは環境によって挙動が変化する場合があります。 上のタートルグラフィクスのライブラリを用いる場合には、Jupyterの同じ頁に結果が表示されます。
別のライブラリを用いてそれが別ウィンドウで結果を表示する場合もあるかもしれません。 いつでも単に実行すればウィンドウがポップアップして表示される環境もあれば、Xウィンドウシステムをあらかじめ起動しておく必要がある場合、Jupyter NotebookをXの仮想端末から起動しておく必要がある場合などがあります。
# 上のタートルグラフィクスのライブラリを読み込んでからこのセルを実行する。以下同様。
def koch(t,x,y) # 函数ではなく手続き。しかし返り値は一応ある。副作用(この場合は描画)のみが結果deaる。
if x <= y
t.forward(x) # 現在のタートルの向きにx進む。ペンが下りていれば描画される。
else
x /= 3
koch(t,x,y) # 再帰呼び出し
t.left(60) # 左に90度向きを変える
koch(t,x,y)
t.right(120) # 右に120度向きを変える
koch(t,x,y)
t.left(60)
koch(t,x,y)
end
end
t = Turtle.new
t.right(90)
koch(t,300,1)
show [t]
def hilbert(t,x,y,theta)
if x >= y
x /= 2
t.right(theta)
hilbert(t,x,y,-theta)
t.forward(y)
t.left(theta)
hilbert(t,x,y,theta)
t.forward(y)
hilbert(t,x,y,theta)
t.left(theta)
t.forward(y)
hilbert(t,x,y,-theta)
t.right(theta)
end
end
t = Turtle.new
t.color('black')
hilbert(t,256,8,90)
show [t]
def sierpinski(t,x,y,theta)
if x < y
t.forward(x)
else
x /= 2
t.left(theta)
sierpinski(t,x,y,-theta)
t.right(theta)
sierpinski(t,x,y,theta)
t.right(theta)
sierpinski(t,x,y,-theta)
t.left(theta)
end
end
t = Turtle.new
t.color('black')
t.right(90)
sierpinski(t,640,5,60)
show [t]
この部分は少々普通ではない説明をしていますので最初は飛ばしてください。課題を解くのにも必要ないと考えられます。
上のプログラムでは再帰呼び出しを行なっていますが、函数には返り値がありません。即ち副作用のみのプログラムです。この資料ではこのような函数を手続き(procedure)と呼ぶことにします。この場合の副作用はウィンドウにタートルの跡を描きつつタートルの位置や向き、色を変更することです。このように副作用のみの場合には再帰呼び出しを行うプログラムの内容の理解やそれを書く場合にどのように考えればよいのでしょうか。これには2つの方法があります。
ここでは2番目の理解の仕方を説明してみます。 手続き(函数)が呼び出され、forwardやleftなどの操作をタートルに行うのを、操作の列として求める函数だと捉えます。それらの操作は順序に意味がありますが、結合律を満たします。即ち$a_i$を個別の操作とすると、$(a_1,a_2),a_3$という操作と$a_1,(a_2,a_3)$という操作は操作として同じことです。また何もしない、という操作も考えられ、それがある種の単位元になっています。こうしてタートルへの操作の列を生成する宣言的なプログラムであると考えることができますが、これだけですと抽象的な説明であって具体性が不十分なので操作の列を生成するような宣言的なプログラムに書き換えてみます。但し$\lambda$式や後で説明するような概念のリストを先走って使っているので、この部分は飛ばしていただいても構いません。またこのような書き換えを行なうと実際に描画が行われる時刻が変わってくるので、その意味では元のプログラムと等価ではなくなります。
次のプログラムでは再帰的に定義されたsierpinski
という函数の代わりにsierpinski_m
という副作用のない函数を定義しています。この函数はタートルへの操作の列(リスト)を値として返します。但し後で実行できる形式とするために未説明の言語要素である$\lambda$式を使っています。変数への代入が行われていますが、これは一回だけの代入であり、mutableな変数として扱ってはいません。再帰呼び出しの際に同じ変数名の変数に何度も代入が行われますが、これもそれぞれが論理的に別の変数であり、宣言的なプログラムであることに変わりはありません。上のプログラムと見比べてみてください。
但しプログラムの最後の部分では、リスト中の操作を全て行なって実際の描画を行なっています。この部分だけが手続き的なプログラムとなっています。
def sierpinski_m(x,y,theta,a0)
if x < y
return a0 + [lambda{|t| t.forward(x)}]
else
x /= 2
a1 = a0 + [lambda{|t| t.left(theta)}]
a2 = sierpinski_m(x,y,-theta, a1)
a3 = a2 + [lambda{|t| t.right(theta)}]
a4 = sierpinski_m(x,y,theta,a3)
a5 = a4 + [lambda{|t| t.right(theta)}]
a6 = sierpinski_m(x,y,-theta,a5)
return a6 + [lambda{|t| t.left(theta)}]
end
end
a1 = [lambda{|x| x.color('black')}, lambda{|x| x.right(90)}]
a2 = sierpinski_m(640,5,60,a1)
t = Turtle.new
a2.each{|x| x.call(t)}
show [t]
ここで改めてオブジェクト指向プログラミングについて説明しておきます。オブジェクトという概念はできるだけ何にでも当てはめられるようなものですので、必然的に抽象的な概念であり、説明も抽象的なものとなります。したがってこの節の内容を実感を伴って理解できない場合、ざっと読飛ばすだけで十分であり、あとでプログラムの具体例を理解してから再読されるとよいと思います。
オブジェクトとは自分自身の局所的なデータを備え、計算を行うものです。また、他のオブジェクトとデータをやり取りします。やり取りするデータのことをメッセージと呼びます。
メッセージの解釈は各オブジェクトが自分自身で行います。各オブジェクトの局所的なデータは特に指定しない限り他のオブジェクトから見たり書き換えたりすることができません。局所的なデータを見たり書き換えたりできる場合があるといっても、それらもメッセージにより行います。基本的には見るためあるいは書き換えるためのメッセージへの反応の仕方を記述したプログラムは、メッセージを送る側ではなく、メッセージを受けるオブジェクト側が持っています。従って外側から見てあるオブジェクトの局所的なデータを操作しているように見えたとしても、外側からはそれらしく見えているだけであって実際にそうなのかどうかは当該オブジェクト自身にしかわかりません。
このように、データとそれへの操作方法をまとめて定義し、それらを外から直接見られなくすることをデータ隠蔽とかカプセル化と呼びます。このようにするとデータとそれへの操作のプログラムをまとめて取り替えることが可能になり、ソフトウェアの部品化が容易になるし、プログラマは部品の余計な内部構造まで考えなくてすみます。この概念はプログラミング言語の発展における重要な概念の一つです。
同種のオブジェクトをまとめたものをクラスと呼ぶ場合があります。すなわち、クラスが同じオブジェクトは、保持している局所的データの種類と個数が同じで、メッセージへの反応の仕方を記述するプログラムも同じです。メッセージにはメソッドと呼ばれる処理プログラムを表す一種のタグ(荷札)がついており、メソッドごとに処理プログラムを記述します。Rubyを含めてクラス概念があるプログラミング言語では、局所変数やメッセージへの反応の仕方を記述するプログラムを与えてクラスを定義します。
クラスを新たに定義する場合、既存クラスの記述を利用することが可能です。この機能をインヘリタンス(継承)と呼びます。
使用しているタートルグラフィクスのライブラリではタートルを複数同時に使うことが可能です。タートルはそれぞれオブジェクトとして実現されています。以下の例では2つのタートルを使って色が異なる2つの図形を同時に描きます。タートルが一つしかなければもう一つ生成し、タートルを表す2つのオブジェクトをleft、rightという変数に代入して操作を行います。例えばleft.forward(i)
とすればleftに入っているオブジェクトにforward(i)
(但しi
はその時点でのi
の値に置き換えられる)というメッセージが送られます。leftは、それが属するTurtleというクラスのforward
メソッドの定義に従って解釈します。この場合そのメソッドの定義はタートルグラフィクスのライブラリの中で与えられているのでユーザが記述する必要はありません。
right = Turtle.new # もうひとつタートルを生成する
left = Turtle.new # 1つ目のタートル
left.locate(-200,0)
right.locate(200,0)
left.color('red')
right.color('blue')
for i in 0..500
left.forward(i)
left.right(121)
right.forward(i)
right.right(119)
end
show [left, right]
以下は深さ優先で木を描くプログラムです。
def dtree(t, depth, theta, length)
t.forward(length) # 現在の方向に進める
if depth > 1 # 葉ではない場合
t.left(theta) # 左に回転
dtree(t, depth - 1, theta, length/1.2) # 長さを一定の比率で縮める
t.right(2*theta) # 右に回転
dtree(t, depth - 1, theta, length/1.2)
t.left(theta)
end
t.forward(-length) # 逆方向に戻す
end
t = Turtle.new
dtree(t,8,15,120)
show [t]
以下は木の分岐ごとにタートルをコピーして増やし、並列に木を描くプログラムです。 (実はこのままだと意図とは異なるプログラムになっています。 ただし描画はするので現在はそのままにしています。) 描画結果は上の深さ優先の場合と変わりません。
def brtree(ts, depth, theta, length)
ts2 = ts.clone # 現在のタートルのリストをコピー
for t1 in ts
t1.forward(length) # 現在 の方向に進める
if depth > 1 # 葉ではない場合
t2 = t1.clone # タートルをコピーする向きや色は同じ
t1.left(theta) # 左に回転
t2.right(theta) # 右に回転
ts2.push(t2) # コピーしたタートルをリストの末尾に加える
end
end
if depth > 1 # 葉ではない場合
brtree(ts2, depth - 1, theta, length/1.2) # 長さを一定の比率で縮める
end
end
t = Turtle.new
brtree([t],8,15,120)
show [t]
タートルグラフィクスのライブラリを読み込み済みであるとして図形を描くプログラムを作成し、kadai08として提出せよ。図形は自分が選んだ、あるいは創作したものとしてよい。ただし描画のプログラムは自分で記述すること。また、kernel restart後に提出プログラム部分のみを実行し表示を確認すること。表示したウィンドウをキャプチャして画像ファイルとしたもの(kadai08.jpg)を添付すること。Windowsの場合、対象ウィンドウをクリックして選択し、[Alt]+[Print screen]キーを押し、ビットマップ画像描画プログラム(MSペイント等)を起動してペーストして保存すれば画像ファイルを作成できる。提出先等は前回と同じである。
これまでに学習したプログラミングの技法以外の技法も用いてもよい。サーチエンジンでRuby言語要素などを調べれば多数の解説サイトが見つかるので、それらを参照すればさまざまなプログラミングの技法やRubyの機能を知ることが可能である。
# e80000 kadai08 課題提出用IDを書き換え
#
# def 描画手続き
# 描画手続き呼び出し
様々なプログラミング言語に出てくる基本的な概念の一つである配列について説明します。Rubyの場合には配列をリストと同じようにも扱えます。
配列とは、何かが入る箱がいくつか並び、箱の位置を数字で指定して中のデータを読み書きできるものです。箱の位置を指定する数字のことを添字(index)と言い、並んだ箱の数のことを配列の大きさと言います。 リストとは、何かが入る箱が繋がったもので、繋がりを途中で切ったり繋いだりできるものです。繋がっている箱の数をリストの長さと言います。 配列とリストは似ていますが、元々は異なるデータ型です。 しかしRubyの場合には配列をリストのようにも扱えます。 また、配列の中に入る物は同じ種類のデータしか許していない言語もありますし、 Rubyのように箱ごとに異なる種類のデータを入れられる言語もあります。 例えば配列の中に配列を入れることも可能です。
Rubyでは、配列もオブジェクトの一つです。 配列からデータを読み出したり書き込んだりするのもメッセージによります。
a = [1,3,5,7,9] # 最初の要素が1、2番目の要素が3...という大きさ5の配列
a
[a.length, a.class] # 配列の大きさとクラス
[1,3,5,7,9]
というのは最初の要素が1、2番目以下の要素が3,5...という大きさ5の配列で、
上の文によりa
という変数に配列が入ります。print
によりこのままの形で表示されます。
添字が0から始まるか1から始まるかの違いを除き、他のプログラミング言語の場合とほぼ同様に以下のようになります。
p a[0],a[3]
a[0] = 10
a
a = [1, 2, 3, 4, 5]
a
a.push(6) # 配列の大きさを1増やし、最後にデータを入れる
a
a.shift # 配列の最初のデータを取り除く。入れたデータを順に取り出す仕組みをキューと呼びます
a
shift
、push
というメッセージで配列をキューとして使えます。今の場合には、最初1,2,3,4,5というデータがこの順にキューに入っていたところ、最後に6というデータを追加し、その後キューから最初のデータである1を取り出しました。
a.pop # 配列の最後のデータを取り除く
a
入れたデータを逆順に取り出す仕組みをスタックと呼びますappend
とpop()
というメッセージで配列をスタックとして使えます。
a = [1, 2, 3, 4]
b = [5, 6, 7, 8]
a + b # 2つの配列をくっつける
a.delete 3 # 要素3をリストから取り除く
a
ここではリストを使った例題として、エラトステネスの篩(ふるい)と呼ばれる手法により、ある数までの素数をすべて求めてみます。以下がそのプログラムです。nil
は通常のデータがないことを表すデータです。
n=300
a=[nil,nil];for i in (2..n.to_i);a.push i;end
a.each_index{|x| (2*x).step(a.size,x) {|i| a[i]=nil} if a[x]}
a.compact
このプログラムでは、最初に2つのnil
があり、その後2以上与えられた数までの数値を要素として持つ配列を2行目で作っています。この時点では、0と1は素数でなく、素数だとわかっているのは2だけで、3以降は素数である可能性がある数値が配列中に残っている状態です。ある添字の要素がnil
でなければその添字の値自身がその添字の要素として入っています。
4-6行目で、配列の各要素について、それがnil
(または0)でなければその数の2倍の添字位置からリストの最後までの、その数の倍数の添字位置の要素をnil
に置き換えるという操作を繰り返し行います。ここではループが2重になっていることに注意してください。また、if文でnil
がfalse
と同様に偽として扱われることにも注意してください。
最下行は合成数の位置に入っているnil
を消しています。
3つ目の課題はオプションです。即ち、提出したい人だけが提出してください。kadai09.rb
先の節である数nまでの素数をすべて求める方法を見てみました。今度はまず、ある数までの素数の表を求め、それを使って数mを素因数分解する函数factor
を書いてください。mは、函数の引数として与えるようにしてください。また、出力の形式は以下のようなものとしてください。
[[素因数1, 個数1], [素因数2, 個数2], … ,[素因数k, 個数k]]
素因数iの個数i乗をi=1,...,k
についてすべて掛け合わせた結果がmとなります。また、個数iは1以上の自然数です。ただし、あらかじめ求めた素数の表が、mの分解に足りない場合には、その旨を出力するようにしてください。その場合、途中まで行える分解の結果も表示してください。
例えば[2,3,5,7]という素数の表を求めていた場合、53の素因数分解には7*7 < 53
なので表が足りません。63の分解であれば3で2回割り切れて7となりますので分解できます。3339=3*3*7*53
の分解は、3で2回割って7で割ると53となり、7*7<53
なので表が足りませんが、[[3,2],[7,1],[53,1]]
というところまでは分解できます。ただしこの場合には最後の53が素数かどうかはわからないということになります(今の場合にはたまたま素数です)。表が足りない場合には、素数かどうかわからない部分も指示するようにしてください。例えば「[[3,2],[7,1],[53,1]]
(ただし最後の因数は素数とは限りません)」などと出力すれば十分です。
これまでに学習したプログラミングの技法以外の技法を用いても構いません。
# e80000 kadai09
#
# def factor(m)