関数表記に関する解説

松田裕幸 matsuda@symbolics.jp (2011.4.48)

レシピA.1 #と&の使い方

質問:『Mathematicaクックブック』 を買ったけど関数型表記の意味と使い方がよく分からない。特に、# と & をどう使ったらいいんだろう。

回答:Mathematicaの典型的な関数を例に基本的な話をします。

In [ ]:
f[a_, b_] := a+b;
f[3, 4]
Out[ ]:
7

次にこの関数定義の表現を分解してみます。

f          ... 関数名
[a_, b_]  ... 引数
:=        ... 定義
a + b    ... 関数本体

これらの中で、どこが一番重要でしょう?もちろん、関数本体(a+b)です。しかし、実際に変数aと変数bに値が決まらなければ計算はできません。値はどこから持ってくるのでしょう?引数からです。

では、引数の役割はなんでしょう?いま、たまたま a と b という名前を使いましたが、これがarg1とarg2でも変わらないことは分かっていただけると思います。もちろん、その場合、関数本体の方も arg1+arg2に変更する必要があります。

ということは、引数名は任意の名称が使えることが分かります。もう一つ引数には重要な情報が含まれています。それは位置に関するものです。変数 a は第1引数、変数 b は第2引数に対応しています。

整理すると引数の名称は何でもいい、しかし、位置情報は守らなければならない。ということで、上記関数定義を次のように書き換えてみます。

