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 形の例外処理をサポートします。
- 演算子オーバーロード
- サポートします。
- メソッドオーバーロード
- 同じ名前で引数の数が異なるメソッドを定義することが可能です。
- 型推論
- サポートします。
- ガベージコレクション
- サポートします。
- クラス
- クラスベースのオブジェクト指向言語です。
- 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
File.open
メソッドを使用して、ファイルを開きます。引数にファイルのパスを指定します。これにより、file
変数にファイルのインスタンスが格納されます。gets_to_end
メソッドを使用して、ファイルの内容を読み込みます。これにより、contents
変数にファイルの内容が格納されます。puts
メソッドを使用して、ファイルの内容を標準出力に出力します。- ファイルを開く際に
File.open
メソッドを使用した場合は、File.close
メソッドを使用してファイルを閉じる必要があります。
ファイルの読出し(2)
[編集]File.open
にブロックを渡すと、ブロックを抜けると自動的にファイルが閉じられます。明示的なファイルのクローズが不要です。
File.open("filename.txt") { |file| contents = file.gets_to_end puts contents }
- File.openメソッド:
File.open("filename.txt")
は、指定されたファイルを開きます。このメソッドはブロックを取り、ブロック内でファイルを操作した後、自動的にファイルを閉じます。ブロックの終了時にファイルが閉じられるため、明示的なファイルのクローズが不要です。
- ファイル操作:
|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 run
やcrystal
コマンドは、開発中にプログラムを素早く試す際には便利ですが、実際のデプロイメントや大規模なプロジェクトのビルドには、通常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
[編集]big
を require
すると 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 + b
は a.+(b)
と意味的に同じで、引数 b を持つ a のメソッド + を呼び出すことになります。
種類 | 演算子 |
---|---|
インデックス アクセサー | [] , []?
|
単項 | + , &+ , - , &- , ! , ~
|
指数 | ** , &**
|
乗除 | * , &* , / , // , %
|
加減 | + , &+ , - , &-
|
シフト | << , >>
|
ビット間 AND | &
|
ビット間 OR/XOR | | ,^
|
等値 | == , != , =~ , !~ , ===
|
比較 | < , <= , > , >= , <=>
|
論理 AND | &&
|
論理 OR | ||
|
Range | .. , ...
|
条件 | ?:
|
代入 | = , []= , += , &+= , -= , &-= , *= , &*= , /= , //= , %= , |= , &= ,^= ,**= ,<<= ,>>= , ||= , &&=
|
スプラット | * , **
|
制御構造
[編集]制御構造(せいぎょこうぞう、control flow)とは、「順次」「分岐」「反復」という基本的な処理のことを言います。
では「条件式」が真・偽はどの様に決まるのでしょう?
Crystalでは false
あるいは nil
であると偽、それ以外が真です。
0
も []
(空のArray) も {}
(空のNamedTuple)も真です。条件分岐
[編集]Crystalの条件分岐には、if, until と caseの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 += 1
はi = 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
クラスは、型T
がComparable
モジュールを実装していることを要求しています。これにより、OrderedBox
のインスタンスに格納されたアイテムが比較可能であることが保証されます。
ジェネリックの利点
[編集]- コードの再利用性: 一度の定義で複数のデータ型に対応できるため、冗長なコードを避けることができます。
- 型安全性: ジェネリックを使用することで、型エラーをコンパイル時に検出できるため、実行時エラーを減少させることができます。
- 柔軟性: 型に依存しないコードを記述できるため、さまざまな状況に適応する柔軟なプログラミングが可能です。
Crystalにおけるジェネリックは、これらの利点を生かして、よりクリーンでメンテナンス性の高いコードを実現するための重要な機能です。
附録
[編集]脚註
[編集]- ^ “Contributors”. github.com. 2022年7月18日閲覧。
- ^ Brian J., Cardiff (2013年9月9日). “Type inference part 1”. crystal-lang.org. 2022年7月18日閲覧。
- ^ Crystalのソースファイルの拡張子は.cr です
- ^ go like defer #2582
- ^ crystalコンパイラーを始めとする、crystalの言語処理系(標準ライブラリーを含む)とツールチェインやユーティリティーなどは、crystal自身で書かれています。
- ^ Literals
- ^ "For" Loop support #830
- ^ Macros - Crystal
- ^ Operatorsaccess-date:2022-07-22
- ^ for も do-while も loop もありません。
- ^ def p!(*expressions) : Nop
外部リンク
[編集]- The Crystal Programming Language — 公式サイト
- ドキュメント
- Compile & run code in Crystal — playground