2013年12月25日水曜日

PowerShell で DSL - Create the DSL for the template engine that generates automatically the C++ library code that hooks the JIT of a program written by C# -

"……君のような勘のいいガキは嫌いだよ"


PowerShell Advent Calendar 2013、25 日目!昨年に引き続き、今年も PowerShell Advent Calendar に参加させていただきました、杉浦と申します。
昨日は @oota_ken さんの『PesterのMock機能をもう少し詳しく│ソフトウェアテストラボ|アプリテスト|スマートフォンテスト|株式会社シフト』でした。単体テスト周りの話は、私自身、テストダブル生成フレームワークを作っていく過程で、今回題材にさせていただいているような DSL を実装するに至ったこともあり、非常にタメになるところです。お疲れ様でした!

さて、直近で 12/21 にありました 第1回 PowerShell勉強会で登壇させていただいたこともあり、今回はそのフォローアップ記事になればと。

発表に使った資料はこちらに公開してあります:

私はこのような場所でセッションを行うのは初めてでしたので、お聞き苦しい点やわかりにくい点もあったかと思いますが、いかがでしたでしょうか?
PowerShell が持つ脅威の柔軟性の、ほんの一端でも伝われば幸いに思います。

では、早速行ってみましょう!!

※今回の記事で扱うのは、PowerShell v2 の情報となります。PowerShell v3/v4 での情報は、また次の機会に・・・。
※まとめる内に、基本的な部分の解説と応用的な部分の解説とでは分けたほうが良さそうに思えてきましたので、今回は基本的な部分を。続きはまた後日執筆させていただければと思います・・・。


こちらの情報を参考にさせていただいています。もしご自分で何かしらの DSL を実装される場合は、何かと参考になるかもしれません! (゚∀゚)
PowerShellでプロトタイプベースのオブジェクト指向を記述する方法 - 趣味の無い人生は虚しい
Writing your own PowerShell Hosting App (Part 1. Introduction) PowerShell Station
JavaScrip のクラスの多重継承(の、ようなもの):みみちゃんblog - プログラムの園:So-netブログ
Rubyist Magazine - Refinementsとは何だったのか
Adding New Type Accelerators - Power Tips - PowerShell.com – PowerShell Scripts, Tips, Forums, and Resources
Detouring Win32 Function Calls in PowerShell Adam Driscoll's Blog
Generating Fakes Assemblies with PowerShell Adam Driscoll's Blog
Inside PowerShell 3 The New Parser and Compiler Adam Driscoll's Blog
http://ps2exe.codeplex.com/
c# - Programmatic equivalent of default(Type) - Stack Overflow
c# - alternative for using slow DynamicInvoke on muticast delegate - Stack Overflow
dot net figured out How to get PowerShell current runspace from C#
c# - How to prevent blank xmlns attributes in output from .NET's XmlDocument - Stack Overflow
powershell Adding the Using Statement - The Technical Adventures of Adam Weigert
オブジェクト指向 - アンサイクロペディア





目次

PSAnonym.Prototype 全体像
資料の中にありますPSAnonym.Linq は、昨年の Advent Calendar で紹介させていただいていますので、今回は PSAnonym.Prototype のほうを。
PSAnonym.Prototype は、多重継承をサポートしたプロトタイプベースのオブジェクト指向言語です。PowerShell には通常存在しない、いわゆるクラス(というか、この場合プロトタイプですね)定義構文を導入するモジュールになります。
勉強会でやったものより、若干大き目のサンプルで、全体像を見てみることにしましょう:
# キャラを表すプロトタイプ
$キャラ = 
    Prototype キャラ {
        # フィールド、プロパティ
        Field 今日は話した $false -Hidden
        Field 好感度 0 -Hidden
        Field 難易度 10 -Hidden
        Field 状態確認回数 10 -Hidden
        Field 名前 ([string].default) -Hidden
        Property 状態 {
            if (0 -lt $Me.状態確認回数-- - $Me.難易度) {
                '好感度: ' + $Me.好感度
            }
        }

        # コンストラクタ
        New {
            if ($null -eq $Params -or 10 -lt $Params[0]) {
                $難易度 = 10
            } else {
                $難易度 = $Params[0]
            }
            $Me.難易度 = $難易度
            $Me.名前 = $Params[1]
        }
        
        # メソッド
        Method 会う {
            $Me.今日は話した = $false
            $Me.会う中身()
        }
        AbstractMethod 会う中身
        
        Method 話す {
            if ($Me.今日は話した) {
                $Me.好感度--
                $Me.話す中身_好感度下げ()
            } else {
                $Me.今日は話した = $true
                $Me.好感度++
                $Me.話す中身_好感度上げ()
            }
        }
        AbstractMethod 話す中身_好感度下げ
        AbstractMethod 話す中身_好感度上げ
        
        AbstractMethod 誘う
    }

