13回目のMATLAB記事です。
今回は「関数ハンドル」について説明したいと思います。
聞き慣れない言葉かもしれませんが、私の記事でも過去、登場しました。
どこで登場したかというと11回と12回、つまり微分方程式を取り上げた回です。
本記事ではこれに関して少し深堀しようと思います。
しかし、最初から言葉で説明すると理解しにくいと思いましたので、まずはMATLABで関数ハンドルを実装します。
その後で言葉による説明を行います。
1. 関数ハンドルの実装
例えば、次のように定義されたSquareという関数があるとします。
function y = Square(x)
y = x.^2;
end
これは入力された配列 x を要素ごとに二乗し、その結果を y として返す関数です。
次にSquare関数の関数ハンドルを作ります。
その方法はとても簡単で、以下のように記述します。
>> @Square;
>> f = @Square; % 上下、どちらを使ってもよい
「@関数名」と記述するだけです。
これを関数ハンドルといいます。
また関数ハンドルは何かしら変数に代入(格納)してもOKです。
その場合、格納した変数が関数ハンドルになってくれます。
変数の名前は任意であり、ここでは f としました。
さて、関数ハンドルを使うとSquare関数を直接呼び出すのと同じような書き方で、関数を呼び出すことができます。
例えば以下のプログラムを実行してみてください。
% 普通にSquare関数を呼び出す
a = 2;
Square(a)
% 関数ハンドルを使ってSquare関数を呼び出す
f = @Square;
f(a)
function y = Square(x)
y = x.^2;
end
ans =
4
ans =
4
最初にSquare関数を直接呼び出して a の二乗を計算しています。
a の値は $2$ ですから計算結果は $4$ です。
次に関数ハンドル f を使って f(a) のように記述します。
すると f によってSquare関数が間接的に呼び出され、結局 a の二乗を計算してくれます。
したがって、どちらの場合を用いても同じ値 $4$ が出てくるわけです。
間接的という言葉を使ったのは、関数ハンドルの実体が関数そのものではないためです。
これについては後述します。
なお、変数 f が関数ハンドルかどうかは
>> isa(f,'function_handle')
を使用すると確認できます。
2. 関数ハンドルの応用
次は関数ハンドルをもう少しガッツリ使いたいと思います。
題材として「無名関数」「積分」「微分方程式」を扱います。
2.1 無名関数
PythonやJavaでラムダ式という言葉を見聞きしたことがありませんか?
ラムダ式とはプログラムにおけるクラスや関数の簡易表現みたいなもので、無名関数とはMATLABにおけるラムダ式です。
先ほど、Square関数をfunctionから書いて、”形式通り”に作りそれを使いました。
しかし、たった一行(しかも非常に簡単な処理内容)しかない関数をわざわざfunctionから書いていくのは、プログラミングに慣れてきた人にとって少々面倒な作業です。
その手間を省くのが無名関数のメリットの一つです。
Square関数を無名関数として実装すると以下のようになります。
>> f = @(x) x.^2;
関数ハンドルを作る時と同様、@から始めます。
これに続くのが名前ではないところがポイント。
名前を省略し、いきなり「関数の引数」と「関数の中身」を書いてしまうのです。
まるで $f(x)=x^2$ と書いているかのようですね。
実際はこの一行で、Square関数を実装し、その関数ハンドルを作り、さらにそれを変数 f に代入することを一気に行ったことになります1。
非常にスマートですね。
実際にちゃんと計算できるのか確かめてみましょう。
>> f = @(x) x.^2
>> f(2)
ans =
4
上手くいきました!
2.2 積分
次は積分です。
例えば $0\leq x\leq 1$ の範囲で $y = x^2$ の積分を計算します。
f = @Square; % 無名関数で実装してもOK!
q = integral(f,0,1) % 積分の計算を行う
q2 = integral(@Square,0,1) % これでも積分の計算ができる
function y = Square(x)
y = x.^2;
end
integral() コマンドで積分が行えます。
第一引数に被積分関数の関数ハンドル、第二引数に積分範囲の下限、第三引数に上限を渡せば自動的に積分の計算をしてくれます。
q =
0.3333
q2 =
0.3333
暗算ができるほど簡単な例です。
答えは $\frac{1}{3}$ で正しい答えです。
なお、無名関数を利用して被積分関数を実装するのもOKです。
f = @(x) x.^2;
q = integral(f,0,1)
q2 = integral(@(x) x.^2,0,1)
q =
0.3333
q2 =
0.3333
さて、これだけでは少し味気ないので二重積分もやってみましょう。
$z = xy$ という関数を以下の三角形領域
\begin{equation}
D=
\left\{
\left.
\begin{pmatrix}
x\\
y
\end{pmatrix}
\right|
0 \leq x \leq 1,\ 0\leq y \leq 1-x
\right\}
\end{equation}
で二重積分してみます2。
f = @func; % 無名関数で実装してもOK!
ymax = @(x) 1 - x; % yの上限を無名関数で実装
q = integral2(f,0,1,0,ymax) % 二重積分を行う
function z = func(x,y)
z = x.*y;
end
integral2() コマンドで二重積分ができます。
また、一見厄介な $y$ の上限 $y=1-x$ ですが、実は無名関数を利用すると楽に実装できます。
q =
0.0417
検算してみましたが、答えは $\frac{1}{24}$ なので表示結果は正しいです(小数点第四位で四捨五入していますが)。
2.3 微分方程式
最後は微分方程式です。
冒頭でも少し述べたように、11回と12回で関数ハンドルが登場しました。
ここでは12回のプログラムを再掲します。
t = 0:0.1:20; % 積分範囲
X0 = [1;0]; % 初期値 [x(0); dx(0)/dt]
[t, Y] = ode45(@func, t, X0); % 微分方程式を解く
% グラフの描画
plot(t, Y(:,1), 'b-', 'LineWidth', 1.3) % y1の描画
hold on
plot(t, Y(:,2), 'r-', 'LineWidth', 1.3) % y2の描画
title('Van der Pol')
legend('y1', 'y2')
xlabel('t')
ylabel('y1, y2')
grid on
set(gca, 'FontSize', 14)
function Ydot = func(t, Y)
Ydot = zeros(2,1);
mu = 2;
Ydot(1,1) = Y(2);
Ydot(2,1) = mu*(1-Y(1)^2)*Y(2) - Y(1);
end
ode45() コマンドの第一引数に、実は関数ハンドルを渡していることが分かります。
3. 関数ハンドル
ここまで関数ハンドルを使った実装例を見てきました。
それでは以上をふまえて関数ハンドルの説明に入ります。
3.1 関数ハンドルの正体
[1]にて、関数ハンドルは「関数への関連付けを格納するMATLABデータ」と説明されています。
私はこれ以上に完結な説明はない気がします。
実際、先の実装例を振り返ってもらえば、なんとなく理解できるのではないでしょうか。
特に本質的なのが”関連付け”という言葉です。
本記事の冒頭で述べましたが、関数ハンドルの実体は(無名関数を例外として)関数そのものではありません。
ある関数があって、その本体を任意の場所から呼び出すために参照するデータ3。
それが関数ハンドルの正体です。
だから関数を直接呼び出すのではなく、間接的に呼び出しているのです。
3.2 関数ハンドルの使いどころ
また、関数ハンドルの使いどころについて、[1]で以下のように説明されています。
関数ハンドルの使いどころ
-
ある関数を別の関数へ引き渡す("関数を引数とする関数" と呼ばれる)
-
コールバック関数の指定
-
無名関数へのハンドルの作成
-
メイン関数外部からのローカル関数の呼び出し
1の例を挙げると、積分のintegral() コマンドです。
Square関数をintegralコマンドの引数に渡しているのです。
また3の無名関数はなかなか優秀です。
やたらと多用するものではありませんが、適切に使えば超強力な武器になります4。
さらに、4は3.1節で説明した通りで、ある関数を任意の場所から参照するためのデータが関数ハンドルだ、ということです。
3.3 関数ハンドルの長所
私の思うに、関数ハンドルの長所はコードの見やすさ、およびコーディングのしやすさを実現するところだと思います。
例えば、もし関数ハンドルや無名関数がなかったら、引数の中に関数をfunctionから記述しなければなりません。
こんな感じ?
% 実行不可能!
q = integral(function y = Square(x)...
y = x.^2;...
end, 0,1) % 積分を行う
見にくいったらありません。
コーディングの観点から完全にアウトですね。
まとめ
今回は関数ハンドルについて説明しました。
俯瞰してみると、一番便利なのは無名関数かな?
無名関数はMATLABに限らず、PythonやJavaでも(ラムダ式という形で)登場します。
もし無名関数や関数ハンドルの有用性に気づいたなら、色んな言語、場面で使ってみてください。
終わりに
私が初めてラムダ式に出会ったのはJavaでした。
その概念やコードの絵面が斬新というか、世の中にはこんな記述方法があるのかと、感心と驚きを味わいました。
せっかく出会ったにもかかわらず、それから現在に至るまでJavaとは絶縁状態です...。
よろしければ次回の記事も読んでくださると大変嬉しいです。
※本記事に対する改善点や修正点、またはこんな事が知りたいといったご意見がありましたらぜひご連絡ください。
参考HP
[1] 関数ハンドルの作成, MathWorks日本, 2022/9/18閲覧