関数の構文
パターンマッチング
コードを保存してコンパイルできるようになったので、より高度な関数を書き始めることができます。これまでに書いてきた関数は非常に単純で、少し物足りないものでした。もっと面白いことをやっていきましょう。最初に書く関数は、性別によって挨拶を変える必要があります。ほとんどの言語では、次のようなものを書く必要があります。
function greet(Gender,Name)
if Gender == male then
print("Hello, Mr. %s!", Name)
else if Gender == female then
print("Hello, Mrs. %s!", Name)
else
print("Hello, %s!", Name)
end
Erlangでは、パターンマッチングを使用することで、大量の定型コードを記述する必要がなくなります。Erlangで同様の関数は次のようになります。
greet(male, Name) ->
io:format("Hello, Mr. ~s!", [Name]);
greet(female, Name) ->
io:format("Hello, Mrs. ~s!", [Name]);
greet(_, Name) ->
io:format("Hello, ~s!", [Name]).
Erlangの出力関数は他の多くの言語よりも醜いことは認めますが、それは要点ではありません。ここでの主な違いは、関数のどの部分を使用するかを定義し、必要な値を同時にバインドするためにパターンマッチングを使用したことです。最初に値をバインドしてから比較する必要はありませんでした!そのため、代わりに
function(Args)
if X then
Expression
else if Y then
Expression
else
Expression
次のように記述します
function(X) -> Expression; function(Y) -> Expression; function(_) -> Expression.
これにより、同様の結果をより宣言的なスタイルで得ることができます。これらの各`function`宣言は、*関数節*と呼ばれます。関数節はセミコロン(`;`)で区切られ、まとめて*関数宣言*を形成します。関数宣言は1つの大きな文としてカウントされ、最後の関数節がピリオドで終わる理由です。ワークフローを決定するためにトークンを「面白く」使用していますが、慣れるでしょう。少なくともそう願うべきです。なぜなら、そこから逃れる方法はないからです!
**注:** `io:format`のフォーマットは、文字列内のトークンを置き換えることによって行われます。トークンを示すために使用される文字はチルダ(`~`)です。改行に変更される`~n`など、いくつかのトークンは組み込みです。他のほとんどのトークンは、データをフォーマットする方法を示しています。関数呼び出し`io:format("~s!~n",["Hello"]).`には、文字列とビット文字列を引数として受け取るトークン`~s`と`~n`が含まれています。したがって、最終的な出力メッセージは`"Hello!\n"`になります。広く使用されているもう1つのトークンは`~p`で、これはErlangの項を適切な方法で出力します(インデントなどを追加します)。
`io:format`関数は、入出力についてより深く扱う後の章で詳しく説明しますが、それまでの間、次の呼び出しを試して、それらが何をするかを確認できます。`io:format("~s~n",[<<"Hello">>])`、`io:format("~p~n",[<<"Hello">>])`、`io:format("~~~n")`、`io:format("~f~n", [4.0])`、`io:format("~30f~n", [4.0])`。これらは可能なことのほんの一部であり、全体として、他の多くの言語の`printf`と少し似ています。I/Oに関する章まで待てない場合は、オンラインドキュメントを読んで詳細を確認できます。
関数のパターンマッチングは、これよりも複雑で強力になる可能性があります。数章前に覚えていないかもしれませんが、リストに対してパターンマッチングを実行して、先頭と末尾を取得できます。やってみましょう! `<a class="source" href="https://learnyousomeerlang.dokyumento.jp/static/erlang/functions.erl" title="完全なモジュール!">functions</a>`という新しいモジュールを作成し、そこで利用可能な多くのパターンマッチングの方法を探索するための関数をいくつか記述します。
-module(functions). -compile(export_all). %% replace with -export() later, for God's sake!
最初に書く関数は`head/1`で、リストを引数として受け取り、その最初の要素を返す`erlang:hd/1`とまったく同じように動作します。これは、cons演算子(`|`)を使用して行われます。
head([H|_]) -> H.
シェルで`functions:head([1,2,3,4]).`と入力すると(モジュールがコンパイルされたら)、値 '1' が返されます。その結果、リストの2番目の要素を取得するには、次の関数を 作成します。
second([_,X|_]) -> X.
リストはパターンマッチングのためにErlangによって分解されます。シェルで試してみてください!
1> c(functions).
{ok, functions}
2> functions:head([1,2,3,4]).
1
3> functions:second([1,2,3,4]).
2
これは、何千もの値まで実用的ではないにしても、必要なだけリストに対して繰り返すことができます。これは、後で説明する再帰関数を記述することで修正できます。ここでは、より多くのパターンマッチングに集中しましょう。実際に始めるで説明した自由変数と束縛変数の概念は、関数にも当てはまります。関数が同じかどうかを比較して知ることができます。このために、2つの引数を取り、それらが同一かどうかを判断する関数`same/2`を作成します。
same(X,X) ->
true;
same(_,_) ->
false.
実に簡単です。関数の仕組みを説明する前に、念のために束縛変数と非束縛変数の概念をもう一度説明します。
この椅子の取り合いゲームがErlangだとしたら、空いている椅子に座りたいと思うでしょう。すでに誰かが座っている椅子に座ると、うまくいきません!冗談はさておき、非束縛変数は、値が割り当てられていない変数です(空の椅子のよう)。変数を束縛するとは、単に非束縛変数に値を割り当てることです。Erlangの場合、すでに束縛されている変数に値を割り当てようとすると、*新しい値が古い値と同じでない限り*、エラーが発生します。右側のヘビを想像してみましょう。別のヘビが来ても、ゲームにはそれほど変化はありません。怒っているヘビが増えるだけです。別の動物(たとえば、ラーテル)が椅子に座ろうとすると、事態は悪化します。束縛変数の値が同じであれば問題ありませんが、異なる場合は問題です。この概念が明確でない場合は、不変変数に関する小章に戻ることができます。
コードに戻りましょう。 `same(a,a)`を呼び出すと、最初の<var>X</var>は非束縛と見なされます。自動的に値`a`を取ります。次に、Erlangが2番目の引数に移ると、<var>X</var>がすでに束縛されていることがわかります。次に、2番目の引数として渡された`a`と比較して、一致するかどうかを確認します。パターンマッチングが成功し、関数は`true`を返します。2つの値が同じでない場合、これは失敗し、2番目の関数節に進みます。2番目の関数節は引数に関心がありません(最後に選択するとき、選り好みはできません!)。代わりにfalseを返します。この関数は事実上あらゆる種類の引数を取ることができることに注意してください!リストや単一の変数だけでなく、あらゆる種類のデータに対して機能します。かなり高度な例として、次の関数は日付を出力しますが、日付が正しくフォーマットされている場合に限ります。
valid_time({Date = {Y,M,D}, Time = {H,Min,S}}) ->
io:format("The Date tuple (~p) says today is: ~p/~p/~p,~n",[Date,Y,M,D]),
io:format("The time tuple (~p) indicates: ~p:~p:~p.~n", [Time,H,Min,S]);
valid_time(_) ->
io:format("Stop feeding me wrong data!~n").
関数ヘッドで`=`演算子を使用できることに注意してください。これにより、タプルの内部のコンテンツ(`{Y,M,D}`)とタプル全体(<var>Date</var>)の両方と一致させることができます。関数は次の方法でテストできます。
4> c(functions).
{ok, functions}
5> functions:valid_time({{2011,09,06},{09,04,43}}).
The Date tuple ({2011,9,6}) says today is: 2011/9/6,
The time tuple ({9,4,43}) indicates: 9:4:43.
ok
6> functions:valid_time({{2011,09,06},{09,04}}).
Stop feeding me wrong data!
ok
ただし、問題があります!この関数は、タプルが`{{A,B,C}, {D,E,F}}`の形式である限り、テキストやアトムであっても、値として何でも取ることができます。これは、パターンマッチングの限界の1つを示しています。既知の数の原子などの非常に正確な値、またはリストの先頭|末尾、<var>N</var>個の要素のタプル、または何でも(`_`と非束縛変数)などの抽象的な値を指定できます。この問題を解決するために、ガードを使用します。
ガード、ガード!
ガードは、関数のパターンマッチングをより表現力豊かにするために、関数のヘッドに追加できる句です。前述のように、パターンマッチングは値の範囲や特定のタイプのデータなどを表現できないため、いくぶん制限されています。表現できなかった概念はカウントです。この12歳のバスケットボール選手はプロとプレーするには背が低すぎますか?この距離は手で歩くには長すぎますか?車の運転には年齢が高すぎますか、それとも低すぎますか?単純なパターンマッチングでは、これらに答えることができませんでした。つまり、運転の質問を次のように表すことができます。
old_enough(0) -> false; old_enough(1) -> false; old_enough(2) -> false; ... old_enough(14) -> false; old_enough(15) -> false; old_enough(_) -> true.
しかし、それは非常に非現実的です。やりたいのであればそうすることができますが、永遠にコードを一人で作業することになります。最終的に友達を作りたい場合は、新しい`<a class="source" href="https://learnyousomeerlang.dokyumento.jp/static/erlang/guards.erl" title="ガードモジュール">guards</a>`モジュールを開始して、運転の質問に対する「正しい」解決策を入力できるようにします。
old_enough(X) when X >= 16 -> true; old_enough(_) -> false.
これで完了です!ご覧のとおり、これははるかに短く、クリーンです。ガード式の基本的なルールは、成功するには`true`を返す必要があることです。ガードは、`false`を返すか、例外をスローすると失敗します。ここで、104歳を超える人の運転を禁止するとします。運転に有効な年齢は、16歳から104歳になりました。それを処理する必要がありますが、どのようにすればよいでしょうか?2番目のガード句を追加してみましょう。
right_age(X) when X >= 16, X =< 104 ->
true;
right_age(_) ->
false.
カンマ(`,`)は演算子`andalso`と同様に機能し、セミコロン(`;`)は`orelse`と少し似ています(「実際に始める」で説明されています)。ガード全体がパスするには、両方のガード式が成功する必要があります。関数を反対の方法で表すこともできます。
wrong_age(X) when X < 16; X > 104 ->
true;
wrong_age(_) ->
false.
そして、それから正しい結果を得ます。必要であればテストしてください(常にテストする必要があります!)。ガード式では、セミコロン(`;`)は`orelse`演算子のように機能します。最初のガードが失敗した場合、2番目のガード、次に次のガードを試します。いずれかのガードが成功するか、すべて失敗するまでです。
関数では、比較やブール評価以外にも、数学演算(`A*B/C >= 0`)や、`is_integer/1`、`is_atom/1`などのデータ型に関する関数など、さらにいくつかの関数を使用できます。(次の章でそれらに戻ります)。ガードに関する1つの отрицательный момент は、副作用のためにユーザー定義関数を受け入れないことです。Erlangは、(Haskellのような)純粋関数型プログラミング言語ではないため、副作用に大きく依存しています。入出力、アクター間のメッセージ送信、エラーのスローは、必要なときに必要なだけ行うことができます。ガードで使用する関数が、複数の関数句で何度もテストされたときにテキストを出力するか、重要なエラーをキャッチするかどうかを判断する簡単な方法はありません。そのため、Erlangは単にあなたを信頼していません(そして、そうするのが正しいかもしれません!)。
そうは言っても、ガードの基本的な構文を理解して、ガードに出会ったときに理解できるはずです。
**注:** ガードの`,`と`;`を演算子`andalso`と`orelse`と比較しました。ただし、これらはまったく同じではありません。前者のペアは例外が発生したときにキャッチしますが、後者はキャッチしません。これは、ガード`X >= N; N >= 0`の最初の部分でエラーがスローされた場合、2番目の部分が引き続き評価され、ガードが成功する可能性があることを意味します。 `X >= N orelse N >= 0`の最初の部分でエラーがスローされた場合、2番目の部分もスキップされ、ガード全体が失敗します。
しかし(常に「しかし」はありますが)、ガード内でネストできるのは`andalso`と`orelse`のみです。つまり、`(A orelse B) andalso C`は有効なガードですが、`(A; B), C`は無効です。用途が異なるため、多くの場合、必要に応じてこれらを混在させるのが最善の戦略です。
Ifとは!?
`If`はガードのように動作し、ガードの構文を共有しますが、関数節のヘッドの外側にあります。実際、`if`節は*ガードパターン*と呼ばれます。Erlangの`if`は、他のほとんどの言語で見られる`if`とは異なります。それらと比較すると、Erlangの`if`は奇妙な生き物であり、別の名前であればもっと受け入れられていたかもしれません。Erlangの国に入るときは、`if`について知っていることはすべて玄関に置いておくべきです。これから冒険に出るので、席に着いてください。
if式がガードとどれだけ似ているかを確認するために、次の例を見てみましょう。
-module(what_the_if).
-export([heh_fine/0]).
heh_fine() ->
if 1 =:= 1 ->
works
end,
if 1 =:= 2; 1 =:= 1 ->
works
end,
if 1 =:= 2, 1 =:= 1 ->
fails
end.
これを`what_the_if.erl`として保存して、試してみましょう。
1> c(what_the_if).
./what_the_if.erl:12: Warning: no clause will ever match
./what_the_if.erl:12: Warning: the guard for this clause evaluates to 'false'
{ok,what_the_if}
2> what_the_if:heh_fine().
** exception error: no true branch found when evaluating an if expression
in function what_the_if:heh_fine/0
おっと!コンパイラは、12行目(`1 =:= 2, 1 =:= 1`)の`if`からの節は、唯一のガードが`false`と評価されるため、決して一致しないという警告を出しています。Erlangでは、すべてが何かを返さなければならず、`if`式も例外ではありません。そのため、Erlangはガードを成功させる方法を見つけられないとクラッシュします。何かを返さないことは*できません*。そのため、何が起きても常に成功する、キャッチオールブランチを追加する必要があります。ほとんどの言語では、これは「else」と呼ばれます。Erlangでは、「true」を使用します(これが、VMが怒ったときに「trueブランチが見つかりません」とスローした理由です)。
oh_god(N) ->
if N =:= 2 -> might_succeed;
true -> always_does %% this is Erlang's if's 'else!'
end.
そして、この新しい関数をテストしてみましょう(古い関数は警告を出し続けます。無視するか、何をすべきでないかを思い出させるものとして受け取ってください)。
3> c(what_the_if).
./what_the_if.erl:12: Warning: no clause will ever match
./what_the_if.erl:12: Warning: the guard for this clause evaluates to 'false'
{ok,what_the_if}
4> what_the_if:oh_god(2).
might_succeed
5> what_the_if:oh_god(3).
always_does
`if`式で複数のガードを使用する方法を示す別の関数を次に示します。この関数は、すべての式が何かを返さなければならないことも示しています。変数`Talk`には`if`式の結果がバインドされ、その後、タプル内の文字列に連結されます。コードを読むと、Erlangにはnull値(つまり、LispのNIL、CのNULL、PythonのNoneなど)がないことを考えると、`true`ブランチがないとどうなるかが簡単にわかります。
%% note, this one would be better as a pattern match in function heads!
%% I'm doing it this way for the sake of the example.
help_me(Animal) ->
Talk = if Animal == cat -> "meow";
Animal == beef -> "mooo";
Animal == dog -> "bark";
Animal == tree -> "bark";
true -> "fgdadfgna"
end,
{Animal, "says " ++ Talk ++ "!"}.
そして、試してみましょう。
6> c(what_the_if).
./what_the_if.erl:12: Warning: no clause will ever match
./what_the_if.erl:12: Warning: the guard for this clause evaluates to 'false'
{ok,what_the_if}
7> what_the_if:help_me(dog).
{dog,"says bark!"}
8> what_the_if:help_me("it hurts!").
{"it hurts!","says fgdadfgna!"}
多くのErlangプログラマーは、制御フローとしてアトム「else」ではなく「true」が選ばれた理由を疑問に思っているかもしれません。結局のところ、「else」の方がはるかに馴染みがあります。Richard O'Keefeは、Erlangメーリングリストで次の回答をしました。私の方がうまく言えないので、直接引用します。
より*馴染みがある*かもしれませんが、それは「else」が良いものだという意味ではありません。Erlangで「else」を取得するには「; true ->」と書くのが非常に簡単な方法であることは知っていますが、それが悪い考えであることを示す数十年にわたるプログラミング心理学の結果があります。私は、以下のように置き換え始めました。
by if X > Y -> a() if X > Y -> a() ; true -> b() ; X =< Y -> b() end end if X > Y -> a() if X > Y -> a() ; X < Y -> b() ; X < Y -> b() ; true -> c() ; X ==Y -> c() end endこれは、コードを*書く*ときは少し煩わしいと感じますが、*読む*ときは非常に役立ちます。
「Else」または「true」ブランチは完全に「避ける」べきです。`if`は通常、*「キャッチオール」*句に頼るのではなく、すべての論理的な終わりを網羅した方が読みやすくなります。
前述のように、ガード式で使用できる関数は限られています(詳しくは「型(または型の欠如)」で説明します)。ここで、Erlangの真の条件分岐能力が発揮されます。`case`式をご紹介します!
**注:** `what_the_if.erl`の関数名で表現されているこの恐怖はすべて、他の言語の`if`の観点から見た場合の`if`言語構成要素に関するものです。Erlangのコンテキストでは、紛らわしい名前を持つ完全に論理的な構成要素であることがわかります。
case ... of の場合
`if`式がガードのようなものである場合、`case ... of`式は関数ヘッド全体のようなものです。各引数に使用できる複雑なパターンマッチングを使用でき、その上にガードを配置できます!
おそらく構文にはかなり慣れてきているでしょうから、あまり多くの例は必要ありません。ここでは、順序付けられていないリストとして表すセット(一意の値のコレクション)の追加関数を記述します。これは効率の面で最悪の実装かもしれませんが、ここで必要なのは構文です。
insert(X,[]) ->
[X];
insert(X,Set) ->
case lists:member(X,Set) of
true -> Set;
false -> [X|Set]
end.
空のセット(リスト)と追加する項`X`を送信すると、`X`のみを含むリストが返されます。そうでない場合、関数`lists:member/2`は要素がリストの一部であるかどうかをチェックし、存在する場合はtrueを、存在しない場合はfalseを返します。セットに既に要素`X`がある場合、リストを変更する必要はありません。そうでない場合、`X`をリストの最初の要素として追加します。
この場合、パターンマッチングは非常に単純でした。より複雑になる可能性があります(コードを私のコードと比較できます)。
beach(Temperature) ->
case Temperature of
{celsius, N} when N >= 20, N =< 45 ->
'favorable';
{kelvin, N} when N >= 293, N =< 318 ->
'scientifically favorable';
{fahrenheit, N} when N >= 68, N =< 113 ->
'favorable in the US';
_ ->
'avoid beach'
end.
ここでは、「ビーチに行くのに適した時間かどうか」という質問に対する答えは、摂氏、ケルビン、華氏の3つの異なる温度システムで示されています。すべての用途を満たす回答を返すために、パターンマッチングとガードが組み合わされています。前述のように、`case ... of`式は、ガード付きの関数のヘッドの束とほぼ同じです。実際、コードを次のように書くこともできました。
beachf({celsius, N}) when N >= 20, N =< 45 ->
'favorable';
...
beachf(_) ->
'avoid beach'.
これは、条件式を実行するために、いつ`if`、`case ... of`、または関数を使用する必要があるのかという疑問を提起します。
どれを使うべきか?
どれを使うべきかは、なかなか答えにくい問題です。関数呼び出しと`case ... of`の違いはごくわずかです。実際、それらは低レベルでは同じように表現され、どちらを使用してもパフォーマンスの面では実質的に同じコストがかかります。両者の違いの1つは、複数の引数を評価する必要がある場合です。`function(A,B) -> ... end.`は、`A`と`B`に対して一致させるガードと値を持つことができますが、case式は次のように少し工夫する必要があります。
case {A,B} of
Pattern Guards -> ...
end.
この形式はめったに見られず、読者を少し驚かせるかもしれません。このような状況では、関数呼び出しを使用する方が適切かもしれません。一方、前に書いた`insert/2`関数は、単純な`true`または`false`節を追跡するための即時関数呼び出しを持つよりも、そのままの方が arguably きれいです。
では、`case`と関数は`if`をガードで包含するのに十分な柔軟性があることを考えると、なぜ`if`を使うのでしょうか?`if`の根拠は非常に単純です。パターンマッチング部分全体を書く必要がない場合に、ガードを使用するための簡単な方法として言語に追加されました。
もちろん、これはすべて個人的な好みと、より頻繁に遭遇するものに関するものです。明確な答えはありません。このトピック全体は、Erlangコミュニティで時々議論されています。理解しやすい限り、誰もあなたを殴ろうとはしません。Ward Cunninghamがかつて言ったように、「クリーンなコードとは、ルーチンを見て、それがほぼ予想通りのものであるときです。」