VB のたまご

作成日: 2015/12/26, 更新日: 2015/12/26


式木はリフレクションみたいなもの、何回も使って慣れてしまおう

式木とは?

  •  式木(式ツリー)とは、ラムダ式をプログラムとしてではなく、データとして扱うことができる技術です。 プログラムをデータとして扱う、と言えば、メタデータを扱うリフレクションがありますが、 式木もリフレクションと同じように、動的な操作をしたり、メンバー取得することができます。

  • Dim m As Func(Of Integer, Boolean) = Function(i) i < 10                ' ラムダ式
    Dim f As Expression(Of Func(Of Integer, Boolean)) = Function(i) i < 10 ' 式木
    
  •  式木はこのようにして宣言します。比較用にラムダ式の宣言も書いていますが、ラムダ式を宣言するのとほとんど同じ書き方をします。 違うのは型の指定に、ラムダ式の型を包んであげるだけです。

スポンサーリンク


式木の内部イメージ

  •  式木は、木構造になっています。そのため、二分木や XML を知っている方であれば、すんなり入ってこれるのではないかと思います。 同じように、最初にソースコードを見るよりも、先に式木のイメージを掴んでおいた方が、結果として理解が早くなると思いますので、 ここでは、いくつかのサンプルを見ながら、木構造のイメージ図と共に説明します。

  •  1つ目は、Integer 型の引数を渡して、その値が 10 未満かどうかを確認するラムダ式です。 メソッドとして実行すると、5 を渡したら True, 20 を渡したら False が返ってきます。

  • イメージ

  •  これを式木で見ると、ラムダ式は、このような木構造になります(つまり、このように分解することができます)。 最初は、引数、メソッドの本体、戻り値の3つに分けられます。引数 i、メソッド本体の式 i < 10、戻り値 Boolean です。 このうち、メソッドの本体は式なので、さらに、単語に分割することができます。i と 10 と、2つを比較する < です。


  • イメージ
  •  デバッグだとこう見えます。この中のうち、説明しているのは Parameters, Body, ReturnType の3つです。



  •  2つ目は、Integer 型の引数を2つ渡して、その2つの値を足し算して、その結果を戻すラムダ式です。 メソッドとして実行すると、2 と 3 を渡したら 5, 10 と 20 を渡したら 30 が返ってきます。

  • イメージ

  •  これを式木で見ると、ラムダ式は、このような木構造になります。 最初は、引数、メソッドの本体、戻り値の3つに分けられます。引数 i1 と i2、メソッド本体の式 i1 + i2、戻り値 Integer です。 このうち、メソッドの本体は式なので、さらに、単語に分割することができます。i1 と i2 と、2つを加算する + です。 また、引数は式ではありませんが、コレクションデータとなっているので、i1 と i2 に分割することができます。


  •  最後3つ目は、Integer 型の引数を3つ渡して、その3つの値を足し算して、その結果を戻すラムダ式です。 メソッドとして実行すると、2 と 3 と 4 を渡したら 9, 10 と 20 と 30 を渡したら 60 が返ってきます。

  • イメージ

  •  先程の2つのサンプルと同じように分解していくのですが、今回は、メソッドの本体の式は、どうやって分解するのでしょうか。 それぞれ同時にどうにかしなきゃ、と考えてしまうと分からなくなってしまいますが、実はその答えは、私たちが普段計算を解いていく過程とおなじです。 つまり、計算しやすいように、小分けにしながら順々に計算していく方法です。

  •  最初は、i1 と i2 を足すことだけを考えます。ここで出た答えに、i3 を足すことで簡単に計算することができるようになります。 式木では、カッコで括って i1 と i2 を明示的に加算します。よって、まずは、i1 + i2 を1つの単語として扱い、3つに分割します。 つまり、( i1 + i2 ) と i3、そして2つを加算する + です。 最後に、( i1 + i2 ) は式のままなので、3つに分割します。i1 と i2、そして2つを加算する + です。

  •  このように、式木は、各値を分割していって、木構造として組み立てている変数であることが分かります。

