VB15の新機能の確認したい

はじめに

3月7日にVisual Studio 2017がリリースされました。

と言うことで今回も露骨なアクセス稼ぎVB15の新機能を確認していきましょう。

どうでもいいですが、VS2017のアイコンって梵字っぽいですよね。筆で書かれたっぽくして色を周りに合わせたら紛れてても気付かなそう。

新機能

新機能はこちらにまとまっているっぽいです。

今回もプログラミングの効率を上げる嬉しい新機能が目白押しではありませんでした。ざんねん

  • 値タプル
  • ByRefによる使用量の戻り値
  • 二進数リテラルと桁区切り

上二つはなんかC#7.0との互換性の為に導入された感がありますし、二進数リテラルも『はぁ、そうですか』感が否めません。

まぁ、とりあえず確認していきましょう。

値タプル

タプルと言えば.NET Framework 4からSystem.Tupleが存在しました。 存在しましたが2つ程欠点が存在しました。

  • System.Tupleは参照型で何だかんだ言ってメモリの使用効率とGCのペナルティが発生する
  • タプルの要素へのアクセスにItem1Item2などといった人の温もりの感じられない名前で扱わないといけない

この2つの問題の解決策として導入されたのが値型のタプルであるSystem.ValueTupleと言語機能による名前付きタプルのサポートです。

値型であるため、何だかんだ言ったメモリの使用効率の改善とGCペナルティの削減が図られています。 がこちらは遅延バインディングと暗黙の型変換とアンリミテッドボックス化ワークスにまみれているVBプログラマには関係ありませんGCによる予測困難な遅延の発生を極限まで抑えたい(≒ゲーム実装)などで効いてくることですので、VBの主戦場である生産性アプリケーションではほとんど関係ありません。正直パフォーマンスは二の次で良いから読めるコードを書いてくれ頼む

で、前置きが長くなりましたがVB15の新機能の1つ目の名前付きタプルのお時間です。

あの無味乾燥で温かみの感じられないItem1などの名前ではなく、SyohizeiKingakuといった温かみのある名前で扱えるようになります。

Function HogeOld(a As IEnumerable(Of Integer)) As Tuple(Of Integer, Integer)
    Return Tuple.Create(a.Min(), a.Max())
End Function

Function Hoge(a As IEnumerable(Of Integer)) As (min As Integer, max As Integer)
    Return (a.Min(), a.Max())
End Function
Dim a = {1, 2, 3, 4, 5, 6, 7, 8, 9}
Dim b = HogeOld(a)
Dim c = Hoge(a)

Console.WriteLine($"min={b.Item1} max={b.Item2}")
Console.WriteLine($"min={c.min} max={c.max}")

もしかしたら弊社の環境が壊れているだけかもしれませんが、この機能はなぜかソリューションを作成しただけでは使えません。

f:id:jyuch:20170310213904p:plain

定義済みの型は定義されていません

とは面妖なエラーメッセージですが、どうにかして解決しましょう。

System.ValueTupleの方はNuGetパッケージをインストールすると解決します。

PM> Install-Package System.ValueTuple

属性の方はNuGetパッケージの方にもないっぽいのでCoreFXのコードからコピペで対応します。

github.com

C#なら単純なコピペで行けますが、VBの場合はVBコードに脳内トランスパイルするか別プロジェクトとしてC#でコピペして参照を追加する必要があり軽い敗北感すら感じます。

ということでパパッとVBコードとして書き換えます。

Namespace Global.System.Runtime.CompilerServices

    <CLSCompliant(False)>
    <AttributeUsage(AttributeTargets.Field Or AttributeTargets.Parameter Or AttributeTargets.Property Or AttributeTargets.ReturnValue Or AttributeTargets.Class Or AttributeTargets.Struct Or AttributeTargets.Event)>
    Public NotInheritable Class TupleElementNamesAttribute
        Inherits Attribute

        Private ReadOnly _transformNames As String()

        Public Sub New(transformNames As String())
            If transformNames Is Nothing Then
                Throw New ArgumentNullException(NameOf(transformNames))
            End If

            _transformNames = transformNames
        End Sub

        Public Function TransformNames() As IList(Of String)
            Return _transformNames
        End Function

    End Class