# ツンキャラを表すプロトタイプ
$ツンキャラ = 
    $キャラ | 
        Prototype ツンキャラ |
            OverrideMethod 会う中身 { 'な、なによ' } | 
            OverrideMethod 話す中身_好感度下げ { 'バカ、しつこい' } | 
            OverrideMethod 話す中身_好感度上げ { 'べ、別に' } | 
            OverrideMethod 誘う { $Me.好感度--; 'バカ、なに考えてるのよ' }

# デレキャラを表すプロトタイプ
$デレキャラ = 
    $キャラ | 
        Prototype デレキャラ |
            OverrideMethod 会う中身 { '....おはよう' } | 
            OverrideMethod 話す中身_好感度下げ { 'しつこいわよ' } | 
            OverrideMethod 話す中身_好感度上げ { 'そうね' } | 
            OverrideMethod 誘う { $Me.好感度++; 'ありがとうっ、楽しみね' }

# ツンデレキャラを表すプロトタイプ
$ツンデレキャラ = 
    $ツンキャラ, $デレキャラ | 
        Prototype ツンデレキャラ {
            OverrideMethod 会う中身 { 
                if ($Me.好感度 % 3 -eq 0) {
                    $ツンキャラ.会う中身()
                } else {
                    $デレキャラ.会う中身()
                }
            }
            
            OverrideMethod 話す中身_好感度下げ { 
                if ($Me.好感度 % 3 -eq 0) {
                    $ツンキャラ.話す中身_好感度下げ()
                } else {
                    $デレキャラ.話す中身_好感度下げ()
                }
            }
            
            OverrideMethod 話す中身_好感度上げ { 
                if ($Me.好感度 % 3 -eq 0) {
                    $ツンキャラ.話す中身_好感度上げ()
                } else {
                    $デレキャラ.話す中身_好感度上げ()
                }
            }
            
            OverrideMethod 誘う { 
                if ($Me.好感度 % 3 -eq 0) {
                    $ツンキャラ.誘う()
                } else {
                    $デレキャラ.誘う()
                }
            }
        } -Force

$舞 = $ツンデレキャラ.New((2, '舞'))

'1 日目: {0} ---' -f $舞.名前
$舞.会う()
$舞.話す()
$舞.話す()
$舞.誘う()

'2 日目: {0} ---' -f $舞.名前
$舞.会う()
$舞.話す()
$舞.誘う()

'3 日目: {0} ---' -f $舞.名前
$舞.会う()
$舞.話す()
$舞.誘う()
$舞.誘う()

# 結果------------------------
# 1 日目: 舞 ---
# な、なによ
# そうね
# バカ、しつこい
# バカ、なに考えてるのよ
# 2 日目: 舞 ---
# ....おはよう
# べ、別に
# バカ、なに考えてるのよ
# 3 日目: 舞 ---
# ....おはよう
# べ、別に
# バカ、なに考えてるのよ
# ありがとうっ、楽しみね
私の Blog には、ちょっと似つかわしくない題材なのですが、大き目のサンプルで複数の生き物を合成するのは、どこかの錬金術師的な流れになる危険がありましたので。。。(^_^;)
もう少し柔らかめな表現しやすいものとして、定番のツンデレキャラを作ってみました。罵倒されても、挫けずに誘い続けることが大切です!・・・みたいな教訓めいたものは何もなく、適当に組んだらそれっぽい結果になったので驚いています。これがクリスマスの魔法でしょうか。

さて、コマンドと宣言構文で、何をしたいかは大体想像がついてしまうかもしれませんが、次節から、各項目の簡単な説明をさせていただければと思います。





Prototype(New-Prototype) 関数
プロトタイプは、そのまんまではありますが Prototype という宣言文で始まります。これは、New-Prototype 関数のエイリアスになっています。コマンドのシグニチャはこんな感じ(※共通のスイッチは…で省略しています):
PS C:\> New-Prototype -?
New-Prototype [-Name] <String> [[-Declaration] <ScriptBlock>] [-InputObject <PSObject[]>] [-Force] …
 
