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

こんにちは！
プログラミング未経験文系出身、Elixirの国に迷い込んだ？！見習いアルケミストのaliceと申します。
今回はElixir1.18の新機能である型推論について学んだことをまとめます。こちらを自分の環境でやってみた記事です。

元記事様（Thx for @RyoWakabayashi🎉）

https://qiita.com/RyoWakabayashi/items/5f42f684f438a7e14dcd


# 目的
Elixir 1.18の新機能である型推論の機能を試したい。


## 実行環境
Windows 11 ＋ WSL2 ＋ Ubuntu 22.04
Elixir v1.18.2 および v1.17.3をasdfで切替
Erlang v27.0


# 型推論やってみた
## Elixir 1.18.2 のインストール
検証時点で最新のElixir1.18.2をインストールします。
```shell:bash
asdf install elixir 1.18.2
```

カレントディレクトリ配下のプロジェクトに対してのみv1.18.2を適用します。
```shell:bash
asdf local elixir 1.18.2
```

## プロジェクトの作成
新しいプロジェクトを作成します
```shell:bash
mix new type_check
cd type_check
```


## User モジュールの定義
`lib/user.ex` を作成します。
CHANGELOGに書いてあったものと同じ定義を書きます。

```elixir:lib/user.ex
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
```
この部分のコードを噛み砕いてみます。

### %User{}を定義
`defstruct [:age, :car_choice]`でUserという構造体を定義します。

構造体`%User{}`には:`age`と`:car_choice`の2つのフィールド(キー)があります。

`:age`と`:car_choice`のデフォルト値は未記載のためnilです。

#### 使用例
```elixir:iex
user = %User{age: 20, car_choice: "Nissan"}
user.age
```

```
20
```

### drive/2 関数を定義
下記で`drive/2`関数を定義します。

`drive/2`は`when age >= 18`の値次第で関数パターンマッチにより分岐されます。

#### age >= 18 のとき
`def drive(%User{age: age, car_choice: car}, car_choices) when age >= 18 do ... end` ブロックを通ります。

また、`car_choices`という**リスト**も受け取るだろうと予想されます。
`:age`と`:car_choice`の値としてそれぞれ`age`と`car`というパラメータ(実引数)を受け取ります。

何故なら変数`car_choice`が使われているif文において[`in/2`(in演算子)](https://hexdocs.pm/elixir/Kernel.html#in/2)を使っているからです。

:::note info
`in/2`は第一引数が第二引数に含まれるかを示す関数です。
このことから`in/2`の第二引数`car_choice`はリストか範囲(range)かのいずれかであると推測されるものの、rangeは数字しかとらないため除外。
消去法で`car_choices`はリストであると推測されます。
:::

#### age < 18 のとき
`def drive(%User{}, _car_choices) do ... end` ブロックを通ります。

`{:error, :not_allowed}`を返します。


## メインモジュールの変更
`lib/type_check.ex` を以下のように変更します
```elixir:lib/type_check.ex
defmodule TypeCheck do
  def hello do
    car_choices = [:toyota]
    User.drive({:ok, %User{}}, car_choices)
  end
end
```

drive/2の定義から、User.driveの第一引数は%User{}であるべきなのですが、ここでは第一引数が`{:ok, %User{}}`となっているためコンパイル時に型推論による警告が出るはずです。
確認してみましょう。


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

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

実行結果
```
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:24:17
        car_choices = [:toyota]

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

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

警告が出て、`--warnings-as-errors`オプションのためコンパイルに失敗しました。


## 1.17 でのコンパイル確認(引数の型チェック)
`mix.exs` Elixir のバージョン下限を編集します。
```diff_elixir:mix.exs
defmodule TypeCheck.MixProject do
  use Mix.Project

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

使用するElixirのバージョンを変更します
```shell:bash
asdf local elixir 1.17.3
```

この状態でコンパイルします
```shell:bash
mix compile --force --warnings-as-errors
```

実行結果
```
Compiling 2 files (.ex)
Generated type_check app
```

1.17.3には型推論の機能が無いので、警告が出ずに問題なくコンパイルできました。


## 戻り値の型チェック
Elixir バージョンを 1.18.2 に戻します

```shell:bash
asdf local elixir 1.18.2
```

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

```diff_elixir:lib/type_check.ex
 defmodule TypeCheck do
+  require Logger

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

これはぱっと見、「case文を使用して`drive/2`の戻り値次第で変数`car`または文字列`"User cannot drive"`を返す」実装に見えますが、実際には`drive/2`の戻り値は下記のいずれかしかとりません。
- {:ok, car}
- {:error, :no_choice}
- {:error, :not_allowed}

つまり、`:error -> Logger.error("User cannot drive")`部分を通ることがありません。

v1.18では戻り値にも型推論が適用されます。
コンパイルしてみましょう。

```shell: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({:error, :no_choice or :not_allowed} or {:ok, term()})

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

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

戻り値の型が不正であるため、警告が出て、`--warnings-as-errors`オプションのためコンパイルに失敗しました。


## 1.17 でのコンパイル確認(戻り値の型チェック)
使用するElixirのバージョンを再度変更します
```shell:bash
asdf local elixir 1.17.3
```

この状態でコンパイルします
```shell:bash
mix compile --force --warnings-as-errors
```

実行結果
```
Compiling 2 files (.ex)
Generated type_check app
```

1.17.3には型推論の機能が無いので、戻り値の型チェックでも警告が出ずに問題なくコンパイルできました。


# ～Elixirの国のご案内～
↓Elixirって何ぞや？と思ったらこちらもどぞ。Elixirは先端のアレコレをだいたい全部できちゃいます:laughing::sparkles::sparkles:

https://qiita.com/piacerex/items/09876caa1e17169ec5e1

↓ゼロからElixirを始めるなら「エリクサーチ」がおすすめ！私もエンジニア未経験から学習中です。

https://elixir-lang.info/topics/entry_column


↓[We Are The Alchemists, my friends!:bouquet:](https://qiita.com/torifukukaiou/items/6bcd5bbb80df9e594e9e "Awesome-san's Awesome article")[^10]
Elixirコミュニティは本当に優しくて温かい人たちばかり！
私が挫折せずにいられるのもこの恵まれた環境のおかげです。
まずは気軽にコミュニティを訪れてみてください。[^11]


https://speakerdeck.com/elijo/elixirkomiyunitei-falsebu-kifang-guo-nei-onrainbian




[^10]: @torifukukaiouさんのAwesomeな名言をお借りしました。Elixirコミュニティを一言で表すと、これに尽きます。


[^11]: @kn339264さんの素敵なスライドをお借りしました。Elixirコミュニティはいろんな形で活動中！