スポンサーリンク


式木を、実際に分割してみる

  •  前節の説明を読んで、もしかしたら、匿名型のデータクラスにメンバーを追加して、何層にもつなげたもの、というイメージを持たれたかもしれません。 例えば、式木.本体.右 のように、ドットつなぎで操作するようなイメージです。これは残念ながら違います。 リフレクションと同じで、専用の操作があります。それでは見ていきます。

  •  まずは、分割処理です。
  • Imports System.Linq.Expressions
    
    Private Sub DebugView(e As Expression, indentSpace As String)
    
        Select Case True
            Case TypeOf e Is LambdaExpression : ShowInfo(CType(e, LambdaExpression), indentSpace)
            Case TypeOf e Is BinaryExpression : ShowInfo(CType(e, BinaryExpression), indentSpace)
            Case TypeOf e Is UnaryExpression : ShowInfo(CType(e, UnaryExpression), indentSpace)
            Case TypeOf e Is ParameterExpression : ShowInfo(CType(e, ParameterExpression), indentSpace)
            Case TypeOf e Is ConstantExpression : ShowInfo(CType(e, ConstantExpression), indentSpace)
            Case Else : ShowInfo(e)
        End Select
    
    End Sub
    
    Private Sub ShowInfo(e As LambdaExpression, indentSpace As String)
    
        Console.Write("{0}【式木 : 種類】", indentSpace)
        Me.ShowInfo(CType(e, Expression))
        Console.WriteLine("")
    
        Dim prms = e.Parameters
        If prms IsNot Nothing AndAlso 0 < prms.Count Then
            Console.Write("{0}【引数部   】", indentSpace)
            For i As Integer = 0 To prms.Count - 1
                If 0 < i Then Console.Write(", ")
                Me.ShowInfo(prms(i), indentSpace)
            Next
            Console.WriteLine("")
        Else
            Console.WriteLine("{0}【引数部   】無し", indentSpace)
        End If
    
        Console.WriteLine("{0}【戻り値   】{1}", indentSpace, e.ReturnType)
        Console.WriteLine("{0}【本体部   】{1}", indentSpace, e.Body)
        Me.DebugView(e.Body, indentSpace & "    ")
    
    End Sub
    
    Private Sub ShowInfo(e As BinaryExpression, indentSpace As String)
    
        Console.Write("{0}【式木 : 種類】", indentSpace)
        Me.ShowInfo(CType(e, Expression))
        Console.WriteLine("")
    
        Console.WriteLine("{0}【戻り値   】{1}", indentSpace, e.Type)
    
        If TypeOf e.Left Is ParameterExpression OrElse TypeOf e.Left Is ConstantExpression Then
            Console.Write("{0}【左     】{1}", indentSpace, e.Left)
            Me.DebugView(e.Left, ", ")
        Else
            Console.WriteLine("{0}【左     】{1}", indentSpace, e.Left)
            Me.DebugView(e.Left, indentSpace & "    ")
        End If
        Console.WriteLine("")
    
        If TypeOf e.Right Is ParameterExpression OrElse TypeOf e.Right Is ConstantExpression Then
            Console.Write("{0}【右     】{1}", indentSpace, e.Right)
            Me.DebugView(e.Right, ", ")
        Else
            Console.WriteLine("{0}【右     】{1}", indentSpace, e.Right)
            Me.DebugView(e.Right, indentSpace & "    ")
        End If
        Console.WriteLine("")
    
    End Sub
    
    Private Sub ShowInfo(e As UnaryExpression, indentSpace As String)
    
        Console.Write("{0}【式木 : 種類】", indentSpace)
        Me.ShowInfo(CType(e, Expression))
        Console.WriteLine("")
    
    End Sub
    
    Private Sub ShowInfo(e As ParameterExpression, indentSpace As String)
        Console.Write("{0}{1} : {2}", indentSpace, e.Name, e.Type)
    End Sub
    
    Private Sub ShowInfo(e As ConstantExpression, indentSpace As String)
        Console.Write("{0}{1} : {2}", indentSpace, e, e.Type)
    End Sub
    
    Private Sub ShowInfo(e As Expression)
        Console.Write("{0} : {1}", e, e.NodeType.ToString())
    End Sub
    
  •  この処理を利用して、テストデータを見てみます。 ただ、今見返してみると、説明用に組み込んだ出力処理が、ソース内容を理解するのを邪魔している感じがしますね。。。 出力処理を消すと、処理内容がぐっとシンプルになります。この分割処理は、DebugView メソッドに式木を渡すところから始まります。 第二引数はインデントとなるスペースの文字列です。最初はインデントは無いので空文字を渡しています。

  •  テストデータ、その1
  • Imports System.Linq.Expressions
    
    Dim f1 As Expression(Of Func(Of Integer, Boolean)) = Function(i) i < 10
    Me.DebugView(f1, String.Empty)
    
    ' 出力結果
    【式木 : 種類】i => (i < 10) : Lambda
    【引数部   】i : System.Int32
    【戻り値   】System.Boolean
    【本体部   】(i < 10)
        【式木 : 種類】(i < 10) : LessThan
        【戻り値   】System.Boolean
        【左     】i, i : System.Int32
        【右     】10, 10 : System.Int32
    
  •  実際の内容です。これを見ると普段とは違う、いくつか気になる点が出てきます。 1つ目は式木のラムダ式です。 Function や Sub と書いて宣言するラムダ式ではなく、引数と、=> という記号と、本体部、という構成で記載されています。 実はこれは、C# 言語での書き方のラムダ式です。これは仕様みたいなので、そういうものとして覚えた方がいいかなと思います。 2つ目は、式木の種類です。 Lambda とか LessThan とか出力していますが、式木を動的に作成する場合、メソッド名で作りこんでいきますので、覚える必要があります。 3つ目は、型の表示名です。VB.NET 形式ではなく、.NET Framework 形式になっています。 例えば、Integer 型は System.Int32 となっています。

  •  ただ、これらの点は、見たら何となく分かるレベルの違いだと思いますので、あまり気にしなくても大丈夫です。 後は、それぞれ分割後に、1つ下の階層に移動するにつれてインデントを深くして表示しています。

  •  テストデータ、その2
  • Imports System.Linq.Expressions
    
    Dim f2 As Expression(Of Func(Of Integer, Integer, Integer)) = Function(i1, i2) i1 + i2
    Me.DebugView(f2, String.Empty)
    
    ' 出力結果
    【式木 : 種類】(i1, i2) => (i1 + i2) : Lambda
    【引数部   】i1 : System.Int32, i2 : System.Int32
    【戻り値   】System.Int32
    【本体部   】(i1 + i2)
        【式木 : 種類】(i1 + i2) : AddChecked
        【戻り値   】System.Int32
        【左     】i1, i1 : System.Int32
        【右     】i2, i2 : System.Int32
    
  •  2つ目のテストデータは、引数が2つに増えていますが、前のテストデータと同様です。 AddChecked は、Add(足し算)の他に、Check(オーバーフローチェック)処理が含まれます。

  •  テストデータ、その3
  • Imports System.Linq.Expressions
    
    Dim f3 As Expression(Of Func(Of Integer, Integer, Integer, Integer)) = Function(i1, i2, i3) i1 + i2 + i3
    Me.DebugView(f3, String.Empty)
    
    ' 出力結果
    【式木 : 種類】(i1, i2, i3) => ((i1 + i2) + i3) : Lambda
    【引数部   】i1 : System.Int32, i2 : System.Int32, i3 : System.Int32
    【戻り値   】System.Int32
    【本体部   】((i1 + i2) + i3)
        【式木 : 種類】((i1 + i2) + i3) : AddChecked
        【戻り値   】System.Int32
        【左     】(i1 + i2)
            【式木 : 種類】(i1 + i2) : AddChecked
            【戻り値   】System.Int32
            【左     】i1, i1 : System.Int32
            【右     】i2, i2 : System.Int32
    
        【右     】i3, i3 : System.Int32
    
  •  3つ目のテストデータは、前節で述べた部分計算をつなげた木構造になっています。それ以外は前のテストデータと同様です。 ここまで、それぞれのテストデータを見てきましたが、イメージ図と一致していることが分かります。

