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でパーサコンビネータを実装してないっぽかったので実装してみました。
実装にあたり、こちらのブログを参考にさせていただきました。感謝
.NETのパーサコンビネータライブラリとしてはSpracheが有名ですが、SelectManyとクエリ構文でパーサの連結を行う機構が想像以上に複雑でちょっと何を言っているのかよくわからない状態なので今回は参考にするのはやめておきました。
パーサコンビネータとラムダ
ここではパーサは以下の引数を取り、以下の返り値を返す関数として定義します。
- 引数
- パース対象の文字列
- パーサが解析を始める先頭からのオフセット
- 返り値
- パースが成功したか否か
- パーサが消費した文字列(パーサの場合)
- 内部のパーサの解析結果の列挙(コンビネータの場合)
- 次に解析を行う先頭からのオフセット
また、今回はパーサを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等のライブラリを使うといい感じになると思います。
.NET FrameworkのRegexの文字クラスはバグっているのか?
はじめに
正規表現のお話ですが、『.
は任意の一文字にマッチする』みたいな文法解説ではありません。
そんなの大手サイトが死ぬほど書いてますしね。
妙なバグを踏み抜くことに定評がある弊社ですが、今回は正規表現の文字クラス関係でわりと意味がわからないバグを踏み抜いたのでそれについてです。
.NET Frameworkの正規表現の互換性
正規表現にはいくつかの系統というか規格(PosixとかJavaScript)が存在しますが、.NETはどこに属しているのでしょうか?
.NET Framework では、正規表現のパターンが特殊な構文または言語で定義されます。この構文または言語には、Perl 5 の正規表現と互換性があるほか、右から左への一致処理など、いくつかの機能が追加されています。
ということでPerl 5と互換性があると言っています。信じて良いんですね?
検証環境
ということで楽しい検証の時間です。
それぞれの言語で以下のサイトを使用しました。
- .NET Framework - Regex Storm
- Mono(mono-4.2.1 (C#5, CLI4.5)) - paiza.IO
- JavaScript - regexper、regex101
- PCRE - regex101
また、paiza.IOはオンラインの開発環境でRegexテスターではないので以下のコードでテストをしました。
余談ですが、補完文字列はC#6からの機能のはずですが、C#5のはずpaiza.IOで使えてます。 更に余談ですが、mono-4.2.1で補完文字列のバグが修正されてるってリリースノートに書いてあります。なにこれ?
using System; using System.Text.RegularExpressions; public class Hello { public static void Main() { foreach(Match it in Regex.Matches("テスト文字列","正規表現")) { Console.WriteLine($"\"{it.Value}\""); } } }
[]
空の文字クラスは.NET・mono・PCREでは受容されませんが、JavaScriptでは受容されます。 まぁ順当な結果ですね。
[5-3]
逆順の文字クラスは.NET・mono・PCRE・JavaScriptの全てで受容されませんでした。 当たり前ですね。
[Z-[]]
Z[
と[]
にマッチするという表現でPCRE・JavaScriptでは問題なく動作しますが、.NET・monoではUnterminated [] set.
ということで弾かれます。
はぁ、そうですか。
[Z-[ ]]
『.NET・monoでは空の文字クラスが弾かれてたじゃん。じゃあ[]
にスペース入れれば動くんじゃね?』がまじで動いたパターン。
PCRE・JavaScriptではZ]
、[]
、]
のパターンにマッチしますが、.NET・monoではZ
のみにマッチします。
[A-Z_-[ ]]
大文字のA
からZ
と_
から[
とのいずれかと
]
にマッチするという表現はそもそも_
>]
なので受容されないはずですが、.NET・monoでは受容されます。
さらにA
〜Z
と_
のみにマッチするという不可解な挙動をします。面白くなってきましたね。
[A-Z-[ ]]
PCRE・JavaScriptではZ]
、[]
、]
、A]
などにマッチしますが、.NET・monoでは相変わらずA
やZ
などの単独の文字にしかマッチしません。
-[ ]
をどこにやった?
おわりに
普通なら思いつかないような謎正規表現をプロダクト環境にぶち込んでくる人がいるので、世の中は多様性で満ち溢れていますね。
バグか他の文法との兼ね合いなのかが分かりませんでしたが、仮にバグだとしても後方互換性を維持するために直すのは難しそうですね。
完全に余談ですが、仮に正規表現が超複雑になるようなものを解析しないといけない場合はIronyやSpracheなんかを使ったほうが後々幸せかもしれません。
おわり