関数の結合性

関数を連続して並べた場合、それらはどのように結合されるのでしょうか?

たとえば、次の式は何を意味するのでしょうか?

let F x y z = x y z

これは、関数yを引数zに適用し、その結果をxの引数にするという意味でしょうか?その場合、次の式と同じになります。

let F x y z = x (y z)

それとも、関数xを引数yに適用し、その結果の関数を引数zで評価するという意味でしょうか?その場合、次の式と同じになります。

let F x y z = (x y) z

答えは後者です。関数適用は左結合的です。つまり、 x y z の評価は (x y) z の評価と同じです。同様に、 w x y z の評価は ((w x) y) z の評価と同じです。これは驚くようなことではありません。すでに見てきたように、これは部分適用の仕組みそのものです。xを2つのパラメータを持つ関数と考えると、 (x y) z は最初のパラメータの部分適用の結果に、zという引数を渡すことになります。

右結合させたい場合は、明示的にかっこを使うか、パイプを使うことができます。次の3つの形式は同じ意味になります。

let F x y z = x (y z)
let F x y z = y z |> x    // 前方パイプを使用
let F x y z = x <| y z    // 後方パイプを使用

練習として、これらの関数の型シグネチャを、実際に評価せずに考えてみてください!

関数の合成

これまで関数の合成について何度か触れてきましたが、実際にはどういう意味なのでしょうか?最初は少し難しく感じるかもしれませんが、実はとてもシンプルな概念です。

たとえば、型「T1」から型「T2」への関数「f」があり、また型「T2」から型「T3」への関数「g」があるとします。この場合、「f」の出力を「g」の入力につなげることで、型「T1」から型「T3」への新しい関数を作ることができます。

具体例を見てみましょう。

let f (x:int) = float x * 3.0  // f は int->float
let g (x:float) = x > 4.0      // g は float->bool

「f」の出力を「g」の入力として使う新しい関数「h」を作ることができます。

let h (x:int) = 
    let y = f(x)
    g(y)                   // gの出力を返す

もっとコンパクトな書き方もあります。

let h (x:int) = g ( f(x) ) // h は int->bool

//テスト
h 1
h 2

ここまでは分かりやすいですね。興味深いのは、「f」と「g」という関数が与えられたとき、それらの型シグネチャを知らなくても、このように組み合わせる新しい関数「compose」を定義できることです。

let compose f g x = g ( f(x) )

これを評価すると、コンパイラが正しく推論していることがわかります。「f」が汎用型 'a から汎用型 'b への関数である場合、「g」は入力として汎用型 'b を持つように制約されます。全体の型シグネチャは次のようになります。

val compose : ('a -> 'b) -> ('b -> 'c) -> 'a -> 'c

(この汎用的な合成操作が可能なのは、すべての関数が1つの入力と1つの出力を持つからです。この方法は非関数型言語では不可能でしょう。)

すでに見たように、composeの実際の定義では >> 記号が使われています。

let (>>) f g x = g ( f(x) )

この定義を使えば、既存の関数から新しい関数を構築するのに合成を利用できます。

let add1 x = x + 1
let times2 x = x * 2
let add1Times2 x = (>>) add1 times2 x

//テスト
add1Times2 3

この明示的なスタイルはやや煩雑です。使いやすさと理解しやすさを向上させるために、いくつかの工夫ができます。

まず、xパラメータを省略して、合成演算子が部分適用を返すようにできます。

let add1Times2 = (>>) add1 times2

そして、これは二項演算なので、演算子を中央に置くことができます。

let add1Times2 = add1 >> times2

これで完成です。合成演算子を使うことで、コードがよりクリーンで分かりやすくなります。

let add1 x = x + 1
let times2 x = x * 2

//古いスタイル
let add1Times2 x = times2(add1 x)

//新しいスタイル
let add1Times2 = add1 >> times2

実践での合成演算子の使用

