F#は複数の引数を受け取れるのか
この記事は F# Advent Calendar 2023 の14日目の記事です。
皆さんこんにちは。
F#っていい機能がたくさんありますよね。いい機能のうちの一つにカリー化[1]があります。
カリー化によって関数の部分適用が簡単になったりといいことずくめですよね、パイプライン演算子|>
との相性は言うまでもないでしょう。
話は変わりますが、haskellという言語において、すべての関数は1個の引数しか受け取りません。カリー化されていない関数は部分適用を簡単に行えないからです。F#はhaskellに影響を受けていますが、F#は必ず1個の引数を受け取るようになっているのでしょうか?
関数の定義
F#で4種類のadd関数を定義してみます。
namespace Fs
module Add =
// int -> int -> int
let add_1 (a: int) (b: int): int = a + b
// int * int -> int
let add_2 (a: int, b: int): int = a + b
// int * int -> int
let add_3 ((a, b): int * int): int = a + b
// struct (int * int) -> int
let add_4 ((a, b): struct (int * int)) = a + b
add_1 1 2 |> printfn "%d"
add_2 (1, 2) |> printfn "%d"
add_3 (1, 2) |> printfn "%d"
add_4 (1, 2) |> printfn "%d"
3
3
3
3
コメントはエディター[2]で表示された型です。
add_1
はint
型を受け取りint -> int
型の関数を返す関数で、1個の引数を受け取る関数になっていそうですね。
add_2
、add_3
はint * int -> int
型で、int * int
型を受け取りint
型を返す関数で、1個の引数を受け取るように見えます。
add_4
はstruct (int * int)
型を受け取りint
型を返す関数で、これも1個の引数を受け取る関数になっていそうです。
ここではadd_1
をカリー化された関数、add_2
をタプル形式の関数、add_3
を参照タプルの関数、add_4
を構造体タプルの関数と呼ぶことにします。
ILで確認してみる1
多分これが一番早いと思います。
.NET 言語で書かれたプログラムは、一度中間言語(IL = Intermediate Language)にコンパイルされます。
それぞれのadd
関数がどのようにコンパイルされるか見てみましょう。
引数の部分だけ抜粋しています。
カリー化された関数(add_1
)とタプル形式の関数(add_2
)はint32
型の引数a
とb
を持っていて、参照タプルの関数(add_3
)はint32
型の引数_arg1_0
と_arg1_1
を持っていることが確認できます。なんと、引数を1個だけ受け取るのは構造体タプルの関数(add_4
)のみで、カリー化、タプル形式、参照タプルの関数は、複数の引数を受け取るメソッドへとコンパイルされたのでした。
メソッドで定義
結論が出ました。やったー、で終わってもよいのですが、もう少しだけ掘り下げて見ようと思います。
というのも、実はタプルを引数とした関数(add_3
)とメソッドではコンパイル結果が変わるからです。
確認のためにAddClass
を定義します。
type AddClass =
// int * int -> int
static member add (a: int, b: int): int = a + b
// int * int -> int
static member add ((a, b): int * int): int = a + b
上がadd_2
、下がadd_3
に対応してます。
これらのadd
メソッドは違う型なので当然オーバーロード可能です。なんだか不思議ですね。
それぞれのメソッドは以下のように呼び出せます。
AddClass.add (1, 2) // 上が呼ばれる
AddClass.add ((1, 2)) // 下が呼ばれる
let param: int * int = 1, 2
AddClass.add param // 下が呼ばれる
明示的に参照タプルを渡すと参照タプルのメソッドが呼ばれるようです。
オーバーロードされていない場合はいずれの方法でも関数を呼び出せます。
ILで確認してみる2
タプル形式のメソッドをコンパイルすると以下の様になります。int32
型を受け取るメソッドにコンパイルされましたが、メソッドで定義したらSystem.Tuple`2<int32, int32>
型の引数_arg1
を受け取るメソッドになりました。このメソッドはただ1個の引数を受け取ります。
C#から呼び出す
今まで定義した関数・メソッドをC#から呼び出してみます。
using Fs;
Add.add_1(1, 2);
Add.add_2(1, 2);
Add.add_3(1, 2);
Add.add_4((1, 2));
AddClass.add(1, 2); // タプル形式
AddClass.add(Tuple.Create(1, 2)); // 参照タプル
add_1
、add_2
、add_3
、add
(タプル形式)はILで確認した通り2個のint32
型引数を持つ関数として呼び出せますね。add_4
の引数System.ValueTuple`2<int32, int32>
はC#では(elem0, elem1)
の形式で生成できます。つまりC#での通常の方法でタプルを生成するとValueTuple
型になります。add
(参照タプル)の引数System.Tuple`2<int32, int32>
はTuple.Create(elem0, elem1)
で生成するか、System.ValueTuple
型を拡張メソッドToTuple()
で変換できます。
C#から見ても構造体タプルの関数と参照タプルのメソッド以外は複数の引数を受け取っていることが確認できました。
複数の引数を受け取るメソッドに変換されることでC#からの呼び出しが簡単になるわけです。
結論
カリー化された関数・メソッド、タプル形式の関数・メソッド、参照タプルの関数は内部的には複数の引数を持っています。
参考
Discussion