f          ... 関数名
[#1, #2]  ... 引数
:=         ... 定義
 #1+#2    ... 関数本体

ここまで来ると、実は引数部分は不要ではないと思えませんか?つまり関数本体だけでいいのではないかということです。実際、そうなります。ただし、関数であることを示すために最後に&を付けます。

 #1+#2&

こういう形の関数を純関数 pure function、と呼びます。また、#をplace holder(収納子)呼びます。

In [ ]:
#1 + #2 &[3, 4]
Out[ ]:
7

解説

Lispの世界にはラムダ式(あるいはラムダ関数)という概念がはるか昔から存在していました。上記Mathematicaの式をラムダ式で表すとこんな感じになります。

(lambda (a b) (+ a b))

Mathematicaでも似たような表記が存在します。ただし、これを積極的に使うことはあまりありませんが。

In [ ]:
Function[{a, b}, a + b][3, 4]
Out[ ]:
7

さて、ずっとほっておいた関数名はどうなったのでしょうか?すっかり忘れていました。関数名って何故必要なのでしょうか?定義に対し名前を付けなければ参照できない、名前付は当然だろうという反論が出てきそうです。しかし、本当にそうでしょうか?

Mathematicaではさきほどの純関数を「任意」の場所で使うことができます。このスタイルを on the flyと呼びます。たとえばこんな感じです。

In [ ]:
data = RandomInteger[{1, 100}, {10}];
Sort[data, #1 > #2 &]
Out[ ]:
{90, 82, 75, 60, 52, 41, 33, 19, 11, 10}

これに対し従来のやり方は、次のように比較関数を定義し、それに名前greaterを付け、それを使うというものでした。

In [ ]:
greater[a_, b_] := a > b;
Sort[data, greater[#1, #2] &]
Out[ ]:
{90, 82, 75, 60, 52, 41, 33, 19, 11, 10}

あるいは次のように書きます。

In [ ]:
Sort[data, greater]
Out[ ]:
{90, 82, 75, 60, 52, 41, 33, 19, 11, 10}

しかし、すでにみたように関数名を付けずにいきなり関数本体を、式の中にはめ込むことが可能です。これはLispのラムダ式も同じです。

それでも名前を付けておくと便利な時もあります。プログラムの意味がつかみやすくなるからです。

In [ ]:
second = #[[2]] &;
data = {a, b, c, d, e};
{First[data], second[data]}
Out[ ]:
{a, b}

では実際に『クックブック』の中から例を持ってきて解説してみます。

例1

In [ ]:
NestList[Translate[Rotate[#, -d], {1.5, 0}] &, shape, 3] // TableForm
Out[ ]:
Output

この例のように未評価のシンボルを利用することで、#にどんな値が入ってくるのか確認できます。この例の場合は、#にshapeが代入されています。

それでもよく分からないということであれば、&(ここまでが関数)までを未評価のシンボルfに置き換えてみます。いかがでしょう?

In [ ]:
Clear[f];
NestList[f[#] &, shape, 3] // TableForm
Out[ ]:
Output

例2

In [ ]:
data = RandomInteger[{1, 100}, {10}];
Sort[data, #1 > #2 &]
Out[ ]:
{87, 79, 64, 60, 44, 13, 8, 6, 3, 2}

もしかしたこのコード、よく考えると不思議にみえるかもしれません。なぜなら#1, #2って何に対応するんだろうという疑問です。並び替え(Sort)アルゴリズムは無数にありますが、通常、2つのデータの大小を比較します。アルゴリズムによって比較されるデータは隣合わせになることもあるし、離れた2つのデータが比較されることもありますが、どちらしても「2つ」のデータを比較する点には変わりありません。

その比較されるデータを#1と#2で表します。

同じ並び替え問題でも少し複雑な問題を考えてみますが、今までと同様簡単に対応できます。

In [ ]:
data = {{"桃太郎", 5}, {"ウルトラマン", 20}, {"一寸法師", 3}, {"花咲爺", 60}};
Sort[data]
Out[ ]:
{{桃太郎, 5}, {花咲爺, 60}, {一寸法師, 3}, {ウルトラマン, 20}}

何も考えないと漢字コードに従って並び替わります。では、年齢に応じて並び替えたいとしたらどうすればいいでしょう?

In [ ]:
Sort[data, #1[[2]] > #2[[2]] &]
Out[ ]:
{{花咲爺, 60}, {ウルトラマン, 20}, {桃太郎, 5}, {一寸法師, 3}}

例3

In [ ]:
Total[MapIndexed[#1 x^First[#2] &, {2, 0, 7, 3}]]
Out[ ]:
Output

この場合も理解が難しければすでに試みたのと同じ方法で組み込み関数を未定義シンボルに置き換えてみます。

In [ ]:
Total[MapIndexed[#1 x^First2[#2] &, {2, 0, 7, 3}]]
Out[ ]:
Output

いかがでしょう?#1には順次、2,0,7,3が代入されているのがわかります。一方、#2にはなんか変なものが入っています。MathematicaではSortの例でも見たようにシステム側が持っている内部情報を#変数を使い、ユーザに開放しています。この#2もその1つです。

MapIndexedは写像する際、その位置も記憶しています。それを{}のに包んで返してきます。したがって、実際に位置を知るにはその中身を取る必要があり、Fristを使っています。上記コードは次のようにも書き直せます。

In [ ]:
Total[MapIndexed[#1 x^#2[[1]] &, {2, 0, 7, 3}]]
Out[ ]:
Output

レシピA.2 @

質問: @って何?そもそもなんでこんな記号が必要なのか分からない。

回答: 説明難しいです(笑)。

解説

本来@は関数Compositon(合成)の中置(infix)表記です。

In [ ]:
Clear[f];
f@g@h[a, b]
Out[ ]:
f[g[h[a, b]]]
In [ ]:
Composition[f, g, h][a, b]
Out[ ]:
f[g[h[a, b]]]
In [ ]:
Sqrt@Plus[3^2 + 4^2]
Out[ ]:
5
In [ ]:
data = RandomInteger[{1, 1000}, {10}]
Length@Select[data, # > 500 &]
Out[ ]:
Out[26]:
{666, 150, 552, 149, 901, 772, 605, 64, 679, 378}
Out[27]:
6

ではよく見かける@@はなんでしょう?実は@@@もあるのですが、これは後で説明します。いきなりですが、こんな例を載せます。

In [ ]:
f @@ {a, b}
Out[ ]:
f[a, b]

これを正確に書き直すと次のようになります。

In [ ]:
f @@ List[a, b]
Out[ ]:
f[a, b]

@は合成の役割を果たすと書きました。では、@@で2回合成すると考えればいいのでしょうか?なぜ@@を2つ合わせることをWolframが思いついたのかは不明ですが、ここでは合成を2回行うという意味ではなく、後者を前者で乗っ取る意味に解釈します。つまり、Listをfで置き換えます。

In [ ]:
f[a, b]
Out[ ]:
f[a, b]

Mathematicaにはすべての関数に前置、中置、後置の3種類の表現を持ちます。もちろん@@は中置形式です。では、この前置形式はなんでしょう? Applyです。

In [ ]:
Apply[f, {a, b}]
Out[ ]:
f[a, b]

Applyの例を2つほど挙げてみます。

In [ ]:
Plus[{1, 2, 3}] (*エラー*)
Out[ ]:
{1, 2, 3}
In [ ]:
{Plus @@ {1, 2, 3}, Apply[Plus, {1, 2, 3}], Plus[1, 2, 3]}
Out[ ]:
{6, 6, 6}

あるいはこんな例も。

In [ ]:
data = {zaiko[10, 20, 30], zaiko[550, 20], zaiko[40, 2222]}
Out[ ]:
{zaiko[10, 20, 30], zaiko[550, 20], zaiko[40, 2222]}
In [ ]:
Map[Apply[Plus, #] &, data] (* Mapについては次に説明します *)
Out[ ]:
{60, 570, 2262}

では@@@はなんでしょう?これもMapを解説した後に説明します。

レシピA.3 /@

質問:Mathematicaの関数型の本質はMap(写像)だとよく聞くけど、今ひとつ理解できない。

回答: Mapの働き自体は単純です。

In [ ]:
Map[f, {a, b, c, d}]
Out[ ]:
{f[a], f[b], f[c], f[d]}

Mapの中置表記が /@です。

In [ ]:
f /@ {a, b, c, d}
Out[ ]:
{f[a], f[b], f[c], f[d]}

いままでことをおさらいするとこんなのも書けます。

In [ ]:
#^2 & /@ {1, 2, 3, 4}
Out[ ]:
{1, 4, 9, 16}
In [ ]:
Map[#^2 &, {1, 2, 3, 4}]
Out[ ]:
{1, 4, 9, 16}

あるいは

In [ ]:
#[Pi/4] & /@ {Sin, Cos, Tan}
Out[ ]:
Output
In [ ]:
Map[#[Pi/4] &, {Sin, Cos, Tan}]
Out[ ]:
Output

レシピA.4 @@@

ではずっととっておいた@@@の解説をしてこのレシピを閉めます。その前に次のようなMapの例を考えてみます。

In [ ]:
data = {{1, 2, 3}, {4, 5, 6}, {7, 8, 9}};
Map[f, data]
Out[ ]:
{f[{1, 2, 3}], f[{4, 5, 6}], f[{7, 8, 9}]}

じつはこれは期待する結果ではなく、本来はf[1,2,3]を期待したとします。しかし、これはMapだけではできません。

In [ ]:
Apply[f, data, {1}] (* {1}は対象リストの操作対象レベルに対応 *)
Out[ ]:
{f[1, 2, 3], f[4, 5, 6], f[7, 8, 9]}

こういうケースは非常に多いのでこれを簡単に表現できるよう@@@が用意されています。

In [ ]:
f @@@ {{1, 2, 3}, {4, 5, 6}, {7, 8, 9}}
Out[ ]:
{f[1, 2, 3], f[4, 5, 6], f[7, 8, 9]}