コンテンツにスキップ

Crystal

出典: フリー教科書『ウィキブックス(Wikibooks)』
Wikipedia
Wikipedia
ウィキペディアCrystal (プログラミング言語)の記事があります。

本書は、Crystalのチュートリアルです。 Crystalは、Ary Borenszweig、Juan Wajnerman、Brian Cardiffと300人以上の貢献者によって設計・開発された汎用オブジェクト指向プログラミング言語です[1]Ruby にヒントを得た構文を持ち、静的型付けコンパイル型言語ですが、変数やメソッドの引数の型は一般には不要です。型は高度なグローバル型推論アルゴリズムによって解決されます。[2]CrystalはApache Licenseバージョン2.0のもと、FOSSとしてリリースされています。

特徴

[編集]

Crystalは、Rubyのシンタックスを持つ静的型付けのプログラミング言語で、以下のような特徴があります。

  • コンパイル型言語:Crystalはコンパイル型言語であり、コンパイルによって実行可能なバイナリが生成されます。これにより、実行速度が速く、静的型付けによって安全性が高まります。
  • オブジェクト指向:Crystalはオブジェクト指向のプログラミング言語であり、クラスや継承などのオブジェクト指向の機能をサポートしています。
  • Rubyとの互換性:CrystalはRubyのシンタックスに似ているため、Ruby開発者にとっては比較的簡単に学習することができます。また、CrystalのコードはRubyのコードに変換できるため、Rubyのライブラリを利用することができます。
  • 静的型付け:Crystalは静的型付け言語であり、型エラーをコンパイル時に検出することができます。これにより、実行時にエラーが発生する可能性を減らし、コードの品質を向上させます。
  • 並列処理:Crystalは、並列処理をサポートするために、Green Threadsと呼ばれる軽量スレッドを利用しています。これにより、CPUのコアを最大限に活用し、処理速度を高めることができます。
  • メタプログラミング:Crystalは、メタプログラミングをサポートしています。これにより、コード生成や動的な型変換など、より高度なプログラミング技術を利用することができます。
  • C言語とのインタフェース:Crystalは、C言語とのインタフェースをサポートしています。これにより、既存のC言語のライブラリを利用することができ、既存のシステムとの連携が容易になります。

Hello, World!

[編集]

お約束のHello_worldですが、ここではRubyとの比較も兼ねて、Ruby#Hello, World!をそのまま実行してみます。

hello.crを用意します[3]

hello.cr
puts 'Hello, World!'
コマンドラインでの操作
% cat hello.cr 
puts 'Hello, World!'
% crystal hello.cr
In hello.cr:1:6

 1 | puts 'Hello, World!'
          ^
Error: unterminated char literal, use double quotes for strings