合成演算子は、他の中置演算子と同様に、通常の関数適用よりも優先順位が低くなっています。これは、合成する関数がかっこなしで引数を取れることを意味します。

たとえば、「add」と「times」関数に追加のパラメータがある場合、合成時にこのパラメータを渡すことができます。

let add n x = x + n
let times n x = x * n
let add1Times2 = add 1 >> times 2
let add5Times3 = add 5 >> times 3

//テスト
add5Times3 1

入力と出力が一致していれば、関与する関数はどのような種類の値でも使えます。たとえば、次の例は関数を2回実行します。

let twice f = f >> f    //シグネチャは ('a -> 'a) -> ('a -> 'a)

コンパイラが、関数fが入力と出力に同じ型を使わなければならないと推論していることに注目してください。

ここで、 + のような関数を考えてみましょう。以前見たように、入力は int ですが、出力は実際には部分適用された関数 (int->int) です。したがって、 + の出力を twice の入力にできます。次のように書けます。

let add1 = (+) 1           // シグネチャは (int -> int)
let add1Twice = twice add1 // シグネチャも (int -> int)

//テスト
add1Twice 9

一方で、次のようには書けません。

let addThenMultiply = (+) >> (*)

なぜなら、 "*" への入力は int 値でなければならず、 int->int 関数(加算の出力)ではないからです。

しかし、最初の関数の出力が単なる int になるように調整すれば、うまく動作します。

let add1ThenMultiply = (+) 1 >> (*) 
// (+) 1 のシグネチャは (int -> int) で、出力は 'int'

//テスト
add1ThenMultiply 2 7

必要に応じて、 << 演算子を使って逆方向の合成も可能です。

let times2Add1 = add 1 << times 2
times2Add1 3

逆合成は主に、コードをより英語的にするために使います。簡単な例を見てみましょう。

let myList = []
myList |> List.isEmpty |> not    // 通常のパイプライン

myList |> (not << List.isEmpty)  // 逆合成を使用

合成 vs. パイプライン

ここまで来ると、合成演算子とパイプライン演算子の違いは何なのか疑問に思うかもしれません。一見すると非常に似ているように見えるからです。

まず、パイプライン演算子の定義を再度見てみましょう。

let (|>) x f = f x

これは単に、関数の引数を関数の後ろではなく前に置けるようにするだけです。関数に複数のパラメータがある場合、入力は最後のパラメータとなります。さっきの例を見てみましょう。

let doSomething x y z = x+y+z
doSomething 1 2 3       // すべてのパラメータが関数の後ろ
3 |> doSomething 1 2    // 最後のパラメータがパイプで渡される

合成は同じものではなく、パイプの代用にはなりません。次の例では、数字 3 は関数ではないので、その「出力」を doSomething に渡すことはできません。

3 >> doSomething 1 2     // 許可されない
// f >> g は g(f(x)) と同じなので、書き直すと
doSomething 1 2 ( 3(x) ) // 3が関数であるべきことを意味します!
// error FS0001: この式に必要な型は 'a->'b' ですが、
//               ここでは次の型が指定されています 'int`

コンパイラは、 "3" が何らかの関数 'a->'b であるべきだと訴えています。

これを、最初の2つの引数が関数でなければならない合成の定義と比較してみましょう。

let (>>) f g x = g ( f(x) )

let add n x = x + n
let times n x = x * n
let add1Times2 = add 1 >> times 2

代わりにパイプを使おうとしても動作しません。次の例では、 add 1 は型 int->int の(部分)関数であり、 times 2 の2番目のパラメータには使えません。

let add1Times2 = add 1 |> times 2   // 許可されない
// x |> f は f(x) と同じなので、書き直すと。
let add1Times2 = times 2 (add 1)    // add1 は int であるべき
// error FS0001: 型が一致しません。 'int -> int' という指定が必要ですが、'int' が指定されました。

コンパイラは、 times 2int->int パラメータを取るべき、つまり型 (int->int)->'a であるべきだと訴えています。

results matching ""

    No results matching ""