Previous ToC Next

4. 文法3 クラスとメソッド (2020/1/24)

Crystal による数値計算入門4回目です。 前章では、配列や、配列全体に対する操作( each や map) を使って、入力デー タに対て色々な処理をすることを学びました。 これで、割合色々なことができるようになっていて、エクセルのデータを CSV 形式で出力したものがあれば、それから新しい表を作るとか集計するとかも ここまでの知識の応用でできます。

本章では、本格的な数値計算、特に常微分方程式の数値計算に入る前に、いく つかの言語の機能、特に class と struct をみておくことにします。

4.1. class の例:基本的な3次元ベクトル

多くの「オブジェクト指向」の概念を取り入れた言語と同じように、 Crystal ではプログラムの中で新しい型を定義することができます。 数値計算でよく使う、3次元ベクトルを表す型を作ることを考えます。 そうすると、数学で普通に使うような演算ができると便利です。

つまり、 a と b が、新しく作ったベクトル型の変数だとして、

  a+b
みたいに書きたいわけです。「+」をメソッドにできると、

 a.+(b)
とは書けるわけですが、まだ a+b とは書けません。Crystal では(というか、大抵の言語で) a+bと書け るように、「演算子」になる文字ないし文字列を決めています。一覧は
文法をみていただくとして、 +,-,*,/といったところを「定義」するこ とができます。これは a の型に対してメソッド + を定義すると、 a+b を a.+(b) というふうに言語側で解釈します、ということです。

以下は、完全な機能を与えてはいませんが加算だけ定義された 3次元ベクトル型と、それのテスト計算の例です。

 class Vector3
   property :x, :y, :z
   def initialize(x : Float64,  y : Float64,  z : Float64)
     @x=x; @y=y; @z=z
   end
   def +(a)
     Vector3.new(@x+a.x, @y+a.y, @z+a.z)
   end
 end
 x=Vector3.new(1,2,3)
 p x
 y=Vector3.new(1,1,1)
 p y
 z=x+y
 p z
以下は実行結果です。

 gravity> crystal minimalvector3.cr
 #<Vector3:0x7f9001313f90 @x=1.0, @y=2.0, @z=3.0>
 #<Vector3:0x7f9001313f60 @x=1.0, @y=1.0, @z=1.0>
 #<Vector3:0x7f9001313f30 @x=2.0, @y=3.0, @z=4.0>
(1,2,3) というベクトルを変数 xに、 (1,1,1) を y にいれて、 z=x+y を計 算し、それぞれを p で出力しています。

実行結果ですが、ベクトルが配列の時のように [1,2,3] とでるのではなくて、

 #<Vector3:0x7fd5b93e1f60 @x=1.0, @y=2.0, @z=3.0>
とちょっと煩雑な形式で出力されているのがわかります。これは、 型名である Vector3、そのアドレスのあと、このベクトルクラスが 中でもっている変数名とその値が @x=1.0 といった形で出力されます。

さて、コードのほうをみていきましょう。クラスの定義は

 class Foo
   色々
 end
ですが、まずそれを使うところをみます。最初は

 x=Vector3.new(1,2,3)
です。これは、Vector3 というクラスそのものの new というメソッドを呼んで います。Vector3 クラスの変数に対するメソッドではないことに注意して下さ い。これは前にも書いた(と思います、、、)クラスメソッドで、一番よく使う のがこの new です。 new は、こちらで定義する必要はないのですが、 呼ばれると、その中でインスタンスメソッドである initialize が呼ばれます。 ここで呼ばれる initialize メソッドが

  def initialize(x : Float64,  y : Float64,  z : Float64)
    @x=x; @y=y; @z=z
  end
で定義されているものです。メソッド(関数)定義は

 def 名前(引数リスト)
   色々
 end
という形です。インデントはみやすくする以上の意味はありません。改行は 実行文の終わり等を示すのに必要な場合があります。逆に、 1行に複数の実行文を書くには

    @x=x; @y=y; @z=z
のようにセミコロンで区切ります。引数リストは

  (x : Float64,  y : Float64,  z : Float64)
のように、(名前 [: 型名], ...) と、名前のあとオプショナルに型名をつけ たものを、複数ならコンマで区切って並べたものです。これを括弧の中に書き ます。

この initiallize の場合は、 x, y, z の3つの数字を受け取り、それらは いずれも64ビット浮動小数点である、と宣言しているわけです。 次の代入で @x といった 「@」がついた名前がでてきますが、 これは「インスタンス変数」と呼ばれるもので、あるクラスの変数が その内部にもっている変数、ということになります。この場合、

  x=Vector3.new(1,2,3)
を実行すると、Vector3 クラスの変数 x が作られて、それに対して

  x.initialize(1,2,3)
が呼ばれて、内部変数 @x, @y, @z にそれぞれ引数からわたってきた 1, 2, 3 が代入されることになります。

