ラベル ラムダ式 の投稿を表示しています。 すべての投稿を表示
ラベル ラムダ式 の投稿を表示しています。 すべての投稿を表示

2015年2月10日

VB.NET Action デリゲート型に Function ラムダ式を代入

タイトル通りなのだが、VB.NET では下記のようなコードがコンパイル&実行可能。

Module TestDelegate
    Sub Main()
        Dim act1 As Action = Function() True
        act1()

        ' 引数があるデリゲートも
        Dim act2 As Action(Of Integer) = Function(i) i + 1
        act2(0)
        ' ラムダ式の引数を省略可能
        act2 = Function() True
        act2(0)
        act2 = Sub() Console.WriteLine("void")
        act2(0)
    End Sub
End Module

'[実行結果]
'> void

「厳密でないデリゲート変換」というもので、ラムダ式が導入された時 (Visual Studio 2008) からあった模様。

「Option Strict On」でも適用されるため、「タイプセーフ?なにそれ?おいしいの?」のような機能・・・
前の投稿とかぶるが、これを無効化する Option はあった方がいい。

ちなみに、自分がやってしまったバグ

Module BugExample
    Sub Main()
        Dim elements As New List(Of Element)
        ' 中略
        elements.Where(適当な条件).ForEach(Function(elem) elem.Enabled = True)
    End Sub
End Module

Class Element
    Public Property Enabled As Boolean
    ' 以下略
End Class
※ ForEach は Interactive Extensions の拡張メソッド。
Sub と Function を間違えたのだが、まったく意味が違ってくる。
代入と比較の記号が同じであるため、コンパイルが通り、テストするまで気付けなかった。

2014年9月2日

コンパイラをごまかすキャスト

※実質、.net - C# non-boxing conversion of generic enum to int? - Stack Overflow の紹介記事です。

ジェネリック型パラメータと Enum の相性は悪い。
型パラメータの制約として、Enum クラスを指定できない。
Enum に対する制約は、せいぜい下記のようにしかできない。(ゆるい制約だとstructだけの場合も)
[サンプル1 : Enum に対する制約]

public static void HandleEnum<TEnum>(TEnum value)
    where TEnum : struct, IComparable, IFormattable, IConvertible
{
}

問題は、型パラメータで受け取った Enum 値を int などにキャストする場合である。
[サンプル1]で value を int へキャストしようとすると、コンパイラに怒られる。
TEnum の制約では、int へ変換可能かどうかを判断できないためである。
int type conversion error

object キャスト

これを解決する最も簡単な方法は、一度 object へキャストすることである。

public static void HandleEnum<TEnum>(TEnum value)
    where TEnum : struct, IComparable, IFormattable, IConvertible
{
    var i = (int)(object)value;
}

ただし、この方法はコードの通りボックス化(ボクシング)→ボックス化解除(アンボクシング)であるため、あまりパフォーマンス的によろしくない。

動的ラムダ式でキャスト

ボックス化を回避できる方法として、式木で動的ラムダ式を構築してキャストする方法がある。
このコードを少し変更(Get メソッドの処理を静的コンストラクタに移動)。

public static class CastTo<T>
{
    private static class Cache<S>
    {
        static Cache()
        {
            // 次のラムダ式を式木で構築
            // (S s) => (T)s
            var p = Expression.Parameter(typeof(S));
            var c = Expression.ConvertChecked(p, typeof(T));
            Caster = Expression.Lambda<Func<S, T>>(c, p).Compile();
        }
        internal static readonly Func<S, T> Caster;
    }

    public static T From<S>(S source)
    {
        return Cache<S>.Caster(source);
    }
}
[使用例]

public static void HandleEnum<TEnum>(TEnum value)
    where TEnum : struct, IComparable, IFormattable, IConvertible
{
    var i = CastTo<int>.From(value);
}

Expression.ConvertChecked でキャストするだけのラムダ式を構築している。
Expression.ConvertChecked を使うことで、コンパイラのキャスト可能チェックをスルーできる。
ちなみにオーバーフロー例外を発生させたくない場合は Expression.Convert を使う。

動的IL でキャスト

ここにもあるのだが、キャスト処理を動的IL で構築することで、ボックス化を回避できる。

public static class IlCastToInt32<T>
{
    static IlCastToInt32()
    {
        var method = new DynamicMethod(
            "IlCastToInt32Caster", typeof(int), new[] { typeof(T) });
        var il = method.GetILGenerator();
        il.Emit(OpCodes.Ldarg_0);
        il.Emit(OpCodes.Conv_Ovf_I4);
        il.Emit(OpCodes.Ret);
        Caster = (Func<T, int>)method.CreateDelegate(typeof(Func<T, int>));
    }
    public static readonly Func<T, int> Caster;
}