[-Name] はプロトタイプの名前で、多重継承の時などに、親プロトタイプを明示的に指定する時にも使います。必須の項目はこれだけですので、空のプロトタイプを作り、後からメソッドやプロパティを足すことも可能です。
次の [-Declaration] は、スクリプトブロックを用いた構文をサポートするために使うものです。上記のサンプルですと、$キャラ や $ツンデレキャラ を定義するのに、この構文を使っていますね。逆に、$ツンキャラ、$デレキャラ の宣言はパイプラインを使って定義されていることも見て取れると思います。

このようなスクリプトブロックを用いた汎用言語に近い構文と、パイプラインを使った構文の両方をサポートするのは、若干手間ではあるのですが、個人的には外せなかった要件でした。スクリプトブロックを用いたほうですと、処理がある程度の大きさになった場合にコメントを挟むなどして説明が入れやすくなるという利点があります。パイプラインでも、途中経過を変数に入れればできないことは無いのですが、どうしても宣言と処理がごっちゃになりやすい・・・。逆にある程度の大きさになるまでは、パイプラインで繋ぐほうが簡潔で読みやすく、また今回の勉強会でやったようなデモをやる場合もお手軽にできるという利点があります。
どちらも一長一短があり、状況に応じて使い分けたかったので、ハイブリッド構文を採用するに至った次第です。DSL を自分で実装する場合、この辺りのさじ加減を自分で決められるのは良い点ですね!

はい、説明に戻ります。最後の [-Force] スイッチは、重複するメンバーが存在する場合に、無理やり上書きするかどうかを指定するフラグです。私が思うに、本来、多重継承がうまく働くのは、状態を持ち、かつ重複しない概念を混ぜ合わせて、新たな概念を作りたくなった場合のはずですので、このフラグが必要になるようなものは、本当にキレイな設計では無い可能性があります。注意して見直しが必要でしょう。
※ただ、実際はそうそううまく分かれることもなかったり・・・。目的であるテンプレートエンジンへの適用という意味では、実益はあったので良いのですが・・・。多重継承の是非みたいな話は、個人的にはこの辺りを辿ると、わからないなりに納得できたように思いましたが、まだ勉強の必要がありそうです。。。





Field(Add-Field) 関数
プロトタイプには Field という宣言文でフィールドの定義が可能です。これも実際は Add-Field 関数のエイリアスですね。コマンドのシグニチャはこんな感じ:
PS C:\> Add-Field -?
Add-Field [-Name] <String> [-Value] <Object> [-InputObject <PSObject>] [-Hidden] …
 
[-Name] はフィールド名です。ここに付けた名前に対し、同じプロトタイプのメンバー(コンストラクタ、メソッドやプロパティ)からであれば、$Me.フィールド名 でアクセスが可能になります。
[-Value] で初期値を指定します。初期値は必須で、かつ最初に与えられた値で型が決まるようになっています。こういう書き方は、はじくようになっているということですね:
PS C:\> $a = Prototype A | Field Value 10
PS C:\> $a.Value = 'aiueo'
. : Cannot convert value "aiueo" to type "System.Int32". Error: "Input string was not in a correct format."

At line:1 char:4
+ $a. <<<< Value = 'aiueo'
    + CategoryInfo          : InvalidOperation: (:) [], RuntimeException
    + FullyQualifiedErrorId : PropertyAssignmentException
 
 
自分が静的言語に関わっている時間が長いこともあり、ちょっとした型注釈は、できればシステム側で見守っていてほしい派だったりします。
このため、フィールドには、型の決定が行えるよう、$null は直接指定できないようにしています。初期値として $null を指定したい場合は、上記の例のように($キャラ を定義している 9 行目)、([型名].default) で初期値を指定するようにします。

後は、[-Hidden] スイッチを付けることで、Get-Member の結果に現れなくなります。そのプロトタイプ内部で使うだけの属性であれば、このスイッチで隠してしまったほうが扱いやすくなると思います。
・・・おっと、[-InputObject <PSObject>] を忘れていました。それもそのはずで、これは通常、意識する必要はありません。パイプラインを使った構文でプロトタイプを宣言する場合に自動的に利用されるもので、プロトタイプ以外のものを指定すると、エラーになるようになっています。





Property(Add-Property) 関数
プロパティの宣言文は Property です。例によって、Add-Property 関数のエイリアスになっています。コマンドのシグニチャを見てみましょう:
PS C:\> Add-Property -?
Add-Property [-Name] <String> [[-Getter] <ScriptBlock>] [[-Setter] <ScriptBlock>] [-InputObject <PSObject>] [-Hidden] …
 
