はじめに
Elixir でパス文字列を扱う際は、Path モジュールや String.split/2 を使うのが一般的です。
ある日、Elixir 本体の Mix の内部状態を扱う Mix.State モジュールのコードを読んでいると、charlist(文字リスト)+パターンマッチ だけでパスを解析している箇所がありました。
この記事では、Mix.State に出てくるパターンを参考にしつつ、
-
../elixir/ebin→:elixir -
../ssl-9.6/ebin→:ssl
のような変換を、charlist とパターンマッチだけで行う例をメモしておきます。
Mix.State に出てくるパス解析
Mix.State の中には、コードパスからアプリ名を推測する関数があります。
この関数では、path を charlist とみなして
- 末尾の
"ebin"ディレクトリを削る - 直前のディレクトリ名からバージョン番号を除いたアプリ名を取り出す
という処理を、パターンマッチだけで実装しています。
charlist で末尾の ebin を落としてアプリ名を取り出す
今回やりたいのは、次のようなパスからアプリ名の atom を取り出す処理です:
-
"../elixir/ebin"→:elixir -
"../ssl-9.6/ebin"→:ssl
defmodule SampleApp.PathUtils do
@moduledoc false
def app_from_path(path) when is_list(path) do
case path |> Enum.reverse() |> discard_ebin() |> collect_dir([]) do
[] -> nil
chars -> List.to_atom(chars)
end
end
# 末尾の "ebin/" または "ebin\\" を削除する
defp discard_ebin(~c"nibe/" ++ rest), do: rest
defp discard_ebin(~c"nibe\\" ++ rest), do: rest
defp discard_ebin(_), do: []
# ディレクトリ名部分からアプリ名を取り出す
defp collect_dir([?/ | _], acc), do: acc # `/` が来たら終了
defp collect_dir([?\\ | _], acc), do: acc # `\` が来たら終了(Windows 用)
defp collect_dir([?- | rest], _acc), do: collect_dir(rest, []) # `-` 以降はバージョン番号として無視
defp collect_dir([ch | rest], acc), do: collect_dir(rest, [ch | acc])
defp collect_dir([], acc), do: acc
end
試してみると、次のような動きになります。
iex> SampleApp.PathUtils.app_from_path(~c"../elixir/ebin")
:elixir
iex> SampleApp.PathUtils.app_from_path(~c"../ssl-9.6/ebin")
:ssl
どう動いているか
app_from_path/1 は、charlist 形式のパス(たとえば ~c"../ssl-9.6/ebin")から、アプリ名(:ssl)を抽出する関数です。
-
パスを逆順にする
- 最後が
"ebin/"であることを想定し、それを削るには逆順が都合良いため。
- 最後が
-
discard_ebin/1で"ebin/"部分を除去-
'nibe/'や'nibe\\'で終わっていれば、それを切り落とす。
-
-
collect_dir/2でディレクトリ名を収集-
/または\にぶつかるまで文字を集める。 -
-(ハイフン)で区切られたバージョン番号部分(例:"9.6")は無視。
-
-
集めた charlist を
List.to_atom/1で atom に変換- これで
:sslや:elixirを得られる。
- これで
バイナリで書くとどうなるか
もちろんバイナリ文字列でも同様の処理を書けます。
iex> "../elixir/ebin" |> Path.basename() |> String.split("-") |> hd() |> String.to_atom()
:elixir
iex> "../ssl-9.6/ebin" |> Path.basename() |> String.split("-") |> hd() |> String.to_atom()
:ssl
こちらの方がなんか直感的な氣がします。
今回紹介した charlist 版は、もともと Erlang 由来の charlist がある場合以外は使うことがなさそうですね。![]()
おわりに
普段のアプリコードでは Path.basename/1 や String.split/2 を使うことが多いと思いますが、Mix.State のようにcharlist をパターンマッチだけで字句処理を書くというスタイルも、知っておいて損はなさそうです。