【Crystalと○○】Crystalと特殊な変数
Crystalでは値を保持する枠組みとして、ローカル変数、インスタンス変数、クラス変数と定数を利用できますが、今回はこれらとは別にコンパイラが用意する特殊な変数をご紹介しましょう。
$
で始まる特殊な変数
Rubyでは、$
で始まる変数名はグローバル変数ですが、Crystalにはグローバルスコープを持った変数は存在しません。Crystalではこれらを特殊変数(Special Variables)と呼びます。
特殊変数はなんらかのトリガ(外部コマンドの実行や正規表現のマッチング)に応じて値がセットされる変数で、トリガがメソッド内で発生した場合は値を参照可能なのがそのメソッド内に限られるなど、特殊変数のスコープはローカル変数とよく似ています。
$?
トップレベルで定義されている system()
メソッドや、`
メソッドなどで外部コマンドを実行した際に、そのコマンドの終了ステータスが Process::Status 型で格納されます。
`ls exist_file`
$? #=> <Process::Status:0x103f7fc60 @exit_status=0>
`ls non_exist_file`
$? #=> <Process::Status:0x103f7fb20 @exit_status=256>
$?.exit_status == 0
もしくは、 $?.success?
で、直前のコマンドが正常終了したかどうかを判別可能です。
一度も、外部コマンド実行しない状態で $?
を参照すると、実行時に例外(NilAssertionError)が発生します。
$?
#=> Unhandled exception: Nil assertion failed (NilAssertionError)
$~
直線に行われた正規表現のマッチング結果が Regex::MatchData 型で格納されます。
"abcde" =~ /cd/
#=> 2
$~
#=> Regex::MatchData("cd")
正規表現が行われていなかったり、直前に行われた正規表現のマッチングで対象の文字列がマッチしなかった場合、$~
を参照すると例外(NilAssertionError)が発生します。
"abcde" =~ /ef/
#=> nil
$~
#=> Unhandled exception: Nil assertion failed (NilAssertionError)
$整数
直前に行われた正規表現のマッチングにおいて、正規表現パターンがキャプチャグループ(()
で括られた部分)を持つ場合、マッチに成功すると n 番目のキャプチャ結果を $n
という変数で参照できます。
"abcde" =~ /a(.+)d/
$1
#=> "bc"
$~
と同様、正規表現が行われていなかったり、直前に行われた正規表現のマッチングで対象の文字列がマッチしなかった場合、例外(NilAssertionError)が発生します。
"abcde" =~ /a(.+)f/
$1
#=> Unhandled exception: Nil assertion failed (NilAssertionError)
また、キャプチャグループの数より大きい n を指定すると、IndexError が発生します。
"abcde" =~ /a(.+)d/
$2
#=> Unhandled exception: Invalid capture group index: 2 (IndexError)
__
で括られた特殊な変数
大文字スネークケースの前後を __
で括った特殊な変数(Rubyだと擬似変数と言ったりしますね)には、以下の4種類が存在します。
__FILE__
__DIR__
__LINE__
__END_LINE__
これらはいずれも、参照されたソースファイルやそのファイル内での参照位置に関する情報が格納される変数です。
ただ、__FILE__
や __DIR__
などは利用シーンがそれなりにみられますが、__LINE__
や __END_LINE__
となるとCrystalのコード自体を処理対象とするような特殊な例を除き、利用シーンはかなり限られそうです。
__FILE__
__FILE__
を記述したソースファイルのフルパスが文字列で格納されています。
プロジェクトディレクトリ内の別ファイルを自ファイルからの相対パスで指定したい場合などに便利です。
__FILE__
#=> "/path/file.cr"
メソッド引数のデフォルト値として使用した場合、格納されるのは __FILE__
を記述したファイルではなく、メソッド呼び出しを記述したファイルのパスになります。
def file_path(path = __FILE__)
path
end
require "./file_method.cr"
file_path
#=> "/path/file_caller.cr"
# __FILE__ は file_method.cr 内に記述されているが、呼び出し側の file_caller.cr が返る
__DIR__
使用したソースファイルが置かれたディレクトリのフルパスが文字列で格納されています。
File.dirname(__FILE__)
と等価で、利用シーンもあまり変わりません。
__DIR__ #=> "/tmp"
メソッド引数のデフォルト値として使用した場合、格納されるのは __DIR__
を記述したファイルではなく、メソッド呼び出しを記述したファイルが置かれたディレクトリのパスになります。
def dir_path(path = __DIR__)
path
end
require "./sub/__dir__method.cr"
dir_path
#=> "/path"
# __DIR__ は /path/sub 内のファイルに記述されているが、呼び出し側のファイルがある /path が返る
__LINE__
参照した時点の行番号が整数値で格納されています。
__LINE__
#=> 1
__LINE__
#=> 3
メソッド引数のデフォルト値として使用した場合、__LINE__
が記述された行ではなく、メソッド呼び出しが記述された行数が参照されます。
def __line__(line = __LINE__)
line
end
__line__
#=> 5
# __LINE__ が記述された1行目ではなく、呼び出し側の5行目が返る
__END_LINE__
__END_LINE__
は __
で始まる変数の中でもことさらに特殊な変数で、普通に参照しようとするとエラーが発生します。
__END_LINE__
#=> Error: __END_LINE__ can only be used in default argument value
エラーメッセージが示す通り、この __END_LINE__
はメソッド引数のデフォルト値としてしか利用できません。
メソッド引数のデフォルト値として __END_LINE__
を指定すると、メソッド呼び出しの 最終行 の行数が格納されます。
例えば、メソッドの呼び出しが1行で完結している場合は、上記 __LINE__
と同じ挙動になります。
def foo(end_line = __END_LINE__)
end_line
end
foo
#=> 5
一方、メソッド呼び出しが複数行にまたがっている場合は、その最終行の行数が格納されます。
def foo(a, b, c, end_line = __END_LINE__)
end_line
end
foo("arg a",
"arg b",
"arg c") # <- この行
#=> 7
また、ブロックを持ったメソッドの引数で使用された場合、__END_LINE__
にはブロック定義の最終行の行数が格納されます。
def foo(end_line = __END_LINE__, &block)
yield end_line
end
foo do |end_line|
bar #=> 7
end # <- この行
つまり、メソッドに対して、__FILE__
, __LINE__
, __END_LINE__
をそれぞれデフォルト値として持つ引数を定義すると、そのメソッド呼び出しの記述が、どのファイルの何行目から何行目までに記述されているかを、メソッド側が認識可能になります。
def location(path = __FILE__, line = __LINE__, end_line = __END_LINE__)
yield "#{path} #{line}:#{end_line}"
end
require "./location.cr"
location do |loc|
loc #=> "/path/location_callsr.cr 3:5"
end
☠️ 特殊な変数への代入
特殊な変数は基本的にコンパイラが値を割り当てるもので、ユーザが自身のコード内で値を代入することは想定されていません。
一部の特殊変数は状況によっては値を代入できてしまいますが、その後のエラーの原因となりますので、原則としてこれらの変数への代入はするべきではありません。
def foo
$? = 1
end
foo
`ls`
Module validation failed: Call parameter type does not match function signature!
%"$?" = alloca %"(Int32 | Process::Status | Nil)", !dbg !9
%"(Int32 | Nil)"* %58 = call i32 @"*foo:Int32"(%"(Int32 | Process::Status | Nil)"* %"$?"), !dbg !81
Call parameter type does not match function signature!
%"$?" = alloca %"(Int32 | Process::Status | Nil)", !dbg !9
%"Process::Status"** %59 = call %String* @"*`<String>:String"(%String* bitcast ({ i32, i32, i32, [3 x i8] }* @"'ls'" to %String*), %"(Int32 | Process::Status | Nil)"* %"$?"), !dbg !82
(Exception)
from raise<Exception>:NoReturn
from raise<String>:NoReturn
from Crystal::CodeGenVisitor#finish:Nil
from Crystal::Compiler#codegen<Crystal::Program, Crystal::ASTNode+, Array(Crystal::Compiler::Source), String>:(Tuple(Array(Crystal::Compiler::CompilationUnit), Array(String)) | Nil)
from Crystal::Compiler#compile<Array(Crystal::Compiler::Source), String>:Crystal::Compiler::Result
from Crystal::Command#run_command<Bool>:Nil
from Crystal::Command#run:(Bool | Nil)
from __crystal_main
from main
Error: you've found a bug in the Crystal compiler. Please open an issue, including source code that will allow us to reproduce the bug: https://github.com/crystal-lang/crystal/issues
今回のまとめ
-
$
始まりや__
で括られた特殊な変数がある - それらはコンパイラが用意するもので、ユーザが値を設定することは原則想定されない
Discussion