Subscribed unsubscribe Subscribe Subscribe

MATLABの無名関数内で条件分岐・複数行実行を行う方法

これらは、以前Obfuscated MATLAB Codeを書いた時に使ったテクニックなのですが、通常のMATLABプログラミングでも使えそうだと思ったので紹介します。
Yet Another Obfuscated MATLAB Code - woodrush’s diary

これらのテクニックの根底にあるのが、fevalという関数です。fevalは、

>> feval(@sin,pi)
ans = 1.2246e-16
>> feval(@(x) x^2, 5)
ans = 25
>> feval(@(x,y) x^2+y^2, 5, 2)
ans = 29

のように、関数ハンドルや無名関数を渡すとその関数を評価してくれる、というものです。

局所変数

条件分岐・複数行実行の基礎には局所変数のテクニックがあります。
これは、

>> f = @(ind) feval((@(array,c) c*ind^2 + sum(array(1:ind))), [1,5,8,13,2,29], 1.3);
>> f(5)
ans = 61.500

のように、無名関数に定数を引数として渡して評価することで、無名関数内でこれらを変数として参照できるようにする、というテクニックです。通常は、「[5,3,6,2](ind)」のように、その場で書いた配列の要素にはアクセス出来ないのですが(Octaveだとできます)、配列を局所変数に入れてしまうことで、その変数を通してインデックス指定によって要素にアクセスすることができるようになります。

条件分岐

これを応用することで、

>> f = (@ (x) ...
         feval((@ (list,ind) list{ind}), ...
               {x^2, sqrt(x)}, ...
               (mod(x,2)==0)*1 + (mod(x,2)==1)*2));
>> f(5)
ans = 2.2361
>> f(6)
ans = 36

のように、条件分岐を行うことができます。これは、xが偶数なら二乗を、奇数ならそのsqrtを返す関数となっています。
この関数fは、「{x^2, sqrt(x)}」の「(mod(x,2)==0)*1 + (mod(x,2)==1)*2」番目の要素にアクセスする関数となっています。
{}内に実行したい候補の関数、「(mod(x,2)…」の部分に「それらを実行したい条件*インデックス」と書いておくことで、条件に応じて実行したい関数を選択することができます。

ただし、{}内は先行評価されてしまうので、例えば

>> array = 1:10;
>> f = (@ (x) ...
         feval((@ (list,ind) list{ind}), ...
               {array(-x), array(x)}, ...
               (x<=0)*1 + (x>0)*2));
>> f(5)
エラー: インデックスが行列の次元を超えています

のようなコードだと、エラーが出てしまいます。このコードはarrayのabs(x)番目の要素を返す関数を書いたものなのですが、「{array(-x), array(x)}」という式が現れた瞬間、自動的に全ての要素が評価されてしまい、必ず負のインデックスが参照されてしまい、エラーになってしまいます。つまり、条件式「(x<=0)*1 + (x>0)*2)」によって選択したい値以外の方の値も評価されてしまっているのです。

これを回避するには、遅延評価をします。これは、

>> array = 1:10;
>> f = (@ (x) ...
         feval( ...
               feval((@ (list,ind) list{ind}), ...
                     {@()array(-x), @()array(x)}, ...
                     (x<=0)*1 + (x>0)*2)));
>> f(5)
ans = 5
>> f(-5)
ans = 5

とすることでできます。このコードの原理は、

>> array = 1:10;
>> @()array(-2)
ans = @()array(-2)
>> feval(@()array(-2))
エラー: インデックスが行列の次元を超えています

のように、無名関数内の表現が、fevalされるまで評価されない事によっています。これは、@()によって中身の式array(-2)が「冷凍」されており、fevalによって「解凍」されて初めて評価される、ということを表しています。つまり、@()によって遅延評価が実現されていることになります。

さきほどのコードでは、「{@()array(-x), @()array(x)}」という式が現れても、@()の中身までもが評価されることはないためエラーが起きず、条件に応じて欲しい式の冷凍品が返ってきます。「冷凍」された式を「解凍」して中身を取り出すにはfevalをしてあげればいいので、この方法では一個前のコードとくらべてfevalを一回を多く行っています。

また、たとえば「5文字以上の文字列を受け取ったらその文字列をそのまま返し返し、5文字未満だった場合受け取ったその文字列の表す関数ハンドルを返す」関数は、次のように書けます。

>> f = (@ (x) ...
         feval((@ (list,ind) ...
                 feval(list{ind}, x)), ...
               {@(y)y, @str2func}, ...
               (length(x)>=5)*1 + ~(length(x)>=5)*2));
>> feval(f('sin'), pi)
ans = 1.2246e-016
>> f('stringover5letters')
ans = stringover5letters

