📐

関数間の二項演算

2023/11/08に公開2

二項演算とは

プログラミングコードのa=1+1+のようなもので、その両側を引数として予め定義された処理の結果を返すものです。+においては足し算の結果です。これ以外にも四則演算[+-*/]、論理演算[||&&^!]、集合演算[&|]、文字列結合[+(++)]などの種類が有ります。これらはintやfloat、string、tupleなどの値がその引数として使用されます。オーバーロード演算子を使用すればクラスも使用できます。

関数の二項演算

値やクラスにおいては二項演算が定義できていますが、関数をその演算対象とできるでしょうか?即ち関数f(x)g(x)に対し、f+gのようなことはできるでしょうか。私自身このようなことを行ったことはなかったのですが、数学においては”点ごと(point-wise)の演算”として(f+g)(x)=f(x)+g(x)のような定義が成されています。多項式を例にすると具体的には以下のようなものです。

(出典:https://content.nroc.org/DevelopmentalMath/COURSE_TEXT2_RESOURCE/U17_L3_T1_text_final.html)

プログラミングにおいてこれを定義しようとすると次数の認識など難しそうですが、Haskell(ghci)においては以下のように単純化して定義出来ます。+と重複を避けるため|+|を新規定義しています。

f x = 5*x + 6
g x = 3*x^2 - 4*x + 8
(m |+| n) x = m x + n x --関数の足し算の定義
h = f |+| g

このhが数学の定義のf+gと同じ結果を返す関数となります。h 3とすると正解の44を返します。通常の足し算が演算前後でその型として性質を変えないことと同様に、関数の二項演算前後で関数としての性質を変えていないことは興味深いです。Int型同士の足し算はInt型で文字列同士は文字列に。同様に関数同士は関数になります。

なお、pythonなどでは2つの関数を引数として1つの関数を返す高階関数として同様に定義出来ます。

実用例

任意のXY座標に在る航空機が、軍艦の攻撃可能エリアに在るかを認識する防衛システムを例とします。

(出典:https://www.cs.yale.edu/publications/techreports/tr1049.pdf)

これにおいては各領域を、XY相対座標を引数としてそれが領域内に在るかをBool値で返す関数と見做せます。
例えば、ある半径rの円内に敵機があるかを判定したいとします。

これは以下のように領域を関数として定義出来ます。

distance (x, y) = sqrt (x ^2 + y^2)
circle r = \p -> (r > distance p)  --ラムダ式で返り値としての関数を定義している
c1 = circle 50

circleは関数を返り値としています。これにより半径50の円をc1として定義しており、以下のように使用できます。

c1 (30,30)  --Trueが返る
c1 (40,40)  --Falseが返る

追加で二項演算を定義します。

reg1 /\ reg2 = \p -> (reg1 p) && (reg2 p)

これは、領域同士の重ね合わせを定義しています。これにより領域を合成して新たな形状の領域を定義出来ます。例えば以下のような中空の円(annulus)領域を定義したいとします。

これは以下の様に定義出来ます。

outside reg = \p -> not (reg p)
annulus r1 r2 = (outside (circle r1)) /\ (circle r2)
a1 = annulus 20 50

以下はa1の検算結果です。

a1 (10,0)   -- Falseを返す
a1 (30,0)   -- Trueを返す
a1 (60,0)   -- Falseを返す

annulusに対して美しさを感じます。circleを再利用し"内側の円の外、外側の円の内"を上手く表現できています。
これを領域を関数として捉えない方法をとると以下のようになります。
distanceや不等号の演算など、抽象度が上記のannulusと比べて落ちます。

def in_annulus_monolis(p_target, r1, r2):
    d = distance(p_target)  
    return (d > r1) and (d < r2)

まとめ

この他にもRPGゲームのダメージ計算も、基本ダメージを引数としてタイプ弱点を考慮したダメージを出力する関数とすればタイプを合成する二項演算も定義できます。
私はこれまでプログラミングの演算対象を関数と見做す考え方自体がやってこなかったのでannulusのように機能的に働くことが興味深く記事にしました。このような手法が実用的かと言われると状況に寄るかと思いますし、自分以外理解できないコードになる可能性も有ります。annulusも領域をクラスとしてオーバーロード演算子を定義すれば同様な書き方ができそうですし。アイデアの1つに成れればと思います。

領域判定に関しては以下の私の個人記事にてもう少し詳細に説明しています。
https://zenn.dev/4ergfbv547uezdf/articles/74647c626d8155

Discussion

Nobuo YamashitaNobuo Yamashita
instance Num b => Num (a -> b) where
  (f + g) x = f x + g x
  ...

でよさそう。

bunnyhopper_isolatedbunnyhopper_isolated

当記事作者です。Haskell習得の際にお書きになった記事を見ていたのでコメント頂き感激です。
ありがとうございます。勉強になります。