End Namespace

ByRefによる使用量の戻り値

誤訳なんじゃねーのってくらい意味が分かりませんが、機能の方も同じくらい意味が分かりません。

何よりも動くサンプルコードが現状見当たりません。

たぶんC#ref returnに対応する何かなんだと思います。

二進数リテラルと桁区切り

Dim answer = &B0010_1010
Console.WriteLine($"生命、宇宙、そして万物についての究極の疑問の答え = {answer}")
生命、宇宙、そして万物についての究極の疑問の答え = 42

おわりに

VB14でC#6.0との言語格差がかなり縮まったのでVBの今後に期待したのですが、VB15とC#7.0で差はまた開きつつありますね。

ちなみにC#7.0の新機能はこちらです。

blogs.msdn.microsoft.com

あくまでも個人的な感想ですが、VBC#は手続き型・オブジェクト指向プログラミング言語として(F#と比べて)かなり近い立ち位置にいると思います。 そんなパラダイムもベースとなるクラスライブラリ群もランタイムも共有する2つの言語を同じようにマイクロソフトがメンテナンスしていくかどうかには疑問を感じます。

VB.NETはVB6からC#への過渡期にVB6プログラマーC#にスムーズに移行させるために作られた。かどうかは分かりませんが現状を見るとC#VBのどちらに未来があるかといわれるとC#に軍配が上がるのは明らかです。 VBは初心者向けなんて言われることがありますが、少なくともVB.NETC#は同じオブジェクト指向というパラダイムの上に成り立っている言語ですから言語としての複雑さは同程度なはずです。むしろ書籍・コミュニティの充実しているC#のほうが学びやすいのではないでしょうか。 仮にVBプログラマC#を理解できないというのなら、それはC#が複雑だからではなくVBすら良く分かってない可能性があります。

とまぁ、お前はVBの何なんだよと言われそうな感じですが、弊社としてはVBは嫌いではないです。 ただ、恐らくはいずれC#(もしくは次の言語)に乗り換えないといけない時が来るのでいつまでも塩漬けにしておくのは得策ではありませんよって事です。

おわり

VB.NETでも遅延バインディングの呼び出し規則を確認したい

はじめに

遅延バインディングを使用したコードを眺めていた時にふと思ったのですが、遅延バインディングはどういう呼び出し規則でメソッドを呼び出しているのだろうと気になりました。

軽く調べてみたところ、言語仕様っぽいのは見つかりませんでした。 分かんないや!これは図書館に行かないとわかんないかも!

Roslynのコードを読めば分かりそうでありますが、あれを読むのは無理なので軽くテストで確認する程度にしておきましょう。

VBは遅延バインディングできるフレンズなんだね

単純な継承関係

Class Hoge

    Public Sub HogeHoge(arg As Object)
        Console.WriteLine("すごーい")
    End Sub

    Public Sub HogeHoge(arg As Fuga)
        Console.WriteLine("たのしー")
    End Sub

End Class
Class Fuga
End Class
Dim h As Object = New Hoge()
h.HogeHoge(new Object)
h.HogeHoge(New Fuga())
すごーい
たのしー

期待通りに呼び出されています。たのしー

亜空間型変換

Class Hoge

    Public Sub HogeHoge(arg As Integer)
        Console.WriteLine("すごーい")
        Console.WriteLine(arg)
    End Sub

End Class
Dim h As Object = New Hoge()
h.HogeHoge("123")
すごーい
123

さも当然かのように型変換をかけてきます。すごーい

拡大と縮小変換

Class Hoge

    Public Sub HogeHoge(arg As Fuga)
        Console.WriteLine("すごーい")
    End Sub

    Public Sub HogeHoge(arg As FugaFuga)
        Console.WriteLine("たのしー")
    End Sub

End Class
Class Fuga
End Class

Class FugaFuga
End Class

Class Piyo

    Public Shared Widening Operator CType(source As Piyo) As Fuga
        Console.WriteLine("拡大変換ができるフレンズなんだね")
        Return New Fuga()
    End Operator

    Public Shared Narrowing Operator CType(source As Piyo) As FugaFuga
        Console.WriteLine("縮小変換ができるフレンズなんだね")
        Return New FugaFuga()
    End Operator

