[TechDays 2010] Dynamic のデモについて

TechDays 2010が終わりましたので、参加者の方へのフォローアップ(デモを失敗した自分へのフォローアップも含みます)を兼ねて、何回か不足していると思われる内容を記述していきます。今回は、Dynamic(動的)に関して説明します。

最初にPythonのコードを使って、以下のような図形を描画しました。
1.Dynamic Python1 
このデモ(BadPaint.exe)は、C#でWPFを使って作成しています。上記のようなシェイプを描画したPythonコードを以下に示します。

 from System.Windows.Controls import Canvas
from System.Windows.Shapes import *
from System.Windows.Media import *
import math

dim = min(Application.Painting.ActualWidth - 20, Application.Painting.ActualHeight - 20) / 2
for i, color in zip(xrange(0, 360, 10), dir(Brushes)):
  rect = Rectangle(Width=20, Height=20, Fill=getattr(Brushes, color))
  Application.Painting.Children.Add(rect)
  Canvas.SetTop(rect, dim * math.sin(i * math.pi * 2 / 360) + dim)
  Canvas.SetLeft(rect, dim * math.cos(i * math.pi * 2 / 360) + dim)

このコードを 実行ボタンで、どのように処理しているかといえば以下のようなコードで行っています。

 var language = 
   _ScriptRuntime.Setup.LanguageSetups[cmbLanguages.SelectedIndex];
ScriptEngine scriptEngine = _ScriptRuntime.GetEngine(language.Names[0]);
dynamic ret = scriptEngine.Execute(txtCode.Text, _ScriptScope);

ScriptRuntimeを使ってIronPythonのScriptEngineインスタンスを取得して、Executeメソッドでスクリプトを実行しています。この時にScriptScopeを指定しているのは理由があります。その理由は、Pythonコードの中で使用している「Application」 というオブジェクトを参照するためです。このApplicationを参照するために記述しているC#のコードを以下に示します。

 // スクリプト スコープと変数の設定
_ScriptScope = _ScriptRuntime.CreateScope();
_ScriptScope.SetVariable("Application", this);

このコードの中でSetVariableメソッドが、BadPaint自身のインスタンスに対して「Application」という変数名を設定しています。このようにDLRのScriptScopeを使用します。ScriptScopeを説明するとすれば、変数のスコープを管理する単位になります。Python言語として表現すれば、暗黙のディクショナリと同じ役目になります(Pythonは、__dict__でスコープを管理しています)。

 

次にRubyコードを以下に示します。 

 Canvas = System::Windows::Controls::Canvas
def callback
  self.application.painting.children.each do |child|
    top, left = Canvas.get_top(child), Canvas.get_left(child)
    run = (left - dim) / dim
    rise = (top - dim) / dim
    angle = Math.atan2 rise, run
    angle += Math::PI / 100
    Canvas.set_top child, dim * Math.sin(angle) + dim
    Canvas.set_left child, dim * Math.cos(angle) + dim
  end
end

このRubyスクリプトは何を定義しているかといえば、「callback」メソッドを定義しているだけです。このスクリプトの中で注目して欲しいのが、「dim」という変数と「self.application」という記述になります。この記述は、以下のようにまとめることができます。

  • dim変数:Pythonスクリプトで定義された変数であり、同一のScriptScopeであれば変数を共有することが可能なことを示しています。
  • self.application:ScriptScope.SetVariableメソッドで設定したBadPaintへの参照を保持する変数。

