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 の文法と機能について、以下を学びました。
-
class, sturuct の定義のしかた
-
メソッドの定義のしかた
-
あるクラスの変数を新しく作って返すクラスメソッド new と、その時に使
われる initialize の関係
-
メソッド定義での引数の書き方、デフォルト値
-
演算子 (+とか)として使えるメソッドの定義
-
クラス変数の「中の」変数へのアクセス方法
-
コメント
-
引数の型を決めていない関数の書き方
-
「メソッドがない」時の処理の定義
-
既存のクラスへのメソッドの追加
4.4. 課題
-
上の、一応色々な定義したベクトルクラスについて、その全ての機能を
テストして結果が正しいことを確認するプログラムを作って下さい。
4.5. 参考
Struct https://crystal-lang.org/reference/syntax_and_semantics/structs.html