End Class
Dim h As Object = New Hoge()
h.HogeHoge(New Fuga())
h.HogeHoge(New FugaFuga())
h.HogeHoge(New Piyo())
すごーい
たのしー
拡大変換ができるフレンズなんだね
すごーい

拡大変換と縮小変換では拡大変換が優先されます。へー

継承と拡大変換

Class Hoge

    Public Sub HogeHoge(arg As Fuga)
        Console.WriteLine("すごーい")
    End Sub

    Public Sub HogeHoge(arg As FugaFuga)
        Console.WriteLine("たのしー")
    End Sub

End Class
Class Fuga
End Class

Class FugaFuga
End Class

Class Piyo
    Inherits FugaFuga

    Public Shared Widening Operator CType(source As Piyo) As Fuga
        Console.WriteLine("拡大変換ができるフレンズなんだね")
        Return New Fuga()
    End Operator

End Class
Dim h As Object = New Hoge()
h.HogeHoge(New Fuga())
h.HogeHoge(New FugaFuga())

Try
    h.HogeHoge(New Piyo())
Catch ex As System.Reflection.AmbiguousMatchException
    Console.WriteLine(ex.Message)
End Try
すごーい
たのしー
これらの引数
    'Public Sub HogeHoge(arg As ConsoleApplication2.Fuga)':
        最も固有ではありません。
    'Public Sub HogeHoge(arg As ConsoleApplication2.FugaFuga)':
        最も固有ではありません。 に最も固有な、パブリック 'HogeHoge' がないため、オーバーロードの解決に失敗しました

継承関係と拡大変換ではオーバーロードを一意に決定できないっぽいので、呼び出しに失敗しSystem.Reflection.AmbiguousMatchExceptionがスローされます。へーきへーき!フレンズによって得意なこと違うから!

ジェネリック

Class Hoge(of T)

    Public Sub HogeHoge(arg As T)
        Console.WriteLine("すごーい")
    End Sub

    Public Sub HogeHoge(arg As Object)
        Console.WriteLine("わーい")
    End Sub

End Class
Class Fuga
End Class

Class FugaFuga
End Class
Dim h As Object = New Hoge(Of Fuga)()
h.HogeHoge(New Fuga())
h.HogeHoge(New FugaFuga())
h.HogeHoge(New Object())
すごーい
わーい
わーい

ジェネリックもちゃんといけます。すっごーい

まとめ

遅延バインディングは遅いと言われますが、実行時の型情報からオーバーロードの決定を毎回やっていたらそりゃ遅いよねってお話。

また、静的なオーバーロードコンパイル時に決定されるのに対して遅延バインディングの動的なオーバーロード解決は実行時に決定されるのでこんなこともできます。

Class Hoge

    Public sub HogeHoge(arg as IEnumerable)
        Console.WriteLine("たのしー")
    End sub
    
    Public Sub HogeHoge(arg As Hashtable)
        Console.WriteLine("すごーい")
    End Sub

    Public Sub HogeHoge(arg As ArrayList)
        Console.WriteLine("わーい")
    End Sub

End Class
Dim a As IEnumerable = new Hashtable()

Dim h As Object = New Hoge()
h.HogeHoge(a)

Dim ho = new Hoge()
ho.HogeHoge(a)
すごーい
たのしー

事前バインディングコンパイル時にどれが呼び出されるかがすでに決定されているので実際の型で呼び出し先のメソッドが変わることはありませんが、遅延バインディングは実行時に決定されるので実際の型で呼び出し先のメソッドを切り替えることができます。 乱用すればパターンマッチングっぽいことができるかもしれません。

でも、こう考えると遅延バインディング自分が何をやっているのかを完全に把握したうえで使うなら結構有用かもしれません。 ただ、コンパイル時ですら意味わかんないのに実行時まで不確定要素が出てくるとさすがにいろいろとアレなので弊社は遠慮しておきます。

たーのしー

VB.NETでもパーサコンビネータを実装したい

はじめに

ヤフーでググったところ、今のところ誰もVBでパーサコンビネータを実装してないっぽかったので実装してみました。

