次の方法で共有


多言語プログラマ

同時実行とチャネル、ドメイン、およびメッセージ

Ted Neward

前回のコラムでは、プログラミング言語の Cobra を紹介しました。Cobra は、Python からゆるやかに派生したオブジェクト指向言語であり、特に、動的にも静的にも行える型指定、Python や Ruby と同様の "スクリプト" の概念、および組み込みの単体テスト機能が取り入れられています。詳しく掘り下げた結果、Cobra には価値があることがわかり、うれしいことに新しい汎用プログラミング言語を学習できました。

汎用言語は興味深く強力ですが、必要としているものが、かなづち、のこぎり、ねじ回しといった万能の道具ではなく、3/16 インチのナット ドライバーということもあります。つまり、多彩な機能を備えたツールを私たち開発者が高く評価しても、作業に適しているツールはきわめて特殊なツールの場合もあります。今回のコラムでは、完全なチューリング マシンを構成するすべての要素を再構築する使い慣れた言語ではなく、ある特定の分野に重点を置く言語に注目して、その分野 (ここでは同時実行) の処理を効果的に実行する方法を探ります。

注意深く熱心な読者であれば、このような同時実行に関する記事が決して珍しくはないことがおわかりでしょう。この数年、同時実行に関する多数の記事が掲載されてきました。この分野に関して MSDN マガジンだけが独自の取り組みを行ってきたわけではなく、ブログ社会、Twitter 界、開発者 Web ポータルなど、いたるところで同時実行についての議論が巻き起こり、「同時実行など、根拠もないのに問題提起しようとする運動の 1 つだ」と考えている反対派さえもいます。中には、(うわさでは、数年前の OOPSLA 会議におけるパネル ディスカッションで)、「同時実行は、今後 10 年の間に退治しなくてはならないドラゴンだ」とまで発言した業界専門家もいます。

