🦁

F#は複数の引数を受け取れるのか

2023/12/14に公開

この記事は F# Advent Calendar 2023 の14日目の記事です。

皆さんこんにちは。
F#っていい機能がたくさんありますよね。いい機能のうちの一つにカリー化[1]があります。
カリー化によって関数の部分適用が簡単になったりといいことずくめですよね、パイプライン演算子|>との相性は言うまでもないでしょう。

話は変わりますが、haskellという言語において、すべての関数は1個の引数しか受け取りません。カリー化されていない関数は部分適用を簡単に行えないからです。F#はhaskellに影響を受けていますが、F#は必ず1個の引数を受け取るようになっているのでしょうか?

関数の定義

F#で4種類のadd関数を定義してみます。

Add.fs
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_1int型を受け取りint -> int型の関数を返す関数で、1個の引数を受け取る関数になっていそうですね。
add_2add_3int * int -> int型で、int * int型を受け取りint型を返す関数で、1個の引数を受け取るように見えます。
add_4struct (int * int)型を受け取りint型を返す関数で、これも1個の引数を受け取る関数になっていそうです。
ここではadd_1カリー化された関数、add_2タプル形式の関数、add_3参照タプルの関数、add_4構造体タプルの関数と呼ぶことにします。

ILで確認してみる1

多分これが一番早いと思います。
.NET 言語で書かれたプログラムは、一度中間言語(IL = Intermediate Language)にコンパイルされます。
それぞれのadd関数がどのようにコンパイルされるか見てみましょう。
引数の部分だけ抜粋しています。
https://github.com/yuuki1293/zenn-content/blob/master/code/add.il#L9-L13
https://github.com/yuuki1293/zenn-content/blob/master/code/add.il#L31-L35
https://github.com/yuuki1293/zenn-content/blob/master/code/add.il#L47-L51
https://github.com/yuuki1293/zenn-content/blob/master/code/add.il#L82-L85
カリー化された関数(add_1)とタプル形式の関数(add_2)はint32型の引数abを持っていて、参照タプルの関数(add_3)はint32型の引数_arg1_0_arg1_1を持っていることが確認できます。なんと、引数を1個だけ受け取るのは構造体タプルの関数(add_4)のみで、カリー化、タプル形式、参照タプルの関数は、複数の引数を受け取るメソッドへとコンパイルされたのでした。

メソッドで定義

結論が出ました。やったー、で終わってもよいのですが、もう少しだけ掘り下げて見ようと思います。
というのも、実はタプルを引数とした関数(add_3)とメソッドではコンパイル結果が変わるからです。
確認のためにAddClassを定義します。

Add.fs
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メソッドは違う型なので当然オーバーロード可能です。なんだか不思議ですね。
それぞれのメソッドは以下のように呼び出せます。

call_add.fs
AddClass.add (1, 2) // 上が呼ばれる

AddClass.add ((1, 2)) // 下が呼ばれる

let param: int * int = 1, 2
AddClass.add param // 下が呼ばれる

明示的に参照タプルを渡すと参照タプルのメソッドが呼ばれるようです。
オーバーロードされていない場合はいずれの方法でも関数を呼び出せます。

ILで確認してみる2

タプル形式のメソッドをコンパイルすると以下の様になります。
https://github.com/yuuki1293/zenn-content/blob/master/code/AddClass.il#L25-L28
関数で定義したら2個のint32型を受け取るメソッドにコンパイルされましたが、メソッドで定義したらSystem.Tuple`2<int32, int32>型の引数_arg1を受け取るメソッドになりました。このメソッドはただ1個の引数を受け取ります。

C#から呼び出す

今まで定義した関数・メソッドをC#から呼び出してみます。

CallAdd.cs
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_1add_2add_3add(タプル形式)は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#からの呼び出しが簡単になるわけです。

結論

カリー化された関数・メソッド、タプル形式の関数・メソッド、参照タプルの関数は内部的には複数の引数を持っています。

参考

https://learn.microsoft.com/en-us/dotnet/fsharp/language-reference/tuples

脚注
  1. カリー化(Currying)はHaskell Curryにちなんで名付けられました。 ↩︎

  2. JetBrains Rider ↩︎

GitHubで編集を提案

Discussion