Crystalのちょっと気になるところ

## はじめに
[Crystal](https://crystal-lang.org/)という言語はご存知でしょうか。

この言語、およびその公式実装の特徴として、

 - Ruby にそっくりな書きやすい文法を持つ
 - 型アノテーションを記述できる
 - ネイティブコードにコンパイルして、実行ファイルを作成できる
 - 比較的高速
 - 標準ライブラリがとても充実している
 - 並列処理が書きやすい
 - マクロが比較的清潔で綺麗

といった点が挙げられます。

そのため、ちょっとしたスクリプトをRubyでガリガリ書くような感覚で書くだけで、高速なネイティブバイナリが得られ、
後から型アノテーション等を加えるだけで保守性が格段に上がるという点で、非常に重宝していましたし、比較的熱心なファンとして応援していました[^meeting]。

[^meeting]:日本ユーザコミュニティの第一回ミーティングに出たことが自慢です

ですが最近ふたたび仕事でガッツリ書く機会があり、その中で様々な点がまだまだ発展途上だなと感じてしまったため、今回は敢えてそうした点を挙げたいと思います。

なお、検証等には以下のバージョンの実装を使用しました

```
$ crystal --version
Crystal 1.8.2 (2023-05-09)

LLVM: 15.0.7
Default target: aarch64-apple-darwin22.5.0
```

## コンパイラの不具合

Crystal（の公式実装）を使用していると、比較的簡単にコンパイラの実装上の不具合に引っ掛かります。
その一例として、 `alias` 周りのカオスさを紹介したいと思います。

Crystal には型に別名をつける `alias` という構文が存在します。
これは元々JSONパーサの実装を目的として、型を再帰させるために[導入されたらしい](https://github.com/crystal-lang/crystal/issues/5155)のですが、実際に `alias` で再帰データ型を表現しようとすると、あらゆることがバグります。
GitHub の issue を軽く見ただけでも、コンパイラが[クラッシュする](https://github.com/crystal-lang/crystal/issues/3804), [無限再帰する](https://github.com/crystal-lang/crystal/issues/12100)などの問題があるようです。

よく当たる（そして割と困る）例としては、以下のようなものがあります。

`alias` は型の別名でしかないため、通常はエイリアス名と元の型を `==` で比較すると `true` が返ります。

```crystal
icr:1> alias T1 = Hash(String, String)
 => nil
icr:2> T1 == Hash(String, String)
 => true
```

しかし、再帰的な型を `alias` で記述すると、等価判定が意図した通りの結果を返しません。

```crystal
icr:3> alias T2 = Hash(String, T2)
 => nil
icr:4> T2 == Hash(String, T2)
 => false
```

再帰型の等価判定は理論的には割と難しいことが知られている[^recursive-type]のですが、さすがにこれくらいは直感に反しない結果を返してくれないと、思わぬところでつまづくことになってしまいます。

[^recursive-type]: らしいです

## 文法の不統一感

### 型の糖衣構文まわり

Crystalでは `T | Nil` を `T?`、`Tuple(T1,T2)` を `{T1, T2}` と書けるなど、よく使う型には便利な糖衣構文が導入されています。
また、`T* #=> Pointer(T)` や `T[N] #=> StaticArray(T, N)`  など、CFFIなどに触らないかぎりあまり使わないであろう型にまで、こうした構文が導入されています。
しかし、最もよく使う型の代表格であるはずの `Array(T)` や `Hash(T, U)`には、こうした構文が存在しません。

また、こうした構文は式が書ける箇所には記述することができないため、 `arr.is_a?(Int32[4])` はコンパイルが通り、 `arr = Int32[4].new(0)` はコンパイルが通らないといった、（仕様を知らなければ）不可解な現象に遭遇します。

### コレクションの初期化の記法

Crystal の標準ライブラリにも、他の多くの言語と同じように `Array`, `Set`, `Hash` など、複数の値をまとめる型（コレクション）が用意されています。
しかし、これらの初期化方法に統一感がまるでなく、混乱の元になってしまっている、というのが実情です。

いくつか例を挙げてみましょう。

#### Array
- `[1,2,3] of Int32`や `[1, 2, 3]`、`[] of Int32` はOKだが、`[]` はダメ（空配列は要素の型を明示する必要がある）
- `Array{1,2,3}`や`Array(Int32){1,2,3}`はOKだが、`Array(Int32){}`はダメ（`{}`は不完全な空の`Hash-like`として扱われる）
- `Int32[1,2,3]`や`Int32[]`はOKだが、`String["test"]`や`String[]`はダメ（`Int32[]` にはマクロが生えているが、`String[]`には生えていない）

#### Hash

- OKなもの
  - `{"a" => 1}`
  - `{} of String => Int32`
  - `{"a" => 1} of String => Int32`
  - `Hash(String, Int32){"a" => 1}`
  - `Hash{"a" => 1}`

- ダメなもの
  - `{} # 空のHashは型を明示する必要がある`
  - `Hash{}`
  - ``Hash(String, Int32){} # パーサの都合上どうしても`{}`の後には `of` が来てほしいらしい``

#### Set
- OKなもの
  - `Set{1,2,3}`
  - `Set(Int32){1,2,3}`
  - `Set(Int32).new`

- ダメなもの
  - `Set{} # {} はどうしても記述できない`
  - `Set(Int32){}`

#### StaticArray
- OKなもの
  - `StaticArray(Int32, 3).new { |i| [1, 2, 3][i] }`
  - `StaticArray(Int32, 3).new(0)`
  - `StaticArray(Int32, 3).new`

- ダメなもの
  - `StaticArray(Int32, 3){1, 2, 3}`
  - `StaticArray(Int32, 3){}`

## 標準ライブラリの辛いところ
### Time

Crystalの標準ライブラリには、日時を扱う型が `Time` しかなく、日付単体を扱う型が存在しません。
どうやら、日付を扱うためには、その日の `00:00:00Z` を代表値として扱うという方針があるように感じます[^time]

[^time]: `YAML` のデシリアライザの挙動などに見て取れます

しかし、これは典型的なバッドプラクティスです。

他の言語の標準ライブラリでは、

  - Rubyは `Date` と `DateTime` が分かれている
  - Java（やKotlin）にも `ZonedDate`, `ZonedDateTime` などがある

など、きちんと日付のみを扱う型が分かれているため、Crystalの標準ライブラリも見習ってほしいと感じます。


### YAML
Crystalの標準ライブラリには、YAMLを扱うための `YAML` というライブラリが存在します。
このライブラリを使用することで、自前のクラス等を簡単にシリアライズ・デシリアライズすることができるのですが、このデフォルトのシリアライザは細かい制御ができません[^yaml]。

例えば、使用するアンカーの命名法などはこちら側で制御したいことが多いかと思います。実際、JVM言語のの有名ライブラリであるJacksonでは、こうした細かい点について、 `JsonIdentityInfo` 等のアノテーションで制御できるようになっています。

[^yaml]: どうしても制御したければ、`to_yaml` を自作してしまえばよいだけですが

## 最後に

Crystal は設計思想的にもその実装的にもいい言語だと思いますが、だからこそ実装の甘い点、発展途上な点が気になってしまいます。個人的には、Crystalコミュニティのますますの発展と、それによる諸問題の解決がされることを期待してやみません[^postscript]。

[^postscript]:とはいえ、今回だいぶ様々なことにハマりましたし、満足なLSP実装もないしなので、次にこの用途で書くならCrystalではなくKotlin/Nativeあたりかなぁという気がしています。