コンピュータサイエンスにおいて、関数合成とは、単純な関数を組み合わせてより複雑な関数を構築する行為または仕組みのことです。数学における通常の関数合成と同様に、各関数の結果が次の関数の引数として渡され、最後の関数の結果が全体の結果となります。
プログラマーは、関数を他の関数の結果に適用することが頻繁にあり、ほとんどすべてのプログラミング言語でそれが可能です。場合によっては、関数の合成自体が独立した関数として興味深いものとなり、後で利用されることがあります。このような関数は常に定義できますが、第一級関数を持つ言語ではより簡単に定義できます。
関数を容易に組み合わせることができるため、保守性とコードの再利用性を高めるために関数を分割(ファクタリング)することが促進されます。より一般的には、大規模なシステムはプログラム全体を組み合わせることによって構築される可能性があります。
厳密に言えば、関数合成は、有限量のデータに対して動作する関数に適用され、各ステップでデータを順次処理してから次のステップに渡します。潜在的に無限のデータ(ストリームやその他のコデータ)に対して動作する関数はフィルタと呼ばれ、関数合成に類似したパイプラインで接続され、並行して実行できます。
例えば、z = f ( y )およびy = g ( x )のように、 2つの関数fとgがあるとします。これらを合成するということは、まずy = g ( x )を計算し、次にyを使ってz = f ( y )を計算するということです。C言語での例を以下に示します。
float x , y , z ; // ... y = g ( x ); z = f ( y );中間結果に名前を付けない場合は、手順を組み合わせることができます。
z = f ( g ( x ));長さの違いにもかかわらず、これら 2 つの実装は同じ結果を計算します。2 番目の実装は 1 行のコードしか必要とせず、口語的に「高度に構成された」形式と呼ばれます。高度に構成された形式の利点の 1 つは、コード行数が少なく、プログラムの「表面積」が最小限に抑えられるため、可読性、ひいては保守性が向上することです。 [ 1 ] DeMarco と Lister は、表面積と保守性の間に逆相関があることを経験的に検証しています。[ 2 ]一方、高度に構成された形式を過剰に使用すると、逆の効果が生じ、コードの保守性が低下する可能性があります。
スタックベースの言語では、関数合成はさらに自然なものになります。それは連結によって実行され、通常はプログラム設計の主要な手法となります。上記のForthの例は次のとおりです。
彼女
これは、スタックにあった値をそのまま取得し、g を適用してから f を適用し、結果をスタックに残します。対応する数学的表記については、後置合成表記を参照してください。
ここで、 g()の結果に対してf()を呼び出すという組み合わせが頻繁に有用であり、それをfoo() という名前で独立した関数として使用したいと仮定します。
ほとんどの言語では、合成によって実装された新しい関数を定義できます。C言語の例:
float foo ( float x ) { return f ( g ( x )); }(中間要素を含む長い形式でも同様に機能します。)Forthの例:
: フー gf ;
C言語のような言語では、新しい関数を作成する唯一の方法はプログラムのソースコード内で定義することであり、つまり実行時に関数を合成することはできません。ただし、定義済みの関数を任意に合成して評価することは可能です。
#include <stdio.h>typedef int FXN ( int );int f ( int x ) { return x + 1 ; } int g ( int x ) { return x * 2 ; } int h ( int x ) { return x - 3 ; }int eval ( FXN * fs [], int size , int x ) { for ( int i = 0 ; i < size ; i ++ ) x = ( * fs [ i ])( x );return x ; }int main () { // ((6 + 1) * 2) - 3 = 11 FXN * arr [] = { f , g , h }; printf ( "%d \n " , eval ( arr , 3 , 6 ));// ((6 - 3) * 2) + 1 = 7 arr [ 2 ] = f ; arr [ 0 ] = h ; printf ( "%d \n " , eval ( arr , 3 , 6 )); }関数型プログラミング言語では、関数合成は高階関数または演算子として自然に表現できます。その他のプログラミング言語では、関数合成を実行するための独自のメカニズムを記述する必要があります。
Haskellでは、上記の例foo = f ∘ gは次のようになります。
foo = f . g
組み込みの合成演算子(.)を使用します。これは、g の後に f が続く、またはg と f が合成されたと読みます。
合成演算子 ∘ 自体は、Haskellではラムダ式 を使って定義できます。
( . ) :: ( b -> c ) -> ( a -> b ) -> a -> c f . g = \ x -> f ( g x )最初の行は、(.) の型を記述しています。これは、関数fと gのペアを受け取り、関数 (2 行目のラムダ式) を返します。Haskell では、f と g の正確な入力型と出力型を指定する必要がないことに注意してください。a、b、c、x はプレースホルダーです。重要なのはfと gの関係(f は g が返す値を受け入れる必要がある) だけです。これにより、(.) は多相演算子となります。
Haskellのポイントフリー記法における、複数の引数を持つ多相演算子
f g x y(.).(.)ここで、パラメータ名は重要ではない[ 3 ] 。上記のとおり。Michal Ševčíkは、追加パラメータを使用した関数合成の体系的な説明をCScalfaniに帰している[ 4 ] 。
Lispの派生言語、特にSchemeは、コードとデータの互換性、および関数の扱い方が、可変引数合成演算子の再帰的な定義に非常に適している。
( define ( compose . fs ) ( if ( null? fs ) ( lambda ( x ) x ) ; 引数が指定されていない場合は、恒等関数( lambda ( x ) (( car fs ) (( apply compose ( cdr fs )) x ))))); 例( define ( add-a-bang str ) ( string-append str "!" ))( define givebang ( compose string->symbol add-a-bang symbol->string ))( givebang 'set ) ; ===> set!; 匿名合成(( compose sqrt - sqr ) 5 ) ; ===> 0+5iAPLの多くの方言には、記号を使用した組み込み関数合成機能があります∘。この高階関数は、関数合成を左辺関数の二項A f∘g B適用に拡張し、となりますA f g B。
foo ← f ∘ gさらに、関数合成を定義することもできます。
o ← { ⍺⍺ ⍵⍵ ⍵ }中括弧を使用したインライン定義をサポートしていない方言では、従来の定義が利用可能です。
∇ r ← ( f o g ) x r ← f g x ∇RakuはHaskellと同様に組み込みの関数合成演算子を持っていますが、主な違いは、∘またはと綴られることですo。
my & foo = & f ∘ & g ;Haskellと同様に、演算子を自分で定義することもできます。実際、以下はRakudoの実装で演算子を定義するために使用されているRakuコードです。
# 実装では、proto sub infix :<∘> (&?, &?) is equiv(&[~]) is assoc<left> { * }を不正に利用しているため、ここで少し異なる行になります。multi sub infix :<∘> () { *. self } # `@array` が空の場合に `[∘] @array` が機能するようにするmulti sub infix :<∘> (&f) { & f } # `@array` に要素が 1 つある場合に `[∘] @array` が機能するようにするmulti sub infix :<∘> (&f, &g --> Block) { ( & f ) . count > 1 ?? -> | args { f | g | args } !! -> | args { f g | args } }# それを「テキサス」の綴りにエイリアスします (テキサスではすべてがより大きく、ASCII です) my & infix: <o> : = & infix: <∘> ; Nimは統一された関数呼び出し構文をサポートしており、メソッド構文.演算子を介して任意の関数合成が可能です。[ 5 ]
func foo ( a : int ) : string = $ a func bar ( a : string , count : int ) : seq [ string ] = for i in 0 .. < count : result.add ( a ) func baz ( a : seq [ string ] ) = for i in a : echo i# 同等! echo foo ( 5 ).bar ( 6 ) .baz ( ) echo baz ( bar ( 6 , foo ( 5 )))Pythonでは、任意の関数群の構成を定義する方法として、functools.reduce関数を使用する方法があります。
from functools import reduce from typing import Callabledef compose ( * funcs ) -> Callable [[ int ], int ]: """関数群 (f(g(h(...)))) を単一の複合関数に合成します。""" return reduce ( lambda f , g : lambda x : f ( g ( x )), funcs )# 例f = lambda x : x + 1 g = lambda x : x * 2 h = lambda x : x - 3# 関数を呼び出す x=10 : ((x - 3) * 2) + 1 = 15 print ( compose ( f , g , h )( 10 ))JavaScriptでは、 2つの関数fとgを受け取り、1つの関数を生成する関数として定義できます。
function o ( f , g ) { return function ( x ) { return f ( g ( x )); } }// または、ES2015 のレスト演算子とラムダ式を使用するconst compose = (... fs ) => ( x ) => fs . reduceRight (( acc , f ) => f ( acc ), x )C#では、Func fとgを受け取り、新しいFuncを生成する拡張メソッドとして定義できます。
// 呼び出し例: // var c = f.ComposeWith(g); // // Func<int, bool> g = _ => ... // Func<bool, string> f = _ => ...public static Func < T1 , T3 > ComposeWith < T1 , T2 , T3 > ( this Func < T2 , T3 > f , Func < T1 , T2 > g ) => x => f ( g ( x ));Rubyのような言語では、二項演算子を自分で構築できます。
class Proc def compose ( other_fn ) -> ( * as ) { other_fn . call ( call ( * as )) } end alias_method :+ , :compose endf = -> ( x ) { x * 2 } g = -> ( x ) { x ** 3 } ( f + g ) . call ( 12 ) # => 13824しかし、Ruby 2.6 ではネイティブ関数合成演算子が導入されました: [ 6 ]
f = proc { | x | x + 2 } g = proc { | x | x * 3 } ( f << g ) . call ( 3 ) # -> 11; f(g(3)) と同一( f >> g ) . call ( 3 ) # -> 15; g(f(3)) と同一構成性や構成可能性の原理を含む構成の概念は非常に普遍的であるため、数多くの研究分野がそれぞれ独自に発展してきた。以下は、構成の概念が中心となる研究の一例である。
プログラムやシステム全体を関数として扱うことができ、入力と出力が明確に定義されていれば、容易に構成できます。[ 7 ]フィルタを簡単に構成できるパイプラインは非常に成功したため、オペレーティングシステムの設計パターンになりました。
副作用を伴う命令型手続きは参照透過性に反するため、きれいに合成することはできません。しかし、コード実行前後の「世界の状態」を入力と出力とみなせば、きれいな関数が得られます。このような関数の合成は、手続きを順番に実行することに相当します。モナド形式はこの考え方を用いて、副作用と入出力(I/O)を関数型言語に組み込んでいます。