🧪

Elixir標準モジュールのキャスト関数をリスト化してみた

2022/02/10に公開
2

動機

先日、Twitterで「キャストメソッド/関数はどちらのクラスに生やせば良いのか」という疑問が投げられ、様々な回答が集まっていました。Togetterにもまとめられています。

https://togetter.com/li/1840511

上記の議論を見て、「Elixirでは、キャスト関数はどういうルールやパターンで実装されているのだろう?」と気になったので、Elixir標準の各モジュールのキャスト関数をリスト化するプログラムを作ってみました。

作ったもの

キャスト関数のリストとソースコードは下記リポジトリに置いてあります。
https://github.com/yellowsman/elixir_cast_function#elixircastfunction

使用方法

iex> ElixirCastFunction.run

使用ライブラリ

  • Floki
  • Jason
  • Req

仕組み

元となるデータは、HexDocのElixirのページを使いました。
https://hexdocs.pm/elixir

HexDocsのサイドバーに関数のリストがモジュール毎にまとまっていたのでFlokiを使いスクレイピングしました。
誤算だったのは、サイドバーの情報はAjaxで動的に取得されてたので単純にHexDocsのHTMLを分析しても結果が得られなかったことです。
そのため、1度HexDocsのHTMLを取得し、HTMLからサイドバーの情報が記載されたJavaScriptのURLを取得して、改めてJavaScriptにリクエストしています。

その後、JavaScriptからモジュール名と関数名のリストを構築し、マークダウンのテーブルの形式になるよう整形してIO.puts/1でコンソールに出力させています。

JavaScriptに書かれた情報がリストの入れ子が連続していたため、かなり複雑な取得ロジックになってしまいました。
この部分は後で書き直すつもりです。

TODO

  • Mermaidで図を作る
  • "DEPRECATED"となっているモジュールをリストから除外する

今後は、関数間のキャストする/されるの関係を分かりやすくするためMermaidを使って図示しようと考えています。
また、非推奨のモジュール(Dict、HashDict、HashDict、Set)の関数もリストに含まれているので、これは除外するつもりです。

このリストが、Elixir開発のお役に立てば幸いです。

2022/02/12 追記

IO.chardata_to_string/1のように、xxx_to_yyyの名前のキャスト関数があることに気が付いたので、抽出条件をstarts_with?からcontains?に修正してリストを更新しました。
また、touch/2などキャスト関数でないものがリストに含まれていたので削除しました。
https://github.com/yellowsman/elixir_cast_function/commit/6edf1c7180c5f8352d049d2bc9ec7e68125fb198

Discussion

koga1020koga1020

面白いですね。一覧で見ると色々あるんだなと参考になります 👍

スクレイピングせずに取るにはどうしたらいいかな?と遊んでみたので共有してみます。
良い勉強になりました、遊ぶ題材をいただきましてありがとうございます🚀

参考: https://elixirforum.com/t/question-on-application-get-key-my-app-module/29391

defmodule Sample do

  def run do
    {:ok, modules} = :application.get_key(:elixir, :modules)

    modules
    |> Enum.map(fn m ->
      {m,
       m.module_info(:functions)
       |> Enum.filter(fn {name, _arity} ->
         String.starts_with?(Atom.to_string(name), ["to_", "from"])
       end)}
    end)
    |> Enum.reject(fn {_, functions} -> functions == [] end)
    # |> Enum.map(fn {module, functions} ->
    #   # arityでまとめるなりtable形式に加工するなり煮るなり焼くなり
    # end)
  end
