Elixir で指定バージョンより後に追加された関数を抽出する

## はじめに

AtCoder で使える Elixir のバージョンが 1.10.2 から 1.15.2 に上がるようです

https://atcoder.jp/contests/language-test-202301

これで表現の幅が広がりますね

以前、 @torifukukaiou さんが「Elixir 1.12で増えた関数を調べたい」という記事を書いていたので、私も ELixir 1.10 より後に追加された関数を出してみました

https://qiita.com/torifukukaiou/items/7230861686ad5adf31f5

もちろん、 Livebook で実装しました

https://github.com/RyoWakabayashi/elixir-learning/blob/main/livebooks/module_checker.livemd

## セットアップ

データフレームでテーブル表示、 CSV エクスポートしたいので、 [KinoExplorer](https://github.com/livebook-dev/kino_explorer) をインストールします

```elixir
Mix.install([
  {:kino, "~> 0.10"},
  {:kino_explorer, "~> 0.1"}
])
```

## モジュール一覧の取得

`:code.all_loaded` で読み込まれたモジュールを全て取得できます

見やすいようにアルファベット順に並べます

```elixir
:code.all_loaded
|> Enum.sort_by(&elem(&1, 0))
```

実行結果

```elixir
[
  {Access, ~c"/usr/local/lib/elixir/bin/../lib/elixir/ebin/Elixir.Access.beam"},
  {Agent, ~c"/usr/local/lib/elixir/bin/../lib/elixir/ebin/Elixir.Agent.beam"},
  {Agent.Server, ~c"/usr/local/lib/elixir/bin/../lib/elixir/ebin/Elixir.Agent.Server.beam"},
  {Application, ~c"/usr/local/lib/elixir/bin/../lib/elixir/ebin/Elixir.Application.beam"},
  {ArgumentError, ~c"/usr/local/lib/elixir/bin/../lib/elixir/ebin/Elixir.ArgumentError.beam"},
  {Base, ~c"/usr/local/lib/elixir/bin/../lib/elixir/ebin/Elixir.Base.beam"},
  {CAStore,
   ~c"/home/livebook/.cache/mix/installs/elixir-1.15.2-erts-14.0.2/a6f4be97ce5869ecd072e376fb58b81a/_build/dev/lib/castore/ebin/Elixir.CAStore.beam"},
  {CAStore.MixProject, []},
  {Code, ~c"/usr/local/lib/elixir/bin/../lib/elixir/ebin/Elixir.Code.beam"},
  {Code.Formatter, ~c"/usr/local/lib/elixir/bin/../lib/elixir/ebin/Elixir.Code.Formatter.beam"},
  {Code.Identifier, ~c"/usr/local/lib/elixir/bin/../lib/elixir/ebin/Elixir.Code.Identifier.beam"},
  {Code.Normalizer, ~c"/usr/local/lib/elixir/bin/../lib/elixir/ebin/Elixir.Code.Normalizer.beam"},
  {Collectable,
   ~c"/home/livebook/.cache/mix/installs/elixir-1.15.2-erts-14.0.2/a6f4be97ce5869ecd072e376fb58b81a/_build/dev/lib/mix_install/consolidated/Elixir.Collectable.beam"},
  ...
```

これらには Elixir 単体で使えるモジュールだけでなく、 Livebook や Kino 、 Explorer などのモジュールも含まれているため、それらを除いて取得します

```elixir
modules =
  :code.all_loaded
  |> Enum.sort_by(&elem(&1, 0))
  |> Enum.map(&elem(&1, 0))
  |> Enum.filter(fn atom ->
    atom
    |> Atom.to_string()
    |> then(&(
      String.starts_with?(&1, "Elixir.")
      && not String.starts_with?(&1, "Elixir.Kino")
      && not String.starts_with?(&1, "Elixir.Explorer")
      && not String.starts_with?(&1, "Elixir.Livebook")
      && not String.starts_with?(&1, "Elixir.Logger")
    ))
  end)
```

実行結果

```elixir
[Access, Agent, Agent.Server, Application, ArgumentError, Base, CAStore, CAStore.MixProject,
 Calendar.ISO, Code, Code.Formatter, Code.Fragment, Code.Identifier, Code.Normalizer, Code.Typespec,
 Collectable, Collectable.BitString, Collectable.File.Stream, Collectable.HashDict,
 Collectable.HashSet, Collectable.IO.Stream, Collectable.List, Collectable.Map, Collectable.MapSet,
 Collectable.Mix.Shell, Config.Provider, Date, DateTime, DynamicSupervisor, Enum, Enumerable,
 Enumerable.Date.Range, Enumerable.Explorer.Series.Iterator, Enumerable.File.Stream,
 Enumerable.Function, Enumerable.GenEvent.Stream, Enumerable.HashDict, Enumerable.HashSet,
 Enumerable.IO.Stream, Enumerable.Kino.Control, Enumerable.Kino.Input, Enumerable.Kino.JS.Live,
 Enumerable.List, Enumerable.Map, Enumerable.MapSet, Enumerable.Range, Enumerable.Stream,
 Enumerable.Table.Mapper, Enumerable.Table.Zipper, ExUnit.Assertions, ...]
```

## ドキュメントからの関数取得

`Code.fetch_docs` にモジュールを渡すことで、そのモジュールのドキュメントを取得できます

```elixir
Code.fetch_docs(List)
```

実行結果

```elixir
{:docs_v1, 2, :elixir, "text/markdown",
 %{
   "en" => "Linked lists hold zero, one, or more elements in the chosen order.\n\nLists in Elixir are specified between square brackets:\n\n..."
 }, %{},
 [
   {{:function, :ascii_printable?, 2}, 657, ["ascii_printable?(list, limit \\\\ :infinity)"],
    %{
      "en" => "Checks if `list` is a charlist made only of printable ASCII characters.\n\nTakes an optional `limit` as a second argument. `ascii_printable?/2` only\nchecks the printability of the list up to the `limit`.\n\n..."
    }, %{since: "1.6.0", defaults: 1}},
   {{:function, :delete, 2}, 133, ["delete(list, element)"],
    %{
      "en" => "Deletes the given `element` from the `list`. Returns a new list without\nthe element.\n\nIf the `element` occurs more than once in the `list`, just\nthe first occurrence is removed.\n\n## Examples\n\n..."
    }, %{}},
   {{:function, :delete_at, 2}, 846, ["delete_at(list, index)"],
    %{
      "en" => "Produces a new list by removing the value at the specified `index`.\n\nNegative indices indicate an offset from the end of the `list`.\nIf `index` is out of bounds, the original `list` is returned.\n\n## Examples\n\n..."
    }, %{}},
    ...
```

実行結果を見ると、タプルの最後（インデックス6）に関数の一覧が入っています

また、各関数のタプルの最後（インデックス4）に `since: "1.6.0"` というように、どのバージョンから追加されたのか、という情報を持っています

これを利用し、 "1.10.2" よりも後に追加された関数だけを抽出します

```elixir
Code.fetch_docs(List)
|> elem(6)
|> Enum.filter(fn function ->
  function
  |> elem(4)
  |> Map.get(:since, "0.0.0")
  |> Version.compare("1.10.2")
  |> Kernel.==(:gt)
end)
|> Enum.map(fn function ->
  %{
    function: function |> elem(2) |> Enum.at(0),
    since: function |> elem(4) |> Map.get(:since)
  }
end)
```

実行結果

```elixir
[
  %{function: "keyfind!(list, key, position)", since: "1.13.0"},
  %{function: "keysort(list, position, sorter \\\\ :asc)", since: "1.14.0"}
]
```

`List.keyfind!` と `List.keysort` が対象として抽出されました

確かに Hexdocs でも 1.10.2 よりも後に追加されたことが確認できます

https://hexdocs.pm/elixir/1.15.4/List.html#keyfind!/3

https://hexdocs.pm/elixir/1.15.4/List.html#keysort/3

ただし、 @torifukukaiou さんの[記事](https://qiita.com/torifukukaiou/items/f4bade3261e14de18797)にあるように、 `List.first/2` と `List.last/2` のように、ドキュメントの since に書いていないが、実は後から追加された関数については抽出できません

https://hexdocs.pm/elixir/1.15.4/List.html#first/2

https://hexdocs.pm/elixir/1.15.4/List.html#last/2

あくまでもドキュメントを信用して抽出します

ここまでの条件を踏まえ、 1.10.2 よりも後に追加された関数の一覧を取得します

```elixir
modules
|> Enum.map(fn module ->
  case Code.fetch_docs(module) do
    {:error, _} -> []
    docs ->
      docs
      |> elem(6)
      |> Enum.filter(fn function ->
        function
        |> elem(4)
        |> Map.get(:since, "0.0.0")
        |> then(fn version ->
          version_length =
            version
            |> String.split(".")
            |> length()
          cond do
            version_length >= 3 -> version
            version_length == 2 -> "#{version}.0"
            true -> "0.0.0"
          end 
        end)
        |> Version.compare("1.10.2")
        |> Kernel.==(:gt)
      end)
      |> Enum.map(fn function ->
        %{
          module: module |> Atom.to_string(),
          function: function |> elem(2) |> Enum.at(0),
          since: function |> elem(4) |> Map.get(:since)
        }
      end)
  end
end)
|> List.flatten()
|> Explorer.DataFrame.new()
|> Explorer.DataFrame.select([:module, :function, :since])
```

![スクリーンショット 2023-07-18 19.16.14.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1485835/bcadf7be-ea64-ad4f-7e2a-6054733b3831.png)

いくつか、 AtCoder に影響しそうなものを挙げておきます

- Enum.count_uintil
- Enum.product
- Enum.slide
- Enum.zip_reduce
- Enum.zip_with
- Float.pow
- Integer.extended_gcd
- Integer.pow
- **
- binary_slice
- ..
- first..last//step
- then
- List.keyfind!
- List.keysort
- Range.new(first, last, step)

他にも色々ありますが、とにかく `then` が嬉しいのと、 `first..last//step` や `Range.new(first, last, step)` でステップが使えるようになるのは良いですね

`Float.pow` `Integer.pow` `**` で累乗が分かりやすくなるのも良いです

## まとめ

今までは手元で OK でもバージョンの問題で AtCoder 上でエラーになることがありました

今後はそのケースが減るのでありがたいです

早く AtCoder でも 1.15.2 で書きたい！