// クラスの型パラメータを省略するための拡張メソッド
public static class IlCastExtensions
{
    public static int CastToInt32<T>(this T value)
    {
        return IlCastToInt32<T>.Caster(value);
    }
}
[使用例]

public static void HandleEnum<TEnum>(TEnum value)
    where TEnum : struct, IComparable, IFormattable, IConvertible
{
    var i = value.CastToInt32();
}

戻り値を決め打ちする必要があるため、動的ラムダ式より汎用性が下がる。
その他の型に変換したい場合は、OpCodes を参照。
また、型パラメータとして渡すもの(ここでは Enum)は public である必要がある。
public でないと TypeAccessException が発生する。

パフォーマンス

[測定コード]

static class TestEnumCast
{
    static void Main(string[] args)
    {
        var flag = int.Parse(args[0]); // 1 ~ 5
        var loopCount = int.Parse(args[1]);
        var source = MuseEnumerable().Take(loopCount).ToArray();
        var temp = new List<int>(loopCount);

        if( flag > 3 ){
            // 4,5 はイニシャルコスト無しの設定
            // -> 一度実行してキャッシュを作成しておく
            flag -= 2;
            CastTo<int>.From(Muse.None);
            Muse.None.CastToInt32();
        }

        var sw = new Stopwatch();
        sw.Start();
        switch( flag ){
        case 1:
            foreach( var item in source ) temp.Add((int)(object)item);
            break;
        case 2:
            foreach( var item in source ) temp.Add(CastTo<int>.From(item));
            break;
        case 3:
            // 拡張メソッドだとメソッド 1 個分のコストがかかるため、直呼び
            foreach( var item in source ) temp.Add(IlCastToInt32<Muse>.Caster(item));
            break;
        }
        sw.Stop();
        Console.WriteLine(sw.Elapsed);
    }

    static IEnumerable<Muse> MuseEnumerable()
    {
        while( true ){
            yield return Muse.Honoka;
            yield return Muse.Kotori;
            yield return Muse.Umi;
        }
    }
}

[Flags]
public enum Muse
{
    None = 0x0,
    Honoka = 0x1,
    Kotori = 0x2,
    Umi = 0x4
}

[結果]
loopCount object 動的ラムダ式 動的IL
イニシャルコスト有 10000 0.0003435 0.0053873 0.0031249
イニシャルコスト無 0.0001409 0.0001416
イニシャルコスト有 100000 0.0035345 0.0067660 0.0044378
イニシャルコスト無 0.0014422 0.0014731
※コマンドプロンプトで実施(csc /optimize)
※10回の算術平均、単位は[s]

結果をまとめると下記。(左の方が高性能)
キャスト : 動的ラムダ式 ≒ 動的IL > object
イニシャルコスト : 動的IL > 動的ラムダ式

キャスト処理に関しては、ボックス化を回避することで 2 倍以上の性能になる。
動的ラムダ式と動的IL で差はほぼないため、
汎用性や型パラメータの public 制限を考慮すると動的ラムダ式、イニシャルコストが少ない方がいいなら動的IL となる。
また、動的IL は拡張メソッドから呼び出すと、わずかだがその分のコストがかかる。

ただ、イニシャルコスト >>> object キャスト 1 回 なので、条件によっては無意味な工夫になる。

検証環境

Windows 7 64bit/Visual Studio 2010 SP1/.NET 4.0
Intel(R) Celeron(R) CPU G530 @ 2.40GHz/DDR3 8GB(4GB x 2)

2014年5月5日

再帰的なラムダ式

あまり実用的ではないが、.NET でラムダ式で再帰を行う方法はいくつかある。
ここでは、階乗を再帰の例とする。

ちなみに、普通の再帰メソッド。

public static int Factorial(int x)
{
    return x > 1 ? x * Factorial(x - 1) : 1;
}

1. ラムダ式割り当て変数で再帰


Func<int, int> fac1 = null;
fac1 = x => x > 1 ? x * fac1(x-1) : 1;
ラムダ式内で、自身を割り当てる変数をメソッドとして使用する方法。
たぶん、一番シンプルな方法になる。
ただし、コンパイラの都合上、変数宣言と同時にラムダ式の代入はできない。
※「未割り当てのローカル変数 '~' が使用されました。」エラーが出る。

また、ひねくれた使い方をするとバグの原因になる。

Func<int, int> fac1 = null;
fac1 = x => x > 1 ? x * fac1(x-1) : 1;
var fac1copy = fac1;
Console.WriteLine(fac1(4));     // 24
Console.WriteLine(fac1copy(4)); // 24