実装にあたり、こちらのブログを参考にさせていただきました。感謝

blog.anatoo.jp

.NETのパーサコンビネータライブラリとしてはSpracheが有名ですが、SelectManyとクエリ構文でパーサの連結を行う機構が想像以上に複雑でちょっと何を言っているのかよくわからない状態なので今回は参考にするのはやめておきました。

github.com

パーサコンビネータとラムダ

ここではパーサは以下の引数を取り、以下の返り値を返す関数として定義します。

  • 引数
    • パース対象の文字列
    • パーサが解析を始める先頭からのオフセット
  • 返り値
    • パースが成功したか否か
    • パーサが消費した文字列(パーサの場合)
    • 内部のパーサの解析結果の列挙(コンビネータの場合)
    • 次に解析を行う先頭からのオフセット

また、今回はパーサをFunc(Of String, Integer, ParseResult)で表されるデリゲートとして実装します。

Public Class ParseResult

    Public ReadOnly Property Success() As Boolean
    Public ReadOnly Property Result() As String
    Public ReadOnly Property InnerResult() As IEnumerable(Of ParseResult)
    Public ReadOnly Property NextPosition() As Integer

    Public Sub New(success As Boolean, result As String, inner As IEnumerable(Of ParseResult), nextPosition As Integer)
        Me.Success = success
        Me.Result = result
        Me.InnerResult = If(inner, Enumerable.Empty(Of ParseResult)())
        Me.NextPosition = nextPosition
    End Sub

    Public Overrides Function ToString() As String
        Return $"[{Success}, {Result}, [{String.Join(",", InnerResult)}], {NextPosition}]"
    End Function

End Class

上記の例として、単純な文字列を解析するパーサを返す関数をメソッドします。

なお、ヘルパメソッドとして以下のメソッドが定義されているとします。 似たようなメソッドでMidがありますが、あちらは序数が1からで混乱のもとになるので使いませんでした。

Private Shared Function Substring(text As String, position As Integer, length As Integer) As String
    If text.Length > position + length Then
        Return text.Substring(position, length)
    ElseIf text.Length > position Then
        Return text.Substring(position)
    Else
        Return ""
    End If
End Function
Public Shared Function Token(word As String) As Func(Of String, Integer, ParseResult)
    Return Function(text As String, position As Integer) As ParseResult
               If Substring(text, position, word.Length) = word Then
                   Return New ParseResult(True, word, Nothing, position + word.Length)
               Else
                   Return New ParseResult(False, Nothing, Nothing, position)
               End If
           End Function
End Function

上記のメソッドはトークンを引数に取り、指定された文字列とオフセットからトークンに一致するかを判定するパーサを返します。 一致した場合は一致した箇所と新しいオフセットを返します。

Dim hoge  = parser.Token("hoge")
Console.WriteLine(hoge("hogehoge", 0))
[True, hoge, [], 4]

また、同様に指定された文字列中の文字かどうか判定するパーサを返すメソッドを定義します。

Public Shared Function [Char](chars As String) As Func(Of String, Integer, ParseResult)
    Return Function(text As String, position As Integer) As ParseResult
               Dim c = Substring(text, position, 1)

               If c <> "" AndAlso chars.Contains(c) Then
                   Return New ParseResult(True, c, Nothing, position + 1)
               Else
                   Return New ParseResult(False, Nothing, Nothing, position)
               End If
           End Function
End Function
Dim hoge  = parser.char("abc")
Console.WriteLine(hoge("abc", 0))
[True, a, [], 1]

ここでの基本的なパーサは以上です。あとはこれらをこねくり回して目的のパーサを組み立ててきます。

まずは選択を表すOrパーサです。 このメソッドは複数のパーサを引数にとり、先頭からマッチを繰り返しマッチに成功した時点でその結果を返すパーサを返します。

Public Shared Function [Or](ParamArray parsers As Func(Of String, Integer, ParseResult)()) As Func(Of String, Integer, ParseResult)
    Return Function(text As String, position As Integer) As ParseResult
               For Each it In parsers
                   Dim r = it(text, position)
                   If r.Success Then
                       Return r
                   End If
               Next

               Return New ParseResult(False, Nothing, Nothing, position)
           End Function