少し細かいことですが、メソッドのほうでは x: Float64 と浮動小数点数がく るとされているのに整数が渡されますが、このような場合には、演算の場合と 同じように整数を勝手に浮動小数点数に変換します。その結果、

 p x
で出力すると
 #<Vector3:0x7fd5b93e1f60 @x=1.0, @y=2.0, @z=3.0>
とでることでわかるように、 x は内部に@x=1.0, @y=2.0, @z=3.0 という値を 持つことになります。

y も同様に、 代入の結果(1,0, 1.0, 1.0) という値をもちます。

 z=x+y
は、既に書いたように x.+(y) が呼ばれ、それを定義しているのが

  def +(a)
    Vector3.new(@x+a.x, @y+a.y, @z+a.z)
  end
です。これは、自分については @x, @y, @zで値を取り出し、 メソッドの引数である a については a.x, a.y, a.z という形で 値を取り出して、それぞれの和を使って新しいベクトルを作ります。 Crystal では、メソッドの最後の文の値がその関数の値になるので、 この新しいベクトルが + 演算の結果ということになります。そういうわけで、
 z には各要素の和がはいって、 (2.0,3.0,4.0)  となります。
なお、class Vector3 の下にある

  property :x, :y, :z
は、上の a.x, a.y, a.z といった形で、ある型の変数の内部の変数(インスタ ンス変数)を、@x といった形でメソッドの中でだけでなく、「外から」アクセ スするtことを許す、という宣言です。 :x という形は「シンボル」というも ので、Crystal では色々なところで変数や関数の名前そのものではなく ":" がついた形を使います。Ruby ではこれは実装の効率の観点で意味があったの ですが、Crystal では本当はいらなくて文字列でよいのでは、という気もしま すが、文法としてはシンボルがあります。これは実際に Symbol というクラス がある、ということになります。

4.2. struct と本格的なベクトル型

以下は、普通に使う演算が一通り定義された Vector3 クラスです。

 struct Vector3
   include YAML::Serializable
   @x : Float64 = 0.0
   @y : Float64 = 0.0
   @z : Float64 = 0.0
   property :x, :y, :z
   def initialize(x : Float64 =0,  y : Float64 =0,  z : Float64 =0)
     @x=x; @y=y; @z=z
   end
 
   def +(a) Vector3.new(@x+a.x, @y+a.y, @z+a.z) end
   def -(a) Vector3.new(@x-a.x, @y-a.y, @z-a.z) end
   def -()  Vector3.new(-@x, -@y, -@z)  end
   def +()  self  end
   def *(a : Vector3) @x*a.x+ @y*a.y+ @z*a.z end    # inner product
   def *(a : Float) Vector3.new(@x*a, @y*a, @z*a)  end
   def /(a : Float) Vector3.new(@x/a, @y/a, @z/a)  end
   def cross(other)                   # outer product
     Vector3.new(@y*other.z - @z*other.y,
                @z*other.x - @x*other.z,
                @x*other.y - @y*other.x)
   end
   def sqr() self*self end
   def to_a() [@x, @y, @z] end
   macro method_missing(call)
     to_a.{{call}}
   end
   def self.zero()
     Vector3.new
   end
   def to_a()
     [@x, @y, @z]
   end
   
 end
 
 class Array
   def to_v() Vector3.new(self[0],self[1],self[2])  end
 end
 
 struct Float
   def *(a : Vector3) a*self end
 end
class ではなく struct にしているのは、Crystal の Ruby との違いの1つです。 struct は class と全く同じように使えるのですが、この Vector3 のような、 メソッドの中身が単純な計算が中心である場合にはより効率的なプログラムになります。 以下、変更・追加されたメソッドをみていきます。まず、initialize ですが、 引数の宣言が変わっています。

  def initialize(x : Float64 =0,  y : Float64 =0,  z : Float64 =0)
このように、 =0 といった形で値を与えることで、デフォルト値、つまり、省略した時の値 を決めておくことができます。なので、 Vector.new はVector.new(0,0,0)と、 また、 Vector.new(1,1) はVector.new(1,1,0) と同じです。引数の数が足り ないと後ろから 順番に省略されているとみなされます。

さて、 z だけに値をいれて、 x, y はデフォルト値のままにしたい、と思っ たらどうすればいいでしょうか? 引数の名前を指定して値を設定する 文法があり、例えば Vector.new(z:1) は Vector.new(0,0,1) と同じです。

  def +(a) Vector3.new(@x+a.x, @y+a.y, @z+a.z) end
は、

  def +(a)
    Vector3.new(@x+a.x, @y+a.y, @z+a.z)
  end
を1行にしただけです。Crystal ではどこに改行が必要でどこにはなくていい か、は結構ややこしいですが、メソッド定義の本体が関数呼び出し1つとか、 引数リストの括弧があればこんなふうに1行にもできます。

   def -()  Vector3.new(-@x, -@y, -@z)  end