式木は、Expression でつながっている

  •  式木を宣言する際、Expression 型として宣言しました。さらに、式木のうち、Body プロパティを見てみると、Expression 型になっています。 しかし、前節の分割処理では、xxxExpression という いろいろな型に型変換して、処理を実施しています。 これは、全ての値は何らかの専用の式木型であり、元をたどると式木の型ということです。 例えば、一番最初のラムダ式は、Expression 型ではあるのですが、細かく見ると LambdaExpression 型というデータ型に属します。 同じように、Body プロパティも、Expression 型ではあるのですが、細かく見ると、xxxExpression 型というデータ型に属しています。 (本体部の型は可変なので、BinaryExpression や UnaryExpression など、命令によって変わります)。 引数や定数も、Expression 型ではあるのですが、細かく見ると、ParameterExpression 型や ConstantExpression 型というデータ型に属します。

  •  これは、例えば、何でもなるフルーツの木、みたいなものです。 リンゴ、バナナ、ココナッツ、モモ、ブドウなど、いろいろな種類のフルーツが身を付けますが、全てがフルーツです。 これと同じように、式木では、いろいろな種類の式木がありますが、全て式木です。

式木を、実際に作成してみる

  •  式木では、ラムダ式を受け取って分割する以外に、無からプログラムを動的に作成することができます。 (リフレクションも無からプログラムを動的に作成することができますが、アセンブラ言語相当の知識が必要になるため敷居が高く、私はちょっと挫折中です。興味がある方は、Reflection.Emit で検索してください)。 最初は、簡単な処理なのに、見慣れないたくさんの命令を、あれこれ組み合わせながら作成していく過程に、引いてしまうかもしれません。 しかし、大変なのは慣れるまでの間だけです。多すぎる命令も、何回か使うことで見慣れます。 分からないなりに、いろんな式木を見て何回も書き写すことが、上達する近道だと思います。

  •  それでは、サンプルです。2つの Integer 型の引数を受け取り、その2つの値を加算して返却するラムダ式です。
  • Imports System.Linq.Expressions
    
    ' 以下のようなラムダ式を動的作成
    ' f As Expression(Of Func(Of Integer, Integer, Integer)) = Function(i1, i2) i1 + i2
    Dim i1 = Expression.Parameter(GetType(Integer), "i1")
    Dim i2 = Expression.Parameter(GetType(Integer), "i2")
    Dim add = Expression.Add(i1, i2)
    Dim lambda = Expression.Lambda(Of Func(Of Integer, Integer, Integer))(add, New ParameterExpression() {i1, i2})
    Me.DebugView(lambda, String.Empty)
    
    ' 式木をコンパイルして、デリゲートを生成。実際に足し算メソッドとして実行してみる
    Dim method = lambda.Compile()
    Dim result = method(2, 3)
    Console.WriteLine("2 + 3 = {0}", result)
    
    ' 出力結果
    【式木 : 種類】(i1, i2) => (i1 + i2) : Lambda
    【引数部   】i1 : System.Int32, i2 : System.Int32
    【戻り値   】System.Int32
    【本体部   】(i1 + i2)
        【式木 : 種類】(i1 + i2) : Add
        【戻り値   】System.Int32
        【左     】i1, i1 : System.Int32
        【右     】i2, i2 : System.Int32
    2 + 3 = 5
    
  •  式木を組み立てる際は、「単語」を準備して、それぞれの単語を引数として使う「式」を準備して、最後に、それぞれの式を使う「ラムダ式」を準備する、という流れになります。 普段書いているラムダ式とは、書く順番が異なります。

参考にさせていただいた記事


スポンサーリンク