Getter と Setter で、それぞれスクリプトブロックを指定する必要がありますので、若干大き目ですが、内容に特筆すべきところは無いですね。
[-Name] にプロパティ名、[[-Getter] <ScriptBlock>] に取得処理のスクリプトブロック、[[-Setter] <ScriptBlock>] に設定処理のスクリプトブロック、となっています。あ・・・、今見ると [-Hidden] スイッチは、Override が絡むとうまく動いていないようですね・・・直さねば (-_-;)

ところで、標準の Add-Member ScriptProperty もそうなのですが、PowerShell のプロパティは、基本的に例外を握りつぶす仕様になっているようです:
PS C:\> $a = New-Object psobject | Add-Member ScriptProperty Value { throw New-Object NotImplementedException } -PassThru
PS C:\> $a.Value

# ここでは何も出ない
PS C:\> $a.psobject.properties.match('value')[0].getterscript.invoke()
invoke : Exception calling "Invoke" with "1" argument(s): "The method or operation is not implemented."

At line:1 char:61
+ $a.psobject.properties.match('value')[0].getterscript.invoke <<<< ()
    + CategoryInfo          : NotSpecified: (:) [], MethodInvocationException
    + FullyQualifiedErrorId : DotNetMethodException
 
# Getter 部分を直接メソッドとして呼び出すと例外が発生していることがわかる
 
PSAnonym.Prototype でも、内部的には ScriptProperty を使っていますので、この動きに習う形になります。ただ、プロパティ内でエラーが発生していることに気づかないと、スクリプト実行後の $Error.Count がどえらいことになっていたりするので、時々調べてみるのをオススメしますです ヽ(;▽;)ノ





New(Set-New) 関数
コンストラクタの宣言文です。これも Set-New 関数のエイリアスとして、New が指定してあります。コマンドのシグニチャは一番単純ですね:
PS C:\> Set-New -?
Set-New [-Body] <ScriptBlock> [-InputObject <PSObject>] …
 
[-Body] でコンストラクタの処理を指定するだけです。構文自体も簡単ですが、その呼び出され方も極力簡略化されたものになっています。
サンプルで $キャラ に定義されたコンストラクタ(17 行目)ですが、その派生プロトタイプのいずれも、明示的に呼んではいないことに気づかれたでしょうか?
標準的な PowerShell の引数の引き回し方($Args 配列によるもの)に習い、PSAnonym.Prototype でも $Params という配列に一連の引数を詰めて引き回します。派生プロトタイプが途中で引数を加工しない限り、この引数はそのまま基底プロトタイプまで引き渡されますので、このような簡略化が可能になっているというわけですね。
ちなみに、自分自身を表す変数が Me だったり、コンストラクタが New だったりするのは、Visual Basic みたいだなと思われた方もいらっしゃるかもしれませんが、間違いなく VB の影響だということをここで告白しておきます。タイプ数が少なく、他と被りにくいキーワードとして優秀なのですよー! ゚ .(・∀・)゚ .゚





Method(Add-Method) 関数
基本的な構文の最後はメソッドの宣言です。Add-Method 関数のエイリアスとして Method が定義してあります。コマンドのシグニチャはこの通り:
PS C:\> Add-Method -?
Add-Method [-Name] <String> [-Body] <ScriptBlock> [-InputObject <PSObject>] [-Hidden] …
 
[-Body] <ScriptBlock> で例外が発生した場合、ちゃんと外側までスローされるのがプロパティとの大きな違いです。
それ以外は特に無いのですが、中心になる機能ということもあり、中身は色々やっています。パフォーマンス向上の工夫、多重継承の実現方法やモジュール性の確保の方法など・・・後日執筆予定の応用編では、それらに触れられればと思いますですね・・・(>_<)。





続く...?
PowerShell Advent Calendar 2013 の 25 日目を担当させていただきましたが・・・うーん、すみません、まとめきれませんでした・・・orz。
この PowerShell の、使えば使うほどできることが広がっていく感じは、実用性だけでなく、単純にプログラミング自体を楽しめるということにも繋がるのでは、なんて思っていたりするのですが、まだまだその域には達していない感じです。精進せねば!
また機会があれば、PowerShell が持つ力を広めるお手伝いができればと思います。それでは、良いお年を!!



0 件のコメント:

コメントを投稿