end
結果
[
  {Atom, [to_char_list: 1, to_charlist: 1, to_string: 1]},
  {Calendar.ISO, [from_unix: 2]},
  {Code.Formatter, [to_algebra: 1, to_algebra: 2]},
  {Date,
   [
     from_erl: 1,
     from_erl: 2,
     from_erl!: 1,
     from_erl!: 2,
     from_gregorian_days: 1,
     from_gregorian_days: 2,
     from_iso8601: 1,
     from_iso8601: 2,
     from_iso8601!: 1,
     from_iso8601!: 2,
     from_iso_days: 2,
     to_erl: 1,
     to_gregorian_days: 1,
     to_iso8601: 1,
     to_iso8601: 2,
     to_iso_days: 1,
     to_string: 1
   ]},
  {DateTime,
   [
     from_gregorian_seconds: 1,
     from_gregorian_seconds: 2,
     from_gregorian_seconds: 3,
     from_iso8601: 1,
     from_iso8601: 2,
     from_iso_days: 4,
     from_map: 1,
     from_naive: 2,
     from_naive: 3,
     from_naive!: 2,
     from_naive!: 3,
     from_naive_with_period: 3,
     from_unix: 1,
     from_unix: 2,
     from_unix: 3,
     from_unix!: 1,
     from_unix!: 2,
     from_unix!: 3,
     to_date: 1,
     to_gregorian_seconds: 1,
     to_iso8601: 1,
     to_iso8601: 2,
     to_iso8601: 3,
     to_iso_days: 1,
     to_naive: 1,
     to_string: 1,
     to_time: 1,
     to_unix: 1,
     to_unix: 2
   ]},
  {Dict, [to_list: 1]},
  {Enum, [to_list: 1, to_sort_by_fun: 1, to_sort_fun: 1]},
  {ErlangError, [from_stacktrace: 1]},
  {File.Stat, [from_record: 1, to_record: 1]},
  {Float,
   [
     to_char_list: 1,
     to_char_list: 2,
     to_charlist: 1,
     to_string: 1,
     to_string: 2
   ]},
  {HashDict, [to_list: 1]},
  {HashSet, [to_list: 1]},
  {Inspect.Algebra, [to_doc: 2]},
  {Inspect.Map, [to_map: 3]},
  {Integer,
   [
     to_char_list: 1,
     to_char_list: 2,
     to_charlist: 1,
     to_charlist: 2,
     to_string: 1,
     to_string: 2
   ]},
  {Kernel.CLI, [to_exit: 3]},
  {Kernel.ParallelCompiler, [to_error: 4, to_padded_ms: 1]},
  {Kernel, [to_calendar_struct: 2]},
  {Keyword, [to_list: 1]},
  {List.Chars.Atom, [to_charlist: 1]},
  {List.Chars.BitString, [to_charlist: 1]},
  {List.Chars.Float, [to_charlist: 1]},
  {List.Chars.Integer, [to_charlist: 1]},
  {List.Chars.List, [to_charlist: 1]},
  {List.Chars, [to_char_list: 1, to_charlist: 1]},
  {List,
   [
     to_atom: 1,
     to_charlist: 1,
     to_existing_atom: 1,
     to_float: 1,
     to_integer: 1,
     to_integer: 2,
     to_list: 1,
     to_string: 1,
     to_tuple: 1
   ]},
  {Macro.Env, [to_match: 1]},
  {Macro, [to_lower_char: 1, to_string: 1, to_string: 2, to_upper_char: 1]},
  {Map, [from_struct: 1, to_list: 1]},
  {MapSet, [to_list: 1]},
  {Module.Types.Unify, [to_union: 2]},
  {NaiveDateTime,
   [
     from_erl: 1,
     from_erl: 2,
     from_erl: 3,
     from_erl!: 1,
     from_erl!: 2,
     from_erl!: 3,
     from_gregorian_seconds: 1,
     from_gregorian_seconds: 2,
     from_gregorian_seconds: 3,
     from_iso8601: 1,
     from_iso8601: 2,
     from_iso8601!: 1,
     from_iso8601!: 2,
     from_iso_days: 3,
     to_date: 1,
     to_erl: 1,
     to_gregorian_seconds: 1,
     to_iso8601: 1,
     to_iso8601: 2,
     to_iso_days: 1,
     to_string: 1,
     to_time: 1
   ]},
  {OptionParser,
   [
     to_argv: 1,
     to_argv: 2,
     to_argv: 3,
     to_existing_key: 2,
     to_switch: 1,
     to_switch: 2,
     to_underscore: 1,
     to_underscore: 2
   ]},
  {Record.Extractor, [from_file: 1, from_lib_file: 1, from_or_from_lib_file: 1]},
  {Set, [to_list: 1]},
  {String.Chars.Atom, [to_string: 1]},
  {String.Chars.BitString, [to_string: 1]},
  {String.Chars.Date, [to_string: 1]},
  {String.Chars.DateTime, [to_string: 1]},
  {String.Chars.Float, [to_string: 1]},
  {String.Chars.Integer, [to_string: 1]},
  {String.Chars.List, [to_string: 1]},
  {String.Chars.NaiveDateTime, [to_string: 1]},
  {String.Chars.Time, [to_string: 1]},
  {String.Chars.URI, [to_string: 1]},
  {String.Chars.Version.Requirement, [to_string: 1]},
  {String.Chars.Version, [to_string: 1]},
  {String.Chars, [to_string: 1]},
  {String,
   [
     to_atom: 1,
     to_char_list: 1,
     to_charlist: 1,
     to_existing_atom: 1,
     to_float: 1,
     to_integer: 1,
     to_integer: 2
   ]},
  {Time,
   [
     from_erl: 1,
     from_erl: 2,
     from_erl: 3,
     from_erl!: 1,
     from_erl!: 2,
     from_erl!: 3,
     from_iso8601: 1,
     from_iso8601: 2,
     from_iso8601!: 1,
     from_iso8601!: 2,
     from_seconds_after_midnight: 1,
     from_seconds_after_midnight: 2,
     from_seconds_after_midnight: 3,
     to_day_fraction: 1,
     to_erl: 1,
     to_iso8601: 1,
     to_iso8601: 2,
     to_seconds_after_midnight: 1,
     to_string: 1
   ]},
  {Tuple, [to_list: 1]},
  {URI, [to_string: 1]},
  {Version, [to_matchable: 2]},
  {:elixir, [to_binary: 1]},
  {:elixir_aliases, [to_partial: 1]},
  {:elixir_config, [to_arity: 1]},
  {:elixir_env, [to_caller: 1, to_erl_var: 2]}
]
Endo ShogoEndo Shogo

コメントありがとうございます!
テーマに興味持っていただけて嬉しいです!

モジュール.module_info(:functions)で取得する関数はプライベート関数も返すんですね(Version.to_matchable/2など)
公開されている関数だけを取得したい場合は、module_info(:exports)の方が良さそうでした。

書いてもらったコードもfilterやrejectの使い方がスッキリしていて分かりやすいので、実装方法を参考にさせていただきます。