Elixir 1.18 の型推論を試してみる

## はじめに

Elixir 1.18 のリリース候補（RC版）が出ました

https://github.com/elixir-lang/elixir/blob/v1.18/CHANGELOG.md

型推論ができるようになったとのことで試してみます

## Elixir 1.18.0-rc.0 のインストール

asdf で 1.18.0-rc.0 をインストールします

```bash
asdf install elixir 1.18.0-rc.0
```

カレントディレクトリー配下で 1.18.0-rc.0 を使うように指定します

```bash
asdf local elixir 1.18.0-rc.0
```

## プロジェクトの作成

新しいプロジェクトを作成します

```bash
mix new type_check
```

Elixir プロジェクトに必要な最低限のディレクトリー構造とファイルが作成されます

ディレクトリー内に移動します

```bash
cd type_check
```

## User モジュールの定義

`lib/user.ex` を作成し、 CHANGELOG に書いてあったものと同じ定義を書きます

```elixir
defmodule User do
  defstruct [:age, :car_choice]

  def drive(%User{age: age, car_choice: car}, car_choices) when age >= 18 do
    if car in car_choices do
      {:ok, car}
    else
      {:error, :no_choice}
    end
  end

  def drive(%User{}, _car_choices) do
    {:error, :not_allowed}
  end
end
```

## メインモジュールの変更

`lib/type_check.ex` を以下のように変更します

```elixir
defmodule TypeCheck do
  def hello do
    car_choices = [:toyota]
    User.drive({:ok, %User{}}, car_choices)
  end
end
```

User.drive に渡している変数の型が不正です

## コンパイルの実行

警告をエラーとしてコンパイルを実行します

```bash
mix compile --force --warnings-as-errors
```

実行結果

```text
Compiling 2 files (.ex)
    warning: incompatible types given to User.drive/2:

        User.drive({:ok, %User{age: nil, car_choice: nil}}, car_choices)

    given types:

        {:ok, %User{age: nil, car_choice: nil}}, non_empty_list(:toyota)

    but expected one of:

        dynamic(%User{age: term(), car_choice: term()}), dynamic()

    where "car_choices" was given the type:

        # type: non_empty_list(:toyota)
        # from: lib/type_check.ex:3:17
        car_choices = [:toyota]

    typing violation found at:
    │
  4 │     User.drive({:ok, %User{}}, car_choices)
    │          ~
    │
    └─ lib/type_check.ex:4:10: TypeCheck.hello/0

Compilation failed due to warnings while using the --warnings-as-errors option
```

型推論により、不正な型が渡されていることが検出できました

## 1.17 でのコンパイル確認

`mix.exs` を開き、 Elixir のバージョン下限を編集します

```diff_elixir
defmodule TypeCheck.MixProject do
  use Mix.Project

  def project do
    [
      app: :type_check,
      version: "0.1.0",
-     elixir: "~> 1.18-rc",
+     elixir: "~> 1.17",
      start_permanent: Mix.env() == :prod,
      deps: deps()
    ]
  end
...
```

使用する Elixir のバージョンを変更します

```bash
asdf local elixir 1.17.3
```

この状態でコンパイルします

```bash
mix compile --force --warnings-as-errors
```

実行結果

```
Compiling 2 files (.ex)
Generated type_check app
```

特に問題なくコンパイルできてしまいました

## 戻り値の型チェック

Elixir バージョンを 1.18.0-rc.0 に戻します

```bash
asdf local elixir 1.18.0-rc.0
```

`lib/type_check.ex` を以下のように変更します

```elixir
defmodule TypeCheck do
  require Logger

  def hello do
    user = %User{age: 18, car_choice: :toyota}
    car_choices = [:toyota]
    case User.drive(user, car_choices) do
      {:ok, car} -> car
      :error -> Logger.error("User cannot drive")
    end
  end
end
```

コンパイルしてみます

```bash
mix compile --force --warnings-as-errors
```

実行結果

```
Compiling 2 files (.ex)
    warning: the following clause will never match:

        :error

    because it attempts to match on the result of:

        User.drive(user, car_choices)

    which has type:

        dynamic({:ok, term()} or {:error, :no_choice} or {:error, :not_allowed})

    typing violation found at:
    │
  9 │       :error -> Logger.error("User cannot drive")
    │       ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    │
    └─ lib/type_check.ex:9: TypeCheck.hello/0

Compilation failed due to warnings while using the --warnings-as-errors option
```

戻り値の型が不正であるため、エラーになりました

このパターンも 1.17 ではコンパイルできてしまいます

## まとめ

Elixir 1.18 で型推論によるコンパイル時の警告が確認できました

これで実行時エラーが減らせますね