はじめに
Elixirのドキュメントにanti patternsがあり、結構いい資料だと思います。
その中の一つalternative-return-typesを読み解いてみます
戻り値の型が変化する関数
ああ~。いかにもダメそうな関数。
でも、例をみてみると、やってしまいそうなデザイン。
アンチパターンのコード例で使われてる、Integer.parse()の動作を復習しておきます。
iex(18)> Integer.parse("123")
{123, ""}
iex(19)> Integer.parse("123a")
{123, "a"}
iex(20)> Integer.parse("b123a")
:error
iex(21)> Integer.parse("b123")
:error
iex(22)>
数字の後に文字がある場合、その文字も取得できるという関数です。
アンチパターンのコードは以下の通り。
defmodule AlternativeInteger do
@spec parse(String.t(), keyword()) :: integer() | {integer(), String.t()} | :error
def parse(string, options \\ []) when is_list(options) do
if Keyword.get(options, :discard_rest, false) do
Integer.parse(string)
else
case Integer.parse(string) do
{int, _rest} -> int
:error -> :error
end
end
end
end
文字列をパースして値を取得する関数です
discard_restがtrueの場合は、整数値のみ。残りの文字列は返さない。
discard_restがfalseだったり指定されてない場合は、Integer.parse()の戻り値をそのまま返す。残りの文字も取得できる。
こんな関数作ってしまいそうです。
何が良くないのか?
まず、返り値の型が一定でないので、扱いずらいです。
この関数は次の二つの機能をもっています。
- 文字列を整数に変換して、整数と、残りの文字を返す
- 整数か、{整数,残りの文字}どちらにするか選択する
一つの関数に複数の機能を持たせている事も、関数を使う人に余計な知識を要求する事になります。
確か、達人プログラマーにflagの使用は良くないとかあったような気がします。
この観点からもアンチパターンですね。
どうするのが良いか
二つの関数に分けます。
パースして整数を得る
パースして、{整数,残りの文字}を得る
だったら、それぞれ一つの機能と言えます。
defmodule AlternativeInteger do
@spec parse(String.t()) :: {integer(), String.t()} | :error
def parse(string) do
Integer.parse(string)
end
@spec parse_discard_rest(String.t()) :: integer() | :error
def parse_discard_rest(string) do
case Integer.parse(string) do
{int, _rest} -> int
:error -> :error
end
end
end
parse_discard_restは、Integer.parseとcase文の複数の処理をしているじゃないか!という突っ込みがあるかもしれませんが、ここで問題にしている複数の機能とは、そいう事ではありません。
一つの機能を実現するために複数の処理を組み合わせる事は、必要な事で問題ではありません。
parse_discard_restは文字列を与えると、parse_discard_restが決めた形式の値を返すという単機能の関数になっています。
別の解決方法
この例は、parseの内容が単純なので、二種類のparse関数を作るので十分だと思いますが、こんな関数を用意する方法もあると思いました。
def discard_rest(parsed_value) do
case parsed_value do
{int, _rest} -> int
:error -> :error
end
end