は、「単項演算子」である「-」、つまり、 a= -b といった式で現れる「-」 を定義します。

   def +()  self  end
も同様です。ここででてくる self は「自分自身」です。単項演算子 + は何 もしないで自分自身を値として返すわけです。 なお、これらは

   def +
     self
   end
と書くこともでき、改行があれば引数がないことを示す () を省略できます。

Vector3 同士の * は内積、Float との積はスカラー 倍、除算は各要素を割る、とし、時々使うので外積を cross という名前で定 義します。sqr は2乗で、これは * を使って定義しています。

なお、1行の中で 「#」からあとはコメントになって、コンパイラからは無視 されます。

ここで、 +(a) 等では a の型が指定されていないことに注意して下さい。 中身で a.x 等を使うので、a は x,y,z が property にあるかあるいはそういうメ ソッドがある型である必要があります。逆にいうと、そうであれば Vector3 でなくてもかまいません。

C++ でも同じようなことはできるのですが、クラステンプレート、関数テンプ レートといったものを使うかなり複雑な記法が必要になります。その辺を より簡潔にするような改良が導入されていますが、どうしても屋上屋を架す感 はあり、今までよりはよいが他の言語に比べるとわかりづらいものになってい るように思います。

Fortranでは現在のところテンプレート自体が導入されておらず、現代的な プログラムを書く上での大きな制約になっています。

次の to_a は、Vector3 型を Array 型に変換します。a が Vector3 だとして、 a.to_a とすると Array になるので、 Array に対して定義されたあらゆるメ ソッドが使えるようになります。

その次の

  macro method_missing(call)
    to_a.{{call}}
  end
は、特別な Hook と呼ばれるものの1つで、例えば a.map{|x| ...} というふ うに、 Vector3 に定義されてないメソッドを使おうとした時のコンパイラの 動作を書きます。これが

    to_a.{{call}}
になっている、ということは、「to_aでArrayにしてからそのメソッドを適用 せよ」ということになり、この場合は a.to_a.map{|x| ...} というコードを コンパイルすることになってめでたしめでたしとなるわけです。もちろん、 Array にもないメソッドであればコンパイルエラーになります。

次の

 class Array
   def to_v() Vector3.new(self[0],self[1],self[2])  end
 end
 
では、元々 Crystal にある Array クラスに to_v という Vector3 に変換す るメソッドを追加します。

最後の

 struct Float
   def *(a : Vector3) a*self end
 end
は、浮動小数点数 * Vector3 の演算を定義しています。 * が交換法則を、な んてことはコンパイラは知らないので、スカラー*ベクトルと ベクトル*スカラーは別に(といっても前者が後者を呼ぶだけですが)書いてお く必要があります。これらのように、すでにあるクラスに自分で定義したメソッ ドを追加できることは、わかりやすいプログラムを書くために非常に有用です。

さて、このプログラムを例えば vector3.cr という名前でもっていたとして、 色々なプログラムでベクトル型を使いたい、ということがあります。それには、もちろん それぞれのプログラムの中でこの型の定義をすればいいですが、そうすると同じも ののコピーが大量に発生します。また、他の人が使う、という時にコピペでは、 修正とか改良した時に全ての人が自分のそれを使っている全てのプログラムを 修正しないといけなくなります。ければならない。そのような無駄を防ぐのが、 プログラムの中で他のプログラムを読込み機能です。

 require "./vector3.cr"
 Vector=Vector3
 a=Vector.new(1,2,3)
 b=Vector.new(1,1,1)
 c=Vector.new(2,1)
 d=Vector.new(y:1)
 
 p a+b+c+d, a*b, c*d
 p! a+b+c+d, a*b, c*d
 
の最初の行のように、

 require "./vector3.cr"
と書くことで、そこで vector3.cr の中身を読み込んでコンパイラに渡すこと ができます。これを実行すると

 gravity> crystal testvector.cr
 [2mShowing last frame. Use --error-trace for full trace.[0m
 
 In [4mvector3.cr:2:11[0m
 
 [2m 2 | [0m[1minclude YAML::Serializable[0m
              [32;1m^-----------------[0m
 [33;1mError: undefined constant YAML::Serializable[0m
です。 p の他、 p! も便利な機能で、こちらは値だけでなくて元のプログラ ムの式自体も出力してくれます。

4.3. まとめ

本章では、Crystal の文法と機能について、以下を学びました。

4.4. 課題

  1. 上の、一応色々な定義したベクトルクラスについて、その全ての機能を テストして結果が正しいことを確認するプログラムを作って下さい。

4.5. 参考

Struct https://crystal-lang.org/reference/syntax_and_semantics/structs.html
Previous ToC Next