End Function
Dim hogeOrFuga  = Parser.Or(Parser.Token("hoge"), parser.Token("fuga"))
Console.WriteLine(hogeOrFuga("hogehoge", 0))
Console.WriteLine(hogeOrFuga("fugahoge", 0))
[True, hoge, [], 4]
[True, fuga, [], 4]

次に複数のパーサの連結を表すSeqです。 このメソッドは複数のパースを引数をとり、順にマッチを行いすべてのパーサが成功したらその結果を返すパーサを返します。

Public Shared Function Seq(ParamArray parsers As Func(Of String, Integer, ParseResult)()) As Func(Of String, Integer, ParseResult)
    Return Function(text As String, position As Integer) As ParseResult
               Dim result = New List(Of ParseResult)()
               Dim p = position

               For Each it In parsers
                   Dim r = it(text, p)

                   If r.Success Then
                       result.Add(r)
                       p = r.NextPosition
                   Else
                       Return New ParseResult(False, Nothing, Nothing, position)
                   End If
               Next

               Return New ParseResult(True, Nothing, result, p)
           End Function
End Function
Dim hogefuga= Parser.Seq(Parser.Token("hoge"), Parser.Token("fuga"))
Console.WriteLine(hogefuga("hogefuga", 0))
Console.WriteLine(hogefuga("fugahoge", 0))
[True, , [[True, hoge, [], 4],[True, fuga, [], 8]], 8]
[False, , [], 0]

Manyは0回以上の任意の回数の繰り返しを表します。

Public Shared Function Many(parser As Func(Of String, Integer, ParseResult)) As Func(Of String, Integer, ParseResult)
    Return Function(text As String, position As Integer) As ParseResult
               Dim result = New List(Of ParseResult)()
               Dim p = position

               While True
                   Dim r = parser(text, p)

                   If r.Success Then
                       result.Add(r)
                       p = r.NextPosition
                   Else
                       Exit While
                   End If
               End While

               Return New ParseResult(True, Nothing, result, p)
           End Function
End Function
Dim hoge = Parser.Many(Parser.Token("hoge"))
Console.WriteLine(hoge("", 0))
Console.WriteLine(hoge("hoge", 0))
Console.WriteLine(hoge("hogehoge", 0))
[True, , [], 0]
[True, , [[True, hoge, [], 4]], 4]
[True, , [[True, hoge, [], 4],[True, hoge, [], 8]], 8]

また、同様にOptionはあっても無くてもかまわない選択を表し、Lazy再帰的なパーサの定義を行う際にパーサの評価を遅延させるために使い、Accumlate入れ子になった結果を平滑化するのに用います。

Public Shared Function [Option](parser As Func(Of String, Integer, ParseResult)) As Func(Of String, Integer, ParseResult)
    Return Function(text As String, position As Integer) As ParseResult
               Dim r = parser(text, position)

               If r.Success Then
                   Return r
               Else
                   Return New ParseResult(True, Nothing, Nothing, position)
               End If
           End Function
End Function

Public Shared Function Lazy(f As Func(Of Func(Of String, Integer, ParseResult))) As Func(Of String, Integer, ParseResult)
    Return Function(text As String, position As Integer) As ParseResult
               Return f()(text, position)
           End Function
End Function

Public Shared Function Accumlate(parser As Func(Of String, Integer, ParseResult)) As Func(Of String, Integer, ParseResult)
    Return Function(text As String, position As Integer) As ParseResult
               Dim r = parser(text, position)

               If r.Success Then
                   Return Accumlate(r)
               Else
                   Return New ParseResult(False, Nothing, Nothing, position)
               End If
           End Function
End Function

入れ子の括弧の対応をとる単純な式のパーサ

を実装します。

まぁ、やってることは完全に参考サイトのVBへの焼き直しなんでアレがアレですね。

まずは数字と演算子をパースするパーサを定義します。 この辺は簡単ですね。

Property Number As Func(Of String, Integer, ParseResult) =
    Parser.Accumlate(Parser.Seq(Parser.Char("123456789"), Parser.Many(Parser.Char("0123456789"))))