本当の問題は何なのだろうと疑問に思う開発者もいらっしゃるでしょう。そもそも .NET では、何年も前から非同期デリゲート呼び出しのイディオム (BeginInvoke/EndInvoke) を通じてマルチスレッド機能が提供されており、現在ではモニターの構成要素 (C# の lock キーワードなど) やその他の明示的な同時実行オブジェクトの構成要素 (System.Threading の Mutex、Event など) を通じてスレッド セーフ メカニズムが提供されています。したがって、この新しい議論はささいなことを大げさに騒ぎ立てているように思われるかもしれません。このように感じる開発者はまったく正しいといえます。ただし、(1) その作成しているコードが完全にスレッド セーフで、しかも、(2) スレッドをスピンアップしたりスレッド プールのスレッドを使用したりして、基盤となるハードウェアに備わっているすべてのコアの使用率を最大限に高めることで、コードでタスクやデータの並列処理を使用できるときは必ず利用しているとすればの話です (どちらの条件も満たしている方は、このコラムを読み飛ばしてください。そのようなコードを作成して秘訣を教えていただければ、なおのことさいわいです)。

マイクロソフト社内では、この問題を解決、または少なくとも軽減するための多数の提案が飛び交いました。例を挙げれば、Task Parallel Library (TPL)、Parallel FX Extensions (PFX)、F# 非同期ワークフロー、Concurrency and Coordination Runtime (CCR) などがありました。どの提案の解決策も、並列処理機能が追加されるようにホスト言語 (通常は C#) を拡張するか、.NET の汎用言語から呼び出せるライブラリを提供することのいずれかでした。しかし、こうした手法のほとんどはホスト言語のセマンティクスに依存しているため、同時実行の観点から適切な処理を行うことがオプションになっており、開発者がその処理を明確に指示して適切なコードを作成する必要があるという欠点がありました。つまり、残念ながら、なんらかの誤った処理を行う危険が伴うため、同時実行に落とし穴が生まれることになります。結果としてコードがこの落とし穴に落ちると、バグが生み出され、最終的に検出されるのを待つことになるため、望ましいことではありません。理想としては、開発ツールでは誤ったことを行えないようにすべきです。たとえば、.NET プラットフォームがガベージ コレクション手法にシフトしたのはこのためです。開発ツールには、Rico Mariani (Microsoft Visual Studio のアーキテクト、以前は CLR パフォーマンスのアーキテクト) が述べているように、開発者が "自然と成功する処理を選択する" ようにする機能が必要です。つまり、開発者が最も簡単だと感じる処理が正しい選択で、難しいまたは不可能だと感じる処理が不適切な選択になるようにします。

そのため、マイクロソフトでは実験的プロジェクトとして、同時実行ドメインに的を絞った、Axum という新しい言語をリリースしました。開発者が "自然と成功する処理を選択" できるようにするという方針から、Axum は、C# や Visual Basic のような汎用言語ではなく、同時実行の問題に的を絞った言語です。開発当初から、ビジネス上の課題を全体で解決する言語スイートの一部になるように設計されています。要するに、Axum はドメイン固有言語 (問題のある特定の領域だけを解決することに特化した言語) の優れた例です。

このコラムの執筆時点では、Axum はバージョン ラベルが 0.2 であり、Axum の言語と操作に関する詳細情報は全面的に異なる内容に変更される場合があります。しかし、このコラムの執筆時点と公開時点の間であれば、中核となる概念は非常に安定しているでしょう。事実、このような概念は Axum に限ったものではなく、(F#、CCR など、前述のプロジェクトの一部を含め) 他の多数の言語やライブラリにも見られるので、Axum 自体が普及しなくても、同時実行について考えるうえで欠かせません。さらに、"問題のドメイン固有の言語" という Axum の基本的な考え方はその重要性が急激に増してきているため、今後のコラムでも単独で取り上げる価値があります。

はじめに

現在ダウンロード可能な Axum のツールと資料は、開発者ラボ Web サイト (msdn.microsoft.com/en-us/devlabs/dd795202.aspx、英語) で公開されています。少なくとも、.msi (Visual Studio 2008 用または Visual Studio 2010 Beta 1 用) と『Axum のプログラマ向けガイド』(英語) の両方をダウンロードしてください。これらのツールをインストールして資料を入手したら、Visual Studio を起動し、Axum のコンソール アプリケーション プロジェクトを新規作成します (図 1 参照)。


図 1 Axum のコンソール アプリケーション プロジェクト

このプロジェクトのコードを図 2 に示すコードに置き換えます。これは、Axum バージョンの Hello World アプリケーションです。他のすべての Hello World アプリケーションと同じように、インストールが正常に行われたことを確認して、基本的な構文を把握するのに役立ちます。すべてがコンパイルされて実行されれば、準備完了です。

図 2 Axum の Hello World

using System;
using Microsoft.Axum;
using System.Concurrency.Messaging;
namespace Hello
{
public domain Program
{
agent MainAgent : channel Microsoft.Axum.Application
{
public MainAgent()
{
String [] args = receive(PrimaryChannel::CommandLine);
// TODO: Add work of the agent here.
Console.WriteLine("Hello, Axum!");
PrimaryChannel::ExitCode <-- 0;
}
}
}
}

ご覧のとおり、Axum の構文は C# に非常によく似ているので、大まかに考えれば Axum は C 言語ファミリと呼ばれる言語カテゴリに属しています。このカテゴリに含まれる言語の特徴は、中かっこを使用してスコープを示すこと、セミコロンを使用してステートメントを終了することなどです。しかしそうは言っても、知っておく必要があるいくつかの新しいキーワード (手始めとしては domain、agent、channel、receive など) や、いくつかの新しい演算子も、もちろんあります。厳密に考えれば、Axum は本当は C# の派生言語ではなく、新機能が追加された選択可能なサブセットです。Axum には、すべての種類の C# 3.0 ステートメント (ラムダ、推定されるローカル変数など) とその式 (代数計算、yield return と yield break など)、メソッド、フィールド宣言、デリゲート、および列挙型が含まれていますが、クラス、インターフェイス、構造体や値型の定義、演算子の定義、プロパティ、定数フィールド、ローカル変数、静的フィールド、静的メソッドはいずれも含まれていません。

ヒューッという音が聞こえたとしたら、それは風が皆さんの耳を吹き抜ける音です。また、胃がちょっと引きつるのを感じても、大丈夫です。思い切って行動するのは、良いことです。

概念と構文

Axum の構文は、C# の構文と同様に、Axum の中核となる概念をそのまま反映しています。ただし、C# のクラスに該当するものとして、Axum にはエージェントがあります。Axum では、エージェントはコードのエンティティですが、従来のオブジェクトとは異なり、エージェントどうしが直接対話することはありません。代わりに、非同期手法で (正確に言えば、チャネルのポート経由で) 相互にメッセージを交換します。ポートでのメッセージの送信は、非同期操作です (送信操作からはすぐに戻ります)。ポートでのメッセージの受信とは、メッセージが到着するまでブロックすることです。これにより、同時に同じデータ要素上でスレッドどうしが相互作用することがないため、事実上、デッドロックや不整合の主要因が一掃されます。

この機能の動作を実際に確認するために、Hello World のコードを考えてみましょう。MainAgent エージェントでは、Application という特別なチャネルを実装します。Application チャネルは Axum ランタイムで直接認識されるチャネルです。MainAgent エージェントでは、入力されるすべてのコマンド ライン引数を処理し、CommandLine ポートにある Application の PrimaryChannel チャネル経由で引数を渡します。MainAgent エージェントは、ランタイムで作成されるときに、receive 操作 (Axum のプリミティブ) を使用して、コマンド ライン引数を使用できるようになるまでそのポートをブロックします。引数を使用できるようになると、MainAgent コンストラクターで処理を続行します。ランタイムでは、処理が完了すると PrimaryChannel の ExitCode ポートでブロックし、アプリケーションの完了通知を待機します。一方、MainAgent では配列を処理して各要素を出力し、この処理が終わると整数リテラル 0 を ExitCode ポートに送信します。ランタイムがこの値を受信すると、処理が終了します。

Axum ではこの同時実行の概念が最初から重視されているのがわかります。開発者が、スレッド、同時実行オブジェクト、ロック オブジェクトなどを作成したり操作したりしなくてよいだけでなく、Axum 言語では Hello World のような単純なアプリケーションの場合でも同時実行手法を前提としています。Hello World のような単純なアプリケーションの場合、これは間違いなく時間の無駄でしょう。しかし、ほとんどの開発者は Hello World ほど単純なアプリケーションばかりを作成しているわけではなく、実際にこのようなアプリケーションをよく作成している開発者であれば、既定では単一スレッドで実行される、従来のオブジェクト指向のアプリケーション言語 (C#、Visual Basic、または開発者の気に入った任意の言語) にいつでも戻ることができます。

図 3 Process アプリケーション

using System;
using Microsoft.Axum;
using System.Concurrency.Messaging;
namespace Process
{
public domain Program
{
agent ProcessAgent : channel Microsoft.Axum.Application
{
public ProcessAgent()
{
String [] args = receive(PrimaryChannel::CommandLine);
for (var i = 0; i < args.Length; i++)
ProcessArgument(args[i]);
PrimaryChannel::ExitCode <-- 0;
}
function void ProcessArgument(String s)
{
Console.WriteLine("Got argument {0}", s);
}
}
}
}

Hello World は初心者用

もう少し興味深い例が、Process アプリケーションです (図 3 参照)。このバージョンでは、Axum の新しい概念の 1 つ、"関数" を使用しています。関数は従来のメソッドとは異なり、Axum のコンパイラが (コンパイル エラーを使用して)、エージェントの内部状態が決して変更されないことを保証します。つまり、関数にはおそらく副作用がありません。副作用を避けていることで、同時実行対応言語としての Axum の有用性が高まっています。副作用がなければ、スレッドから共有状態を変更する (そのために不整合や正しくない結果が発生する) コードを誤って作成することがきわめて難しくなります。

しかし、これで全部ではありません。Axum では、スレッド セーフなコードの作成を容易にすることだけでなく、スレッド対応コードの作成を容易にすることも目標としています。2 コアや 4 コアのコンピューターがいよいよ主流になり、8 コアや 16 コアのコンピューターの時代が到来しつつある現在、並列処理を実行できるかどうか確認することは重要です。繰り返しになりますが、スレッドから対話を分離して上位レベルの構成要素を操作できるようにすることで、Axum ではこの作業が簡略化されています。図 4 は、変更したバージョンの ProcessAgent を示しています。

ここでは、2 つの新しい機能を使用しています。まず、対話ポイントを作成しています。これは、メッセージの送信側または受信側としての役割を果たします (この場合は送受信両方)。次に、転送演算子を使用して、対話ポイントから ProcessArgument 関数へのメッセージのパイプラインを作成しています。このようにすると、"arguments" 対話ポイントに送信されたすべてのメッセージが (処理などのために) ProcessArgument に転送されます。その後は、コマンド ライン引数を反復処理して、各引数をメッセージとして arguments 対話ポイントに (ループ内で送信演算子 <-- 演算を使用して) 送信するだけで、処理が始まります。

注: Axum では、このような処理はデータ フロー ネットワークとして厳密に定義され、単純な一対一のパイプラインを作成する以外の処理もできますが、この概要コラムでは説明しません。興味をお持ちの方は、『Axum のプログラマ向けガイド』(英語) を参照してください。

図 4 変更したバージョンの ProcessAgent

using System;
using Microsoft.Axum;
using System.Concurrency.Messaging;
namespace Process
{
public domain Program
{
agent ProcessAgent : channel Microsoft.Axum.Application
{
public ProcessAgent()
{
String [] args = receive(PrimaryChannel::CommandLine);
// Second variation
//
var arguments = new OrderedInteractionPoint<String>();
arguments ==> ProcessArgument;
for (var i = 0; i < args.Length; i++)
arguments <-- args[i];
PrimaryChannel::ExitCode <-- 0;
}
function void ProcessArgument(String s)
{
Console.WriteLine(“Got argument {0}”, s);
}
}
}
}

データをパイプライン経由で渡したので、処理を実行するためにスレッドをスピンアップする方法とタイミングを Axum が決定できます。この単純な例では、この作業は便利でも効果的でもありませんが、より高度なシナリオでは、便利で効果的になることが多くなります。arguments 対話ポイントは順序が指定された対話ポイントなので、パイプラインに送信されるメッセージ (さらに重要な返される結果) は、各メッセージが異なるスレッドで処理されても順序が維持されます。

メッセージを対話ポイント間でパイプライン処理するという考え方は、F# などの関数型言語から借用している強力な概念です。図 5 は、パイプラインに別の処理を簡単に追加できることを示しています。

ここでも、UpperCaseIt 関数では内部の ProcessAgent の状態が変更されず、渡されたオブジェクトのみで操作が行われているのがわかります。また、ProcessArgument 関数の前に UpperCaseIt 関数が含まれるようにパイプラインを変更しているので、直感的に処理されます。つまり、UpperCaseIt 関数の結果が ProcessArgument 関数に渡されます。

頭の体操として、この処理を実行するには C# のコードが何行必要か考えてみましょう。しかも、この処理全体を単一の実行スレッドに限定せず、複数のスレッドに分散して実行する必要があることを念頭に置いて考えてください。次に、正常に実行されるかどうかコードをデバッグして確認するところを想像しましょう (実際には必ずしも想像する必要はなく、ILDasm、Reflector などのツールを使用して、生成された IL を確認できます。これは非常に重要です)。

ところで、ここでちょっと告白しておくことがあります。ここに紹介したサンプルは、正常に実行されません。上記のコードを実行すると、何も表示されずにアプリケーションから結果が返されます。これは、Axum のバグではありません。これは意図的な動作であり、同時実行の考え方によるプログラミングにどれほど発想の転換が必要かを示すものです。

arguments 対話ポイントで送信演算子 (<--) を使用してコマンド ライン引数を受信しているときは、これらの送信が非同期に実行されます。ProcessAgent ではこれらのメッセージを送信する際にブロックしていないため、パイプラインがある程度複雑になると、引数がすべて送信されてループが終了し、ExitCode が送信されてアプリケーションが終了し、コンソールに何も表示されないうちにすべてが終了します。

これを解決するには、パイプラインの操作が完了するまで ProcessAgent でブロックする必要があります。ここでは、Console.ReadLine() などを使用して、スレッドが削除されないように保持する必要があります (これは、実際には注意が必要です。詳細については、Axum チームのブログを参照してください)。別の方法として、ProcessAgent の動作方法を変えます。今回は、Axum の機能をさらにいくつか紹介するために、この 2 つ目の方法を紹介します。

ProcessAgent では、その内部で処理を実行せずに、新しいエージェントに処理を任せます。ただし、ここではもう少し興味深いコードにするために、引数を大文字と小文字のいずれにするかをこの新しいエージェントで判断するようにします。こうした判断を行うには、新しいエージェントで、もっと複雑なメッセージ (操作対象の文字列だけでなく、大文字の場合は true となるブール値も受け取るメッセージ) を定義する必要があります。そのためには、Axum でスキーマを定義する必要があります。

図 5 メッセージのパイプライン処理

using System;
using Microsoft.Axum;
using System.Concurrency.Messaging;
namespace Process
{
public domain Program
{
agent ProcessAgent : channel Microsoft.Axum.Application
{
public ProcessAgent()
{
String [] args = receive(PrimaryChannel::CommandLine);
// Third variation
//
var arguments = new OrderedInteractionPoint<String>();
arguments ==> UpperCaseIt ==> ProcessArgument;
for (var i = 0; i < args.Length; i++)
arguments <-- args[i];
PrimaryChannel::ExitCode <-- 0;
}
function String UpperCaseIt(String it)
{
return it.ToUpper();
}
function void ProcessArgument(String s)
{
Console.WriteLine("Got argument {0}", s);
}
}
}
}

引数を処理する

ここまで明確には述べてきませんでしたが、Axum の目的の 1 つは、さまざまな実行エンティティ (エージェント) どうしを分離することでした。この目標は、オブジェクトを直接渡す代わりにメッセージのコピーを作成してエージェントに渡すことで、達成されてきました。パラメーター経由の場合を含めても共有状態がないので、誤ってスレッドの競合が発生することはありません。

この手法は、.NET 基本クラス ライブラリで定義済みの型ではうまく機能しますが、ユーザー定義型の場合 .NET プログラマがで適切な処理を行っていなければすぐに役に立たなくなります。この問題を克服するために、Axum では新しい種類のメッセージをスキーマ型として定義する必要があります。スキーマ型は、メソッドがなく、必須またはオプションのフィールドがあるオブジェクトで、次のように schema キーワードを使用します。

schema Argument
{
required String arg;
optional bool upper;
}

上記のコードでは、Argument という新しいデータ型を定義しています。Argument は、大文字または小文字にする文字列を格納する必須フィールド (arg)、および文字列を大文字と小文字のいずれにするかを示すオプションのフィールド (upper) で構成されています。今度は、次のように要求/応答用ポートが設定された新しいチャネルを定義します。このポートでは、Argument 型のメッセージを受け取って String 型のメッセージを返します。

channel Operator
{
input Argument Arg : String; // Argument in, String out
}

このチャネルを定義すると、チャネルを実装するエージェントの作成がかなり簡単になります (図 6 参照)。このエージェントは、技術的にはコンストラクターが終了しないことがわかります。単にループを実行し、Arg ポートで receive を呼び出して、Argument のインスタンスがこのポートに送信されるまでブロックしたら、ToUpper または ToLower の結果を同じポートに送信しているだけです (処理はもちろん upper の値によって決まります)。

図 6 チャネルを実装するエージェント

agent OperatingAgent : channel Operator
{
public OperatingAgent()
{
while (true)
{
var result = receive(PrimaryChannel::Arg);
if (result.RequestValue.upper)
result <-- result.RequestValue.arg.ToUpper();
else
result <-- result.RequestValue.arg.ToLower();
}
}
}

呼び出し元 (ユーザー) の観点から考えると、OperatingAgent を使用することも ProcessAgent 自体を使用することもまったく変わりありません。OperatingAgent を使用するには、組み込みの CreateInNewDomain メソッドを使用して OperatingAgent のインスタンスを作成し、Argument のインスタンスを Arg ポートに送信します。ただし、この処理ではオブジェクトが返され、最終的にこのオブジェクトでエージェントから応答を生成します。これは、結果を (もう 1 つの receive() 操作を使用して) 取得する唯一の方法です (図 7 参照)。

実行すると、Argument を送信した時点のミリ秒値に基づいて、コマンド ラインの文字列が大文字と小文字のいずれかになります。この場合でも、開発者はまったくスレッドを直接操作せずにすべての処理を実行できます。

小さな言語でも大きい変化をもたらす

Axum では、プログラミングについて、通常とは異なる考え方が明確に示されており、C# とまったく異なっていても、ほとんどの汎用プログラミング言語の機能を多数実装できます。ただし、その主な目的は同時実行とスレッド対応のコードに集中しているため、同時実行を必要とする (同時実行が役立つ) 問題の解決に適しています。

さいわい、Axum の開発チームは、C# 言語を改良しようとはしていません。他の .NET 言語と同様に、Axum は .NET アセンブリにコンパイルされるため、アプリケーションの同時実行にとって重要な部分を (コンソール アプリケーションではなくクラス ライブラリ プロジェクトとして) Axum で作成して、C# や Visual Basic から呼び出すのはかなり簡単です。配布されている Axum には、このような処理を実行するサンプル (WinForms アプリケーションにおける同時実行の問題を説明した DiningPhilosophers サンプル) が付属しているので、少し時間を割いて、Axum でコンパイルされたライブラリに対して Reflector を使ってみれば、このような相互運用性が占める領域に関するさまざまな情報を知ることができます。Axum を使用して、別の言語 (C# または Visual Basic) で使用できるライブラリを構築すると、同時実行にとって重要な処理を、他の .NET テクノロジ (Web アプリケーションやデスクトップ アプリケーション) で幅広く使用できるようにするうえで、大いに役立ちます。

図 7 CreateInNewDomain メソッドの使用

 

agent ProcessAgent : channel Microsoft.Axum.Application
{
public ProcessAgent()
{
String [] args = receive(PrimaryChannel::CommandLine);
var opAgent = OperatingAgent.CreateInNewDomain();
var correlators = new IInteractionSource<String>[args.Length];
for (int i=0; i<args.Length; i++)
correlators[i] = opAgent::Arg <-- new Argument {
arg = args[i],
upper = ((System.DateTime.Now.Millisecond % 2) == 0) };
for (int i=0; i<correlators.Length; i++)
Console.WriteLine("Got {0} back for {1}",
receive(correlators[i]), args[i]);
PrimaryChannel::ExitCode <-- 0;
}
}

さらに、Axum では、多くの興味深い機能を提供する実験的な C# コンパイラを使用しています。これらの機能は C# 3.0 (3.0 に非同期メソッドを加えたもの) のスーパーセットです。ちなみにいずれの機能も C# 4.0 対応にはなっていません。しかし、C# と Axum のコードを同じプロジェクト内で組み合わせる機能は実行でき、これは現時点では Axum に固有の機能です。詳細については、『Axum のプログラマ向けガイド』(英語) を参照してください。Axum には、大規模なサービスの作成を容易にするための WCF との密接な関係など、ここで紹介していなくても『Axum のプログラマ向けガイド』(英語) に詳細が説明されている他の機能もありますが、Axum でアプリケーション、ライブラリ、またはサービスの構築を始めるには、このコラムの概要で十分です。また、Axum は研究プロジェクトであり、ユーザーが開発者ラボ Web サイトの Axum フォーラムを通じてフィードバックを提供できることを覚えておいてください。

Axum が正式な製品にならない場合や、読者の皆さんが正式版リリース前に使用しようと考えた場合はどうすればよいでしょう。まず、Axum が成功するかどうかについては、かなりの要素がユーザーの反応にかかっているため、品質に関するフィードバックによって Axum の運命が決まります。どうぞ Axum チームまでご意見をお寄せいただき、公式リリースへのご要望をお知らせください。それでも Axum が正式な製品にならなかった場合は、Axum を支える概念が Axum だけのものではないことを忘れないでください。学術的な言い回しでは、Axum は同時実行のアクター モデルを体現しており、運用向けの言語が今すぐ必要な .NET 開発者は、他にも数種類のアクター モデルを使用できます。オープンソース プロジェクトの Retlang は、F# 言語 (F# の MailboxProcessor 型を参照してください)、および Microsoft Concurrency and Coordination Runtime (CCR) と同様に、このような概念のいくつかを体現しています。どちらのプロジェクトも、既にリリースされているか、リリース直前のどちらかの状態です。

最後に、忘れないでいただきたいのですが、このコラムの目的は存在するあらゆる CLR ベースの言語を使用してプロジェクトを作成することではなく、特定の問題を解決でき、必要な場合に使用できる言語を見つけることです。

また、「人には、話せる言語の数と同じ人数分の価値がある」ということわざもお忘れなく。

Ted Neward は、Neward & Associates の社長で、信頼性の高いエンタープライズ システムの構築について講演し、執筆し、指導しています。多数の著書があり、世界中のカンファレンスで指導や講演を行った経験があり、現在は INETA の講演者です。また、3 つの専門領域で MVP を受賞しています。連絡先は、ted@tedneward.com (英語のみ) です。また、blogs.tedneward.com (英語) にブログを公開しています。