Crystal言語 2019年の破壊的変更
2019/12/20
この記事は Crystal Advent Calendar 2019 の20日目のエントリです。
2019年のCrystal
2018年末に 0.27.0 だったCrystalのバージョンも、2019年12月19日時点で 0.32.1 までアップデートされています。
2019年の Crystal コンパイラに絡む特に大きなトピックは、マルチスレッド対応に道筋が付けられたことでしょう。現時点ではプレビュー機能としての実装で、使用するには専用のコンパイルフラグ(-Dpreview_mt
)が必要ですが、これまで単一スレッドでTSS的に処理を切り替えながら行っていた並行処理を、複数スレッドに振り分けて並列に処理できるようになりました。
とはいえ、これ以外にも各リリースでは細かなバグ修正や機能追加が行われており、同時に言語仕様やコンパイラの動作に対する破壊的な変更も加わっています。
実は、個人的に2019年以降にリリースされた Crystal コンパイラの CHANGELOG の雑訳を公開しています。
ここでは、その中から (breaking change)(破壊的変更)タグがついた項目をピックアップしてみることにします。
v0.28.0
Enum
の定義内でメンバーの区切りに空白文字を使用できなくなり、改行文字、;
、,
だけが使用可能になった(,
の使用は非推奨で、フォーマッタによって ;
に置き換えられる)(#7607、#7618)
# 今後はエラーになる
enum A
B C D
end
# これはOK(ただし非推奨)
enum A
B, C, D
end
# これならOK
enum A
B; C; D
end
# 普通はこう書くよね
enum A
B
C
D
end
フラグとして注釈された Enum
型で、Enum.from_value
を使って None
を生成できるようになった(#6516)
フラグ注釈(@[Flags]
)付きで Emun
を定義すると、Enum
の各メンバーをビットフラグのように扱って複数のフラグの重ね合わせを表現できるようになる。
@[Flags]
enum Color
Blue #=> 1
Red #=> 2
Green #=> 4
end
cyan = Color::Blue | Color::Red
cyan2 = Color.from_value(3)
cyan == cyan2 #=> true
同時にどのフラグも立っていない状態(上の例だとColor::None
)と全てのフラグが立っている状態(Color::All
)が暗黙的に定義される。
Color.from_value
に 0
を与えた場合、それまでは明示的な 0
に対応するフラグが定義されていないため Unknown enum Color value: 0
なっていたのが、Color::None
を返してくれるようになった。
PartialComparable
に非推奨である旨の注釈を追加(#7664)
ある型(T
)のオブジェクトとの間で大小関係を比較可能である場合に mix-in する Comparable(T)
モジュールに対して、その型のオブジェクトにの中に比較可能なものと比較不可能なものが混在する場合に mix-in する PartialComparable(T)
モジュールが用意されていたが、PartialComparable
の機能を Comparable
へ統合するにあたり、このバージョンからは非推奨となっている。
Int#/
が v0.29.0 からは Float
型の結果を返すようになるため、整数として除算結果を取得したい場合は Int#//
を使用するようにとのワーニングメッセージを追加(#7639)
最終的に Int#/
が整数演算(除算結果を整数に丸める)ではなく算術演算(除算結果が少数になる)とする前段階として、v0.27.0 で整数演算結果を返す Int#//
が実装された。
この時点では、Int#/
はまだ整数演算を行っているが、ワーニングを有効にすると Int#/
を使用した際に警告メッセージが出るようになった。
Array#sort
が #<=>
メソッドだけを使用するようなり、部分的に比較可能な場合に #<=>
が nil
を返せるようにした(#6611)
2つ上で書いた PertialComparable
を Comparable
へ統合する実装と、これまで Array#sort
の要素比較に >
や =>
などが使われていたのを、基本的に <=>
のみを使用するように変更。
Iterator#rewind
をなくして要素が循環するよう変更(#7440)
Iterator(T)
モジュールには要素の先頭に戻る #rewind
が抽象メソッドとして定義されていて、mix-in した型全てで #rewind
の実装を個別に行う必要があった。その割に #rewind
の利用シーンは多くなく、そのほとんどが next
で最後に到達したイテレータを先頭に巻き戻して循環させるというものだったので、#rewind
の定義を無くして、要素が循環するオブジェクトを返す #cycle
メソッドが追加された。
これに伴い、標準ライブラリのソースコードから大量の #rewind
実装が除去されている。
YAML#libyaml_version
の返り値の型を SemanticVersion
へ変更(#7555)
セマンティックバージョニングに応じたバージョン比較が可能に。
Time
型のコンストラクタ名を変更。Time.now
は非推奨となり、Time.utc
もしくは Time.local
の使用を推奨(#5346、#7586)
now
ってどこ時間よ? みたいな議論があった模様。
日数やその他の単位で時刻を変更する Time#add_span
の名前を Time#shift
へ変更(#6598)
Time#date
が Tuple
({year, month, day}
)を返すよう変更。(#5822)
これまではその時刻が属する日の午前0時を返していた(Crystal には Date
型は存在しない)が、これはあらゆる意味で date
(日付)ではない。と言うことで、シンプルに「年」「月」「日」をタプルで返す実装に変更になった。
IO
型の flush_on_newline
プロパティを廃止、TTYデバイスの時も sync
プロパティを使う(#7470)
STDOUT
や STDERR
が改行が入るごとに flush していたため、出力を直接ファイルへリダイレクトしていても速度が出ない状態だった。
今回、STDOUT
や STDERR
が TTY デバイスであれば sync
を有向にし、そうでなければ sync
が無効になるようになった。
HTTP::Multipart
を MIME::Multipart
に変更(#7085)
OAuth2エラー時のJSONパースを廃止(#7467)
RFC だと USASCII で記述された単一のエラーコードを指定しなければいけない error
に JSON オブジェクトをぶち込んでくる行儀の悪いサービス(Faceb○○k とか)がいるため。
RequestProcessor
のコネクション再利用ロジックを修正(#7055)
HTTP.default_status_message_for(Int)
を HTTP::Status.new(Int).description
で置き換え(#7247)
URI
の実装上の問題を修正(#6323)
パスやスキーマ、ポート指定なんかの処理がRFCなどに準拠する形で整理された。
OpenSSL::HMAC
の digest
/hexdigest
におけるアルゴリズム指定に、シンボルではなく OpenSSL::Algorithm
型を使用するよう変更し、LibCrypt の PKCS5_PBKDF2_HMAC
メソッドに対応(#7264)
予め提示されているいくつかの候補から選択するような場合は、シンボルより専用の enum を使った方が良いよね。
v0.29.0
String#at
メソッドが廃止予定になり、String#char_at
メソッドの使用を推(#7633)
at
が示す対象が明確に。
String#to_i
メソッドは先頭に 0o
がある場合のみ8進数として解釈するようになった(#7691)
それまでは String#to_i
に prefix: true
を指定して実行すると、先頭が "0"
である場合に8進数として解釈されていたが、そうすると"0".to_i(prefix: true)
がエラーになってしまっていた。
String#to_i
メソッドの引数のいくつかを Bool
型のみ受け取るように制限(#7436)
引数のデフォルト値指定はあったものの、型制約がなかったせいで、ぶっちゃけなんでも指定できていた。
Slice#pointer
を削除(#7581)
Slice#pointer
は引数として size
を一つとる。size
の値がその Slice
のインデックス範囲外であれば例外を発生させ、そうでなければ size
の値に関係なく 先頭要素の ポインタを返す、という仕様だった。たしかにこの挙動は混乱を招く。
元々、同じポインタを返す Slice#to_unsafe
があったこともあり廃止に。
File::DEVNULL
を File::NULL
に名称変更(#7778)
IO.copy
メソッドが UInt64
型の値を返すようになった(#7660)
64bit 対応。
Crypto::Bcrypt::Password#==
メソッドを #verify
へ名称変更(#7790)
require 時の相対パスの解決方法を修正(#7758)
代入式の左辺が ‘!’ や ‘?’ で終わることを禁止(#7582)
クラス変数やインスタンス変数はすでに !
や ?
で終わる名前が禁止されていたし、メソッド名にも foo!=
や bar?=
は定義できなかったのに、ローカル変数だけできてたのが明確に禁止された。
モジュールが絡んだ場合の new
/initialize
メソッドの検索順を修正(#7818)
ある型に固有の #initialize
が定義されていなければ、引数なしでなにもしない #initialize
があるものとして引数しのコンストラクタ(A.new
)を使うことができるが、引数付きの #initialize(x : Int32)
などが定義されている場合には、引数なしのコンストラクタはエラーになる。
それまでは、モジュール内で定義した引数付きの #initialize
があっても、モジュールを include した型で引数なしのコンストラクタが使えてしまっていて、これが正しくエラーを返すようになった。
module B
def initialize(x : Int32)
end
end
class A
include B
end
# 0.28.0 まではOK、0.29.0からエラー
A.new
抽象構造体やモジュールの sizeof
を事前計算しないよう変更(#7801)
mix-in 先や継承先の具象型の構成次第で必要なサイズが変わってくるため。
v0.30.0
抽象メソッドは返り値の型を明示しなければならなくなった(#7956、#7999、#8010)
このことにより、抽象メソッドの実装時に正しい型の値を返していることをコンパイラがチェックしてくれるようになった。
複数行にまたがった範囲リテラルは使用できなくなった(#7888)
1..
2
は 1..2
の範囲オブジェクトとしては認識されなくなった。
開始条件もしくは終了条件がない範囲オブジェクトを定義できるようになったため、1..
はそれ単体が 1
から終了条件がない範囲オブジェクトとして正しい記述になり、1..
と 2
という2つのオブジェクトになる。
ダングリングポインタ問題を解決するため、UUID#to_slice
を無くし UUID#bytes
を追加(#7901)
JSON: トークンやパーサの種別に Symbol
型でなく専用の Enum
型を使用するよう変更(#7966)
無効な値が渡されることをコンパイラレベルで防止できる。
URLエンコードを強化。URI.escape
と URI.unescape
は、それぞれ URI.encode_www_form
と URI.decode_www_form
へ名称変更され、URI.encode
と URI.decode
を追加(#7997、#8021)
v0.31.0
標準ライブラリから Markdown
を除去(#8115)
コンパイラがドキュメント生成用に使う目的で実装されたものだったんだけど、機能が限定的だったり Markdown の仕様に完全には準拠できていなかったりするので、ひとまず標準添付からは外された。(ドキュメント生成用には、コンパイラのソース側に Crystal::Doc::Markdown
として機能が残されている)
代替は、icyleaf/markd あたり?
OptionParser#parse!
は廃止予定となり、代わりに OptionParser#parse
を使用するように(#8041)
引数をとる OptionParser.parse(args)
と、引数を取らない OptionParser.parse!
があって、!
付きは ARGV
を対象として解釈する実装だった。
今回、OptionParser.parse(args = ARGV)
とすることで、引数がない場合に OptionParser.parse!
と同じ挙動をするようになった。
固定長整数のオーバーフロー発生が標準動作になった(#8170)
ずっと以前は、固定調整数が表現可能な範囲を超える演算結果(Int32::MAX + 1
とか)に対しては結果が保証されなかった。
v0.27.1 以降、オーバーフローチェック機能が実装されて、-Dpreview_overflow
フラグをつけてコンパイルするとオーバーフローエラーが出せるようになっていたけれど、今回オーバーフローエラーを出す動作が標準になった。
逆にオーバーフローチェック絵を行わない -Ddisable_overflow
が追加されたが、追々こちらもなくなる模様。
/
が全ての型で算術除算結果を返すようになった(#8120)
Int#/
を含め、数値の除算がすべて算術演算結果を返すよう、動作が変更されている。
XML::Type
の名称を XML::Node::Type
へ変更し、XML::Reader::Type
を導入(#8134)
Type
って何の? 問題。
HTTP::Server::Response#respond_with_error
を #respond_with_status
に置き換え(#6988)
渡すステータス情報は別にエラー(4XX
、5XX
)じゃなくても構わないし。
HTTP::Request.from_io
が非常に長いURIや非常に大きなヘッダフィールドを正しく処理できるようにして、HTTP::Request::BadRequest
を廃止(#8013)
長すぎるURLや大きすぎるHTTPヘッダといったよろしくないリクエストに対して、とりあえず HTTP::Request::BadRequest
を返していたのを、状況に応じて
414 Request-URI too long
や 431 Request Header Fields Too Large
といった適切なステータスを返すようになった。
spec で定義するブロックに focus
のサポートを追加. (#8125、#8178、#8208)
spec を定義する describe
、context
、it
の各ブロックに、forcus: true
を追加で指定できるようになった。
forcus: true
が指定されたブロックが存在する場合、forcus: true
が指定されたブロックないのテストのみが実行される。
v0.32.0
標準ライブラリから Readline
を除去(#8364)
今後は crystal-lang/crystal-readline から shard として利用可能。
文字列リテラルの式展開を String.interpolation
メソッドを呼ぶ方式に変更(#8400)
これまでは String.build
を使って逐次追加していく処理がコンパイラ自身のコードとして実装されていた。こうすることで、コンパイラのソースを変更せずに式展開の処理を変更できるようになる。
String#codepoint_at(index)
を廃止。 今後は#char_at(index).ord
を使用(#8475)
あまり使われないショートカットメソッドな上に、本来の記述の方が文字数が少ない。
s = "Hello"
p s.codepoint_at(0) #=> 72
p s.char_at(0).ord #=> 72
Enumerable#grep
を廃止。今後は Enumerable#select
を使用。(#8452)
XML::Reader#expand
が展開できない場合に例外を出すようになり、これまでと同様の動作をする XML::Reader#expand?
を導入(#8186)
File.expand_path
と Path#expand
が標準ではホームディレクトリ(~
)を展開しなくなり、必要であれば引数で明示する方式に変更された(#7903)
home: false
がデフォルト状態で、~
を展開しない。
home: true
は ~
を実行ユーザのホームディレクトリ(Path.home
)に展開する。
Path
型か String
型で home:
を指定すると、そのパスをホームディレクトリとして ~
を展開する。
OpenSSL::DigestBase#base64digest
が Base64.strict_encode
を使用して改行を含まない結果を返すように変更(#8215)
雑感
今年は、マルチスレッド対応以外に利用者が直面する機能面での大きな変更というと、固定長整数のオーバーフローが標準動作になったことでしょうか。
そのほかは全体を通して、メソッド名の変更や統廃合、ライブラリの取捨選択などが多く行われており、v1.0の仕様凍結を見据えたAPIのブラッシュアップが進んでいる印象です。
v1.0 に向けた2つの大きな課題が「マルチスレッド」と「Windows対応」ですが、前者はほぼ目処がついたように思います。残る「Windows対応」についても必要なコアライブラリのほぼ6割(13/22)はでき上がっているようですので、今後の進捗に期待ですね。