トップページへ

Haskell言語のmap関数

Access Count : 1939

Copyright © 2020 TAKEHANA TADASHI
著作日時: 2020.07.14.火. 18:50:00 著作者、竹花 忠
Haskell言語のmap関数:
 旧来の手続き型言語では、繰り返しを実現するには、while関数やfor関数を使用して、そして、そのループのための変数を用意して、そのループ変数のカウントアップの記述とループのブレークの記述を行う。そして、そのwhileループやforループの中で、ループ変数を添え字にして配列にアクセスして、それに対して目的の処理の記述を行う。
 これによって、配列の先頭から順に繰り返し目的の処理が実行される。
 プログラムコードの一例を示すと次のようになる。
int dat[10] = {1,2,3,4,5,6}
for(i = 0; i < 6; i++){
dat[i] = dat[i] * 3;
}
 3倍している* 3が目的の処理である。

 これに対して関数型言語のHaskellでは、繰り返しを実現するための処理の詳細が、表面上削除・隠蔽、された記述形式が用意されている。
 上記のプログラムとほぼ同様な結果、[3,6,9,12,15,18]をdatに得るためのHaskell言語でのプログラムコードの一例を示すと次のようになる。
dat = map (*3) [1,2,3,4,5,6]
 Haskell言語では、繰り返しを実現するために、ループ変数を用意しなくていい。ループ変数の初期値や終了値のための値を用意しなくていい。繰り返しの対象となる連続するデーターのうちのどのデーターにアクセスするかの記述をしなくていい。ただし、繰り返しの対象となる連続するデーターであるリスト自体は、繰り返しを実現するmap関数の第2引数に設定する。
 Haskell言語では、繰り返しを実現すたるめの関数mapの直後の第1引数に、目的の処理の内容を記述して、その次の第2引数に、繰り返しの対象とする連続データーであるリストを記述するだけで、目的の処理が繰り返し実行できる。
 つまり、Haskell言語では、繰り返しを実現するについて、ループ変数関係の記述が一切存在しないので、それにまつわる記述の負担やそれにまつわる記述ミスが完全に回避できる。
 なので、Haskell言語では、繰り返しが簡潔に記述できる。

 while関数やfor関数での繰り返しに比べて、map関数での繰り返しでは、ループ変数関係の記述が、表面上削除・隠蔽、されている。
 なので、その分だけ、map関数を用いた記述は、while関数やfor関数を用いた記述に比べて、簡潔な・宣言的な・隠蔽的な・簡略な、記述になっている。

 ただし、実際には・具体的には、map関数は、ループ変数関係の処理を隠蔽して繰り返しを実現しているものではない。
 map関数の中では、リストを、リスト構築子を用いた記述形式で受け取って、リストをその先頭要素と後続のリストに分解している。それに該当する部分が、(x : xs)である。
 そして先頭要素に対して目的の処理を実行する。そして後続のリストに対しては、map関数を再帰呼び出しする。この2つをリスト構築子で連結する。それを返り値としている。それに該当する部分が、(f x) : map f xsである。
 なお、再帰呼び出しが、空のリストに対して実行された時には、その時だけは特別に、空のリストを返り値としている。それに該当する部分が、map f [] = []である。
 以上によって、リストの先頭から順に、目的の処理が実行されてゆく、そしてそれはリスト構築子で連結されながら。なので結局、リストの全要素に目的の処理が実行された結果、その結果が連なったリストが得られる。
 なので、map関数は、リスト構築子を用いた、引数であるリストの分割や目的の処理の、再帰呼び出し時のリストの先頭要素に対しての実行や、その時の後続のリストに対しての再帰呼び出しや、その2つの結果のリスト構築子による連結や、空リストで再帰呼び出しをした時には空リストを返すこと、を隠蔽している。
 実際に・具体的に、map関数が隠蔽しているものは、そのような処理であって、ループ変数関係の処理ではない。ただし、手続き型言語における繰り返しと比較してみると、Haskell言語のmap関数は、ループ変関係の処理を隠蔽しているかのように映る。
 Haskell言語のmap関数のプログラムコードの一例を示すと次のようになる。
