Tsuの雑記¯\_(ツ)_/¯

主に製作メモ・備忘録として使用。製作したアプリのリンクもあります。

【Unity】Unity + C# でカリー化【C#】

Unity 独自のクラスを用いて,メソッドをカリー化します。

Method(a, b) → Method(a)(b)

(Unity 2019.4.17f1 Personal)


始めに

Wikipedia』では,カリー化を次のように説明しています。

カリー化 (currying, カリー化された=curried) とは、複数の引数をとる関数を、引数が「もとの関数の最初の引数」で戻り値が「もとの関数の残りの引数を取り結果を返す関数」であるような関数にすること(あるいはその関数のこと)である。

1文で簡潔にまとめられていますが,以下の実例にて補足いたします。

using UnityEngine;

public class NewBehaviourScript : MonoBehaviour
{
    // int と float をパラメータとして string を返すメソッド
    string NumberToString(int a, float b)
    {
        return $"{a}, {b}";
    }

    // ↓カリー化

    // int をパラメータとして Func<float, string> を返すメソッド
    System.Func<float, string> NumberToString(int a)
    {
        // float をパラメータとして string を返すデリゲート
        return ((float b) => $"{a}, {b}");
    }

    // 呼び出し例
    void Start()
    {
        // カリー化前の呼び出しかた
        NumberToString(1, 2.3f);
        // カリー化後の呼び出しかた
        NumberToString(1)(2.3f);
        // int のみ入力した状態も作れる(部分適用)
        var A = NumberToString(1);
        // 残った float を入力
        A(2.3f);
    }
}

カリー化後は呼び出しかたが変わったり,一部の引数を固定化(部分適用)できたりといった特徴が見られます。

カリー化は主に後者の利点から用いられると思いますが,今回は前者に注目します。

Translate(float, float, float)メソッドに見られるように,Unity ではfloat型の引数でVector3を操作する事があります。

docs.unity3d.com

そこで,本稿では「Method(0, 0, 0)(1, 1, 1);」のような記述でVector3を操作するメソッドを作ってみたいと思います。


カリー化前のメソッドを準備

あまり使い道はなさそうですが,「2点の座標の中点を返す」というメソッドを用意します。

Vector3 型パラメータ

目標はfloat型パラメータですが,まずはパラメータ数が少ないVector3型で作ってみます。

using UnityEngine;

public class NewBehaviourScript : MonoBehaviour
{
    /// <summary>
    /// 2つの座標の中点を返す
    /// </summary>
    /// <param name="v1">座標1</param>
    /// <param name="v1">座標2</param>
    /// <returns>座標1,2の中点</returns>
    Vector3 Middle(Vector3 v1, Vector3 v2)
    {
        // 2つの座標の中点を返す
        return ((v1 + v2) / 2f);
    }

    void Start()
    {
        // 2つの座標の中点をログ出力
        Debug.Log(Middle(Vector3.zero, Vector3.one));
    }
}

Console ウィンドウに "(0.5, 0.5, 0.5)" と出力されている画像
Console ウィンドウに出力された値

Vector3.zeroVector3.oneの中点なので,期待どおりに(0.5, 0.5, 0.5)が出力されました。

次は,パラメータをVector3型ではなくfloat型にしてみましょう。

float 型パラメータ

using UnityEngine;

public class NewBehaviourScript : MonoBehaviour
{
    /// <summary>
    /// 2つの座標の中点を返す
    /// </summary>
    /// <param name="x1">座標1のX</param>
    /// <param name="y1">座標1のY</param>
    /// <param name="z1">座標1のZ</param>
    /// <param name="x2">座標2のX</param>
    /// <param name="y2">座標2のY</param>
    /// <param name="z2">座標2のZ</param>
    /// <returns>座標1,2の中点</returns>
    Vector3 Middle(float x1, float y1, float z1, float x2, float y2, float z2)
    {
        // 2つの座標の中点を返す
        return ((new Vector3((x1 + x2), (y1 + y2), (z1 + z2)) / 2f));
    }

    void Start()
    {
        // 2つの座標の中点をログ出力
        Debug.Log(Middle(0, 0, 0, 1, 1, 1));
    }
}

うーん,呼び出しかた(Middle(0, 0, 0, 1, 1, 1))が美しくないですね。

何とか,floatを3つずつに区切りたい所です。