ここでは、入力に恒等関数(@(x)x)を適用することで、入力を変化させないというテクニックを使っています。このように、入力に対して適用したい関数を選ぶ、という形で条件分岐を表現することもできます。

複数行実行

{}内が先行評価されてしまうことを逆手に取って、複数行の実行を行うことができます:

>> hold on;
>> f = (@ (x1,x2,x3,x4) ...
          {plot([x1(1) x2(1)],[x1(2) x2(2)],'-'), ...
           plot([x2(1) x3(1)],[x2(2) x3(2)],'-'), ...
           plot([x3(1) x4(1)],[x3(2) x4(2)],'-'), ...
           plot([x4(1) x1(1)],[x4(2) x1(2)],'-')});
>> f([1,0],[0,1],[-1,0],[0,-1]);

とすることで、四点をつないだプロットを行う関数を作ることができます。

ただしこの方法の弱点は、値を返却しない式を実行するとエラーが出ることです。実際、

>> f = (@ (x1,x2,x3,x4) ...
          {hold on,
           plot([x1(1) x2(1)],[x1(2) x2(2)],'-'), ...
           plot([x2(1) x3(1)],[x2(2) x3(2)],'-'), ...
           plot([x3(1) x4(1)],[x3(2) x4(2)],'-'), ...
           plot([x4(1) x1(1)],[x4(2) x1(2)],'-')});
>> f([1,0],[0,1],[-1,0],[0,-1]);
??? エラー ==> hold: 出力引数が多すぎます

と出てしまいます。値を返さない関数には例えばdispなどもあるため、無名関数内で文字列表示を行う処理はできません。幸いplotやplot3は値を返してくれるため、このような表記ができます。
どうしても文字列表示を行いたい場合は、

%% mydisp.m
function ret = mydisp(str)
   disp(str);
   ret = 0;
end

のように、値を返す自前のdisp関数を作っておくことで、

>> hold on;
>> f = (@ (x1,x2,x3,x4) ...
          {mydisp('A square has been drawn.'),
           {plot([x1(1) x2(1)],[x1(2) x2(2)],'-'), ...
            plot([x2(1) x3(1)],[x2(2) x3(2)],'-'), ...
            plot([x3(1) x4(1)],[x3(2) x4(2)],'-'), ...
            plot([x4(1) x1(1)],[x4(2) x1(2)],'-')}});
>> f([1,0],[0,1],[-1,0],[0,-1]);

のように、無理やり無名関数内で実行させることができます。ここで、出力引数の次元を合わせるために一つ余分にセル配列{}を作っています。

無名再帰

余談ですが、これらの技術に、Yコンビネータという高階関数を組み合わせて使うことで、無名関数だけで再帰を実現する無名再帰を実装することができます。例えば、

f = (@ (in) ...
      feval((@ (Y) ...
              feval(Y( ...
                      (@ (fact) ...
                        (@ (n) ...
                          feval(feval((@ (list,ind) list{ind}), ...
                                      {(@ () 1), (@ () n*fact(n-1))}, ...
                                      (n==1)*1+(n~=1)*2))))), ...
                    in)), ...
            (@ (f) ...
              feval((@ (g1) ...
                      (@ (m1) feval(f(g1(g1)),m1))), ...
                    (@(g2) ...
                      (@ (m2) feval(f(g2(g2)),m2)))))))

とすることで、再帰によって階乗を計算する関数を作ることができます。実際、

>> feval((@ (in) ...
           feval((@ (Y) ...
                   feval(Y( ...
                           (@ (fact) ...
                             (@ (n) ...
                               feval(feval((@ (list,ind) list{ind}), ...
                                           {(@ () 1), (@ () n*fact(n-1))}, ...
                                           (n==1)*1+(n~=1)*2))))), ...
                         in)), ...
                 (@ (f) ...
                   feval((@ (g1) ...
                           (@ (m1) feval(f(g1(g1)),m1))), ...
                         (@(g2) ...
                           (@ (m2) feval(f(g2(g2)),m2))))))), ...
           10)
ans = 3628800

と出てきます。

この階乗関数の作成、およびYコンビネータの勉強には、amachang氏の記事
Y コンビネータって何? - IT戦記
を参考にしました。こちらではJavaScriptを用いてYコンビネータを実装しており、その実装をそのままMATLABの無名関数を使って翻訳したのが上記のコードになります。上記のコードでは、最上層のfevalの引数、「(@ (f)…」以降の表現がYコンビネータと呼ばれる関数になっています。Yコンビネータは無名再帰のテクニックの核をなしている概念で、それ自体でかなり面白いものです。