fac1 = x => x * x;
Console.WriteLine(fac1(4));     // 16 : 4 * 4
Console.WriteLine(fac1copy(4)); // 36 : 4 * fac1(3) = 4 * (3 * 3)

// fac1copy は最初の fac1 の内容から変わってないが、再帰として fac1 を使っているため、
// fac1 を変更すると影響を受けてしまう。

2. リフレクションで再帰


Func<int, int> fac2 = x =>
    x > 1 ? x * (int)MethodBase.GetCurrentMethod().Invoke(null, new object[]{x-1}) : 1;
リフレクションにより、自身を呼び出す方法。
javascript の arguments.callee (今や非推奨)に近い。
リフレクションなのでパフォーマンスはアレだが、1. のような副作用はない。

3. 不動点コンビネータで再帰

再帰メソッドを引数で渡せばいいかも?という発想から下記を定義する。

Func<Func<int, int>, Func<int, int>> fac3base = (Func<int, int> f) => {
    return (int x) => {
        return x > 1 ? x * f(x - 1) : 1;
    };
};

// 型推論バージョン
// Func<Func<int, int>, Func<int, int>> fac3base = f => x => x > 1 ? x * f(x-1) : 1;

ここで、fac3base の引数は?ということに頭を悩ませると思う。
なぜなら、渡したい中身は fac3base に定義されているから・・・

とりあえず、Func<Func<int, int>, Func<int, int>> から Func<int, int> を取り出す処理、つまり、
Func<
  Func<Func<int, int>, Func<int, int>>,
  Func<int, int>
>
となるメソッド/ラムダが欲しい、ということで不動点コンビネータFixed-point combinator)の出番である。

  • Z コンビネータ
wikipedia どおりの実装。

// 自身を引数に持つデリゲート ※外部に定義する
delegate T SelfApplicable<T>(SelfApplicable<T> self);

Func<Func<Func<int, int>, Func<int, int>>, Func<int, int>> Z =
(Func<Func<int, int>, Func<int, int>> f) => {
    SelfApplicable<Func<int, int>> Z1 = (SelfApplicable<Func<int, int>> x) => {
        return f((int y) => {
            return x(x)(y);
        }
    };
    return Z1(Z1);
};

// 型推論バージョン
// Func<Func<Func<int, int>, Func<int, int>>, Func<int, int>> Z = f => {
//     SelfApplicable<Func<int, int>> Z1 = x => f(y => x(x)(y));
//     return Z1(Z1);
// };

// 使用方法
var fac3z = Z(fac3base);
Console.WriteLine(fac3z(4)); // 24

  • Y コンビネータ
ここの引用。
※C# による実装なので、厳密には Z コンビネータ

// SelfApplicable は Z コンビネータと同じもの
SelfApplicable<Func<Func<Func<int, int>, Func<int, int>>, Func<int, int>>> Y =
(SelfApplicable<Func<Func<Func<int, int>, Func<int, int>>, Func<int, int>>> y) => {
    return (Func<Func<int, int>, Func<int, int>> f) => {
        return (int x) => {
            return f(y(y)(f))(x);
        };
    };
};
Func<Func<Func<int, int>, Func<int, int>>, Func<int, int>> YFix = Y(Y);

// 型推論バージョン
// SelfApplicable<Func<Func<Func<int, int>, Func<int, int>>, Func<int, int>>> Y =
// y => f => x => f(y(y)(f))(x);
// var YFix = Y(Y);

// 使用方法
var fac3y = YFix(fac3base);
Console.WriteLine(fac3y(4)); // 24

なんで動いてるの?という疑問については、wikipedia や引用元サイト参照。
不動点コンビネータとは、要するに fac3base に渡す引数を fac3base から作り出してくれるものである。

おまけ

3. の汎用メソッド

public static class Combinators<TIn, TOut>
{
    static Combinators()
    {
        SelfApplicable<Func<Func<Func<TIn, TOut>, Func<TIn, TOut>>, Func<TIn, TOut>>>
        Y = y => f => x => f(y(y)(f))(x);
        YCombinator = Y(Y);
    }

    public static readonly
    Func<Func<Func<TIn, TOut>, Func<TIn, TOut>>, Func<TIn, TOut>> ZCombinator = f => {
        SelfApplicable<Func<TIn, TOut>> Z1 = x => f(y => x(x)(y));
        return Z1(Z1);
    };

    public static readonly
    Func<Func<Func<TIn, TOut>, Func<TIn, TOut>>, Func<TIn, TOut>> YCombinator;
}
public delegate T SelfApplicable<T>(SelfApplicable<T> self);

参考URL