タプル型パラメータ

値を区切る方法として,まずタプルが考えられます。

docs.microsoft.com

using UnityEngine;

public class NewBehaviourScript : MonoBehaviour
{
    /// <summary>
    /// 2つの座標の中点を返す
    /// </summary>
    /// <param name="v1">座標1</param>
    /// <param name="v2">座標2</param>
    /// <returns>座標1,2の中点</returns>
    Vector3 Middle((float x, float y, float z) v1, (float x, float y, float z) v2)
    {
        // 2つの座標の中点を返す
        return ((new Vector3((v1.x + v2.x), (v1.y + v2.y), (v1.z + v2.z)) / 2f));
    }

    void Start()
    {
        // 2つの座標の中点をログ出力
        Debug.Log(Middle((0, 0, 0), (1, 1, 1)));
    }
}

(float x, float y, float z)という即席の型を,Vector3に転用しています。

これでも悪くないのですが,かっこ( )やカンマ,が多すぎるように思えます。

そろそろ本題のカリー化に移りますが,例によってまずはVector3型で試してみましょう。


Vector3 型パラメータ(カリー化)

using UnityEngine;

public class NewBehaviourScript : MonoBehaviour
{
    /// <summary>
    /// 2つの座標の中点を返す
    /// </summary>
    /// <param name="v1">座標1</param>
    /// <param name="v1">座標2</param>
    /// <returns>座標1,2の中点を返すデリゲート</returns>
    System.Func<Vector3, Vector3> Middle(Vector3 v1)
    {
        // 2つ目の座標をパラメータとして1つ目の座標との中点を返すデリゲートを返す
        return ((Vector3 v2) => (v1 + v2) / 2f);
    }

    void Start()
    {
        // 2つの座標の中点をログ出力
        Debug.Log(Middle(Vector3.zero)(Vector3.one));
    }
}

冒頭で例示したような,Middle(Vector3.zero)(Vector3.one)という形式で記述できていますね。

次はいよいよ,Middle(0, 0, 0)(1, 1, 1)という呼び出し形式を実現させます。


float 型パラメータ(カリー化)

using UnityEngine;

public class NewBehaviourScript : MonoBehaviour
{
    /// <summary>
    /// 2つの座標の中点を返す
    /// </summary>
    /// <param name="x1">ベクトルのX座標</param>
    /// <param name="y1">ベクトルのY座標</param>
    /// <param name="z1">ベクトルのZ座標</param>
    /// <returns>座標1,2の中点を返すデリゲート</returns>
    System.Func<float, float, float, Vector3> Middle(float x1, float y1, float z1)
    {
        // 2つ目の座標をパラメータとして1つ目の座標との中点を返すデリゲートを返す
        return ((x2, y2, z2) => new Vector3(x1 + x2, y1 + y2, z1 + z2) / 2f);
    }

    void Start()
    {
        // 2つの座標の中点をログ出力
        Debug.Log(Middle(0, 0, 0)(1, 1, 1));
    }
}

パラメータが多いため難しそうに見えますが,前項のVector3型を用いた物と大差はありません。

new Vector3(0, 0, 0)new Vector3(1, 1, 1)の中点なので,期待どおりに(0.5, 0.5, 0.5)が出力されました。

(1枚目と代わり映えしないため,画像は省きます。)


おまけ(部分適用の実例)

冒頭で触れた「部分適用」に関しても,同じメソッドで御説明いたします。

// 他は直前のコードと変わらないため変更部分のみ抜粋
void Start()
{
    // 2つの座標のうち1つ目に原点を指定
    var HalfOf = Middle(0, 0, 0);
    // 2つの座標の中間をログ出力
    Debug.Log(HalfOf(1, 1, 1));
}

1つ目の座標が原点であれば“中点”は2つ目の座標の半分に等しいので,1つ目の座標に原点を指定した状態のデリゲートをHalfOfと定義します。

varキーワードで宣言していますが,実際の型はFunc<float, float, float, Vector3>です。

結果は同じなので再び画像は省きますが,期待どおりに(0.5, 0.5, 0.5)が出力されました。


終わりに

カリー化について,2つの利点から解説いたしました。

まだまだ面白い活用法はあると思うので,またカリー化に関する記事が書けると嬉しいですね。

以上,Unity でカリー化を行う例でした。