このことから、スクリプト同士の変数の共有とスクリプトをホストする側(C#)で変数を自由に受け渡しできることを確認することができます。
BadPaintの言語コンボボックスでIronRubyを選択してから実行ボタンを押します。そうするとシェイプが動き出します。一種のアニメーションのように回る動作になっています(TechDaysの会場では、動かなかったのですが)。この動きが、rubyのcallbackメソッドで実現しています。この動きを実現するためにBadPaintでは、スクリプトの実行に続いて以下のようなコードを記述しています。

 Action callbackAction;
if (_ScriptScope.TryGetVariable(
         "callback", out callbackAction))
{
    _Callback = callbackAction;
}

このコードは、rubyのcallbackメソッドをScriptScope.TryGetVariableメソッドで取得しています。後は取得したcallbackメソッド(Actionデリゲート)をタイマーイベントで呼び出すことで、アニメーションのような動きを実現しています。ここまでの動きから理解できるのは、以下のようなことです。

  • IronPythonとIronRubyというスクリプトの相互連携。
  • C#とスクリプトの相互連携。

callbackメソッドは、引数も持ちませんし、戻り値も持っていません。これだけだと、本当に相互連携ができるの?と疑問に感じることもあるでしょう。このように考えて、以下のようなサンプルも用意しています。
1.Dynamic Python2 
シェイプをランダムに描画しています。この描画に使用しているPythonスクリプトを以下に示します。

 from System.Windows.Controls import Canvas
from System.Windows.Shapes import *
from System.Windows.Media import *
from System import Random
rand = Random()

for i in xrange(100):
  rect = Rectangle(Width=20, Height=20, Fill=Brushes.Blue)
  Application.Painting.Children.Add(rect)
  Canvas.SetLeft(rect, rand.Next(Application.Painting.ActualWidth))
  Canvas.SetTop(rect, rand.Next(Application.Painting.ActualHeight))

このコードに続いてBadPaint内に以下のようなインターフェースを用意してあります。

 public interface IObjectUpdater
{
    void Update(object target);
}

このインターフェースを利用するPythonスクリプトを以下に示します。

 from System.Windows.Controls import Canvas
from System.Windows.Shapes import *
from System.Windows.Media import *
from BadPaint import IObjectUpdater

def callback(): pass

class Tracker(object, IObjectUpdater):
  def __init__(self, xvelocity, yvelocity):
    self.xvelocity = xvelocity
    self.yvelocity = yvelocity
  def Update(self, target):
    if((Canvas.GetLeft(target) + self.xvelocity)
        >= Application.Painting.ActualWidth)
        or ((Canvas.GetLeft(target) + self.xvelocity) <= 0):
      self.xvelocity = -self.xvelocity
    if((Canvas.GetTop(target) + self.yvelocity)
         >= Application.Painting.ActualHeight)
         or ((Canvas.GetTop(target) + self.yvelocity) <= 0):
      self.yvelocity = -self.yvelocity
    Canvas.SetTop(target, Canvas.GetTop(target) + self.yvelocity)
    Canvas.SetLeft(target, Canvas.GetLeft(target) + self.xvelocity)

rand = Random()

def tracker(target):
  return Tracker(rand.Next(10) - 5, rand.Next(10) - 5)

このコードで注目して欲しいのは、以下の個所になります。

  • from BadPaint import IObjectUpdater
  • calss Tracker(object, IObjectUpdater) と Updateメソッド定義
  • def tracker(target)

最初の「from BadPaint ...」で IObjectUpdaterインターフェースを取り込んでいます。次にTrackerクラス定義では、IObjectUpdaterインターフェースを継承しています。 インターフェースで定義しているUpdateメソッドは引数を取りますので、Pythonでも引数を定義しています。最後のtracker関数は、1つの引数をとりTrackerクラスのインスタンスを戻すように定義しています。つまり、このコードによって実現しようとしていることは、以下のような事です。

  • tracker関数を取り出す。
  • tracker関数を呼び出して、Trackerオブジェクトのインスタンスを戻り値として受け取る。
  • Tracker.Updateメソッドに引数を指定して呼び出す。

このスクリプトを実行ボタンを押せば、実行されてランダムに表示されたシェイプがアニメーションのように動き出します。この動きを実現するために記述しているC#のコードを以下に示します。

 Func<object, IObjectUpdater> trackerForInterface = null;
if (_ScriptScope.TryGetVariable(
       "tracker", out trackerForInterface))
{
   _TrackMakerForInterface = trackerForInterface;
}

traker関数を取得できれば、IObjectUpdaterインターフェースを実装していますから、Updateメソッドを呼び出すのが容易であることがわかることでしょう。上記の例では、インターフェースを継承するというパターンを説明しましたが、もちろんdynamicのキーワードを使ってインターフェースを使用しないようにすることもできます。どちらが良いかは、作成する人が考える制約として選択すれば良いことでしょう。

PS.サンプルコードそのものは、TechDaysのオフィシャルサイト経由で公開させていただきます。参加された方は、公開されているPPTと上記で説明した内容を応用すればきっと同じことを実現できると思います。