💎

【Crystalと○○】Crystalと特殊な変数

2021/04/23に公開

📖【Crystalと○○】コンテンツ一覧

Crystalでは値を保持する枠組みとして、ローカル変数、インスタンス変数、クラス変数と定数を利用できますが、今回はこれらとは別にコンパイラが用意する特殊な変数をご紹介しましょう。

$ で始まる特殊な変数

Rubyでは、$ で始まる変数名はグローバル変数ですが、Crystalにはグローバルスコープを持った変数は存在しません。Crystalではこれらを特殊変数(Special Variables)と呼びます。

特殊変数はなんらかのトリガ(外部コマンドの実行や正規表現のマッチング)に応じて値がセットされる変数で、トリガがメソッド内で発生した場合は値を参照可能なのがそのメソッド内に限られるなど、特殊変数のスコープはローカル変数とよく似ています。

$?

トップレベルで定義されている system() メソッドや、` メソッドなどで外部コマンドを実行した際に、そのコマンドの終了ステータスが Process::Status 型で格納されます。

存在するファイルを指定した ls コマンド実行
`ls exist_file`
$? #=> <Process::Status:0x103f7fc60 @exit_status=0>
存在しないファイルを指定した ls コマンド実行
`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__を記述したソースファイルのフルパスが文字列で格納されています。
プロジェクトディレクトリ内の別ファイルを自ファイルからの相対パスで指定したい場合などに便利です。

/path/file.cr
__FILE__
#=> "/path/file.cr"

メソッド引数のデフォルト値として使用した場合、格納されるのは __FILE__ を記述したファイルではなく、メソッド呼び出しを記述したファイルのパスになります。

/path/file_method.cr
def file_path(path = __FILE__)
  path
end
/path/file_caller.cr
require "./file_method.cr"

file_path
#=> "/path/file_caller.cr"
# __FILE__ は file_method.cr 内に記述されているが、呼び出し側の file_caller.cr が返る

__DIR__

使用したソースファイルが置かれたディレクトリのフルパスが文字列で格納されています。
File.dirname(__FILE__) と等価で、利用シーンもあまり変わりません。

/tmp/dir.cr
__DIR__ #=> "/tmp"

メソッド引数のデフォルト値として使用した場合、格納されるのは __DIR__ を記述したファイルではなく、メソッド呼び出しを記述したファイルが置かれたディレクトリのパスになります。

/path/sub/dir_method.cr
def dir_path(path = __DIR__)
  path
end
/path/dir_caller.cr
require "./sub/__dir__method.cr"

dir_path
#=> "/path"
# __DIR__ は /path/sub 内のファイルに記述されているが、呼び出し側のファイルがある /path が返る

__LINE__

参照した時点の行番号が整数値で格納されています。

/path/line.cr
__LINE__
#=> 1
__LINE__
#=> 3

メソッド引数のデフォルト値として使用した場合、__LINE__ が記述された行ではなく、メソッド呼び出しが記述された行数が参照されます。

/path/line2.cr
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__ と同じ挙動になります。

/path/end_line.cr
def foo(end_line = __END_LINE__)
  end_line
end

foo
#=> 5

一方、メソッド呼び出しが複数行にまたがっている場合は、その最終行の行数が格納されます。

/path/end_line2.cr
def foo(a, b, c, end_line = __END_LINE__)
  end_line
end

foo("arg a",
    "arg b",
    "arg c")  # <- この行
#=> 7

また、ブロックを持ったメソッドの引数で使用された場合、__END_LINE__ にはブロック定義の最終行の行数が格納されます。

/path/end_line3.cr
def foo(end_line = __END_LINE__, &block)
  yield end_line
end

foo do |end_line|
  bar #=> 7
end # <- この行

つまり、メソッドに対して、__FILE__, __LINE__, __END_LINE__ をそれぞれデフォルト値として持つ引数を定義すると、そのメソッド呼び出しの記述が、どのファイルの何行目から何行目までに記述されているかを、メソッド側が認識可能になります。

/path/location.cr
def location(path = __FILE__, line = __LINE__, end_line = __END_LINE__)
  yield "#{path} #{line}:#{end_line}"
end
/path/location_caller.cr
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