Property [Operator] As Func(Of String, Integer, ParseResult) = Parser.Char("+-")

次に括弧に包まれた式をパースするパーサを定義します。

括弧はいらないので括弧の中身だけを取り出すパーサを定義します。 また、Expressionはまだ定義していないのでLazyでパーサの評価を実行時まで遅延させます。

Property Parenthesis As Func(Of String, Integer, ParseResult) = Parser.Lazy(
    Function() As Func(Of String, Integer, ParseResult)
        Return Function(text As String, position As Integer) As ParseResult
                   Dim p = Parser.Seq(Parser.Token("("), Expression, Parser.Token(")"))
                   Dim r = p(text, position)

                   If r.Success Then
                       Return r.InnerResult.Skip(1).First()
                   Else
                       Return New ParseResult(False, Nothing, Nothing, position)
                   End If
               End Function
    End Function)

オペランドは数字か括弧で包まれた式なので、それをパースするパーサを定義します。

Property Atom As Func(Of String, Integer, ParseResult) = Parser.Or(Number, Parenthesis)

あとは1+2+3

[True, , [[True, 1, [], 1],[True, +, [], 2],[True, 2, [], 3],[True, +, [], 4],[True, 3, [], 5]], 5]

となるように頑張ってパーサを実装します。

Property Expression As Func(Of String, Integer, ParseResult) =
    Function(text As String, position As Integer) As ParseResult
        Dim p = Parser.Seq(Atom, Parser.Many(Parser.Seq([Operator], Atom)))
        Dim r = p(text, position)

        If Not r.Success Then
            Return New ParseResult(False, Nothing, Nothing, position)
        End If

        Dim ir = r.InnerResult.ToList()

        If ir.Count = 1 Then
            Return ir(0)
        Else
            Dim rs = New List(Of ParseResult)()

            rs.Add(ir(0))
            For Each i In ir(1).InnerResult
                For Each it In i.InnerResult
                    rs.Add(it)
                Next
            Next

            Return New ParseResult(True, Nothing, rs, r.NextPosition)
        End If
    End Function

あとはパース結果を見やすくするメソッドと、括弧の対応を取りながら計算するメソッドを定義します。

Sub Display(result As ParseResult, level As Integer)
    If Not ReferenceEquals(result.Result, Nothing) Then
        Console.WriteLine($"{New String(" "c, level)} {result.Result}")
    Else
        For Each it In result.InnerResult
            Display(it, level + 1)
        Next
    End If
End Sub

Function Calc(results As IEnumerable(Of ParseResult)) As Integer
    Dim r = New Queue(Of ParseResult)(results)
    Dim sum = 0

    Dim first = r.Dequeue()
    If Not ReferenceEquals(first.Result, Nothing) Then
        sum += Integer.Parse(first.Result)
    Else
        sum += Calc(first.InnerResult)
    End If

    While r.Count <> 0
        Dim op = r.Dequeue().Result
        Dim operand = r.Dequeue()
        Dim operandNum As Integer

        If Not ReferenceEquals(operand.Result, Nothing) Then
            operandNum = Integer.Parse(operand.Result)
        Else
            operandNum = Calc(operand.InnerResult)
        End If

        If op = "+" Then
            sum += operandNum
        Else
            sum -= operandNum
        End If
    End While

    Return sum
End Function
Dim r = Expression("1+2+(3+(5-6))", 0)
Console.WriteLine(r)

Display(r, 0)
Console.WriteLine($"result: {Calc(r.InnerResult)}")
  1
  +
  2
  +
   3
   +
    5
    -
    6
result: 5

正しくパースと計算が出来ているっぽいです。

おわりに

250行程度のコードでも再帰的な構造を持つ構文を解析できました。

今回実装したパーサコンビネータはパース結果を他のオブジェクトへマップする機能を完全にオミットしたり静的型付け言語の制約上結果が入れ子になって扱いずらい等、あまり完成度は高くありませんがパーサコンビネータのコンセプトを理解するのには十分だと思います。って偉そうなこと書いてますが、完全に参考サイトの受け売りなのでアレですが、まぁ弊社自身もなるほどなぁといった感じです。

実際に使用する場合はSprache等のライブラリを使うといい感じになると思います。