読者です 読者をやめる 読者になる 読者になる

C# で 劣化 Variant を書いてみた

.NET

VB2005でのお話。そして現在調査中なのですが、できればいいなーというレベルで関数の戻り値のみが異なるようなオーバーロードをやってみました。

Private Function GetConfig(ByVal keyword As String) As String  
'(省略)   
End Function  
Private Function GetConfig(ByVal keyword As String) As Date  
'(省略)   
End Function

このように関数の戻り値を異なる値にしてオーバーロードしようとすると「戻り値の型のみが異なるため、お互いをオーバーロードすることはできません」というエラーが発生します。

Private Function GetConfig(ByVal keyword As String) As String  
'(省略)   
End Function  
Private Function GetConfig(ByVal keyword As Date) As Date  
'(省略)   
End Function

のように引数の数か型を変えてあげれば、オーバーロードできます。引数を同一にした状態で戻り値のみ異なる型にしてオーバーロードを行う方法はあるのでしょうか。

他の言語だったらOKって訳じゃないですよね。

CLS Rule 37 と CLS Rule 38 絡みの話はあちらのコメント欄に書いたので置いておくとして,C# Generics と implicit operator を利用して劣化 Variant みたいなのを作ってみました.

public class LazyVariant<T1, T2>
{
    private readonly Func<T1> _func1;
    private readonly Func<T2> _func2;
    private T1 _memoedValue1;
    private T2 _memoedValue2;
    private bool _memoed1 = false;
    private bool _memoed2 = false;
    public LazyVariant(Func<T1> func1, Func<T2> func2)
    {
        this._func1 = func1;
        this._func2 = func2;
    }
    public static implicit operator T1(LazyVariant<T1, T2> variant)
    {
        //TODO: use Interlocked API
        if (!variant._memoed1)
        {
            variant._memoedValue1 = variant._func1();
            variant._memoed1 = true;
        }
        return variant._memoedValue1;
    }
    public static implicit operator T2(LazyVariant<T1, T2> variant)
    {
        //TODO: use Interlocked API
        if (!variant._memoed2)
        {
            variant._memoedValue2 = variant._func2();
            variant._memoed2 = true;
        }
        return variant._memoedValue2;
    }
}

これはおおざっぱに言えば,「T1 または T2 型」みたいな型で,F# や Nemerle の variant を意識しています.Visual Basic や COM の variant とは大違いなので注意.詳しくは id:akiramei:20050323:p2 や id:akiramei:20050324:p1,F# 本などを参照してくださいませ.(といいつつ,variant は「どれかひとつが入っている箱」なイメージなので,別の名前の方がよいかもしれず.LazyChoice とかかなぁ.Achiral に収録される日が来るとしたら,多分名前変えます.)
LazyVariant は,コンストラクタでデリデートを受け取ります.T1,T2 それぞれへの暗黙の変換が定義されていて,デリゲートの評価は implicit conversion の呼び出しをトリガーとしています.つまり遅延評価されます.また,評価結果はメモ化されます.
例えば以下のように使います.メソッドをオーバーロードする代わりに,戻り値の型を「string または DateTime 型」という感じに多重化しています.型の方を多重化することで,メソッド自体はひとつで済むわけです.

public class MyDB
{
    private string GetConfigAsString(string keyword)
    {
        return DateTime.Now.ToString() + ", " + keyword;
    }
    private DateTime GetConfigAsDateTime(string keyword)
    {
        return DateTime.Now;
    }
    public LazyVariant<string, DateTime> GetConfig(string keyword)
    {
        return new LazyVariant<string, DateTime>
        (
            () => GetConfigAsString(keyword),
            () => GetConfigAsDateTime(keyword)
        );
    }
}

得られた結果は string を受け取るメソッドにも,DateTime を受け取るメソッドにも同じように使えます.

static class Program
{
    static void Foo(string str){}
    static void Bar(DateTime datetime){}

    static void Main(string[] args)
    {
        Test1();
        Test2();
    }
    static void Test1()
    {
        var mydb = new MyDB();
        Foo(mydb.GetConfig("hello")); // string を受け取る場所に使える
        Bar(mydb.GetConfig("hello")); // DateTime を受け取る場所にも使える
    }
    static void Test2()
    {
        var mydb = new MyDB();
        var config = mydb.GetConfig("hello");

        Foo(config); // string 版が遅延評価される
        Bar(config); // DateTime 版が遅延評価される
        Bar(config); // メモ化された値が使われる
    }
}

C++ や Java 由来の言語/クラスライブラリは,メソッドオーバーロードを多用することである種の「シンタックス的分かりやすさ」を実現しています.では (C++ や Java,C# 的な意味での) メソッドオーバーロードを完全に廃止してしまったら本当に困るでしょうか? 実際は,似たようなシンタックスを実現するだけなら別のアプローチでも十分に可能ということはないでしょうか? その辺の意識しながら,F# や Haskell の文法やプログラミングスタイルを見るようになってから,私はへーと思う機会が増えました.