Elixir勉強日記4 〜パターンマッチング・制御構造編〜
引き続きElixir勉強日記を書いていきます
ほんとは毎日できるといいんですけどうまくいかないです
毎日動画投稿しているYoutuberさんはすごいな 〜とか思う今日このごろです
今回はパターンマッチングと制御構造のところをまとめてやっていきをしていきます
そろそろ勉強を進めながらなにかしら作ってみたい気持ちになってきてますね
パターンマッチング
パターンマッチングはElixirの強力な部品で、単純な値やデータ構造、果ては関数でさえもマッチすることができます。 このレッスンではパターンマッチングの使い方から見ていきます。
強そう(小並)
マッチ演算子
Elixirでは、=演算子は実際には代数学での等号に値するマッチ演算子です。このマッチ演算子を通して値を代入し、その後マッチさせることができます。マッチングに成功すると方程式の結果を返します。失敗する場合はエラーを投げます。
iex> x = 1 1
一般的にプログラミング言語の=
って等号よりは代入(:=
)を表すことが多いですがElixir
においては代数学における等号に値する
とあるので認識が違うみたいですね
上の例だと普通に代入しているように見えます
iex> 1 = x 1 iex> 2 = x ** (MatchError) no match of right hand side value: 1
あーーね
左辺に数字があるのは経験上気持ちわるいのですが、等号
と同じという役割であるとなんとなく納得ができる感じがします
マッチ演算子はコレクションでも利用できます
# リスト iex> list = [1, 2, 3] iex> [1, 2, 3] = list [1, 2, 3] iex> [] = list ** (MatchError) no match of right hand side value: [1, 2, 3] iex> [1 | tail] = list [1, 2, 3] iex> tail [2, 3] iex> [2|_] = list ** (MatchError) no match of right hand side value: [1, 2, 3] # タプル iex> {:ok, value} = {:ok, "Successful!"} {:ok, "Successful!"} iex> value "Successful!" iex> {:ok, value} = {:error} ** (MatchError) no match of right hand side value: {:error}
コレクションにおいても同様に使えるところをみるとコレクションを集合って考えたら代数学的に等号
っていう表現がなんとなくわかるような気がします
ピン演算子
マッチ演算子は左辺に変数が含まれている時に代入操作を行います。 この変数を再び束縛するという挙動は望ましくない場合があります。 そうした状況のために、ピン演算子(^)があります。
ピン演算子で変数を固定すると、新しく再束縛するのではなく既存の値とマッチします。
変数定義・代入したときに使うのがマッチ演算子=
で、マッチするか確認するのに使うのがピン演算子なんですかね
iex> x = 1 1 iex> ^x = 2 ** (MatchError) no match of right hand side value: 2 iex> {x, ^x} = {2, 1} {2, 1} iex> x 2
上の例はピン演算子を使うともともと1が代入されているxと2はマッチしないんですがwwwって怒られてる感じですかね
上の例は分かるピン演算子で違う値に代入しようとしてMatchErrorを出しているからな
だが下の例はどういうことだアァァァァ!!??
落ち着いて考えるならマッチ演算子でタプルの左のx
に2が代入されて、ピン演算子で1を代入しようとしたけど値を変える代入なので代入をやめたっていう認識をしました
Elixir 1.2ではマップのキーや関数の節でのピン演算子がサポートされました。
iex> key = "hello" "hello" iex> %{^key => value} = %{"hello" => "world"} %{"hello" => "world"} iex> value "world" iex> %{^key => value} = %{:hello => "world"} ** (MatchError) no match of right hand side value: %{hello: "world"}
この例がマップのキーを文字列からタプルに変えようとしているのに気づくまで1分かかりました
iex> greeting = "Hello" "Hello" iex> greet = fn ...> (^greeting, name) -> "Hi #{name}" ...> (greeting, name) -> "#{greeting}, #{name}" ...> end #Function<12.54118792/2 in :erl_eval.expr/5> iex> greet.("Hello", "Sean") "Hi Sean" iex> greet.("Mornin'", "Sean") "Mornin', Sean" iex> greeting "Hello"
ファッ
関数内のgreeting
と引数をピン演算子で確認して第一引数が"Hello"
だったら"Hi"
を表示してそれ以外の場合はそのまま表示してるって感じか
てかしれっと関数のマッチングしてそう
実際に動かすコードでこういう書き方ってするのか私気になります
制御構造
ifとunless
ひょっとすると以前にif/2と出くわしているかもしれませんし、Rubyを使っていればunless/2をご存知でしょう。Elixirではこの2つはほとんど同じように作用しますが、言語の構成要素としてではなく、マクロとして定義されています。この実装はKernel moduleで知ることができます。
Elixirでは偽とみなされる値はnilと真理値のfalseだけだということに、留意すべきです。
false
とnil
のみが偽となるのは基本的にはRuby
と同様の制御がされるっぽいですね
言語の構成要素としてではなくマクロとして定義されているっていうあたりは、モジュールを呼び出す必要がないってことでいいんですかね
case
複数のパターンに対してマッチする必要があるなら、case/2を使うことができます
こちらもRuby
と似たような使い方ができそうです
iex> case {:ok, "Hello World"} do ...> {:ok, result} -> result ...> {:error} -> "Uh oh!" ...> _ -> "Catch all" ...> end "Hello World"
あくまでも関数型言語なので分岐した先は関数っていう感覚で書くっていうのに慣れて生きていきたいです
_
変数はcase/2
命令文の中に含まれる重要な要素です。これが無いと、マッチするものが見あたらない場合にエラーが発生します
Ruby
でいうところのdefault
ってことなんすかね
iex> case :even do ...> :odd -> "Odd" ...> end ** (CaseClauseError) no case clause matching: :even iex> case :even do ...> :odd -> "Odd" ...> _ -> "Not Odd" ...> end "Not Odd"
どうでもいいけどcase/2
の引数に定数を渡すの気持ち悪いですね
_
を”他の全て”にマッチするelseと考えましょう。
case/2
はパターンマッチングに依存しているため、パターンマッチングと同じルールや制限が全て適用されます。既存の変数に対してマッチさせようという場合にはピン^演算子を使わなくてはいけません
iex> pie = 3.14 3.14 iex> case "cherry pie" do ...> ^pie -> "Not so tasty" ...> pie -> "I bet #{pie} is tasty" ...> end "I bet cherry pie is tasty"
この辺り覚えてないと詰みかねないですね
case/2のもう1つの素晴らしい特徴として、ガード節に対応していることがあげられます
iex> case {1, 2, 3} do ...> {1, x, 3} when x > 0 -> ...> "Will match" ...> _ -> ...> "Won't match" ...> end "Will match"
ガード節 分岐ができて いい感じ(575)
cond
値ではなく、条件をマッチさせる必要がある時には、cond/1を使うことができます。これは他の言語でいうところのelse ifやelsifのようなものです
引数のないcase/2
みたいな認識でいいんすかね
iex> cond do ...> 2 + 2 == 5 -> ...> "This will not be true" ...> 2 * 2 == 3 -> ...> "Nor this" ...> 1 + 1 == 2 -> ...> "But this will" ...> end "But this will"
caseのように、condはマッチしない場合にエラーを発生させます。これに対処するには、trueになる条件を定義すればよいです
それはそう
with
特殊形式の
with/1
はネストされたcase/2
文を使うような時やきれいにパイプできない状況に便利です。with/1式はキーワード, ジェネレータ, そして式から成り立っています。ジェネレータについてはリスト内包表記のレッスンでより詳しく述べますが、今は<-の右側と左側を比べるのにパターンマッチングが使われることを知っておくだけでよいです。
with/1
の簡単な例から始め、その後さらなる例を見てみましょう
なんかややこしそうなやつですね
iex> user = %{first: "Sean", last: "Callan"} %{first: "Sean", last: "Callan"} iex> with {:ok, first} <- Map.fetch(user, :first), ...> {:ok, last} <- Map.fetch(user, :last), ...> do: last <> ", " <> first "Callan, Sean"
これってuser
のマップにfirst
とlast
のkeyがあるか確認して表示してるってことなんすかね
式がマッチに失敗した場合はマッチしない値が返されます
iex> user = %{first: "doomspork"} %{first: "doomspork"} iex> with {:ok, first} <- Map.fetch(user, :first), ...> {:ok, last} <- Map.fetch(user, :last), ...> do: last <> ", " <> first :error
ここではMap.fetch/2
の戻り値が:error
なのでそいつが返ってきてるということですね
それでは、with/1を使わない長めの例と、それをどのようにリファクタリングできるかを見てみましょう
まずはcase/2
で書いた場合ですね
case Repo.insert(changeset) do {:ok, user} -> case Guardian.encode_and_sign(user, :token, claims) do {:ok, jwt, full_claims} -> important_stuff(jwt, full_claims) error -> error end error -> error end
それをwith/1
に置き換えてこうじゃ!
with {:ok, user} <- Repo.insert(changeset), {:ok, jwt, full_claims} <- Guardian.encode_and_sign(user, :token, claims), do: important_stuff(jwt, full_claims)
優勝!!!wwww
Elixir 1.3からはwith/1でelseを使えます
import Integer m = %{a: 1, c: 3} a = with {:ok, number} <- Map.fetch(m, :a), true <- is_even(number) do IO.puts "#{number} divided by 2 is #{div(number, 2)}" :even else :error -> IO.puts("We don't have this item in map") :error _ -> IO.puts("It is odd") :odd end
これは
case
のようなパターンマッチングを提供することで、エラーを扱いやすくします。渡されるのはマッチングに失敗した最初の表現式の値です。
これってwith {:ok, number} <- Map.fetch(m, :a)
で:error
だったらエラーを表示して:error
を返してるってことか!!!
天才!www
第4回まとめ
パターンマッチング
制御構造
if/2
やunless/2
はカーネルモジュールが与えている機能でおおよそRuby
と同じ動作をするcase/2
もおおよそRuby
と同様に動作するがマッチングしない場合はdefault
ではなく_
case/2
のマッチングにはパターンマッチングと同じ制限が適用されるため既存の変数に対してはピン演算子を使う必要があるcond/1
はelse if
と似たような動作をするcase/2
がネストしていたりする場合にはwith/1
を使うといい感じにリファクタできる場合がある(errorのハンドラーとか)
Elixir
特有の要素を掘り出していくとこれ使えるのかどうか疑問に思うことがある機能がある気がしますが、今回やったあたりをスマートに使いこなせるようになりたいですね