LoginSignup
2
2

More than 5 years have passed since last update.

7つの言語 7つの世界 ~第6章 Erlang 2日目~

Posted at

はじめに

 昨日(http://qiita.com/alancodvo/items/94bf1e565332e3349819)
に引き続き、「7つの言語 7つの世界 -オーム社-」の第6章Erlangの2日目を勉強していく。

2日目本編

制御構文

 まず、Erlangの基本制御構文を勉強する。Erlangでは、平行アプリケーションを書くときに処理すべきメッセージを解釈するため、case文を使うことが多く、if文は主流ではないらしい。

case

 Animalという変数の値に基いてコードを条件分岐したいときは、

1> Animal = "dog".
2> case Animal of
2>     "dog" -> underdog;
2>     "cat" -> thundercat
2> end.
underdog

 この例では、文字列"dog"が最初の節にマッチするので、underdogというアトムが返される。次に、任意の値にマッチさせる。その時使うのがアンダースコアである。これは、Prologを同様らしい。

3> case Animal of
3>     "elephant" -> dumbo;
3>     _ -> something_else
3> end.
something_else

 Animalは"dog"であり、"elephant"ではないため、最後の節にマッチする。case文の最後の節はセミコロンが必要ないため、後々編集する際には気をつけること。

if

 case文では、パターンマッチングを使用したが、if文ではガードを使用する。Erlangのガードとは、マッチが成功するために満たす条件のことである。

1> X = 0.
2> if
2>   X > 0 -> positive;
2>   X < 0 -> negative
2> end.
エラー

 上記の例ではif文のどの節にもマッチしなかったためエラーとなった。Erlangのif文は関数なので、いずれかの条件で真になる必要があり、各条件で値を返す必要がある。そのため、elseに当たる部分が欲しかったら最後のガードをtrueにすれば良い。

1> X = 0.
2> if
2>   X > 0 -> positive;
2>   X < 0 -> negative;
2>   true -> zero
2> end.
zero

無名関数

 Erlangでは、任意の関数を変数に代入し、その変数を他のデータ型と同じように受け渡しすることが可能である。

1> Negata = fun(I) -> -I end.
2> Negate(1).
-1
3> Negate(-5).
5

 この例では、無名関数が単一の引数Iを受け取り、その負の値-Iを返している。また、無名関数を定義しているのがfunである。この無名関数をNegateという変数に代入している。

リストと高階関数

リストに関数を適用する

 ここでは、関数を使用してリストを管理してみる。最初に、基本的な繰り返しを行ってみる。losts:foreachメソッドは、1つの関数と1つのリストを引数として受け取る。引数に指定する関数は無名関数でも構わないらしい。

1> Numbers = [1, 2, 3, 4].
[1, 2, 3, 4]
2> lists:foreach(fun(Number) -> io:format("~p~n", [Number]) end, Numbers).
1
2
3
4
ok

 この例では、まずlists:foreachという関数を呼び出す。第1引数は無名関数fun(Number) -> io:format(~p~n", [Number]) end)である。この関数は1つの引数をとり、渡された引数の値をio:format関数を用いて出力する。~pはプリティプリント用、~nは改行、[Number]は出力する引数のリストである。第2引数はNumbers、すなわち1行目で定義したリストである。
 今回の例の第1引数を別の関数に定義することでわかりやすくなる。

1> Numbers = [1, 2, 3, 4].
[1, 2, 3, 4]
2> Print = fun(X) -> io:format("~p~n", [X]) end.
3> lists:foreach(Print, Numbers).
1
2
3
4
ok

 基本的な繰り返しは以上である。次にマッピングを行う関数を勉強する。Rubyのcollectと似たようなものらしく、リストの各値を関数に渡して、関数で処理された結果のリストを生成する。lists:foreachと同様、lists:mapは1つの関数と1つのリストを引数として受け取る。mapを使用して、数値リストの各値を1つだけ増やしてみる。

1> Numbers = [1, 2, 3, 4].
[1, 2, 3, 4]
2> lists:map(fun(X) -> X + 1 end, Numbers).
[2, 3, 4, 5]

 先ほどの繰り返しのように今度はfun(X) -> X + 1 endが無名関数である。これによって各値を1だけ増分し、lists:mapがその結果値のリストを生成する。

 次にフィルタリングを行う。

1> Numbers = [1, 2, 3, 4].
2> Small = fun(X) -> X < 3 end.
3> Small(4).
false
4> Small(1).
true
5> lists:filter(Small, Numbers).
[1, 2]
6> lists:all(Small, [0, 1, 2]).
true
7> listsall(Small, [3, 4, 5]).
false
8> lists:any(Small, [0, 1, 2, 3]).
true
9> lists:any(Small, [3, 4, 5]).
false
10> lists:any(Small, []).
false
11> lists: all(Small, []).
true

 一度にたくさん書いてしまったが、5行目のlists:filter関数は、Smallを満たすリストを返している。lists:allはリスト内の全ての要素がSmallを見たしている場合のみtrueを返す。lists:anyはリスト内のいずれかの要素がフィルタ条件(Small)を満たしていればtrueを返す。
 最後に、空リストを引数として与えた場合の挙動を試したが、allはtrueを返し、anyはfalseを返した。この結果は空リストであれば変わらない。
 また、リストのヘッド部分にあってフィルタ条件を満たすすべての要素のリストを生成したり、リストの先頭部分にあってフィルタ条件を満たすすべての要素を破棄したりすることもできるので紹介する。

12> lists:takewhile(Small, Numbers).
[1,2]
13> lists:dropwhile(Small, Numbers).
[3,4]
14> lists:takewhile(Small, [1, 2, 1, 4, 1]).
[1, 2, 1]
15> lists:dropwhile(Small, [1, 2, 1, 4, 1]).
[4, 1]

 これらのテスト関数は、メッセージのヘッダ部分を処理したり破棄したりするのに便利みたいだ。これで生成したリストを変数に入れて。。。とかになる気がする。

foldl

 本書では、この概念については既に知っているものとされ、少し違った方法で説明をしているらしい。しかし、私自身は初めて聞くのでコードを見ながら理解していきたい。
 本書によると、これらの関数(foldl, foldr)は、リストの各項に対して関数を適用した結果を畳み込むときに便利らしい。引数の1つがアキュムレータとして機能し、残りの引数が各要素を表すらしい。ちなみに、少し調べたらpythonのreduceと同じだった。笑

Pythonのreduceを使うとちょうはかどる話 - Qiita

 早速試してみよう。lists:foldlは、関数、アキュムレータの初期値、およびリストを引数として受け取る。

1> Numbers = [1, 2, 3, 4].
2> lists:foldl(fun(X, Sum) -> X + Sum end, 0, Numbers).
10

 少し単純化するために、無名関数を変数に代入し、各変数の名前もわかりやすくする。

3> Adder = fun(ListItem, SumSoFar) -> ListItem + SumSoFar end.
4> InitialSum = 0.
0
5> lists:foldl(Adder, InitialSum, Numbers).
10

 中間合計を保持し続けることがわかりやすくなった。SumSoFarと、Numbersリストの各数値を1つずつ関数Adderに渡すたびに合計値が大きくなるのだ。lists:foldl関数は、中間合計を覚えていて、それをAdderに戻す。最後に、最終合計を返すということだ。

高度なリストの概念

 これまで説明してきたことは、簡単なコードブロックによる極めて基本的な抽象化であり、他の言語でも見たことのある概念の拡張にすぎない。しかし、リストはもう少し高度な扱い方ができる。

リストの構築

 可変状態なしでリストを作り出すのは、一見難しく見えるかもしれない(RubyやIoではリストにそのまま要素を追加していく)。しかし、要素が追加された新しいリストを返すという方法も存在する。リストには通常、頭から順に要素を追加する。ここでは、[H|T]ここでは、マッチの右側で使用する。次のプログラムは、リスト構築手法を用いてリストの各要素を2倍する。

double.erl
-module(double).
-export([double_all/1]).

double_all([]) -> [];
double_all([First|Rest]) -> [First + First|double_all(Rest)].

 最初の節は、空リストに対してdouble_allを適用すると空リストが返されることを示す。このルールによって、再帰が終了される。
 2番目のルールは[H|T]構文を、関数定義部分だけでなく、マッチの述語部分でも使っている。[First|Rest]により、リストを先頭要素と残りの要素に分割できる。よって、左側はリストの分割を行っているだけである。
 これをマッチの右側に指定すると、リストの分割ではなくリストの構築を行うことができる。First + Firstを先頭要素として、double_all(Rest)を残りの部分とするリストを構築している。コンソール上で試した例を見ると理解しやすいかもしれない。

1> c(double).
{ok, double}
2> double:double_all([1, 2, 3]).
[2, 4, 6]
3> [1| [2, 3]].
[1, 2, 3]
4> [[2, 3]| 1].
[[2, 3]|1]
5> [[]| [2, 3]].
[[], 2, 3]
6> [1| []].
[1]

 上記の例を見ても分かる通り、第2引数はリストでなければならない。左側には何を書いたとしてもリストの先頭要素として追加される。

リスト内包表記

 普段Pythonを使用しているのでリスト内包表記についてはお手の物と言いたいところである。笑
 まずは従来の方法でマッピングを行ってみる。

1> Fibs = [1, 1, 2, 3, 5].
[1, 1, 2, 3, 5]
2> Double = fun(X) -> X * 2 end.
3> lists:map(Double, Fibs).
[2, 2, 4, 6, 10]

 最初にFibsという数値リストと、渡されたものを2倍にするDoubleという無名関数を定義している。その後、lists:mapを呼び出して、各要素に対してDoubleを呼び出し、得られた結果からリストを構築している。これを内包表記で書くと、

4> [Double(X) || X <- Fibs].
[2, 2, 4, 6, 10]
5> [X * 2 || X <- [1, 1, 2]].
[2, 2, 4]

 4行目を文章にすると、「Fibsというリストの各要素Xに対して、XのDoubleを計算する」となる。5行目のように変数や無名関数を定義しなくても書くことができる。
 mapをコンパクト定義すると、

map(F, L) -> [ F(X) || X <- L].

 これを文章にすると「関数Fに対してリストLえおマッピングすることにより、Lの各メンバであるF(X)のコレクションを生成する」となる。
 他の例を見てみよう、

1> Cart = [{pencil, 4, 0.25}, {pen, 1, 1.2}, {paper, 2, 0.2}].

[{pencil, 4, 0.25}, {pen, 1, 1.2}, {paper, 2, 0.2}]

2> WithTax = [{Product, Quantity, Price, Price * Quantity * 0.08} || {Product, Quantity, Price} <- cart].

[{pencil, 4, 0.25, 0.08}, {pen, 1, 1.2, 0.096}, {paper, 2, 0.2, 0.032}]

3> Cat = [{Product, Price} || {Product, _, Price} <- Cat].

[{pencil, 0.125}, {pen, 0.6}, {paper, 0.1}]

4> DiscountedCat = [{Product, Price / 2} || {Product, Price} <- Cat].

[{pencil, 0.125}, {pen, 0.6}, {paper, 0.1}]

 変数Cartには、商品、数量、価格のタプルのリストが定義されている。WithTaxでは、1ドルにつき8セントの消費税をタプル内に追加したリストを返している。
 3行目では、Cartから数量を除外したタプルのリストを返している。そして、4行目で価格を50%オフしたリストを返している。
 このように、リスト内包表記を使用するとコンパクトで、読みやすくなる。最後に特徴をあげておく。

  • リスト内包表記は[式 || 節1, 節2, ..., 節N]という形式を持つ。
  • リスト内包表記には任意の数の節を含めることができる。
  • 節には、ジェネレータまたはフィルタを指定できる。
  • フィルタには、ブール式またはブール値を返す関数を指定できる。
  • Match <- Listという形式のジェネレータは、左辺のパターンを右辺のリストの各要素とマッチングする。

 ジェネレータは追加操作、フィルタは削除操作を行う。これにはPrologの影響が色濃く出ているらしい。ジェネレータは取りうる値を決定し、フィルタは指定された条件にしたがってリストから要素を取り除く。以下にいくつか例を示す。

1> [X || X <- [1, 2, 3, 4], X < 4, X > 1].
[2, 3]
2> [{X, Y} || X <- [1, 2, 3, 4], X < 3, Y <- [5, 6]].
[{1,5}, {1, 6}, {2, 5}, {2, 6}]

 このように複数のジェネレータを指定することもできる。

2日目で学んだこと

 リストの構築のところが少し難しかったが、double_all(Rest)の部分に注目して再帰を考えると理解出来た。自分も再帰に慣れていないので説明が難しいが。。。
 [H|T]構文の使い方についてももう少し学習する必要がある。1日目でも触れていたのだ(昨日の記事では飛ばしていた)がそれほど重要では無いと思っていた。しかし、今日の例を見ると使い道は多そうである。
 3日目では、とうとう並行プログラミングをするみたいなので楽しみである。

セルフスタディ2日目

  • [{erlang, "a functional language"}, {ruby, "an OO language"}] のようなキーワード値タプルを考える。リストとキーワードを受け取り、そのキーワードに対応する値を返す関数を書け。

 これわからなかった。。。

  • [{item quantity price}, ...]という形式の買い物リストを考える。この時、[{item total_price}, ...]という形式のitemリストを構築するリスト内包表記を書け。ただし、total_priceは、quantity*priceとする。
1> Cart = [{pencil, 4, 0.25}, {pen, 1, 1.2}, {paper, 2, 0.2}].
[{pencil,4,0.25},{pen,1,1.2},{paper,2,0.2}]
2> [{Product, Price*Quantity} || {Product, _, Price} <- Cart].
* 1: variable 'Quantity' is unbound
3> [{Product, TotalPrice} || {Product, _, Price*Quantity} <- Cart].    
* 1: illegal pattern
4> [{Product, TotalPrice} || {Product, Quantity, Price} <- Cart].  
* 1: variable 'TotalPrice' is unbound
5> [{Product, Price} || {Product, Quantity, Price} <- Cart].     
[{pencil,0.25},{pen,1.2},{paper,0.2}]
6> [{Product, Price*Quantity} || {Product, Quantity, Price} <- Cart].
[{pencil,1.0},{pen,1.2},{paper,0.4}]

 これは簡単でしたね(何回も間違えてるけど笑)。

  • 長さ9のリストまたはタプルとして表現された三目並べのキーボードを読み込むプログラムを書け。勝者が確定している場合はその勝者(xまたはoのどちらか)を、これ以上指し手がない場合はcatを、勝負がついていない場合はno_winnerを返すようにせよ。

 ボーナス問題は後々。。。

2
2
1

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
2