Previous | ToC | Next |
Crystal による数値計算入門3回目です。
前章では、プログラムの中で変数に数値をいれて、それを使って計算し、その
結果を出力してみました。しかし、これなら電卓でもできるわけで、
あんまりプログラム書いて嬉しい感じがしません。また、計算機の能力を
有効に使っている感じもあまりしません。
というわけで、この数値計算入門では、手計算ではできないような繰り返し計
算で数値的に微分方程式を解いていくわけですが、その前にプログラムへの入
力とその繰り返しの方法をみておきます。
まずは前章と同じ変数 a, b ですが、プログラムの中で値を設定するのではな
く、キーボードから入手できるようにしてみましょう。
gets は、標準入力からの入力(改行まで)を読みとり、それを文字列として返
します(String型)。但し、入力がなかった時、例えば、
入力をファイルからとして、ファイルの最後まで読んでしまった時には、
String 型ではない、 nil というものを返します。これは Ruby の nil
と同じで、「そこに何もない」ということを表すもの、つまり、
この場合、 gets が文字を読まなかった、ということを表すものです。
Crystal では (Rubyと同様) nil は Nil という型の、もつことができる
唯一の値です。なので、 gets という関数は、 String 型または Nil型の
値を返すことができる、ということになります。このような、複数の型をもて
る、ということを Crystal では
なので、ちゃんとしたプログラムにするなら、 gets の返した値が String 型
であるかどうかをチェックして、そうでなかったらエラー終了するとか、
さらに文字列が数字でなかったらエラー終了するとかするべきですが、
その辺はコンパイラと実行時ライブラリに任せるなら、Nil が返ってきたら無
理矢理(長さ0の)文字列にしちゃう、文字列であればそのままに、という関数
を使えばいいことになります。それをするのが to_s です。
関数というと、 to_s(なんとか) のように書くのが数学での普通ですが、
いわゆる「オブジェクト指向言語」ではある型の変数や定数に対する関数を
なんとか.to_s のように、 変数 + "." + 関数という形で書けるようになって
います。これを、インスタンスメソッドと呼びます。インスタンスとは、
ある型(クラス、ここでは型とクラスは同じものです)の、変数ないし定数のこ
とです。単にメソッドといわないのは、インスタンスメソッドの他にクラス自
体のメソッド、クラスメソッドというものがあるからですが、通常メソッドと
いうとインスタンスメソッドのことになります。
また、同じ名前でもクラス毎に別の関数として定義できるので、この例のよう
に、 gets.to_s で、 gets が nil を返すと Nil クラスのメソッドである to_sが、
String を返すと String クラスのメソッドである to_s が(この場合何もしないで受け取った文字列を
返す)呼ばれることになります。なお、「Nil クラスのメソッドである to_s」
といちいち書く代わりに、Ruby や Crystal のドキュメントでは
「Nil#to_s」と書くようです。この文書でも以下この表記を使います。
さらに、ある型の値を整数型に変換する関数(メソッド)が to_i です。普通の
関数だと to_i(to_s(gets)) となるわけで、括弧の対応を考えつつ後ろから読
んでいかないと意味がわからないわけですが、gets.to_s.to_i だと、 「標準
入力を読んで、文字列に強制して、整数に変換する」となって理解しやすい、
というのがこの形式のメリットであり、それ以上の本質的な意味はないと思い
ますが、理解しやすい、というのはプログラミングの上では極めて重要です。
「オブジェクト指向言語」以前の言語、例えば Fortran77 や C言語では、ある名前の関
数が受け取る引数は決まった型のものでした。Fortran77 では、言語の側で提
供する数学関数や read/write は特別だし、Cでも varargs という機能で
scanf/printf といった入出力関数を実装していますが、我々が書くプログラ
ムで使う機能ではありません。オブジェクト指向言語では、
引数の型や数が違う関数は「別のもの」になり、違う型のメソッドは
従って全て別のものになります。
なお、この、「別のものになる」という機能が必ずしも嬉しくないことがあり
ます。このノートでの主題の1つになりますが、常微分方程式の数値積分を
考えてみます。そうすると、従属変数が1つだと、Float64なりFloat32ですが、
多変数だと配列(Array。本章の後半ででてきます)になるし、もっと複雑な
データ型を使いたいこともあります。そうすると、型毎に
数値積分公式、例えばルンゲクッタとかシンプレクティック公式とか
いったものを実現する関数を書く必要がでてくるわけです。Ruby のような動的言語では、
型を指定しないで関数を書くことで、受け取った型がもっているメソッドを使っ
て数値積分公式を実現することができます。Crystal でも、実行時ではなくコ
ンパイル時に型推論をしますが、同様のことができます。(あんまり意味がわ
からないかもですが、次章あたりで実例をみます)
とはいえ、単にキーボードからいれた数字を変数に入力するだけでこんな大変
なのかよ?C++ でも Fortran でももっと簡単だぜ、と思われたのではないか
と思います。確かに、1つ読むだけなら、
とはいえ、多少複雑な処理を、となると Crystal (というかその元になってい
る Ruby)ではより簡潔かつ間違いにくく書ける、という場合があります。以下、
そういう例をみていきます。
今、 sum-sample.in というテキストファイルに以下の数値がはいっていると
します。
このプログラムでは3つ新しいことがでてきます。
まず while は
また、 これも C と同様、代入も「式」であり、その値は左辺に
代入された値となります。なので、
Int、Float に対しては大小比較
while は条件が成り立っている間の繰り返しですが、制御構造としては、他に
if, unless, until があります。ここでまとめておきましょう。
なお、if ... end まで全体も式であり、値をもちます。これは、実際に実行
された実行部の値になります。普通のプログラムであんまり使わないですが、
自分で関数を定義する時にはその返す値を関係に表現できます。
また、 Ruby でもよく使いますが、 if をあとに置く、以下のような形式があ
ります
C言語 と同様に +=、 -=、 *= といった演算が定義されています。但し、 ++
(+=1)、- - 等はありません。 += に限らず、全ての2項演算子について = がつ
いたものが機械的に利用可能で、
文字列の中に #{式} と書くと、その部分が式の値(を、 to_s で文字列に変換したもの)で置き換えられます。
ということで、もう一度
while や if を使うのはどんな言語でも基本的なことではありますが、間違い
のもとでもあります。ということで、ここでは、明示的にそういうものを使わ
ないプログラムを作ってみます。これは、高尚ないいかたでは「関数的プログ
ラミング」ということになります。以下のプログラムを考えます。
実行結果は、 sum.cr の場合と同じなので省略します。つまり、このプログラ
ムでも、ファイルの全行の数値を読んで、その合計をだす、ということはでき
ています。
このプログラムは何をしているかを1行づつみていきます。
a=s.to_s.split
は、(また nil かもしれないので文字列にした値)、 split という関数にファ
イル全体の文字列を渡します。 split は、デフォルト(引数なし)では、
空白文字、タブ、改行等で文字列を切り分けて、それらからなる「配列」(Crystal では
Array)に変換します。Crystal の配列は、定数で書くと、例えば
sum = aint.sum
は、配列に対して、 sum というメソッドがあらかじめ定義されていて、それ
は全要素の合計を返す、というものなので、それを単に呼んでいます。
まあその、この例では、考え方はともかくなんかかえってプログラムは長くて
複雑ではないか、という気もしますが、では以下ではどうでしょう?
以下のような2次元の表があるとします。
この表について、以下の処理をするプログラムを作ってみます。
さて、このようにして配列ができてしまうと、後は割合簡単ですが、
まずは 2番めの「各行の合計を行列毎に出力」をやってみましょう。
プログラムは
各列の合計は色々な考え方があります。普通の考え方はまず、要素の数だ
け0が並んだ配列をつくって、繰り返しで各行の値を足す、となるでしょう。
繰り返しに each を使うと
さて、1行の数値の合計だと a.sum ですんだわけですが、各要素毎の和、となると
そうはいきません。Array クラスに対して + 演算子は定義されてますが、そ
れは単に二つの配列を連結するものだからです。ここでは、 sum を一般化し
た reduce メソッドを使ってみます。
最後に、「4,5,6 列目の合計を行毎に出力した上で、その合計も出力」です。
こちらは普通に、 each で sum に加算していくなら
本章では、Crystal の文法と機能について、以下を学んだ。
Int https://crystal-lang.org/api/0.32.1/Int.html
Float https://crystal-lang.org/api/0.32.1/Float.html
gets https://crystal-lang.org/api/0.32.1/IO.html#gets(delimiter:Char,limit:Int,chomp=false):String?-instance-method
Bool https://crystal-lang.org/api/0.32.1/Bool.html 3. 文法2 入力と繰り返し (2020/1/18)
3.1. 入力
print "enter a:\n"
a=gets.to_s.to_i
print "enter b:\n"
b=gets.to_s.to_i
print a+b, "\n"
これに 2, 4 という入力を与えた結果は以下のようになります。キーボードからの
入力としては enter a: がでてから2をいれてリターン、次に
enter b: がでてから4をいれてリターン、だと思いますが以下の例は入力が
先にでています。なお、 crystal foo.cr と、 run を省略しても run があるのと同じになるようです。
gravity> crystal add-with-input.cr
2
4
enter a:
enter b:
6
gets.to_s.to_i という大変謎な表現がでてくるので、が何か、というのをみ
ていきます。
String|Nil
と表現し、特に、基本的にある形 Foo なんだけど Nil になれる、ということ
を
Foo?
と表現します。従って、関数 gets の型は String ではなく String? である、
ということになります。こういう仕掛けにしておくことで、文字列が返ってこ
なかった時の処理をプログラム側で容易に記述できることになります。
3.1.1. 関数と「メソッド」
3.2. 入力と繰り返し
C++: a<<cin;
Fortran: read(*,*) a
ですむわけで、こっちのほうが簡単です。
1
2
3
4
5
6
7
8
9
10
この合計を計算し、出力するプログラムを考えてみます。普通の言語での考え
方は、
となります。以下はそれを素直に表現したものです。
sum=0
while s=gets
sum += s.to_i
end
print "sum=#{sum}\n"
実行結果は
gravity> crystal sum.cr < sum-sample.in
sum=55
"<" は「リダイレクト」で、標準入力を、キーボードから入力する代わりに
ファイルから読むようにします。
以下順番に解説します。
3.2.1. While と条件式
while 条件式
色々処理
end
の形で、まず条件式が真であれば「色々処理」の部分を実行、また条件式を評
価して、、、を、条件式が偽になるまで繰り返すものです。では「真」とか
「偽」は何か、ということですが、 Crystal の文法では
です。Bool型は true と false の2つの値をとる型で、通常の論理演算を行う
ことができます。
== 比較演算子。一致していれば真
!= 比較演算子。一致していなければ真
^ 排他的論理和
| 論理和
& 論理積
! 否定
なお、 C の影響を受けた多くの言語と同様、 &, | と &&, || があり、前者
は「ビット毎の論理演算」、後者は「真か偽か」を返すものですが、 &&, ||
はちょっと変わっていて、
&&: 左辺が偽でなければ、右辺の値、左辺が偽ならその値
||: 左辺が偽でなければ、左辺の値、左辺が偽なら右辺の値
となります。これは、左側から順番に評価して、そこをで値が決まったら右側はもう評価しない、という規則を明文化したものです。
while s=gets
色々
end
と書くと、
という処理をすることになります。
<
<=
>
>=
があり、常識的な意味になります。
3.2.1.1. While 以外の制御構造
3.2.1.2. if と unless
if 条件式1
実行部1
elseif 条件式2
実行部2
....
else
実行部n
end
の形で、条件式1が真なら実行部1を、条件式1が偽で条件式2が真なら実行部2
を、、、と elsif が沢山あれば順番にチェックしていって、全て偽なら
(else の部分があれば) 実行部2を実行します。
unless は、if の反対で
unless 条件式1
実行部1
else
実行部2
end
で、条件式1が偽なら実行部1を、真なら2を実行します。
a = x if x > 0
これは
if x > 0
a=x
end
と同じです。
3.2.1.3. until
until 条件式
色々
end
は
while !条件式
色々
end
と同じです。
3.2.2. +=
a += b
は
a = a + b
と同じ(+のところを任意の演算子として)です。なので、この列での
sum += s.to_i
は、 sum に s を整数に変換した値を加算する、となります。
3.2.3. 文字列の中の #{sum}
3.2.4. ここまでのまとめ
sum=0
while s=gets
sum += s.to_i
end
print "sum=#{sum}\n"
を見ると、これは
ということになるわけです。最初のサンプルでは、
a=gets.to_s.to_i
と、gets の結果が nil である場合を考慮する必要がありましたが、この繰り
返しの例では
sum += s.to_i
で、余計な to_s がないことに注意して下さい。これは、 while のところで
条件を評価しているので、ここでは s が niil でないことが「コンパイラに」
わかっていて、 s の型が String になっているからです。
3.3. 制御構造を使わないプログラミング
s=gets("")
a=s.to_s.split
aint = a.map{|x| x.to_i}
sum = aint.sum
print "sum=#{sum}\n"
これだと、いくつかの関数を呼んでいるだけで、 while も if もありません。
つまり、制御構造がないプログラムになっているわけです。
s=gets("")
は、おなじみの gets ですが、("") がついているのが今までと違います。
関数 gets は delimiter という引数をとることができて、それでどこまで
読むか、を指定していて、デフォルトは "\n" になっています。なので、
単に
gets
と書くと、1行読むのですが、そこで "" と長さ0の文字列を指定するとファイ
ル全体を一度に読むことができます。
[1, 2, 3]
といったもので、これは Int32 型の配列(型としては Array(Int32)) となり
ます。
a= [1, 2, 3]
とすれば、 a[0], a[1], a[2] がそれぞれ 1, 2, 3 です。C言語風に配列の添字は0からです。例えば、
"1 2 3".split
の実行結果は
["1", "2", "3"]
ということになります。プログラムの例では、 a は
["1", "2", "3", "4", "5", "6", "7", "8", "9", "10"]
となるでしょう。次の
a.map{|x| x.to_i}
で、map は、配列 a の各要素に {} 内の操作をした新しい配列を作ってそれ
を返すメソッドです。{}内では ||の中、この場合では x が、元の配列の要素の値になり、
その後の部分で x を使って色々操作をした最後の文の値が新しい配列の対応
する要素の値になります。なので、 aint は
[1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
になるわけです。
sum = gets("").to_s.split.map{|x| x.to_i}.sum
print "sum=#{sum}\n"
これでは、ファイルを全体呼んで(gets)文字列に強制的にして(to_s)要素毎に
配列にして(split)、各要素を整数にして(map{|x| x.to_i})
合計する(sum)のを、全て文字列全体や配列全体へのメソッドの適用の形で
実現していて、余計な変数等もなくて意味も明瞭ではないかと思います。
3.4. もう少し複雑な例: 表の読み込みと処理
9 26 14 74 3 82 86 75 82 92
28 14 48 32 90 78 20 53 68 21
53 3 44 92 84 98 0 16 38 98
79 79 5 76 51 2 70 83 14 54
16 46 53 42 64 24 49 99 46 84
36 49 40 68 59 9 6 53 74 13
4 98 6 7 49 38 18 75 62 66
84 8 25 12 16 39 18 34 51 34
4 46 98 40 37 39 27 33 93 58
50 78 76 70 30 29 78 8 87 17
24 83 64 83 38 16 15 0 39 18
50 80 98 86 52 40 1 60 13 52
78 80 31 84 39 64 74 30 45 11
11 84 84 49 83 58 17 54 59 26
34 7 50 60 35 11 85 15 89 51
65 11 10 10 98 69 77 9 18 66
50 26 59 57 27 36 80 74 35 68
3 42 40 84 81 34 30 70 86 76
98 88 67 27 63 88 45 74 65 82
16 59 5 73 58 72 6 5 36 65
このようなデータは色々なところで現れます。常微分方程式の数値解なら1行
がある時刻での解、というファイルかもしれないし、多数の粒子を使ったシミュ
レーションや格子を使ったシミュレーションの結果ならある時刻での各粒子や
格子での値が1行にはいっているでしょう。
上でやったように、ファイル全体を読み込んでから処理、という方法を考えて
みます。そうすると、まずは
gets("").to_s
ですね。これを行毎に分割するのは split("\n") でできて、これで1行が要素
になった配列ができます。さらに、その行毎に、先ほどと同じ
split.map{|x| x.to_i} を行えば、各行が整数の配列にかわります。つまり
gets("").to_s.split("\n").map{|s| s.split.map{|x| x.to_i}}
でよさそうです。ところが、以下を実行してみると
a=gets("").to_s.split("\n").map{|s| s.split.map{|x| x.to_i}}
print a.size, "\n"
21 という答になって、余計なものがはいっていることがわかります。これは、
ファイルの最後の文字が "\n"で、 split したので、最後に「何もない行」が
要素としてできてしまったからです。これを防ぐには
a=gets("").to_s.chomp.split("\n").map{|s| s.split.map{|x| x.to_i}}
と、文字列にしたあとで chomp というメソッドで、最後の "\n" を取り除き
ます。
a=gets("").to_s.chomp.split("\n").map{|s| s.split.map{|x| x.to_i}}
a.each{|x| p x}
を実行してみると
[9, 26, 14, 74, 3, 82, 86, 75, 82, 92]
[28, 14, 48, 32, 90, 78, 20, 53, 68, 21]
[53, 3, 44, 92, 84, 98, 0, 16, 38, 98]
[79, 79, 5, 76, 51, 2, 70, 83, 14, 54]
[16, 46, 53, 42, 64, 24, 49, 99, 46, 84]
[36, 49, 40, 68, 59, 9, 6, 53, 74, 13]
[4, 98, 6, 7, 49, 38, 18, 75, 62, 66]
[84, 8, 25, 12, 16, 39, 18, 34, 51, 34]
[4, 46, 98, 40, 37, 39, 27, 33, 93, 58]
[50, 78, 76, 70, 30, 29, 78, 8, 87, 17]
[24, 83, 64, 83, 38, 16, 15, 0, 39, 18]
[50, 80, 98, 86, 52, 40, 1, 60, 13, 52]
[78, 80, 31, 84, 39, 64, 74, 30, 45, 11]
[11, 84, 84, 49, 83, 58, 17, 54, 59, 26]
[34, 7, 50, 60, 35, 11, 85, 15, 89, 51]
[65, 11, 10, 10, 98, 69, 77, 9, 18, 66]
[50, 26, 59, 57, 27, 36, 80, 74, 35, 68]
[3, 42, 40, 84, 81, 34, 30, 70, 86, 76]
[98, 88, 67, 27, 63, 88, 45, 74, 65, 82]
[16, 59, 5, 73, 58, 72, 6, 5, 36, 65]
という感じの出力になるはずです。ここで each は map に似ていますが、
単に {} の中を実行するだけで新しい配列を作らないものです。 p は、適当
なフォーマットで出力する、という割合便利な関数で、上のように配列だと[]
の中で各要素を 「,」で区切って出力してくれます。
a=gets("").to_s.chomp.split("\n").map{|s| s.split.map{|x| x.to_i}}
a.each{|x| print x.sum,"\n"}
となりますね。もちろん、
gets("").to_s.chomp.split("\n").map{|s| s.split.map{|x| x.to_i}}.each{|x| print x.sum,"\n"}
でも同じです。 「.]の前や後で改行して
gets("").to_s.chomp.split("\n")
.map{|s| s.split.map{|x| x.to_i}}.each{|x| print x.sum,"\n"}
でも大丈夫です。結果は
gravity> crystal print_line_sum.cr < 20x10table.in
543
452
526
513
523
407
423
321
475
523
380
532
536
525
437
433
512
546
697
395
となるはずです。
a=gets("").to_s.chomp.split("\n").map{|s| s.split.map{|x| x.to_i}}
sum=Array.new(a[0].size,0)
a.each{|x| x.each_index{|i| sum[i]+=x[i]}}
p sum
こんな感じです。実行結果は
gravity> crystal print_column_sum.cr < 20x10table.in
[792, 1007, 917, 1126, 1057, 926, 802, 920, 1100, 1052]
です。ここで、 Array.new(size, value) は、 全要素の値が value で要素数
が size の配列を作ります。配列 a に対して a.size は要素数です。
この new は、インスタンスメソッドではありません。 Array はクラスそのものであっ
て、その型の変数ではないからです。なので、new は「クラスメソッド」の例
になり、そのクラスの変数を新しく作るメソッド、ということになります。
ないところから新しく作るので、インスタンスメソッドではできないわけです。
a=gets("").to_s.chomp.split("\n").map{|s| s.split.map{|x| x.to_i}}
p a.reduce{|sum,x| sum=sum.map_with_index{|val,i| val+x[i]}}
ここで a.reduce{|sum,x| 何か} は、
となって、これの値は最後に実行された何かの値です。
sum.map_with_index{|val,i| val+x[i]}}
のほうは、 sum の各要素について、その対応する添字も使って新しい値を計
算し、それがはいった配列を作ります。なので、この場合は、sum の各
要素が sum[i]+x[i] で置き換えることになり、これを各行について実行する
ことで合計が求まる、ということになります。
sum=0
gets("").to_s.chomp.split("\n").map{|s| s.split.map{|x| x.to_i}}
.each{|x| localsum=x[3..5].sum
print localsum,"\n"
sum+= localsum}
print "Total=", sum, "\n"
ですが、合計に sum を使うなら、
sum=gets("").to_s.chomp.split("\n").map{|s| s.split.map{|x| x.to_i}}
.map{|x| localsum=x[3..5].sum
print localsum,"\n"
localsum}.sum
print "Total=", sum, "\n"
です。これではあまり簡単になってないですが、各行での和は書かないなら
sum=gets("").to_s.chomp.split("\n").map{|s| s.split.map{|x| x.to_i}}
.map{|x| x[3..5].sum}.sum
print "Total=", sum, "\n"
と簡単になります。ここで x[3..5] は配列xの(最初を0として 3番目から5番
目の要素からなる配列です。もちろん、x[3..5].sum の代わりに
x[3]+x[4]+x[5] でも同じです。
3.5. まとめ
3.6. 課題
1 3
といった形で、1行で2つの数を入力する時、その合計を出力するプログラ
ムを作成して下さい。
科目1 科目2 科目3
太郎 90 80 70
花子 95 80 60
次郎 80 80 50
があったとして、各人の平均点(浮動小数点で)、各科目の平均点、全員、
全科目の平均点を計算するプログラムを作成して下さい。科目の数・人数が
この例とは違っても実行できるようにして下さい。。
9.000000e+00
16
0 0.0625 0.46109 -0.00650077 0.522333 0.259502 0.0840111 -0.148161
1 0.0625 1.26554 0.396113 0.0961424 0.876689 0.547619 -0.421987
2 0.0625 1.03187 0.488032 0.770863 0.0883532 0.29302 -0.181856
3 0.0625 -0.41411 -0.721194 -2.06982 0.0677951 -0.219516 -0.556578
4 0.0625 -1.78952 -0.427667 0.611626 -0.884486 -0.159703 -0.0363011
5 0.0625 -1.33761 -0.470841 -0.157951 -0.27971 -0.220079 -0.0240035
6 0.0625 -2.36992 -0.802938 -0.312251 -0.855476 -0.107062 0.054993
7 0.0625 1.04302 0.739473 0.0177287 0.276359 0.116819 0.366697
8 0.0625 0.821259 0.489359 -0.438738 0.759536 -0.628465 0.193383
9 0.0625 -0.183086 -0.638546 0.202722 0.25997 0.0713873 -0.420833
10 0.0625 1.3905 0.219563 -0.0124254 0.0101251 0.459834 -0.266435
11 0.0625 -0.182858 -0.686118 0.0301138 -0.733042 -0.498407 0.477569
12 0.0625 0.240498 0.472115 -0.288607 -0.141425 -0.0349425 0.229265
13 0.0625 -0.992641 0.553993 0.973543 -0.221324 -0.211653 0.422114
14 0.0625 1.18694 0.857442 -0.042799 0.423532 0.136452 0.210578
15 0.0625 -0.170962 -0.462286 0.097523 0.0936016 0.370684 0.101557
1行目は時刻、2行目は粒子数、その後に1行に1粒子で
粒子番号 質量 位置(x,y,z成分) 速度(x,y,z成分)
と8個の数字が並んでいます。このデータを読み込み、
を計算するプログラムを作成して下さい。
2行目の粒子数は使わなくてもかまいませんが、使うなら、nが整数であるとして
n.times{|i| 処理}
で「処理」を n 回繰り返す機能を利用して下さい。 i には、 0, 1, 2
... n-1 が順番にはいります。
3.7. 参考
Previous | ToC | Next |