電柱商事

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_value0 を与えた場合、それまでは明示的な 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つ上で書いた PertialComparableComparable へ統合する実装と、これまで 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#dateTuple{year, month, day})を返すよう変更。(#5822

これまではその時刻が属する日の午前0時を返していた(Crystal には Date 型は存在しない)が、これはあらゆる意味で date(日付)ではない。と言うことで、シンプルに「年」「月」「日」をタプルで返す実装に変更になった。

IO 型の flush_on_newline プロパティを廃止、TTYデバイスの時も sync プロパティを使う(#7470

STDOUTSTDERR が改行が入るごとに flush していたため、出力を直接ファイルへリダイレクトしていても速度が出ない状態だった。

今回、STDOUTSTDERR が TTY デバイスであれば sync を有向にし、そうでなければ sync が無効になるようになった。

HTTP::MultipartMIME::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::HMACdigest/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_iprefix: 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::DEVNULLFile::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.escapeURI.unescape は、それぞれ URI.encode_www_formURI.decode_www_form へ名称変更され、URI.encodeURI.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

渡すステータス情報は別にエラー(4XX5XX)じゃなくても構わないし。

HTTP::Request.from_io が非常に長いURIや非常に大きなヘッダフィールドを正しく処理できるようにして、HTTP::Request::BadRequest を廃止(#8013

長すぎるURLや大きすぎるHTTPヘッダといったよろしくないリクエストに対して、とりあえず HTTP::Request::BadRequest を返していたのを、状況に応じて 414 Request-URI too long431 Request Header Fields Too Large といった適切なステータスを返すようになった。

spec で定義するブロックに focus のサポートを追加. (#8125#8178#8208

spec を定義する describecontextit の各ブロックに、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_pathPath#expand が標準ではホームディレクトリ(~)を展開しなくなり、必要であれば引数で明示する方式に変更された(#7903

home: false がデフォルト状態で、~ を展開しない。

home: true~ を実行ユーザのホームディレクトリ(Path.home)に展開する。

Path 型か String 型で home: を指定すると、そのパスをホームディレクトリとして ~ を展開する。

OpenSSL::DigestBase#base64digestBase64.strict_encode を使用して改行を含まない結果を返すように変更(#8215

雑感

今年は、マルチスレッド対応以外に利用者が直面する機能面での大きな変更というと、固定長整数のオーバーフローが標準動作になったことでしょうか。

そのほかは全体を通して、メソッド名の変更や統廃合、ライブラリの取捨選択などが多く行われており、v1.0の仕様凍結を見据えたAPIのブラッシュアップが進んでいる印象です。

v1.0 に向けた2つの大きな課題が「マルチスレッド」と「Windows対応」ですが、前者はほぼ目処がついたように思います。残る「Windows対応」についても必要なコアライブラリのほぼ6割(13/22)はでき上がっているようですので、今後の進捗に期待ですね。