【Elixir】if や case の中で代入・束縛を行うべきでない

こんにちは、Elixir を 1 年くらい業務で使っていた Nash です。

この記事は下記についてです。

  • Elixir で if/case の中で代入・束縛を行うべきではない点についての説明。

「if/case などのブロック内で束縛をするでないよー」と、かなりの回数の PR 指摘を行った記憶がある。

あと、自分も最初はよくやっていたしこともあるので、Elixir 初心者あるあるだと思う誤りなので、これから学ぶ人は注意すると良いと思う。

(追記:このシンタックスは Ruby からの系譜なのを後で知った。関数型は関係ないじゃん、というセルフツッコミを入れておく)

バッドパターン:if/case の中で束縛を行ってしまう

# バッドパターン
if is_nil(user) do
  user_money = get_money(user) # ①Bad
  user_gacha = get_gacha(user) # ②Bad
  user_tax = calc(user_money)  # ③Bad
else
  user_money = 0
  user_gacha = nil
end

この書き方はバッドパターンだ。おそらく JavaScript とかに慣れ親しんでいると自然にこういう書き方になってしまうのではないかな?と思う。

Elixir らしい書き方はこう書く

# グッドパターン(その1) ( 上のバッドパターンと同じ内容 )
{ user_money, user_gacha } =
  if is_nil(user) do
	{ get_money(user), get_gacha(user) } # ①②Good
  else
  	{ 0, nil }
  end

user_tax = calc(user_money) # ③Good
  • バッドパターンの ①② として、if-elseの中で束縛を行ってしまっている。そうすると、else-endの中にも同様の束縛を行う行が必要になってしまう。①② のグッドパターンのように、if-endブロックの返り値として値を返そう。その際に使うのはアトムの形式で値を返す・受け取るのが通例だ。
  • バッドパターンの ③ として、user_taxについての処理を行っている場所が悪い。この書き方だと、elseブロック内では束縛しないがifブロック内でのみ束縛する変数が生まれてしまう。こうなると後続処理にて未定義変数へのアクセスが可能になる余地が大きく発生しバグの温床なので、if-else-endの外に出せるなら出すと良い。

さて、この間違いの表層的なところは「if / case などの中で束縛を行ってしまう」だろう。だが、そもそも、このような書き方になってしまわないように根本的に思想を変えるほうが良い。その考えとして、if-else-endブロックは 1 つの関数だとみなそう。実際にコードとして書く場合は下記の通りとなる。

# グッドパターン(その2) ( 上と同じ内容 )
{ user_money, user_gacha } = get_data(user) # ①②Good
user_tax = calc(user_money) # ③Good

...
def get_data(user) when is_nil(user), do: {0, nil}
def get_data(user) do: {get_money(user), get_gacha(user)}

これでget_dataという関数になりすごくスッキリとなった!!

ただし、この思想をベースにすべてのif / case などを関数化するのは「過剰な関数化」を行ってしまっている印象だ。この思想が絶対的な是なら最終的にif/ case は容認されない、ということになってしまう。あくまで、「グッドパターン(その 2)」はコードリファクタリングされた最終的なゴールの一つとして、「グッドパターン(その 1)」の状態に留めておくことも可能なのが Elixir の良さだと思う。

つまり、

  • 「グッドパターン(その1)」のように、if-endブロックのままにするか?
  • 「グッドパターン(その2)」のように、すべて関数化するか?

の選択は

  • 処理を簡潔にしたいか?
  • 処理を部品化させたいか?
  • 処理に名称をつけたいか?(関数名で表現できる)

などのその時々の背景を元に書くと良いと思う。

結論

  • 「関数化しつくした最終形態をイメージできること」
  • 「その上で、あえて関数化しないという選択肢も持つこと」

の 2 点だと思う。ただ、最初の頃は「全部関数化するんじゃぁー」くらいのスタンスの方が関数型的案思考になるのでおすすめ。

おまけ:Elixir で if を使うべきか使わないべきか?

よく Elixir を書いていると「if 文はやめて case のみにすべき」や「すべて関数にすべき」的な発言を目にした。

「すべて関数にすべき」か?の答えはこの記事に書いてあるとおりでだが、「if 文はやめて case のみにすべき」は自分は否定的だ。

if の場合は true/false の 2 択だが、case の場合は複数選択肢の場合やガード節でなにかをしたい時にのみ書くべきだと思う。

case user.is_admin do
  true -> # 処理1
  _ -> # 処理2
end

# これだけの記述なら、ifで十分書けるしコードリーディング的にも、上よりも下の方が良い
if user.is_admin do
  # 処理1
else
  # 処理2
end

if で書かれている場合はその時点で「選択肢は2つなんだな」ととなるが、case が出てくると「どういうパターンがそもそもあるんだ?」と思考の選択余地が増えてしまい、コードリーディング的にあまりエンジニアに優しくないように思う。また、柔軟に case の値を取れる分、バグの温床になりがちなので、True/false のみに出来るなら if にすると良いと思う。