map f [] = []
map f (x : xs) = (f x) : map f xs
 このような2行のコードからmap関数はできている。
 ここで、:がリスト構築子である。
 リスト構築子は、(x : xs)と記述すると、:の左のxがリストの先頭要素で、:の右のxsがその後続リストである。
 x : y : lisと記述すると、右端だけ、lisだけ、がリストで、それ以外は、それぞれどれも各々リストの1要素。一番右側の要素yから、そのすぐ右に位置している唯一つのリストlisの中に、そのリストの先頭要素として連結されてゆく。
 そして、順番に次はその左の要素xが、先ほど完成したリストの中にその先頭要素として連結される。
 つまりたとえば、1 : 9 : [6,1]と記述すると、まず最初、1 : [9,6,1]となり、そして、最後に、[1,9,6,1]となる。
 fには、mapの第1引数、つまり、目的の処理、が格納される。
 xには、呼び出し時のリストの先頭要素が格納される。
 xsには、その後続のリストが格納される。
 (f x)は、呼び出し時のリストの先頭要素xに、目的の処理fを実行した結果である。
 map f xsは、第1引数のfはそのままで、第2引数を、呼び出し時のリストの先頭要素を除いた後続のリストxsに差し替えた上での再帰呼び出しである。
 したがって、
dat = map (*3) [1,2,3,4,5,6] の実行にあっては、
 まず、この右辺 map (*3) [1,2,3,4,5,6]が、map f (x : xs) = (f x) : map f xsの左辺 map f (x : xs)にマッチングすることで、
map (*3) (1 : [2,3,4,5,6]) = ((*3) 1) : map (*3) [2,3,4,5,6]
となる。
 つまり、新たに、((*3) 1) : map (*3) [2,3,4,5,6] という右辺が得られる。
 以下、再帰呼び出しの記述の部分をその都度、
map f [] = []
map f (x : xs) = (f x) : map f xs
のいずれかとマッチングした方を用いて、その時その時の、f,x,xsの当該値を当てはめた、(f x) : map f xsかあるいは[]かに置き換えてゆく。
 すると、map (*3) [1,2,3,4,5,6]は、最初は先ほどの((*3) 1) : map (*3) [2,3,4,5,6] に置き換わり、順次、
((*3) 1) : ((*3) 2) : map (*3) [3,4,5,6]
((*3) 1) : ((*3) 2) : ((*3) 3) : map (*3) [4,5,6]
((*3) 1) : ((*3) 2) : ((*3) 3) : ((*3) 4) : map (*3) [5,6]
((*3) 1) : ((*3) 2) : ((*3) 3) : ((*3) 4) : ((*3) 5) : map (*3) [6]
((*3) 1) : ((*3) 2) : ((*3) 3) : ((*3) 4) : ((*3) 5) : ((*3) 6) : map (*3) []
((*3) 1) : ((*3) 2) : ((*3) 3) : ((*3) 4) : ((*3) 5) : ((*3) 6) : []
へと置き換わる。
 そして最後は、
((*3) 1) : ((*3) 2) : ((*3) 3) : ((*3) 4) : ((*3) 5) : ((*3) 6) : []
が、リスト連結子によって、右端から順に連結されてゆく。つまり、
((*3) 1) : ((*3) 2) : ((*3) 3) : ((*3) 4) : ((*3) 5) : [18]
((*3) 1) : ((*3) 2) : ((*3) 3) : ((*3) 4) : [15,18]
((*3) 1) : ((*3) 2) : [9,12,15,18]
((*3) 1) : [6,9,12,15,18]
[3,6,9,12,15,18]
 以上で、
dat = map (*3) [1,2,3,4,5,6]
の右辺が得られた。
 したがって、
dat = [3,6,9,12,15,18]

 つまり、map関数を使用すれば、ややこしいことは隠蔽されて、第1引数に目的の処理を記述し、第2引数に目的の処理を適用するデーターの連なったリストを記述してやれば、難しいこと抜きで、簡略に、リストの全データーに目的の処理を適用した結果から成るリストが得られる。
 これがHaskell言語のmap関数のすばらしさである。