% sed -i -e "s@'@Q@g" -e 's@Q@"@g' hello.cr
% cat hello.cr 
puts "Hello, World!"
% crystal hello.cr
Hello, World!
Crystalでは、文字列の場合は二重引用符(")を使用するので、' を " に sed で置換えました。
修正後の hello.cr も問題なく ruby で実行できます。

特徴

[編集]
コンパイル型言語
1つまたは複数のソースコードをコンパイルして実行ファイルを生成し、生成した実行ファイルを実行します。
静的型付け
値・変数・関数のパラメーター・関数の戻値などの型はコンパイル時に検証され、型安全性が担保されます。
例外
begin / rescue / else / ensure / end 形の例外処理をサポートします。
演算子オーバーロード
サポートします。
メソッドオーバーロード
同じ名前で引数の数が異なるメソッドを定義することが可能です。
型推論
サポートします。
ガベージコレクション
サポートします。
クラス
クラスベースのオブジェクト指向言語です。
全てがオブジェクト
Javaで言うプリミティブもオブジェクトで、メソッドを持ちます。
祖先クラス
Object
コンストラクター
initalize() と名付けられたメソッドがインスタンス化に使われます。
デストラクター
ありません。
継承
単一継承をサポートします。
抽象クラス
JavaGo の interface や Swift の protocol はありませんが、abstract class があります。
Mix-in
include 文でサポートします。
名前空間
モジュールが名前空間を持ちます。
defer
提案されましたが採用されませんでした[4]。例えば、File.open() で開いたファイルの始末は、File.open(){…}とブロックを渡すことで、File.open()の終了とともに close() される Ruby 流のモデルを使えるので、不要とのことです。また、deferを採用するとerrdferはいらないのかと連鎖的に問題が広がる懸念があります(従来のコードとの意味論の整合性も)。
制御構造は式
if, while の制御構文は式です。
文末の;(セミコロン)
必要ありません。

コード・ギャラリー

[編集]

実際にCrystalで書かれたのコードを見てみましょう。

ファイルの読出し(1)

[編集]

Crystalを使ってファイルを読み込むには、Fileクラスを使用します。

以下は、Crystalでファイルを読み込む例です。

file = File.open("filename.txt")
contents = file.gets_to_end
puts contents
file.close
  1. File.open メソッドを使用して、ファイルを開きます。引数にファイルのパスを指定します。これにより、 file 変数にファイルのインスタンスが格納されます。
  2. gets_to_end メソッドを使用して、ファイルの内容を読み込みます。これにより、 contents 変数にファイルの内容が格納されます。
  3. puts メソッドを使用して、ファイルの内容を標準出力に出力します。
  4. ファイルを開く際に File.open メソッドを使用した場合は、 File.close メソッドを使用してファイルを閉じる必要があります。

ファイルの読出し(2)

[編集]

File.openにブロックを渡すと、ブロックを抜けると自動的にファイルが閉じられます。明示的なファイルのクローズが不要です。

File.open("filename.txt") { |file|
  contents = file.gets_to_end
  puts contents
}
  1. File.openメソッド:
    • File.open("filename.txt")は、指定されたファイルを開きます。このメソッドはブロックを取り、ブロック内でファイルを操作した後、自動的にファイルを閉じます。ブロックの終了時にファイルが閉じられるため、明示的なファイルのクローズが不要です。
  2. ファイル操作:
    • |file|の部分は、ブロックパラメータで、この場合はfileという名前の変数がファイルオブジェクトを表します。
    • file.gets_to_endは、ファイルから現在の位置から末尾までの内容を読み込みます。ファイル全体を読み込むことができるメソッドです。
    • puts contentsは、読み込んだ内容を出力します。

このコードは、ファイルを開いて内容を読み込む際にブロックを使用し、ブロックを抜けるとファイルが自動的に閉じられるというCrystalの一般的なイディオムを示しています。 この方法は、ファイルの操作後にクローズ処理を忘れることを防ぎ、コードをより安全かつ簡潔にします。

エラトステネスの篩

[編集]

エラトステネスの篩を、若干 Crystal らしく書いてみました。

エラトステネスの篩
def eratosthenes(n)
  sieve = [] of Bool
  n.times do |i| sieve.push(i >= 2) end
  n.times do |i|
    next unless sieve[i]
 
    (i * i).step(to: n, by: i, exclusive: true) do |j|
      sieve[j] = false
    end

    break if i * i >= n
  end
  n.times do |i|
    print("#{i} ") if sieve[i]
  end
end
 
eratosthenes(1000)
実行結果
2 3 5 7 11 13 17 19 23 29 31 37 41 43 47 53 59 61 67 71 73 79 83 89 97 101 103 107 109 113 127 131 137 139 149 151 157 163 167 173 179 181 191 193 197 199 211 223 227 229 233 239 241 251 257 263 269 271 277 281 283 293 307 311 313 317 331 337 347 349 353 359 367 373 379 383 389 397 401 409 419 421 431 433 439 443 449 457 461 463 467 479 487 491 499 503 509 521 523 541 547 557 563 569 571 577 587 593 599 601 607 613 617 619 631 641 643 647 653 659 661 673 677 683 691 701 709 719 727 733 739 743 751 757 761 769 773 787 797 809 811 821 823 827 829 839 853 857 859 863 877 881 883 887 907 911 919 929 937 941 947 953 967 971 977 983 991 997

プログラミング環境

[編集]

Crystalのプログラムを作り、コンパイル・実装するには、「オンライン実行環境を使う」・「エディト・コンパイル・実行環境を用意してそれを使う」の2通りの方法があります。

オンライン実行環境

[編集]

公式のオンライン実行環境、 https://play.crystal-lang.org/ があります。 まずは、これを使って本書に例示されているコードを実行してみることをお勧めします。

エディト・コンパイル・実行環境

[編集]

エディタについては本書では触れませんが、プログラミング時間の大半はエディタの操作に費やされるため、良いエディタを選択することが重要です。

Crystal の言語処理系は、 https://crystal-lang.org/install/ から入手します。

自分の、OSやGNU/Linuxであればディストリビューションに合わせてインストールしてください。 また、FreeBSDのように crystal と shards が別パッケージとなっていることもあるので、その場合は shards も追加インストールします。

ソースコードからのビルド

[編集]

多くの場合、インストールされた crystal はスタティック リンクされているので、ダイナミック リンク版の crystal を入手するには、ソースコードからビルドします。 また、interactive Crystalを有効にするためにも、ソースコードからのビルドが必要です。

crystal は、ソースコードが Github の https://github.com/crystal-lang/crystal.git で公開されているので、必要に応じてソースコードからビルドすることができます。 crystalは、セルフホスティング言語[5]なので、最初にバイナリーを入手してブートストラップするか、クロスビルドしたバイナリーを持込むか、パッケージシステムからインストールし、ターゲットでセルフコンパイル出来る状態を作る方法があります。

ビルドには、Chromebook(メモリー4GB, Celeron N4000, OS Version: reven-release/R104-14909.124.0, Chromebrew version: `1.25.3`, llvm-15.0.0)で約30分かかりました。

crystal コマンド

[編集]

Crystalは、Crystalプログラミング言語のソースコードをコンパイルし実行するためのコマンドラインツールです。主に、プログラムのビルド、コンパイル、実行に使用されます。

crystal コマンドは、Crystalプログラミング言語で書かれたソースコードをコンパイルして実行するためのツールで、crystal run ファイル名 を簡略化した形で、crystal ファイル名 としても利用できます。

通常、crystal run は指定されたCrystalプログラムを内部ビルドツールでコンパイルし、コンパイル後に即座に実行します。この動作は、インタープリタ的な実行とは異なり、ソースコードを最初にコンパイルしてから実行する仕組みです。

基本的な使い方
crystal ファイル名
このコマンドは、指定したCrystalプログラム(ファイル名.cr)をコンパイルし、その後即座に実行します。
crystal hello_world.cr
上記のコマンドは、hello_world.cr というCrystalソースファイルをコンパイルして実行します。
詳細
crystal コマンドは内部的にビルドツールを使用しており、実行の際にはコンパイルを一度行います。これにより、インタープリタのような逐次的な処理ではなく、事前にコンパイルされたバイナリが実行されるため、高速に動作します。コンパイル時には最適化や型推論など、Crystalのコンパイル時特有の利点が活かされます。
注意点
crystal runcrystal コマンドは、開発中にプログラムを素早く試す際には便利ですが、実際のデプロイメントや大規模なプロジェクトのビルドには、通常 crystal build を使用して別途コンパイルされたバイナリを使用することが推奨されます。

Ruby との違い

[編集]

Crystalは、Rubyに触発された構文を持つものの、Rubyとの互換性をゴールに定めてはいません。 このため、細部を見ると仕様に差異があり、Rubyのソースコードをcrystalに掛けても前節の 'Hello World' の様にコンパイルに失敗することがあります。 また、コンパイルできても実行結果に違いが出ることがあります。

ここでは、Ruby との違いについて実際のコードと双方の結果を比較することで、差異についての理解を深めていきます。

整数型の特性

[編集]
大きな整数
p 2 ** 999
p (2 ** 999).class
rubyの実行結果
5357543035931336604742125245300009052807024058527668037218751941851755255624680612465991894078479290637973364587765734125935726428461570217992288787349287401967283887412115492710537302531185570938977091076523237491790970633699383779582771973038531457285598238843271083830214915826312193418602834034688
Integer
crystalの実行結果
Unhandled exception: Arithmetic overflow (OverflowError)
  from /usr/local/share/crystal/share/crystal/src/int.cr:295:9 in '**'
  from pow.cr:1:1 in '__crystal_main'
  from /usr/local/share/crystal/share/crystal/src/crystal/main.cr:115:5 in 'main_user_code'
  from /usr/local/share/crystal/share/crystal/src/crystal/main.cr:101:7 in 'main'
  from /usr/local/share/crystal/share/crystal/src/crystal/main.cr:127:3 in 'main'
  from /usr/local/lib64/libc.so.6 in '__libc_start_main'
  from /usr/local/.cache/crystal/crystal-run-pow.tmp in '_start'
  from ???
Ruby の整数は、桁あふれが起こると自動的に多倍長整数に型変換されるので、継ぎ目なしに大きな数を扱うアルゴルズムが使えます。
Crystal の整数は、固定長です(大きさについては後述)。なので大きな答えになる式を評価すると桁あふれが生じます。桁あふれが生じますが、C言語のように寡黙に処理を続けるのではなく、実行時に例外(OverflowError)が上がるので、例外を捕捉し然るべき処置を施すことが可能です。

BigInt

[編集]

bigrequire すると BigInt が使えるようになります。

BigInt
require "big"

p BigInt.new(2) ** 999
p (BigInt.new(2) ** 999).class
実行結果
5357543035931336604742125245300009052807024058527668037218751941851755255624680612465991894078479290637973364587765734125935726428461570217992288787349287401967283887412115492710537302531185570938977091076523237491790970633699383779582771973038531457285598238843271083830214915826312193418602834034688
BigInt
BigIntはプリミティブではなので、リテラル表現はありません。また、
n : BigInt = 2
Error: type must be BigInt, not Int32
のように型アノテーションすることも出来ません。

リテラルと型

[編集]
様々なリテラルと型
[nil, false, true, 42, 2.73, 'Q', "string", [1,2,3], {a:1, b:2}].each{|x|
  p [x, x.class]
}
rubyの実行結果
[nil, NilClass]
[false, FalseClass]
[true, TrueClass]
[42, Integer]
[2.73, Float]
["Q", String]
["string", String]
[[1, 2, 3], Array] 
[{:a=>1, :b=>2}, Hash]
crystalの実行結果
[nil, Nil]
[false, Bool]
[true, Bool]
[42, Int32]
[2.73, Float64]
['Q', Char]
["string", String]
[[1, 2, 3], Array(Int32)] 
[{a: 1, b: 2}, NamedTuple(a: Int32, b: Int32)]
Crystal の整数は Int32、浮動小数点数は Float64 です。
サイズを指定した数リテラル
[1_i64, 2_u32, 3_u64, 4_i32, 5_i16, 6_u8, 7_i128, 8_u128, 3.14_f32, 1.44_f64].each{|x|
  p [x, x.class]
}
ruby
Rubyでは、サーフィックスの付いた数値リテラルは無効
crystalの実行結果
[1, Int64]
[2, UInt32]
[3, UInt64]
[4, Int32]
[5, Int16]
[6, UInt8]
[7, Int128]
[8, UInt128]
[3.14, Float32] 
[1.44, Float64]
Crystal では、数値リテラルに _ で始まるサーフィックスを付け { i:符号付き整数, u:符号なし整数, f:浮動小数点数 } と { 8,16,32,64,128 } のビット幅の組合せです[6]

Setは組込みクラス

[編集]

Crystalでは、Set(集合)は組込みクラスなので、require "set"は不要です。

集合の例
a = Set.new(10.times)
b = Set.new(5.times.map{|i|2*i})
p! a,
	b,
	a + b,
	a - b,
	a & b,
	a | b
	a ^ b
実行結果
a     # => Set{0, 1, 2, 3, 4, 5, 6, 7, 8, 9}
b     # => Set{0, 2, 4, 6, 8}
a + b # => Set{0, 1, 2, 3, 4, 5, 6, 7, 8, 9}
a - b # => Set{1, 3, 5, 7, 9}
a & b # => Set{0, 2, 4, 6, 8}
a | b # => Set{0, 1, 2, 3, 4, 5, 6, 7, 8, 9}

for式がない

[編集]

Crystal には、Ruby にはある for式がありません。

Rubyのfor式の構文
for 変数 in コレクション
  
end
コレクションは Range, Array, Hash など内部構造を持つオブジェクトです。
for式は、最後に評価した値を返すので、forです。
for式のeachメソッドによる置換え
for x in [ 2, 3, 5, 7, 11 ] do
  p x
end
# ↓
[ 2, 3, 5, 7, 11 ].each do | x |
  p x
end
の様にコレクションの each メソッドで置換え可能なので、Rubyからの移植でも小規模な書換えで済みます[7](後述のマクロで実装できないかと思いましたが、いまのところ無理のようです)。

また loop 式もありませんが while true; … end で間に合います。Ruby では while 式の条件の次に do が置けますが、Crystal では置けません。

自作のforメソッド

[編集]

Rubyのforに似せるという縛りがなければ、(マクロを使うまでもなく)簡単に実装できます。 偶然ですが、Scalaのforメソッドに似てしまいました(あれも、イテレーション メソッドに展開されるのである程度は必然)。Scalaと同じ様にジェネレターと組合わせて多次元に拡張することもできそうです。

自作のforメソッド
def for(collection)
  collection.each do |elm|
    yield(elm)
  end
end

for [1,2,3,4] do |x|
  p! x * x
end

for ({3,4,5,6}) do |x|
  p! x + x
end

for (1...999) do |x|
  p! x - 1
  break if x > 5
end

for (9999.times) do |x|
  p! x ** 3
  break if x > 7
end
実行結果
x * x # => 1
x * x # => 4
x * x # => 9
x * x # => 16
x + x # => 6
x + x # => 8
x + x # => 10
x + x # => 12
x - 1 # => 0
x - 1 # => 1
x - 1 # => 2
x - 1 # => 3
x - 1 # => 4
x - 1 # => 5
x ** 3 # => 0
x ** 3 # => 1
x ** 3 # => 8
x ** 3 # => 27
x ** 3 # => 64
x ** 3 # => 125
x ** 3 # => 216
x ** 3 # => 343
x ** 3 # => 512

eval()がない

[編集]

Crystal には eval() はありません。 Crystalはコンパイル型言語ですので、無理もないことです。 もし、Crystal で eval() を実装しようとすると、Common Lisp の様にインタープリターを丸ごとランタイムに含む必要があります。 これはリーズナブルな選択ではありません。 Crystal では、eval() が必要なケースに(限定的ですが)マクロを使うことで実現出来る可能性があります。

eachメソッドが値を返さない

[編集]

Rubyでは、Arrayなどのeachメソッドは self を返し、break で中断すると nil を返します。

そこで、このようなコードがかけます。

100以下の素数を求めるコード(Ruby)
primes = []
(2..100).each do |i|
  primes.push(i) if primes.each do |prime|
    break if i % prime == 0
  end
end
p primes
実行結果
[2, 3, 5, 7, 11, 13, 17, 19, 23, 29, 31, 37, 41, 43, 47, 53, 59, 61, 67, 71, 73, 79, 83, 89, 97]
素数集合(primes)から1つ素数(prime)を取出し、候補(i)をそれで割り、割切れたら break します。
break すると each は nil を返し、素数集合への候補の追加は行われません。

これに対し Crystal では、Arrayなどのeachメソッドは self を返さない(常に nil を返す)ので上記のようなトリッキーなコードはかけずフラッグを使います。

100以下の素数を求めるコード(Crystal)
primes = [] of Int64
(2..100).each do |i|
  flag = true
  primes.each do |prime|
    if i % prime == 0
      flag = false
      break
    end
  end
  primes.push(i) if flag
end
p primes

マクロがある

[編集]

Crystalには、Rubyにはないマクロがあります[8]。Rubyは実行時にすべてのオブジェクトにアクセス出来て、メソッド生やし放題なのでマクロは必要ありませんが、Crystalはコンパイル時に型やメソッドを確定する必要があり、特にメソッドジェネレターとしてのマクロにニーズがあります。また、テンプレート言語的なマクロなので、環境変数による条件分岐や、コンテナを渡し繰返し処理する構文もあります。


Rubyの文法とCrystalの文法の違い

[編集]

RubyとCrystalの主な文法の違いを表形式でまとめました。

文法 Ruby Crystal
変数宣言 x = 1 x : Int32 = 1
型指定 不要 必要(型推論で省略可能)
関数定義
def foo(x)
  x + 1
end
def foo(x : Int32)
  x + 1
end
クラス定義
class MyClass
  attr_accessor :name
  def initialize(name)
    @name = name
  end
end
class MyClass
  property name : String
  def initialize(@name : String)
  end
end
モジュール定義
module MyModule
  def foo
    "foo"
  end
end
module MyModule
  def foo
    "foo"
  end
end
if
if x == 1
  "x is 1"
elsif x == 2
  "x is 2"
else
  "x is neither 1 nor 2"
end
if x == 1
  "x is 1"
elsif x == 2
  "x is 2"
else
  "x is neither 1 nor 2"
end
unless
unless x == 1
  "x is not 1"
end
unless x == 1
  "x is not 1"
end
case
case x
when 1
  "x is 1"
when 2
  "x is 2"
else
  "x is neither 1 nor 2"
end
case x
when 1
  "x is 1"
when 2
  "x is 2"
else
  "x is neither 1 nor 2"
end
while
while x < 10 do
  puts x
  x += 1
end
while x < 10
  puts x
  x += 1
end
for
fruits = ["apple", "banana", "cherry"]

for fruit in fruits
  puts fruit
end
N/A(以下は代替実装)
fruits = ["apple", "banana", "cherry"]

fruits.each  do |fruit|
  puts fruit
end

キーワード

[編集]

Crystalのキーワード( keywords ) は、以下の通り。

abstract alias as asm begin break case class def do else elsif end ensure enum extend for fun if include instance_sizeof lib macro module next of out pointerof private protected rescue return require select sizeof struct super then type typeof uninitialized union unless until when while with yield

演算子

[編集]

Crystalは、1つ、2つ、または3つのオペランドを持つ数多くの演算子をサポートしています[9]

演算子式は、実際にはメソッド呼び出しとしてパースされます。例えば、a + ba.+(b) と意味的に同じで、引数 b を持つ a のメソッド + を呼び出すことになります。

演算子の優先度
種類 演算子
インデックス アクセサー [], []?
単項 +, &+, -, &-, !, ~
指数 **, &**
乗除 *, &*, /, //, %
加減 +, &+, -, &-
シフト <<, >>
ビット間 AND &
ビット間 OR/XOR |,^
等値 ==, !=, =~, !~, ===
比較 <, <=, >, >=, <=>
論理 AND &&
論理 OR ||
Range .., ...
条件 ?:
代入 =, []=, +=, &+=, -=, &-=, *=, &*=, /=, //=, %=, |=, &=,^=,**=,<<=,>>=, ||=, &&=
スプラット *, **

制御構造

[編集]

制御構造(せいぎょこうぞう、control flow)とは、「順次」「分岐」「反復」という基本的な処理のことを言います。

Crystalの真理値
制御構造は「条件式」が真であるか偽であるかによって分岐や反復の振る舞いが変わります。

では「条件式」が真・偽はどの様に決まるのでしょう?

Crystalでは false あるいは nil であると偽、それ以外が真です。

なので 0[](空のArray) も {}(空のNamedTuple)も真です。

条件分岐

[編集]

Crystalの条件分岐には、if, untilcaseの3つの構文があります。

if

[編集]

ifは条件式によって実行・否を切り替える構造構文で、評価した式の値を返すので条件演算子でもあります。

ifの例
a = 0.0 / 0.0

if a < 0
    puts "minus"
elsif a > 0
    puts "plus"
elsif a == 0
    puts "zero"
else
    puts a
end

p! (
    if a < 0
        "minus"
    elsif a > 0
        "plus"
    elsif a == 0
        "zero"
    else
        a
    end
)
実行結果
NaN 
(if a < 0 
 "minus" 
else 
 if a > 0 
   "plus" 
 else 
   if a == 0 
     "zero" 
   else 
     a 
   end 
 end 
end) # => NaN
elsif節
ifは、オプショナルな elsif 節を設け、条件式が偽であった時に別の条件に合致した処理を実行させることが出来ます。
else節
ifは、オプショナルな else 節を設け、条件式が偽であった時に処理を実行させることが出来ます。
ifは値を返すので、メソッドの実引数に使うことが出来ますし、代入演算の右辺にも使えます。

後置のif

[編集]

Crystalには、RubyやPerlのような後置のifがあります。

後置のifの例
n = 0

puts "nは0" if n == 0
puts "nは1" if n == 1
実行結果
nは0

unless

[編集]

unless(アンレス)は条件式によって実行・否を切り替える構造構文ですが、ifとは条件式に対する挙動が逆です。

unless文の例
a = 0.0 / 0.0

unless a == 0
    puts "Non-zero"
else
    puts a
end
実行結果
Non-zero
else節
unless文は、オプショナルな else 節を設け、条件式が真であった時に処理を実行させることが出来ます。
また、unless文は elsif 節は持てません。

後置のunless

[編集]

Crystalには、RubyやPerlのような後置のunlessがあります。

後置のunlessの例
n = 0

puts "nは0" unless n == 0
puts "nは1" unless n == 1
実行結果
nは1ではない

case

[編集]

caseは、複数の条件式によって処理を降る分ける用途の為に用意されています。

caseの例
n = 2

case n
when 1
    puts "one"
when 2
    puts "two"
when 3
    puts "three"
else
    puts "other"
end

p! (
case n
when 1
    "one"
when 2
    "two"
when 3
    "three"
else
    "other"
end
)
実行結果
two 
(case n 
when 1 
 "one" 
when 2 
 "two" 
when 3 
 "three" 
else 
 "other" 
end) # => "two"
C言語系のswitch文に慣れた人はbreakがないことに気がつくと思います。Crystalのcaseはfall throughしませんし、fall throughさせる方法もありません。
when節が定数でなく式を受付けます
[編集]

ifを使ったコードをcaseに書き換えてみましょう。

case の式の省略
a = 0.0 / 0.0
 
case
when a < 0
  puts "minus"
when a > 0
  puts "plus"
when a == 0
  puts "zero"
else
  puts a
end
 
p! (
case true
when a < 0
  "minus"
when a > 0
  "plus"
when a == 0
  "zero"
else
  a
end
)
実行結果
NaN 
(case true 
when a < 0 
 "minus" 
when a > 0 
 "plus" 
when a == 0 
 "zero" 
else 
 a 
end) # => NaN

このコードは when 節の式の値とcaseの式を === で比較し、最初に一致した when に対応する式が実行される事を利用しています。

型による分岐
[編集]

when 節が式ではなく型であった場合、caseの式を is_a? で評価し、最初に一致した when に対応する式が実行されます。

型による分岐
p! 0.class,
	0.is_a?(Object),
	0.is_a?(Int32),
	0.is_a?(Number),
	0.is_a?(String)
 
case 0
when String
    puts "String"
when Number
    puts "Number"
when Int32
    puts "Int32"
when Object
    puts "Object"
else
    puts "Unknown"
end
実行結果
0.class         # => Int32 
0.is_a?(Object) # => true 
0.is_a?(Int32)  # => true 
0.is_a?(Number) # => true 
0.is_a?(String) # => false 
Number

暗黙のオブジェクト構文を使うと

case 0
when .is_a?(String)
    puts "String"
when .is_a?(Number)
    puts "Number"
when .is_a(Int32)
    puts "Int32"
when .is_a(Object)
    puts "Object"
else
    puts "Unknown"
end
と書くことが出来ます。
メソッドは、.is_a? に限定しないので、 .odd? .even? .include? など Bool を返すメソッドなら何でも使えます。

when に対応する式は、1つのことが珍しくないので、その場合は省略可能な then を補うと、1行で書けます。

case 0
when String then puts "String"
when Number then puts "Number"
when Int32  then puts "Int32"
when Object then puts "Object"
else             puts "Unknown"
end
タプルとダミー識別子 _
[編集]

Crystalにおいて、タプルとは、複数の値をグループ化して1つのオブジェクトとして扱うための機能です。タプルは、角括弧 [ ] 内にコンマで区切られた値を指定して作成します。

例えば、次のように2つの値を持つタプルを作成できます。

tuple = [1, "hello"]

タプルの要素にアクセスするには、インデックスを指定して取得します。

tuple[0] #=> 1
tuple[1] #=> "hello"

また、Crystalにはダミー識別子 _ があります。この識別子は、未使用の引数や変数を表すために使用されます。例えば、次のように _ を使って、2番目の要素を無視して1番目の要素だけを取得することができます。

tuple = [1, "hello"]
a, _ = tuple
puts a #=> 1

ここでは、2番目の要素を _ で受け取っています。このように、ダミー識別子 _ を使うことで、ある変数や引数が使用されないことを明示することができます。

網羅性の検査
[編集]

when の代わりに in を使用すると、exhaustive case 式が作成されます。exhaustive case では、必要な in 条件を省略するとコンパイル時にエラーとなります。exhaustive case 式では、when 節と else 節を含むことはできません。

Enumの網羅性チェック(網羅不完全)
enum Colours
  Red
  Green
  Blue
end

colour : Colours = Colours::Red
q = case colour
    in Colours::Red then "赤"
    in .green?      then "緑"
#    in .blue?       then "青"
    end

p q
コンパイルエラー
Showing last frame. Use --error-trace for full trace.

In enumcase.cr:8:5

 8 | q = case colour
         ^
Error: case is not exhaustive for enum Colours.

Missing members:
 - Blue
case - in 式の in が列挙型の要素を網羅していないと、コンパイル時にこの様にエラーになります。
Colours::Red と .red? は同義です(enum では、要素名を小文字にし最後に ? が付いたメソッドが生えてきます)。
Enumの網羅性チェック(網羅完全)
enum Colours
  Red
  Green
  Blue
end

colour : Colours = Colours::Red
q = case colour
    in Colours::Red then "赤"
    in .green?      then "緑"
    in .blue?       then "青"
    end

p q
実行結果
"赤"

[TODO:短絡評価 && || ]

繰返し

[編集]

Crystalには、他のプログラミング言語のような繰返し構文と、イテレーターメソッドがあります。

繰返し構文

[編集]

Crystalの繰返し構文には、while と untilの2つがあります[10]

while
[編集]

while(ホワイル)は条件がである間、式を実行しつづけます。

構文
while 条件式
    式1
    式2
      
    式n
end
Rubyと違い、条件式の後ろに do をつけることは出来ません。
while文のコード例
i = 0
p! (
while i < 10
    p! i
    i += 1
    break i if i > 5
end
)
2行目の i < 5が真の間、次の2行を繰返します。
4行目の i += 1i = i + 1 の構文糖
実行結果
(while i < 10 
 p!(i) 
 i = i + 1 
 if i > 5 
   break i 
  end 
end)# => 
i # => 0 
i # => 1 
i # => 2 
i # => 3 
i # => 4 
i # => 5 
6
until
[編集]

until(アンティル)は条件がである間、式を実行しつづけます。whileとは条件に対する挙動が逆です。

構文
until 条件式 [ do ]
    文1
    文2
      
    文n
end
do は省略できます。
untilのコード例
i = 0
until i == 3
    puts i
    i += 1
end
2行目の i == 3が偽の間、次の2行を繰返します。
実行結果
0
1
2
for
[編集]

Crystalにはforがありませんが、コレクションのイテレーションメソッドを使うことで繰返しを簡素に実現出来ます。

Rangeオブジェクト

[編集]

Rangeオブジェクトは、整数の区間を表し範囲演算子 開始 .. 終了開始 ... 終了 で生成します。 範囲演算子の終了は省略でき、その場合は数学の半開区間(半閉区間)となり、例えば、1 ..は自然数となります(ただし、日本的な0を自然数に含まない場合)。

コード
rng = 1..3
puts rng.class
rng.each do | n |
  puts "#{n}番";
end
実行結果
Range(Int32, Int32) 
1番 
2番 
3番

Arrayオブジェクト

[編集]

Arrayオブジェクトは、任意の Crystal オブジェクトを要素として持つことができます。 配列式[ 要素1, 要素2, … 要素n ] で生成します。

コード
animals = [ "ネコ", "金魚", "ハムスター" ]

puts animals.class
animals.each do | animal |
  puts "動物 #{animal}"
end

p! ([ "イヌ", *animals , "イグアナ" ])
実行結果
Array(String)
動物 ネコ
動物 金魚
動物 ハムスター
(["イヌ", *animals, "イグアナ"]) # => ["イヌ", "ネコ", "金魚", "ハムスター", "イグアナ"]

Tupleオブジェクト

[編集]

Tupleオブジェクトは、任意の Crystal オブジェクトを要素として持つことができます。 配列式{ 要素1, 要素2, … 要素n } で生成します。

コード
animals = { "ネコ", "金魚", "ハムスター" }

puts animals.class
animals.each do | animal |
  puts "動物 #{animal}"
end

p! ({ "イヌ", *animals , "イグアナ" })
実行結果
Tuple(String, String, String)
動物 ネコ
動物 金魚
動物 ハムスター
({"イヌ", *animals, "イグアナ"}) # => {"イヌ", "ネコ", "金魚", "ハムスター", "イグアナ"}

Setオブジェクト

[編集]

Setオブジェクトは集合です。任意の Crystal オブジェクトを要素として持つことができますが、1つの値は重複して持てません。 Set.newに配列式{ 要素1, 要素2, … 要素n } などを渡し初期化します。

コード
animals = Set.new({ "ネコ", "金魚", "ハムスター" })
 
puts animals.class
animals.each do | animal |
  puts "動物 #{animal}"
end
 
p! animals,
  animals.includes?("ネコ"),
  animals.includes?("イヌ")
 
animals.delete "ネコ"
animals.add    "イヌ"
 
p! animals,
  animals.includes?("ネコ"),
  animals.includes?("イヌ")
 
animals = Set.new({ "ネコ", "イヌ", "金魚", "ハムスター", "カナリヤ", "クサガメ" })
mammals = Set.new({ "ネコ", "イヌ", "ハムスター" })
p!  animals , mammals,
   animals & mammals,
   animals | mammals,
   animals + mammals,
   animals ^ mammals,
   animals - mammals,
   mammals - animals
実行結果
Set(String)
動物 ネコ
動物 金魚
動物 ハムスター
animals                 # => Set{"ネコ", "金魚", "ハムスター"}
animals.includes?("ネコ") # => true
animals.includes?("イヌ") # => false
animals                 # => Set{"金魚", "ハムスター", "イヌ"}
animals.includes?("ネコ") # => false
animals.includes?("イヌ") # => true
animals           # => Set{"ネコ", "イヌ", "金魚", "ハムスター", "カナリヤ", "クサガメ"}
mammals           # => Set{"ネコ", "イヌ", "ハムスター"}
animals & mammals # => Set{"ネコ", "イヌ", "ハムスター"}
animals | mammals # => Set{"ネコ", "イヌ", "金魚", "ハムスター", "カナリヤ", "クサガメ"}
animals + mammals # => Set{"ネコ", "イヌ", "金魚", "ハムスター", "カナリヤ", "クサガメ"}
animals ^ mammals # => Set{"金魚", "カナリヤ", "クサガメ"}
animals - mammals # => Set{"金魚", "カナリヤ", "クサガメ"}
mammals - animals # => Set{}

NamedTupleオブジェクト

[編集]

NamedTupleオブジェクトは、任意の Crystal オブジェクトをキーに、任意の Crystal オブジェクトを値に持つことができる連想配列です。 NamedTuple式{キー1 => 値1, キー2 => 値2, キーn => 値n} で生成します。 また、キーが Symbol の場合 NamedTuple式{キー1: 値1, キー2: 値2, キーn: 値n} で生成することが出来ます。

コード
animals = {cat: "ネコ", gold_fish: "金魚", hamster: "ハムスター"}
 
puts animals.class
 
animals.each do | en, animal |
  puts "動物 #{en}: #{animal}"
end
実行結果
NamedTuple(cat: String, gold_fish: String, hamster: String)
動物 cat: ネコ
動物 gold_fish: 金魚
動物 hamster: ハムスター

このように、Crystalではforがなくてもコレクションのメソッドで同様の処理を実現できます。

loop

[編集]

loop ありません。 while true で代用します。

loopの代用コード例
i = 1
while true
    puts "0b%b" % i
    i <<= 1
    break if i > 2**8
end
実行結果
0b1
0b10
0b100
0b1000
0b10000
0b100000
0b1000000
0b10000000 
0b100000000
5行目の、break if i > 2**8でループを脱出するようにしています。この様に break や return あるいは例外が上がらないとループは永久に終わりません。
このコードは、Crystalにはない do-while文を模倣する例にもなっています。

イテレーターメソッド

[編集]
Integer#times
[編集]

Integer#timesは与えられたブロックをオブジェクトの示す整数値回くりかえします。

コード
3.times{ puts "Hello, world!" }
実行結果
Hello, world!
Hello, world!
Hello, world!
ループ変数を使た例
3.times do |i|
  puts "#{i}の倍は#{2 * i}"
end
実行結果
0の倍は0
1の倍は2
2の倍は4
ブロックを伴わないtimesメソッド
iter = 3.times
puts iter.class
p! iter.next
p! iter.next
p! iter.next
p! iter.next
p! iter.next
p! iter.next
# puts iter.next #  `next': StopIteration: iteration reached an end
実行結果
Int::TimesIterator(Int32) 
iter.next # => 0 
iter.next # => 1 
iter.next # => 2 
iter.next # => #<Iterator::Stop:0x7fb5bedd7fe0> 
iter.next # => #<Iterator::Stop:0x7fb5bedd7fe0> 
iter.next # => #<Iterator::Stop:0x7fb5bedd7fe0>
Integer#times にブロックを渡さないと、Int::TimesIterator([T])オブジェクトが返ります。
Int::TimesIterator([T])オブジェクトは外部イテレーターと呼ばれnextメソッドで反復を行えます。

オブジェクト

[編集]

Crystal では、全てがオブジェクトです。

オブジェクトのリテラルとクラス

[編集]
オブジェクトのリテラルとクラス
[nil, false, true, 1, 3.14, "abc", :abc, 1..10, 1...10, 1..,
 [1, 2_u8, 3_i128],
 [1, 2, 3], [1, "abc"],
 {1, 2, 3}, {1, "abc"},
 {"a" => 1, "b" => 2},
 {a: 1, b: 2},
 Set.new([:a, :bc, :def]),
 ->(x : Int32) { 2 * x },
 100.times,
 (1..).each,
 [1, 2, 3].each,
 {1, 2, 3}.each,
 {"a" => 1, "b" => 2}.each,
#  {a:1, b:2}.each, # Error: 'NamedTuple(a: Int32, b: Int32)#each' is expected to be invoked with a block, but no block was given
].each do |obj|
  p [obj, obj.class]
end
実行結果
[nil, Nil]
[false, Bool]
[true, Bool]
[1, Int32]
[3.14, Float64]
["abc", String]
[:abc, Symbol]
[1..10, Range(Int32, Int32)]
[1...10, Range(Int32, Int32)]
[1.., Range(Int32, Nil)]
[[1, 2, 3], Array(Int128 | Int32 | UInt8)]
[[1, 2, 3], Array(Int32)]
[[1, "abc"], Array(Int32 | String)]
[{1, 2, 3}, Tuple(Int32, Int32, Int32)]
[{1, "abc"}, Tuple(Int32, String)]
[{"a" => 1, "b" => 2}, Hash(String, Int32)]
[{a: 1, b: 2}, NamedTuple(a: Int32, b: Int32)]
[Set{:a, :bc, :def}, Set(Symbol)]
[#<Proc(Int32, Int32):0x5a6c0ffabcf0>, Proc(Int32, Int32)]
[#<Int::TimesIterator(Int32):0x7e2b4be59e80 @n=100, @index=0>, Int::TimesIterator(Int32)]
[#<Range::ItemIterator(Int32, Nil):0x7e2b4be5dfc0 @range=1.., @current=1, @reached_end=false>, Range::ItemIterator(Int32, Nil)]
[#<Indexable::ItemIterator(Array(Int32), Int32):0x7e2b4be58da0 @array=[1, 2, 3], @index=0>, Indexable::ItemIterator(Array(Int32), Int32)]
[#<Indexable::ItemIterator(Tuple(Int32, Int32, Int32), Int32):0x7e2b4be5dfa0 @array={1, 2, 3}, @index=0>, Indexable::ItemIterator(Tuple(Int32, Int32, Int32), Int32)]
[#<Hash::EntryIterator(String, Int32):0x7e2b4be58d80 @hash={"a" => 1, "b" => 2}, @index=0>, Hash::EntryIterator(String, Int32)]

Rubyのオブジェクトのリテラルとクラス

[編集]
Rubyのオブジェクトのリテラルとクラス
require 'set'

[nil, false, true, 1, 3.14, "abc", :abc, 1..10, 1...10, 1..,
# [1, 2_u8, 3_i128],
 [1, 2, 3], [1, "abc"],
# {1, 2, 3}, {1, "abc"},
 {"a" => 1, "b" => 2},
 {a: 1, b: 2},
 Set.new([:a, :bc, :def]),
 ->(x) { 2 * x },
 100.times,
 (1..).each,
 [1, 2, 3].each,
# {1, 2, 3}.each,
 {"a" => 1, "b" => 2}.each,
#  {a:1, b:2}.each, # Error: 'NamedTuple(a: Int32, b: Int32)#each' is expected to be invoked with a block, but no block was given
].each do |obj|
  p [obj, obj.class]
end
実行結果
[nil, NilClass]
[false, FalseClass]
[true, TrueClass]
[1, Integer]
[3.14, Float]
["abc", String]
[:abc, Symbol]
[1..10, Range]
[1...10, Range]
[1.., Range]
[[1, 2, 3], Array]
[[1, "abc"], Array]
[{"a"=>1, "b"=>2}, Hash]
[{:a=>1, :b=>2}, Hash]
[#<Set: {:a, :bc, :def}>, Set]
[#<Proc:0x000014af26147eb0 Main.rb:10 (lambda)>, Proc]
[#<Enumerator: 100:times>, Enumerator]
[#<Enumerator: 1..:each>, Enumerator]
[#<Enumerator: [1, 2, 3]:each>, Enumerator]
[#<Enumerator: {"a"=>1, "b"=>2}:each>, Enumerator]

クラス

[編集]

シンプルなクラス

[編集]
シンプルなクラス
class Hello
  def initialize(@name : String = "World")
  end

  def greeting
    puts "Hello #{@name}!"
  end
end

hello = Hello.new()
hello.greeting

universe = Hello.new("Universe")
universe.greeting
実行結果
Hello World!
Hello Universe!
初期化メソッド
  def initialize(@name : String = "World")
  end
Rubyであれば
  def initialize(name = "World")
    @name = name
  end
とするところですが、Crystalでは、型アノテーション  : String を使い、引数の型を限定しました。
また、(@ 付きの)アトリビュート名を仮引数にすると、そのままアトリビュート(a.k.a. インスタンス変数)に仮引数が代入されます。
これは、C++のコンストラクターのメンバー初期化リストと同じアイディアですが、Crystalではインスタンス変数に @ が前置されるので、仮引数に @ が出現すればインスタンス変数の初期値だと自明で、聡明な選択です。


都市間の大圏距離

[編集]

Ruby#ユーザー定義クラスの都市間の大圏距離を求めるメソッドを追加した例を、Crystalに移植しました。

都市間の大圏距離
class GeoCoord
  getter :longitude, :latitude

  def initialize(@longitude : Float64, @latitude : Float64)
  end

  def to_s(io)
    ew, ns = "東経", "北緯"
    long, lat = @longitude, @latitude
    ew, long = "西経", -long if long < 0.0
    ns, lat = "南緯", -lat if lat < 0.0
    io << "(#{ew}: #{long}, #{ns}: #{lat})"
  end # https://github.com/crystal-lang/crystal/issues/259
  def distance(other)
    i, r = Math::PI / 180, 6371.008
    Math.acos(Math.sin(@latitude*i) * Math.sin(other.latitude * i) +
              Math.cos(@latitude*i) * Math.cos(other.latitude * i) * Math.cos(@longitude * i - other.longitude * i)) * r
  end
end

# メソッドの先頭を大文字に出来ないのでクラス名のメソッドは作ることが出来ない
# def GeoCoord(lng : Float64, lat : Float64)
#  GeoCoord.new(lng, lat)
# end

Sites = {
  "東京駅":         GeoCoord.new(139.7673068, 35.6809591),
  "シドニー・オペラハウス": GeoCoord.new(151.215278, -33.856778),
  "グリニッジ天文台":    GeoCoord.new(-0.0014, 51.4778),
}

Sites.each { |name, gc|
  puts "#{name}: #{gc}"
}

puts ""

keys, len = Sites.keys, Sites.size
keys.each_with_index { |x, i|
  y = keys[(i + 1) % len]
  puts "#{x}#{y}: #{Sites[x].distance(Sites[y])} [km]"
}
実行結果
東京駅: (東経: 139.7673068, 北緯: 35.6809591)
シドニー・オペラハウス: (東経: 151.215278, 南緯: 33.856778)
グリニッジ天文台: (西経: 0.0014, 北緯: 51.4778)

東京駅 ⇔ シドニー・オペラハウス: 7823.269299386704 [km]
シドニー・オペラハウス ⇔ グリニッジ天文台: 16987.2708377249 [km]
グリニッジ天文台 ⇔ 東京駅: 9560.546566490015 [km]
Crystal には、attr_accessor はありませんが、標準ライブラリーのマクロに getterがあるので
  getter :longitude, :latitude
としました。
将来、attr_accessor が実装される可能性はありますが、姉妹品のsetter との併用が下位互換性を考えると確実です。
to_s は、Ruby ならば
  def to_s()
    "(#{ew}: #{long}, #{ns}: #{lat})"
ですが、Crystalでは追加の引数 io が必要で
  def to_s(io)
    io << "(#{ew}: #{long}, #{ns}: #{lat})"
Ruby にはクラス名と同じ名前のメソッドで .new を呼出す文化があるのですが、Crystalはメソッドの先頭を大文字に出来ないので、これは見送りました。

包含と継承

[編集]

JavaScript/クラス#包含と継承を、Rubyに移植したRuby#包含と継承を、Crystalに移植しました。

包含と継承の例
class Point
  def initialize(@x = 0, @y = 0)
  end

  def inspect(io)
    io << "x:#{@x}, y:#{@y}"
  end

  def move(dx = 0, dy = 0)
    @x, @y = @x + dx, @y + dy
    self
  end
end

class Shape
  def initialize(x = 0, y = 0)
    @location = Point.new(x, y)
  end

  def inspect(io)
    @location.inspect(io)
  end

  def move(x, y)
    @location.move(x, y)
    self
  end
end

class Rectangle < Shape
  def initialize(x = 0, y = 0, @width = 0, @height = 0)
    super(x, y)
  end

  def inspect(io)
    super(io)
    io << ", width:#{@width}, height:#{@height}"
  end
end

rct = Rectangle.new(12, 32, 100, 50)
p! rct,
  rct.is_a?(Rectangle),
  rct.is_a?(Shape),
  rct.is_a?(Point),
  rct.move(11, 21)
(END)
実行結果
rct                  # => x:12, y:32, width:100, height:50 
rct.is_a?(Rectangle) # => true 
rct.is_a?(Shape)     # => true 
rct.is_a?(Point)     # => false 
rct.move(11, 21)     # => x:23, y:53, width:100, height:50
crystal tool hierarchy
% crystal tool hierarchy inclusion-and-inheritance.cr -e Shape
- class Object (4 bytes)
  |
  +- class Reference (4 bytes)
     |
     +- class Shape (16 bytes)
        .   @location : Point (8 bytes)
        |
        +- class Rectangle (24 bytes)
               @width  : Int32 (4 bytes)
               @height : Int32 (4 bytes)
crystal の tool hierarchy サブコマンドで、クラスの継承関係がわかります。

superclass と subclasses

[編集]

Crystal には、RubyのClassにあるメソッド superclass と subclasses がないので、マクロで実装しました。

superclass と subclasses
class Class
  def self.superclass
    {{ @type.superclass }}
  end

  def self.subclasses : Array(self.class)
    {{ @type.subclasses }}.map(&.as(self.class))
  end

  def self.all_subclasses : Array(self.class)
    {% begin %}
      [{{ @type.all_subclasses.join(",").id }}] of self.class
    {% end %}
  end
end
 
class A end
class AA < A end
class AAA < AA end
class AAB < AA end
class AB < A end

p! A,
  A.subclasses,
  A.all_subclasses,
  AAA.superclass,
  A.superclass
 
c = AAA
while !c.is_a? Nil
  p! c.superclass
  c = c.superclass
end
実行結果
A                # => A 
A.subclasses     # => [AA, AB] 
A.all_subclasses # => [AA, AAA, AAB, AB] 
AAA.superclass   # => AA 
A.superclass     # => Reference 
c.superclass # => AA 
c.superclass # => A 
c.superclass # => Reference 
c.superclass # => Object 
c.superclass # => nil

抽象クラス

[編集]

Java/抽象クラスを、Crystalに移植しました。

抽象クラスの宣言
abstract class クラス名
  #
end
このクラス名は、 .new でインスタンス化出来ません。
Error: can't instantiate abstract class クラス名
となります。
インスタンス化することは出来ませんが、抽象クラスを別のクラスが継承する事は出来ます。
また、抽象クラスを super() を使うことでメソッドを呼び出せるので、抽象メソッドではないメソッド(具象メソッド)を持つことも、インスタンス変数も持つことも出来ます。
抽象クラスの例では、Shapeのinitializeメソッドが抽象クラスの具象メソッドとなっています。
抽象メソッドの宣言
  abstract def メソッド名
派生先のクラスで、「メソッド名」を定義(def)し忘れると
Error: abstract `def クラス名#メソッド名()` must be implemented by クラス名
となります
抽象クラスの例
abstract class Shape
  def initialize(@x = 0.0, @y = 0.0)
  end

  abstract def to_s(io)
  abstract def area
end

class Square < Shape
  def initialize(x, y, @wh = 0.0)
    super(x, y)
  end

  def to_s(io)
    io << "Square(#{@x}, #{@y}, #{@wh})"
  end

  def area
    @wh * @wh
  end
end

abstract class Shape
  def initialize(@x = 0.0, @y = 0.0)
  end

  abstract def to_s(io)
  abstract def area
end

class Square < Shape
  def initialize(x, y, @wh = 0.0)
    super(x, y)
  end

  def to_s(io)
    io << "Square(#{@x}, #{@y}, #{@wh})"
  end

  def area
    @wh * @wh
  end
end

class Recrangle < Shape
  def initialize(x, y, @w = 0.0, @h = 0.0)
    super(x, y)
  end

  def to_s(io)
    io << "Rectanle(#{@x}, #{@y}, #{@w}, #{@h})"
  end

  def area
    @w * @h
  end
end

class Circle < Shape
  def initialize(x, y, @r = 0.0)
    super(x, y)
  end

  def to_s(io)
    io << "Circle(#{@x}, #{@y}, #{@r})"
  end

  def area
    3.1425926536 * @r * @r
  end
end

shapes = [
  Square.new(5.0, 10.0, 15.0),
  Recrangle.new(13.0, 23.0, 20.0, 10.0),
  Circle.new(3.0, 2.0, 20.0),
] of Shape

shapes.each do |shape|
  puts("#{shape}: #{shape.area}")
end
実行結果
Square(5.0, 10.0, 15.0): 225.0
Rectanle(13.0, 23.0, 20.0, 10.0): 200.0
Circle(3.0, 2.0, 20.0): 1257.03706144
crystal tool hierarchy
% crystal tool hierarchy abstract.cr -e Shape
- class Object (4 bytes)
  |
  +- class Reference (4 bytes)
     |
     +- class Shape (24 bytes)
        .   @x : Float64 (8 bytes)
        .   @y : Float64 (8 bytes)
        |
        +- class Circle (32 bytes)
        |      @r : Float64 (8 bytes)
        |
        +- class Recrangle (40 bytes)
        |      @w : Float64 (8 bytes)
        |      @h : Float64 (8 bytes)
        |
        +- class Square (32 bytes)
               @wh : Float64 (8 bytes)
crystal の tool hierarchy サブコマンドで、クラスの継承関係がわかります。
「包含と継承の例」と比べると、ShapeとRectangleが同じ階層にあることがわかると思います。

メソッド

[編集]

オブジェクトの値や機能を呼び出すためには、メソッドを使います(多くの演算子もメソッドです)。

クラスのメソッド一覧

[編集]

Crystal には、Objectクラスにmethodsメソッドがないので、マクロで実装しました。

RubyのObject#methods
p Object.methods.sort,
  Integer.methods.sort,
  Float.methods.sort,
  Array.methods.sort,
  Range.methods.sort
実行結果
[:!, :!=, :!~, :<, :<=, :<=>, :==, :===, :=~, :>, :>=, :__id__, :__send__, :alias_method, :allocate, :ancestors, :attr, :attr_accessor, :attr_reader, :attr_writer, :autoload, :autoload?, :class, :class_eval, :class_exec, :class_variable_defined?, :class_variable_get, :class_variable_set, :class_variables, :clone, :const_defined?, :const_get, :const_missing, :const_set, :const_source_location, :constants, :define_method, :define_singleton_method, :deprecate_constant, :display, :dup, :enum_for, :eql?, :equal?, :extend, :freeze, :frozen?, :hash, :include, :include?, :included_modules, :inspect, :instance_eval, :instance_exec, :instance_method, :instance_methods, :instance_of?, :instance_variable_defined?, :instance_variable_get, :instance_variable_set, :instance_variables, :is_a?, :itself, :kind_of?, :method, :method_defined?, :methods, :module_eval, :module_exec, :name, :new, :nil?, :object_id, :prepend, :private_class_method, :private_constant, :private_instance_methods, :private_method_defined?, :private_methods, :protected_instance_methods, :protected_method_defined?, :protected_methods, :public_class_method, :public_constant, :public_instance_method, :public_instance_methods, :public_method, :public_method_defined?, :public_methods, :public_send, :remove_class_variable, :remove_instance_variable, :remove_method, :respond_to?, :send, :singleton_class, :singleton_class?, :singleton_method, :singleton_methods, :subclasses, :superclass, :taint, :tainted?, :tap, :then, :to_enum, :to_s, :trust, :undef_method, :untaint, :untrust, :untrusted?, :yield_self]
[:!, :!=, :!~, :<, :<=, :<=>, :==, :===, :=~, :>, :>=, :__id__, :__send__, :alias_method, :allocate, :ancestors, :attr, :attr_accessor, :attr_reader, :attr_writer, :autoload, :autoload?, :class, :class_eval, :class_exec, :class_variable_defined?, :class_variable_get, :class_variable_set, :class_variables, :clone, :const_defined?, :const_get, :const_missing, :const_set, :const_source_location, :constants, :define_method, :define_singleton_method, :deprecate_constant, :display, :dup, :enum_for, :eql?, :equal?, :extend, :freeze, :frozen?, :hash, :include, :include?, :included_modules, :inspect, :instance_eval, :instance_exec, :instance_method, :instance_methods, :instance_of?, :instance_variable_defined?, :instance_variable_get, :instance_variable_set, :instance_variables, :is_a?, :itself, :kind_of?, :method, :method_defined?, :methods, :module_eval, :module_exec, :name, :nil?, :object_id, :prepend, :private_class_method, :private_constant, :private_instance_methods, :private_method_defined?, :private_methods, :protected_instance_methods, :protected_method_defined?, :protected_methods, :public_class_method, :public_constant, :public_instance_method, :public_instance_methods, :public_method, :public_method_defined?, :public_methods, :public_send, :remove_class_variable, :remove_instance_variable, :remove_method, :respond_to?, :send, :singleton_class, :singleton_class?, :singleton_method, :singleton_methods, :sqrt, :subclasses, :superclass, :taint, :tainted?, :tap, :then, :to_enum, :to_s, :trust, :try_convert, :undef_method, :untaint, :untrust, :untrusted?, :yield_self]
[:!, :!=, :!~, :<, :<=, :<=>, :==, :===, :=~, :>, :>=, :__id__, :__send__, :alias_method, :allocate, :ancestors, :attr, :attr_accessor, :attr_reader, :attr_writer, :autoload, :autoload?, :class, :class_eval, :class_exec, :class_variable_defined?, :class_variable_get, :class_variable_set, :class_variables, :clone, :const_defined?, :const_get, :const_missing, :const_set, :const_source_location, :constants, :define_method, :define_singleton_method, :deprecate_constant, :display, :dup, :enum_for, :eql?, :equal?, :extend, :freeze, :frozen?, :hash, :include, :include?, :included_modules, :inspect, :instance_eval, :instance_exec, :instance_method, :instance_methods, :instance_of?, :instance_variable_defined?, :instance_variable_get, :instance_variable_set, :instance_variables, :is_a?, :itself, :kind_of?, :method, :method_defined?, :methods, :module_eval, :module_exec, :name, :nil?, :object_id, :prepend, :private_class_method, :private_constant, :private_instance_methods, :private_method_defined?, :private_methods, :protected_instance_methods, :protected_method_defined?, :protected_methods, :public_class_method, :public_constant, :public_instance_method, :public_instance_methods, :public_method, :public_method_defined?, :public_methods, :public_send, :remove_class_variable, :remove_instance_variable, :remove_method, :respond_to?, :send, :singleton_class, :singleton_class?, :singleton_method, :singleton_methods, :subclasses, :superclass, :taint, :tainted?, :tap, :then, :to_enum, :to_s, :trust, :undef_method, :untaint, :untrust, :untrusted?, :yield_self]
[:!, :!=, :!~, :<, :<=, :<=>, :==, :===, :=~, :>, :>=, :[], :__id__, :__send__, :alias_method, :allocate, :ancestors, :attr, :attr_accessor, :attr_reader, :attr_writer, :autoload, :autoload?, :class, :class_eval, :class_exec, :class_variable_defined?, :class_variable_get, :class_variable_set, :class_variables, :clone, :const_defined?, :const_get, :const_missing, :const_set, :const_source_location, :constants, :define_method, :define_singleton_method, :deprecate_constant, :display, :dup, :enum_for, :eql?, :equal?, :extend, :freeze, :frozen?, :hash, :include, :include?, :included_modules, :inspect, :instance_eval, :instance_exec, :instance_method, :instance_methods, :instance_of?, :instance_variable_defined?, :instance_variable_get, :instance_variable_set, :instance_variables, :is_a?, :itself, :kind_of?, :method, :method_defined?, :methods, :module_eval, :module_exec, :name, :new, :nil?, :object_id, :prepend, :private_class_method, :private_constant, :private_instance_methods, :private_method_defined?, :private_methods, :protected_instance_methods, :protected_method_defined?, :protected_methods, :public_class_method, :public_constant, :public_instance_method, :public_instance_methods, :public_method, :public_method_defined?, :public_methods, :public_send, :remove_class_variable, :remove_instance_variable, :remove_method, :respond_to?, :send, :singleton_class, :singleton_class?, :singleton_method, :singleton_methods, :subclasses, :superclass, :taint, :tainted?, :tap, :then, :to_enum, :to_s, :trust, :try_convert, :undef_method, :untaint, :untrust, :untrusted?, :yield_self] 
[:!, :!=, :!~, :<, :<=, :<=>, :==, :===, :=~, :>, :>=, :__id__, :__send__, :alias_method, :allocate, :ancestors, :attr, :attr_accessor, :attr_reader, :attr_writer, :autoload, :autoload?, :class, :class_eval, :class_exec, :class_variable_defined?, :class_variable_get, :class_variable_set, :class_variables, :clone, :const_defined?, :const_get, :const_missing, :const_set, :const_source_location, :constants, :define_method, :define_singleton_method, :deprecate_constant, :display, :dup, :enum_for, :eql?, :equal?, :extend, :freeze, :frozen?, :hash, :include, :include?, :included_modules, :inspect, :instance_eval, :instance_exec, :instance_method, :instance_methods, :instance_of?, :instance_variable_defined?, :instance_variable_get, :instance_variable_set, :instance_variables, :is_a?, :itself, :kind_of?, :method, :method_defined?, :methods, :module_eval, :module_exec, :name, :new, :nil?, :object_id, :prepend, :private_class_method, :private_constant, :private_instance_methods, :private_method_defined?, :private_methods, :protected_instance_methods, :protected_method_defined?, :protected_methods, :public_class_method, :public_constant, :public_instance_method, :public_instance_methods, :public_method, :public_method_defined?, :public_methods, :public_send, :remove_class_variable, :remove_instance_variable, :remove_method, :respond_to?, :send, :singleton_class, :singleton_class?, :singleton_method, :singleton_methods, :subclasses, :superclass, :taint, :tainted?, :tap, :then, :to_enum, :to_s, :trust, :undef_method, :untaint, :untrust, :untrusted?, :yield_self]


Crystalに実装したmethodsマクロ
class Object
  macro methods
    {{ @type.methods.map(&.name.stringify).sort.uniq }}
  end
end

p! Object.methods,
  Reference.methods,
  Array.methods,
  Box.methods,
  Channel.methods,
  Deque.methods,
  Dir.methods,
  Exception.methods,
  ArgumentError.methods,
  DivisionByZeroError.methods,
  IndexError.methods,
  InvalidByteSequenceError.methods,
  Fiber.methods,
  Hash.methods,
  IO.methods,
  File.methods,
  Mutex.methods,
  PrettyPrint.methods,
  Process.methods,
  Regex.methods,
  String.methods,
  Thread.methods,
  Bool.methods,
  Int32.methods,
  Float64.methods,
  Proc.methods
実行結果
Object.methods                   # => ["!=", "!~", "==", "===", "=~", "class", "crystal_type_id", "dup", "hash", "in?", "inspect", "itself", "not_nil!", "pretty_inspect", "pretty_print", "tap", "to_s", "try", "unsafe_as"]
Reference.methods                # => ["==", "dup", "exec_recursive", "exec_recursive_clone", "hash", "inspect", "object_id", "pretty_print", "same?", "to_s"]
Array.methods                    # => ["&", "*", "+", "-", "<<", "<=>", "==", "[]", "[]=", "[]?", "calculate_new_capacity", "check_needs_resize", "check_needs_resize_for_unshift", "clear", "clone", "compact", "compact!", "concat", "delete", "delete_at", "dup", "each_repeated_permutation", "fill", "first", "flatten", "increase_capacity", "increase_capacity_for_unshift", "index", "initialize", "insert", "inspect", "internal_delete", "last", "map", "map_with_index", "needs_resize?", "pop", "pop?", "pretty_print", "product", "push", "reject!", "remaining_capacity", "repeated_permutations", "replace", "reset_buffer_to_root_buffer", "resize_if_cant_insert", "resize_to_capacity", "resize_to_capacity_for_unshift", "reverse", "root_buffer", "rotate", "rotate!", "select!", "shift", "shift?", "shift_buffer_by", "shift_when_not_empty", "shuffle", "size", "size=", "skip", "sort", "sort!", "sort_by", "sort_by!", "to_a", "to_lookup_hash", "to_s", "to_unsafe", "to_unsafe_slice", "transpose", "truncate", "uniq", "uniq!", "unsafe_fetch", "unsafe_put", "unshift", "unstable_sort", "unstable_sort!", "unstable_sort_by", "unstable_sort_by!", "|"]
Box.methods                      # => ["initialize", "object"]
Channel.methods                  # => ["close", "closed?", "dequeue_receiver", "dequeue_sender", "initialize", "inspect", "pretty_print", "receive", "receive?", "receive_impl", "receive_internal", "receive_select_action", "receive_select_action?", "send", "send_internal", "send_select_action"]
Deque.methods                    # => ["+", "<<", "==", "buffer", "clear", "clone", "concat", "delete", "delete_at", "dup", "each", "halfs", "increase_capacity", "initialize", "insert", "inspect", "internal_delete", "pop", "pop?", "pretty_print", "push", "reject!", "rotate!", "select!", "shift", "shift?", "size", "size=", "to_s", "unsafe_fetch", "unsafe_put", "unshift"]
Dir.methods                      # => ["children", "close", "each", "each_child", "entries", "initialize", "inspect", "path", "pretty_print", "read", "rewind", "to_s"]
Exception.methods                # => ["backtrace", "backtrace?", "callstack", "callstack=", "cause", "initialize", "inspect", "inspect_with_backtrace", "message", "to_s"]
ArgumentError.methods            # => ["initialize"]
DivisionByZeroError.methods      # => ["initialize"]
IndexError.methods               # => ["initialize"]
InvalidByteSequenceError.methods # => ["initialize"]
Fiber.methods                    # => ["cancel_timeout", "dead?", "enqueue", "initialize", "inspect", "makecontext", "name", "name=", "next", "next=", "previous", "previous=", "push_gc_roots", "resumable?", "resume", "resume_event", "run", "running?", "stack_bottom", "stack_bottom=", "timeout", "timeout_event", "timeout_select_action", "timeout_select_action=", "to_s"]
Hash.methods                     # => ["==", "[]", "[]=", "[]?", "add_entry_and_increment_size", "clear", "clear_entries", "clear_impl", "clear_indices", "clone", "compact", "compact!", "compare_by_identity", "compare_by_identity?", "compute_indices_bytesize", "delete", "delete_entry", "delete_entry_and_update_counts", "delete_impl", "delete_linear_scan", "dig", "dig?", "do_compaction", "double_indices_size", "dup", "each", "each_entry_with_index", "each_key", "each_value", "empty?", "entries", "entries_capacity", "entries_full?", "entries_size", "entry_matches?", "fetch", "find_entry", "find_entry_with_index", "find_entry_with_index_linear_scan", "first_entry?", "first_key", "first_key?", "first_value", "first_value?", "fit_in_indices", "get_entry", "get_index", "has_key?", "has_value?", "hash", "indices_malloc_size", "indices_size", "initialize", "initialize_clone", "initialize_clone_entries", "initialize_compare_by_identity", "initialize_copy_non_entries_vars", "initialize_default_block", "initialize_dup", "initialize_dup_entries", "inspect", "invert", "key_for", "key_for?", "key_hash", "keys", "last_entry?", "last_key", "last_key?", "last_value", "last_value?", "malloc_entries", "malloc_indices", "merge", "merge!", "merge_into!", "next_index", "pretty_print", "proper_subset_of?", "proper_superset_of?", "put", "realloc_entries", "realloc_indices", "rehash", "reject", "reject!", "resize", "select", "select!", "set_entry", "set_index", "shift", "shift?", "size", "subset_of?", "superset_of?", "to_a", "to_a_impl", "to_h", "to_s", "transform_keys", "transform_values", "transform_values!", "update", "update_linear_scan", "upsert", "values", "values_at"]
IO.methods                       # => ["<<", "check_open", "close", "closed?", "decoder", "each_byte", "each_char", "each_line", "encoder", "encoding", "flush", "getb_to_end", "gets", "gets_peek", "gets_slow", "gets_to_end", "has_non_utf8_encoding?", "peek", "peek_or_read_utf8", "peek_or_read_utf8_masked", "pos", "pos=", "print", "printf", "puts", "read", "read_at", "read_byte", "read_bytes", "read_char", "read_char_with_bytesize", "read_fully", "read_fully?", "read_line", "read_string", "read_utf8", "read_utf8_byte", "rewind", "seek", "set_encoding", "skip", "skip_to_end", "tell", "tty?", "utf8_encoding?", "write", "write_byte", "write_bytes", "write_string", "write_utf8"]
File.methods                     # => ["delete", "initialize", "inspect", "path", "read_at", "size", "truncate"]
Mutex.methods                    # => ["initialize", "lock", "lock_slow", "synchronize", "try_lock", "unlock"]
PrettyPrint.methods              # => ["break_outmost_groups", "breakable", "comma", "current_group", "fill_breakable", "flush", "group", "group_queue", "group_sub", "indent", "initialize", "list", "nest", "newline", "surround", "text"]
Process.methods                  # => ["channel", "close", "close_io", "copy_io", "ensure_channel", "error", "error?", "exists?", "finalize", "initialize", "input", "input?", "output", "output?", "pid", "signal", "stdio_to_fd", "terminate", "terminated?", "wait"]
Regex.methods                    # => ["+", "==", "===", "=~", "capture_count", "clone", "dup", "finalize", "hash", "initialize", "inspect", "internal_matches?", "match", "match_at_byte_index", "matches?", "matches_at_byte_index?", "name_table", "options", "source", "to_s"]
String.methods                   # => ["%", "*", "+", "<=>", "==", "=~", "[]", "[]?", "ascii_only?", "blank?", "byte_at", "byte_at?", "byte_delete_at", "byte_index", "byte_index_to_char_index", "byte_slice", "byte_slice?", "bytes", "bytesize", "calc_excess_left", "calc_excess_right", "camelcase", "capitalize", "center", "char_at", "char_bytesize_at", "char_index_to_byte_index", "chars", "check_no_null_byte", "chomp", "clone", "codepoint_at", "codepoints", "compare", "count", "delete", "delete_at", "downcase", "dump", "dump_char", "dump_hex", "dump_or_inspect", "dump_or_inspect_char", "dump_or_inspect_unquoted", "dump_unquoted", "dup", "each_byte", "each_byte_index_and_char_index", "each_char", "each_char_with_index", "each_codepoint", "each_grapheme", "each_grapheme_boundary", "each_line", "empty?", "encode", "ends_with?", "find_start_and_end", "grapheme_size", "graphemes", "gsub", "gsub_append", "gsub_ascii_char", "has_back_references?", "hash", "hexbytes", "hexbytes?", "includes?", "index", "insert", "insert_impl", "inspect", "inspect_char", "inspect_unquoted", "just", "lchop", "lchop?", "lines", "ljust", "lstrip", "match", "matches?", "partition", "presence", "pretty_print", "rchop", "rchop?", "remove_excess", "remove_excess_left", "remove_excess_right", "reverse", "rindex", "rjust", "rpartition", "rstrip", "scan", "scan_backreferences", "scrub", "single_byte_optimizable?", "size", "size_known?", "split", "split_by_empty_separator", "split_single_byte", "squeeze", "starts_with?", "strip", "sub", "sub_append", "sub_index", "sub_range", "succ", "titleize", "to_f", "to_f32", "to_f32?", "to_f64", "to_f64?", "to_f?", "to_f_impl", "to_i", "to_i128", "to_i128?", "to_i16", "to_i16?", "to_i32", "to_i32?", "to_i64", "to_i64?", "to_i8", "to_i8?", "to_i?", "to_s", "to_slice", "to_u128", "to_u128?", "to_u16", "to_u16?", "to_u32", "to_u32?", "to_u64", "to_u64?", "to_u8", "to_u8?", "to_unsafe", "to_unsigned_info", "to_utf16", "tr", "underscore", "unicode_delete_at", "unsafe_byte_at", "unsafe_byte_slice", "unsafe_byte_slice_string", "upcase", "valid_encoding?"]
Thread.methods                   # => ["detach", "event_base", "gc_thread_handler", "gc_thread_handler=", "initialize", "join", "main_fiber", "next", "next=", "previous", "previous=", "scheduler", "stack_address", "start", "to_unsafe"]
Bool.methods                     # => ["!=", "&", "==", "^", "clone", "hash", "to_s", "to_unsafe", "|"]
Int32.methods                    # => ["!=", "&", "&*", "&+", "&-", "*", "+", "-", "/", "<", "<=", "==", ">", ">=", "^", "clone", "leading_zeros_count", "popcount", "to_f", "to_f!", "to_f32", "to_f32!", "to_f64", "to_f64!", "to_i", "to_i!", "to_i128", "to_i128!", "to_i16", "to_i16!", "to_i32", "to_i32!", "to_i64", "to_i64!", "to_i8", "to_i8!", "to_u", "to_u!", "to_u128", "to_u128!", "to_u16", "to_u16!", "to_u32", "to_u32!", "to_u64", "to_u64!", "to_u8", "to_u8!", "trailing_zeros_count", "unsafe_chr", "unsafe_div", "unsafe_mod", "unsafe_shl", "unsafe_shr", "|"]
Float64.methods                  # => ["!=", "*", "**", "+", "-", "/", "<", "<=", "==", ">", ">=", "ceil", "clone", "fdiv", "floor", "next_float", "prev_float", "round_away", "round_even", "to_f", "to_f!", "to_f32", "to_f32!", "to_f64", "to_f64!", "to_i", "to_i!", "to_i128", "to_i128!", "to_i16", "to_i16!", "to_i32", "to_i32!", "to_i64", "to_i64!", "to_i8", "to_i8!", "to_s", "to_u", "to_u!", "to_u128", "to_u128!", "to_u16", "to_u16!", "to_u32", "to_u32!", "to_u64", "to_u64!", "to_u8", "to_u8!", "trunc"]
Proc.methods                     # => ["==", "===", "arity", "call", "clone", "closure?", "closure_data", "hash", "internal_representation", "partial", "pointer", "to_s"]

マクロ

[編集]

Crystalにはマクロがあります。マクロは、プログラムのコンパイル時に実行され、コードの生成や変換を行うための強力なツールです。Crystalのマクロは、プログラムの柔軟性を高めるために広く使用されます。

Crystalのマクロはmacroキーワードを使用して定義されます。以下は、基本的なマクロの例です:

macro say_hello
  puts "Hello, world!"
end

# マクロを呼び出す
say_hello

このマクロは、say_helloを呼び出すとコンパイル時にputs "Hello, world!"が挿入され、プログラムが実行される際にはそのままputs "Hello, world!"が実行されます。

マクロは、引数を取ることもできます。例えば、以下のマクロは引数を受け取ってそれを使って出力します:

macro say(message)
  puts message
end

# マクロを呼び出す
say "Hello, world!"

マクロは、コンパイル時にコードを生成するため、プログラムの最適化やパフォーマンスの向上にも役立ちます。しかし、過度に使用すると可読性が低下する可能性があるため、慎重に使う必要があります。

{% ... %} 構文

[編集]

{% ... %}は、テンプレートエンジンやマクロなどの制御構造を含むテンプレート言語の一部として使用される構文です。Crystalにおいてもこのような構文が存在します。

Crystalでは、{% ... %}はマクロのブロックを定義するために使用されます。マクロはコンパイル時にコードを生成するためのものであり、{% ... %}ブロック内のコードはコンパイル時に実行されます。

以下は、Crystalでの{% ... %}を使用したマクロの例です:

macro generate_code
  {% for i in 1..3 %}
    puts "Generated code {{i}}"
  {% end %}
end
 
# マクロを呼び出す
generate_code
    #=>  Generated code 1
    #    Generated code 2
    #    Generated code 3

この例では、generate_codeというマクロが定義されています。マクロの中で{% for ... %}ブロックを使ってループを定義し、それぞれのループイテレーションでコンパイル時にコードが生成されます。

このように{% ... %}構文を使用することで、Crystalではコンパイル時の制御構造やコード生成を行うことができます。


マクロを使ったattr_accessorのイミュレーション

[編集]
class Point
  def initialize(@x : Int32, @y : Int32)
  end

  # macro定義
  macro attr_accessor(*attrs)
    {% for attr in attrs %}
      def {{attr.id}}() @{{attr.id}} end
      def {{attr.id}}=(var) @{{attr.id}} = var end
    {% end %}
  end

  # macro呼出し
  attr_accessor :x, :y
end

pt = Point.new(20, 30)
p [pt.x, pt.y]
t = pt.x
pt.x = pt.y
pt.y = t
p [pt.x, pt.y]
実行結果
[20, 30]
[30, 20]
Ruby には、attr_accessor と言う「クラスのメンバーのアクセサーを自動生成するメソッド」がありますが、Crystalにはないようなので、マクロで実装しました。
attr_accessor :name からは
def name()     @name end
def name=(val) @name = val end
相当のコードが生成されます。

マクロの機能と構文

[編集]

Crystalのマクロは、実行時ではなくコンパイル時に実行される機能であり、ソースコードを操作することができます。以下はCrystalのマクロの主な機能と構文の説明です。

* の付いた引数

[編集]

マクロを定義するときに、引数に * を付けることができます。これにより、任意の数の引数を受け取ることができます。例えば、以下のマクロは、任意の数の引数をコンマで区切った文字列を出力します。

macro join_arguments(*args)
  {{ args.join(", ") }}
end

このマクロを使用すると、以下のようにコードを記述できます。

puts join_arguments(1, 2, 3) # => "1, 2, 3"
puts join_arguments("foo", "bar") # => "foo, bar"

{{引数}}

[編集]

マクロの中で、{{}}で囲んだ式を埋め込むことができます。この式は、コンパイル時に評価され、その結果がコードに挿入されます。例えば、以下のマクロは、引数に指定された式を2倍にして返します。

macro double(expr)
  {{ expr }} * 2
end

このマクロを使用すると、以下のようにコードを記述できます。

puts double(4) # => 8
puts double(6 + 2) # => 16

マクロの条件分岐と反復

[編集]

Crystalのマクロは、条件分岐や反復処理を行うことができます。以下に、それぞれの構文と使用例を示します。

条件分岐

[編集]

条件分岐は、if/elsif/elseやcase/when/elseのような構文を使用することができます。マクロ内では、条件分岐によって生成するコードを制御することができます。

if/elsif/else
[編集]
macro is_positive(number)
  {% if number > 0 %}
    puts "The number is positive"
  {% elsif number == 0 %}
    puts "The number is zero"
  {% else %}
    puts "The number is negative"
  {% end %}
end

is_positive(5)  # => The number is positive
is_positive(0)  # => The number is zero
is_positive(-5) # => The number is negative
case/when/else
[編集]
macro to_japanese_weekday(day)
  {% case day %}
  {% when "Sunday" %}
    "日曜日"
  {% when "Monday" %}
    "月曜日"
  {% when "Tuesday" %}
    "火曜日"
  {% when "Wednesday" %}
    "水曜日"
  {% when "Thursday" %}
    "木曜日"
  {% when "Friday" %}
    "金曜日"
  {% when "Saturday" %}
    "土曜日"
  {% else %}
    raise "Invalid weekday: \#{day}"
  {% end %}
end

to_japanese_weekday("Monday") # => "月曜日"
to_japanese_weekday("foobar") # => Invalid weekday: foobar (RuntimeError)

反復処理

[編集]

反復処理は、whileやuntil、forのような構文を使用することができます。マクロ内では、反復処理によって生成するコードを制御することができます。

while
[編集]
macro countdown(start)
  {% while start >= 0 %}
    puts "\#{start}..."
    {% start -= 1 %}
  {% end %}
  puts "Blast off!"
end

countdown(3)
# => 3...
#    2...
#    1...
#    Blast off!

until

[編集]
macro countdown(start)
  {% until start < 0 %}
    puts "\#{start}..."
    {% start -= 1 %}
  {% end %}
  puts "Blast off!"
end

countdown(3)
# => 3...
#    2...
#    1...
#    Blast off!
for
[編集]
macro fizzbuzz(n)
  {% for i in 1..n %}
    {% if i % 15 == 0 %}
      puts "FizzBuzz"
    {% elsif i % 3 == 0 %}
      puts "Fizz"
    {% elsif i % 5 == 0 %}
      puts "Buzz"
    {% else %}
      puts i
    {% end %}
  {% end %}
end

fizzbuzz(15)
# => 1
#    2
#    Fizz
#    4
#    Buzz
#    Fizz
#    7

マクロ p!

[編集]

メソッド p は、与えられた「式」の inspaect() の返す値を puts しますが、マクロ p! は、それに先んじて(評価前の)「式」を表示します[11]

p!の例
x, y = true, false

p! x,y,x && y, x || y, x ^ y, !x, x != y, x == y

ary = [ 1, 2, 3 ]

p! ary

p! ary.map(&. << 1)

p! ary.map(&.to_f)
実行結果
x      # => true
y      # => false
x && y # => false
x || y # => true
x ^ y  # => true
!x     # => false
x != y # => true
x == y # => false
ary # => [1, 2, 3]
ary.map(&.<<(1)) # => [2, 4, 6]
ary.map(&.to_f) # => [1.0, 2.0, 3.0]

入れ子のp!

[編集]

マクロ p! は入れ子に出来ます。また、一旦ASTに変換してから再度ソースコードに変換するので、等価な別の構文に変換されることがあります。

入れ子のp!
p! (
  100.times{|i|
    p! i
    break i if i > 12
  }
)
実行結果
(100.times do |i|
 p!(i)
 if i > 12
   break i
 end
end) # => i
# => 0
i # => 1
i # => 2
i # => 3
i # => 4
i # => 5
i # => 6
i # => 7
i # => 8
i # => 9
i # => 10
i # => 11
i # => 12
i # => 13
13

まとめ

[編集]
# 引数の数
macro count_args(args)
  {{ args.size }}
end

# テンプレート
macro tpl(value)
  {% if value.is_a?(String) %}
    "{{ value }}"
  {% else %}
    {{ value }}
  {% end %}
end

# インスタンス変数
macro instance_var(name)
  @{{ name }}
end

# 繰り返し
macro loop(num, &block)
  {% for i in 0...num %}
    {{ block.call(i) }}
  {% end %}
end

# if-else文
macro ifelse(condition, if_block, else_block)
  {% if condition %}
    {{ if_block }}
  {% else %}
    {{ else_block }}
  {% end %}
end

# while文
macro while(condition, &block)
  {% while condition %}
    {{ block.call }}
  {% end %}
end

# switch文
macro switch(value, cases)
  {% for case in cases %}
    {% if case[:value] == value %}
      {{ case[:block] }}
    {% end %}
  {% end %}
end

# 変数の存在確認
macro defined?(name)
  {{ "@#{name}" rescue nil }}
end

## これらのマクロは、Crystalのプログラム中で使用することができます。たとえば、以下のように使用することができます。

puts count_args("one", "two", "three")  # 3

puts tpl(123)  # 123
puts tpl("hello")  # "hello"

puts instance_var("name")  # @name

loop(5) do |i|
  puts i
end

ifelse(true, "It is true", "It is false")

i = 0
while i < 10 do
  puts i
  i += 1
end

switch(2, [
  { value: 1, block: "It is 1" },
  { value: 2, block: "It is 2" },
  { value: 3, block: "It is 3" }
])

puts defined?("name")  # @name
puts defined?("nonexistent")  # nil

ジェネリック

[編集]

Crystalにおけるジェネリックは、型安全性を維持しながらコードの再利用を可能にする強力な機能です。ジェネリックを使用することで、特定のデータ型に依存せずに、さまざまなデータ型に対応したクラスやメソッドを定義することができます。

ジェネリックの基本的な使い方

[編集]

ジェネリックは、クラスやメソッドの定義時に型パラメータを使用して実装されます。以下は、基本的なジェネリックのクラスの例です:

class Box(T)
  def initialize(@item : T)
  end

  def item : T
    @item
  end
end

# 使用例
int_box = Box(Int32.new(42))
puts int_box.item  # => 42

string_box = Box("Hello")
puts string_box.item  # => "Hello"

この例では、Boxクラスは型パラメータTを持ち、任意の型のアイテムを保持することができます。これにより、整数型や文字列型のボックスを作成することが可能になります。

ジェネリックメソッド

[編集]

メソッドでも同様にジェネリックを定義することができます。以下は、ジェネリックメソッドの例です:

def swap(T1, T2)(a : T1, b : T2) : Tuple(T2, T1)
  { b, a }
end

# 使用例
x, y = 1, 2
swapped = swap(x, y)
puts swapped # => {2, 1}

このswapメソッドは、異なる型の引数を受け取り、入れ替えた結果を返します。このように、ジェネリックメソッドを使用することで、型に依存せずに再利用可能なコードを書くことができます。

ジェネリック制約

[編集]

ジェネリックにおいて、型に制約を設けることも可能です。特定のモジュールやクラスを含む型のみを受け取るようにすることができます。以下は、制約を持つジェネリックの例です:

module Comparable
  def compare(other : self) : Int32
    # 比較ロジック
  end
end

class OrderedBox(T)
  # TがComparableモジュールを実装していることを要求
  {{ T < Comparable }}

  def initialize(@item : T)
  end

  def compare(other : T) : Int32
    @item.compare(other)
  end
end

この例では、OrderedBoxクラスは、型TComparableモジュールを実装していることを要求しています。これにより、OrderedBoxのインスタンスに格納されたアイテムが比較可能であることが保証されます。

ジェネリックの利点

[編集]
  • コードの再利用性: 一度の定義で複数のデータ型に対応できるため、冗長なコードを避けることができます。
  • 型安全性: ジェネリックを使用することで、型エラーをコンパイル時に検出できるため、実行時エラーを減少させることができます。
  • 柔軟性: 型に依存しないコードを記述できるため、さまざまな状況に適応する柔軟なプログラミングが可能です。

Crystalにおけるジェネリックは、これらの利点を生かして、よりクリーンでメンテナンス性の高いコードを実現するための重要な機能です。

附録

[編集]

脚註

[編集]
  1. ^ Contributors”. github.com. 2022年7月18日閲覧。
  2. ^ Brian J., Cardiff (2013年9月9日). “Type inference part 1”. crystal-lang.org. 2022年7月18日閲覧。
  3. ^ Crystalのソースファイルの拡張子は.cr です
  4. ^ go like defer #2582
  5. ^ crystalコンパイラーを始めとする、crystalの言語処理系(標準ライブラリーを含む)とツールチェインやユーティリティーなどは、crystal自身で書かれています。
  6. ^ Literals
  7. ^ "For" Loop support #830
  8. ^ Macros - Crystal
  9. ^ Operatorsaccess-date:2022-07-22
  10. ^ for も do-while も loop もありません。
  11. ^ def p!(*expressions) : Nop

外部リンク

[編集]