次の方法で共有


多言語プログラマ

さまざまな言語を組み合わせて使用する

Ted Neward

目次

多言語プログラミング
実践的な多言語使用
選択に伴うコスト

Web とクライアント/サーバー プログラミングが登場するずっと以前は、アプリケーション全体を 1 つのプラットフォームまたはシステム上で 1 つの言語を使用して記述することが一般的でした。たとえば、何年にもわたってアプリケーション プログラミングの中心的存在として広く普及していた FoxPro プラットフォームを思い出してください。FoxPro プラットフォームには、ユーザー インターフェイス フォーマット言語とライブラリ、データ アクセスとストレージ形式、さらに、数学や会計など以前から使用されているライブラリをサポートするルーチンのコレクションが用意されていました。言語とプラットフォームの両方が 1 つの環境に統合されていたため、FoxPro は、プログラマが 1 つの言語/プラットフォームを習得し、成功し続け、さらに重要なことには雇用される手段となっていた、典型的な例と言えます。

ところが、その後、分化の時代を迎えます。それぞれが特定の固有の用途を持つ、新しい言語とツールが登場しました。リレーショナル データベースが普及し、データベースへのアクセスとその変更を行うための言語として SQL が使用されるようになりました。GUI クライアントの開発は、当初は手続き型言語である C や Pascal に移行し、その後、オブジェクト指向言語である C++ や Delphi が登場しました。また、Perl や Python などの言語が、コンソール ベースのオペレーティング システム (最も一般的なものは UNIX ベースのシステム) を管理する強力なツールとして登場し、Bash シェルや Korn シェルなどのコマンド シェルの機能の拡張に使用されました。そして、Web の時代を迎えます。これによって、HTML、CSS、JavaScript がユーザー インターフェイスを表示する言語として選択されるようになりました。

プログラム言語の世界が穏やかで変化のない時代を迎えたことはありませんが、特にここ数年は、プログラミングの現場に膨大な数の言語が登場しました。その中には、Ruby、Windows PowerShell、Python などの手続き型言語や、F#、Scala などの関数型言語も含まれます。既存の言語には、マイクロソフトが C# に LINQ として追加したデータ クエリ機能のような、新しい機能が追加され続けています。加えて、プログラミング言語開発は新しい段階に入りました。ドメイン固有言語 (DSL) と呼ばれる新しいカスタム言語が、特定のタスクまたはアプリケーション用に開発されているところです。DSL の例については、MSDN マガジンの 2008 年 Visual Studio 発売記念号 (LAUNCH 号) の Service Station のコラム (msdn.microsoft.com/magazine/cc164250) を参照してください。

プログラマがどの方向へ転身したとしても、新しい言語は必ず出現します。もちろん複数の言語を使用するプログラマは以前から存在していましたが、"多言語プログラミング (polyglot programming)" という用語は Neal Ford が 2006 年 12 月にブログに掲載した「Polyglot Programming (多言語プログラミング)」(memeagora.blogspot.com/2006/12/polyglot-programming.html) というタイトル (予想どおりのタイトルですよね) の投稿に由来するものです。

多言語プログラミング

さて、世の中に多数の言語があることはわかっています。また、プログラマが対処する必要のある問題に最適な言語が、おそらく 1 つあることもわかっています。したがって、経験豊富な .NET 開発者であれば、これらの言語を最適に組み合わせる方法を理解する必要があります。このコラムでは、この点について紹介していきます。

現在の最も一般的な問題について考えてみましょう。開発者は、以前とは比べられないほど多数のユーザーに対応できるようにプログラムを拡張することを求められています。これは特に、Web サイトや Web サービスでは顕著です。現在、顧客は、自分のアカウント情報に直接アクセスできるようになっています。つまり、数百人のユーザーに対応できるように拡張する必要があったアプリケーションであれば (たとえば、急激な成長に合わせてコール センターの従業員数が増えた場合など)、現在は、何百万人とは言わないまでも、潜在的な何千人ものユーザーに対応できるように拡張する必要があるということを意味します。

Web サイトを担当している開発者は、スレッドセーフと優れたパフォーマンスの両方を実現しなければなりません。システム全体にわたってアクセスをシリアル化するために、単にすべてのメソッドをロックするという方法は、拡張に対応できないため優れた解決策ではありません。同時実行の適切な管理は、特にアプリケーションが拡張されるほど難しくなります。上級開発者であっても、夜遅くまでかかって取り組む問題です。静的コード分析や単体テストの順列を実行してマルチスレッドのバグを検出する PEX (research.microsoft.com/projects/pex) や CHESS (research.microsoft.com/projects/chess) のような新しいツールがリリースされたとしても、プログラマ自身が同時実行を制御し、ごく低レベルの C# の "lock" ステートメントや System.Threading のさまざまな同時実行制御クラスを使用する必要があることに変わりはありません。

皆さんはこの問題に対応できますか。同時実行に関するスキルは十分でしょうか。System.Threading.Monitor と System.Threading.Mutex と System.Threading.Semaphore の相違点をすぐに答えられますか。

まず、特定の言語を選択することで得られる価値を確認する必要があります。おそらく、このような種類の複雑なコードの記述と管理は、F# のような関数型言語を選択することでより簡単になります。これは、F# が持つ不変性という一般的な傾向と副作用の無さにより、明示的な同時実行制御、つまり、Ruby で記述されたドメイン固有言語が必要ないからです。Ruby は、使用している開発者から同時実行の詳細を見えなくするために設計された言語です。

この概念をより具体的に理解するため、ファイル ベースの I/O の実行 (または、データベース呼び出しや Web サービス呼び出し) のように、従来の同期操作を必要とする Web アプリケーションについて考えてみましょう。通常、C# コードからこれを実行する最も簡単な方法は、次のように、従来から使用されているステートメントを使用してファイルを開き、内容を読み取り、それをバイト配列に格納して、ファイルを閉じる方法です。

byte[] pixels = null
BinaryReader br = 
         new BinaryReader(new FileStream(filename, FileMode.Open)); pixels = br.ReadBytes(4096);

これは非常にシンプルですが、非常にシリアル化されてもいます。ファイル I/O 操作が実行されている間に、このスレッドでこれ以外の処理を実行することはできません (1 つの簡単なファイル I/O 操作の場合、サイトの規模を拡大するなどして、これらの操作の数が非常に多くならない限り、これが大きな問題となる可能性はありません)。このため、図 1 に示すように、CLR スレッド プールを介して利用できる非同期操作を使用してファイルを読み取る方法がより適しています。

図 1 ファイルの非同期読み取り

delegate byte[] AsyncOpenMethod(string filename);
static byte[] AsyncOpen(string filename)
{
    byte[] pixels = null;
    using (BinaryReader br =
        new BinaryReader(new FileStream(filename, FileMode.Open)))
    {
        Pixels = br.ReadBytes(4096);
    }
}
static void AsyncOpenTheFile(string filename)
{
    byte[] pixels = null;
    AsyncOpenMethod aom = new AsyncOpenMethod(Class1.AsyncOpen);
    IAsyncResult iar = aom.BeginInvoke(filename, null, null);
    while (iar.IsCompleted == false)
    {
        // Do something?
    }
    pixels = aom.EndInvoke(iar);
}

ただし、非常に簡単だったコードが、開発者の母親だけが愛するような難しいコードになり、ファイルを読み取るだけのコードとは思えません。では、同じルーチンを F# で記述してみましょう。

async {
use inStream = File.OpenRead(filename)
let! pixels = inStream.AsyncRead(4096)
}

F# コードでどのようにこの操作が実行されているかについては詳細に説明しませんが、let! 式は非同期式として式を生成することを F# コンパイラに指示する式です。また、AsyncRead メソッドは拡張メソッドと呼ばれる F# の言語機能を使用して F# によって標準の System.IO.Stream 派生クラスに追加されるメソッドです。このコードでは、ファイルが非同期で効率的に読み取られ、式の左側でその結果がバイト配列にダンプされます。これ以外のコードを開発者が追加する必要はありません。

これを実際のコンテキストで考えてみましょう。Web サービスでは夜間のメンテナンス処理の一環として、バックアップ用のオフラインでの保存を簡単に実行できるように、複数のファイルのコピーを作成する必要があります。このファイルのコピーを完全に非同期で実行する F# コードは、図 2 のようになります。

図 2 バックアップ用コピーの作成

#light

open System.IO

let CopyFileAsync filename =
    async {
        use inStream = File.OpenRead(filename)
        let! pixels = inStream.AsyncRead(4096)
        use outStream = File.OpenWrite(filename + ".back")
        do! outStream.AsyncWrite(pixels)
    }

let tasks = [ for i in 1 .. 10 -> CopyFileAsync("data" + i.ToString()) ]
let taskResults = Async.Run (Async.Parallel tasks)

このコードでは、読み取りと書き込みの両方が非同期で実行され、10 のタスクのコレクション全体 (データ ファイルごとに 1 つ) も非同期で処理されます。これを C# で実現する方法を考えた場合、どちらの言語を選択するでしょうか (F# と非同期プログラミングの詳細については、Chance Coble が MSDN マガジンの 2008 年 10 月号の「簡単な非同期: 単純な F# の式から同時実行アプリケーションを構築する」で説明しています)。

実践的な多言語使用

実際に、F# と C# (または、その他の CLR 言語) との相互運用は、両方の言語のコードの "形" (言語が IL レベルで何になるか) をしっかり理解してしまえば比較的単純です。一見、これは非常に簡単に見えます。結局のところ、共通言語仕様 (CLS) でパラメータ置換、基本型、バイト オーダーなど、それ以外の厄介な問題が指定されるのであれば、2 つの言語の間のメソッド呼び出しの違いはそれほど大きくありません。

では、実際に確かめてみましょう。最初に簡単な F# コードの例を挙げ、これをコンパイルして DLL を作成し、ILDasm (中間言語逆アセンブラです。Reflector でもかまいません。使いやすいツールを使用してください) を使用して、どのように表示されるかを確認します。その後で、先ほど紹介した Chance Coble の 2008 年 10 月の記事に掲載されている非同期ワークフロー コードのような、もう少し複雑な F# 式を見ることにしましょう。

まずは、簡単な F# コードを見てください。

let x = 2

これが、Module1.fs と呼ばれる既定の "F# ライブラリ" プロジェクト ファイル内にあると仮定した場合、図 3 に示す IL が生成されます (IL リストを簡潔にするため、一部の行はコメント アウトしてあります)。

図 3 let x=2 の背後で実行される IL

.assembly Library1
{
  .custom instance void [FSharp.Core]Microsoft.FSharp.Core.
  FSharpInterfaceDataVersionAttribute::.ctor(int32, int32,
  int32) = ( 01 00 01 00 00 00 09 00 00 00 06 00 00 00 00 00 ) 

  // ...
}

.class public abstract auto ansi sealed beforefieldinit Module1
       extends [mscorlib]System.Object
{
  .custom instance void [FSharp.Core]Microsoft.FSharp.Core.
  CompilationMappingAttribute::.ctor(valuetype [FSharp.Core]Microsoft.
  FSharp.Core.SourceConstructFlags) = ( 01 00 07 00 00 00 00 00 ) 

  .method public static int32  get_x() cil managed
  {
    // Code size       6 (0x6)
    .maxstack  4
    IL_0000:  ldsfld     int32 '<StartupCode$Library1>'.$Module1::x@3
    IL_0005:  ret
  } // end of method Module1::get_x

  .method private specialname rtspecialname static 
          void  .cctor() cil managed
  {
    // Code size       13 (0xd)
    .maxstack  3
    IL_0000:  ldc.i4.0
    IL_0001:  stsfld     native int '<StartupCode$Library1>'.$Module1::_init
    IL_0006:  ldsfld     native int '<StartupCode$Library1>'.$Module1::_init
    IL_000b:  pop
    IL_000c:  ret
  } // end of method Module1::.cctor

  .property int32 x()
  {
    .custom instance void [FSharp.Core]Microsoft.FSharp.Core.
    CompilationMappingAttribute::.ctor(valuetype [FSharp.Core]Microsoft.
    FSharp.Core.SourceConstructFlags) = ( 01 00 09 00 00 00 00 00 ) 
    .get int32 Module1::get_x()
  } // end of property Module1::x
} // end of class Module1

.class private abstract auto ansi sealed beforefieldinit 
    '<StartupCode$Library1>'.$Module1
       extends [mscorlib]System.Object
{
  .field static assembly native int _init
  .custom instance void [mscorlib]System.Runtime.CompilerServices.
   CompilerGeneratedAttribute::.ctor() = ( 01 00 00 00 ) 
  .field static assembly initonly int32 x@3
  .method private specialname rtspecialname static 
          void  .cctor() cil managed
  {
    // Code size       8 (0x8)
    .maxstack  3
    IL_0000:  nop
    IL_0001:  ldc.i4.2
    IL_0002:  stsfld     int32 '<StartupCode$Library1>'.$Module1::x@3
    IL_0007:  ret
  } // end of method $Module1::.cctor

} // end of class '<StartupCode$Library1>'.$Module1

IL を通してコードを見ていくと、2 つのことに気が付くと思います。まず、F# コードの "x" という名前が、CLS レベルでは Module1 というクラスの静的プロパティとしてバインディングされています。また、その初期値である 2 は定数としてバインディングされるのではなく、アセンブリが読み込まれるときに、コンパイラ生成の StartupCode$Library1 クラスの型コンストラクタ (.cctor) を使用して初期化されています。つまり、プログラマは x をコンパイラによるインラインが可能な定数値と考えがちであるのに反して、コンパイラは x を静的プロパティとして表現することを選択しています。

したがって、この x というバインディングにアクセスするには、次のような C# コードが必要になります。

namespace ConsoleApplication1
{
    class Program
    {
        static void Main(string[] args)
        {
            Console.WriteLine("F#'s x = {0}", Module1.x);
        }
    }
}

ここまでは順調です。しかし、皆さんは、x が非常に単純なバインディングであるだけに、そのアクセスは簡単だろうと期待していたのではないでしょうか。もう少し複雑なコードでは、油断ならない問題がありますから、もう少し複雑なコードに話を進めて、F# から CLS へのマッピングの理解を確実にしましょう。

次の F# コードを見てください。

let add a b = a + b

コンパイルされた F# DLL の "Module1" クラスには、コンパイラによって IL が追加生成されています。

.method public static int32  'add'(int32 a,
                                   int32 b) cil managed
{
  // Code size       5 (0x5)
  .maxstack  4
  IL_0000:  nop
  IL_0001:  ldarg.0
  IL_0002:  ldarg.1
  IL_0003:  add
  IL_0004:  ret
} // end of method Module1::'add'

これは、C# の関数表現にかなり似ていますから、詳細に説明する必要はないでしょう。この呼び出しも簡単です。

ただし、F# の利点の 1 つは、関数を最上位の値として扱う点です。これは、引数として関数を受け取る関数を作成し始めると明らかに (そして難しく) なります。引数として関数を受け取る例として、次の関数を見てください。これは、組み込みの F# ライブラリ map 関数と同等で、引数としてリストの各要素に適用する関数と共にリストを受け取り、結果を格納した新しいリストを返します。

let mymap (l : 'a list) (f : 'a -> 'b) = List.map f l

これは、リスト処理に部分的に "関数型" を使用する方法です。関数型言語では、アイテムごとに反復処理を行う代わりに、パラメータとして関数を受け取り、リストの各要素にその関数を適用して、単一の結果の生成 ("fold" 操作と呼ばれます) または各結果を格納した新しいリストの生成 (ここで示した "map" のことです) を行います。

図 4 を見ると、コンパイル後に少し複雑な IL になっていることがわかるでしょう。繰り返しになりますが、Module1 クラスのパブリックな静的メソッドにマップされているのは当然のことです。ただし、C# からの操作を難しくしている原因は、静的メソッドが F# リスト (つまり、型がパラメータ化された Microsoft.FSharp.Collections.List のインスタンス) を入力型および戻り値型として受け取ると共に、2 つ目の入力として関数 (つまり、型が二重にパラメータ化された Microsoft.FSharp.Core.FastFunc のインスタンス) を受け取る点です。

図 4 マッピング関数の IL

  .method public static class [FSharp.Core]Microsoft.FSharp.Collections.
       List'1<!!B> 
          mymap<A,B>(class [FSharp.Core]Microsoft.FSharp.Collections.
              List'1<!!A> l, class [FSharp.Core]Microsoft.FSharp.Core.
                         FastFunc'2<!!A,!!B> f)
          cil managed
  {
    // Code size       11 (0xb)
    .maxstack  4
    IL_0000:  nop
    IL_0001:  ldarg.1
    IL_0002:  ldarg.0
    IL_0003:  tail.
    IL_0005:  call       class [FSharp.Core]Microsoft.FSharp.Collections.
       List'1<!!1>
       [FSharp.Core]Microsoft.FSharp.Collections. 
       ListModule::map<!!0,!!1>(class [FSharp.Core]Microsoft.
       FSharp.Core.FastFunc'2<!!0,!!1>,
       class [FSharp.Core]Microsoft.FSharp.Collections.List'1<!!0>)
    IL_000a:  ret
  } // end of method Module1::mymap

このため、正しく機能する少し本格的な C# コードが必要になります。この例の場合は、整数のコレクションを取得し、それを対応する文字列形式に変換する C# コードが必要です (C# 内からこれを実行するのは私にとってとても簡単だという事実はここでは無関係な話ですが、難しい問題に取り組む前に簡単なことを実行する方法については説明する必要があります)。これを C# から呼び出すには、いくつかの動作が正常に実行される必要があります。入力コレクションは F# リスト型に変換される必要があり、各要素に適用される関数は F# の "FastFunc" インスタンスに変換される必要があります。また、返される F# リストは C# で使用できる型に変換されるか、ネイティブの F# 形式で直接使用される必要があります。

C# コードにはこのような F# 型が必要であるため、最初の手順では適切な F# アセンブリ参照を追加します。この例の場合は、FSharp.Core.dll になります。ただし、F# リストの作成方法は、C# リストの作成方法とは異なります。C# のようにコンストラクタを介してコレクションに渡すのではなく、F# では、"cons" 演算子を使用してリストが作成されます。"cons" 演算子は、F# List<> クラスの静的メソッドです。言い換えると、次の F# コードは、むしろ見苦しい IL になります。

let l1 = [1; 2; 3;]

この IL は図 5 に示してあります。

図 5 let l1 = [1; 2; 3;]

  IL_0000:  ldc.i4.1
  IL_0001:  ldc.i4.2
  IL_0002:  ldc.i4.3
  IL_0003:  call       class [FSharp.Core]Microsoft.FSharp.Collections.
List'1<!0> class [FSharp.Core]Microsoft.FSharp.Collections.
List'1<int32>::get_uniq_Empty()
  IL_0008:  newobj     instance void class [FSharp.Core]Microsoft.FSharp.
Collections.List'1/_Cons<int32>::.ctor(!0,
              class [FSharp.Core]Microsoft.FSharp.Collections.List'1<!0>)
  IL_000d:  newobj     instance void class [FSharp.Core]Microsoft.FSharp.
Collections.List'1/_Cons<int32>::.ctor(!0,
              class [FSharp.Core]Microsoft.FSharp.Collections.List'1<!0>)
  IL_0012:  newobj     instance void class [FSharp.Core]Microsoft.FSharp.
Collections.List'1/_Cons<int32>::.ctor(!0,
              class [FSharp.Core]Microsoft.FSharp.Collections.List'1<!0>)

このように、mymap メソッドに渡すのに適した F# リストに C# 配列を変換するには、先頭に新しいアイテムが渡されるたびに、リストで静的メソッド Cons を繰り返し呼び出す必要があります。追加されるアイテムはリストの先頭に置かれるため、C# 配列と同じ順序を F# リストで保つには、配列の末尾から処理を開始する必要があることに注意してください。

int[] scores = {1, 2, 3, 4, 5};

var fs_scores = Microsoft.FSharp.Collections.List<int>.get_uniq_Empty();
for (int i = scores.Length-1; i >= 0; i--)
{
  fs_scores = Microsoft.FSharp.Collections.List<int>.Cons(scores[i],
                                                          fs_scores);                
}

そろそろ苦痛になってきましたね。F# の FastFunc 型インスタンスの取得は、さらにうんざりするような作業です。これは、FastFunc 型が、コアの System.Delegate 型と同様に、プログラマによってインスタンス化されることを意図して作成されておらず、F# コンパイラで処理することを想定しているからです。通常、F# コードでこのインスタンスを作成すると、コンパイラでは実際に FastFunc から派生する内部クラスが生成されます。これは、C# からも実行できますが、手間はもっとかかります。

この F# 関数を突然呼び出すことは、もたらされる利点に対して作業が多すぎると思われます。

以上のことからわかるように、基礎となるプラットフォームへの言語のマッピングは、多言語プログラミングにおいて、どこでどのように言語を使用するかを決定するうえで重要です。たとえば、F# 内部では簡単に実行できることが、C# から直接実行しようとすると難しい場合があります。だからと言って、多言語主義の考えをあきらめる必要はありません。単に、組み合わせて使用する言語と目的に合った言語を十分に検討して選択する必要があるだけです。もっと前向きな視点で、Chance が 10 月の記事で紹介したコード例に続きを追加したものを見てみましょう (図 6 を参照してください)。

図 6 元のコードに修正を追加したコード

open System
open System.IO
open Microsoft.FSharp.Control.CommonExtensions

#nowarn "057"
let aCountSpace filename size = 
  async {
     let space = Convert.ToByte ' '
     use stream = File.OpenRead (filename)
     let bytes = Array.create size space
     let! nbytes = stream.ReadAsync (bytes,0,size)
     let count = 
       bytes
       |> Array.fold_left (fun acc x -> if (x=space) then acc + 1 else acc) 0
     return count
  }

let aCounted (files : FileInfo array) = 
    files
    |> Array.map (fun f -> aCountSpace (f.FullName) (int f.Length))
    |> Async.Parallel 
    |> Async.Run

少しだけ修正してあります。ただし、修正は、"aCounted" メソッドの "files" 引数を指定して、FileInfo の配列にしたことだけです。これは、Chance の元のコードでは、F# コンパイラによって処理されていました。このように修正すると、"aCounted" の IL シグネチャは次のようになります。

 .method public static int32[] aCounted(class [mscorlib]System.IO.FileInfo[] files) cil managed

これによって、C# からの呼び出しが次のように非常に簡単になります。

int[] fileCounts = 
  Module1.aCounted(new DirectoryInfo(@"C:\Projects\Test").GetFiles("*.txt"));
foreach (var ct in fileCounts)
{
    Console.WriteLine("Count = {0}", ct);
}

この新しいコードは呼び出しが簡単になったうえに、Chance が 10 月に説明した完全な非同期実行機能も残されています。

ここで C# を使うかどうかは、皆さんの自由です。単に、私の場合は、Visual Basic や C++/CLI よりも C# を使うほうが楽だったというだけの理由で使いました。とは言うものの、同じコードが Visual Basic や C++/CLI ではどうなるかを提示するのが、難しいわけではありません。

選択に伴うコスト

もちろん、多言語アプローチを取る場合にもコストは必ずかかります。最初に挙げる一番の問題は、既に説明した内容ですから、よくおわかりのことと思いますが、1 つのプロジェクトでさまざまな言語を開発者が使用する場合、それらの言語が基礎となるプラットフォームにどのようにマップされているか、また、それらの言語が相互にどのようにマップされているかを理解する必要があるということです。これは、コンパイラ任せですべてがうまくいくという考え方が、通用しないことを意味します。

開発者は、コンパイラによって言語の構成要素が基礎となるプラットフォーム (この場合は、CLR) にどのようにマップされるか、他の言語によってこれらの構成要素がどのように取り出されるかを、確認する必要があります。これに関しては、C# や Visual Basic のように、この 10 年間で非常に急激な変化が見られた言語であっても、長期的に見ればそれほど大きな変化がないという事実は良い情報です。つまり、いったん習得した言語のマッピングに関する情報は、比較的一定のまま変化しません。

2 つ目の問題は 1 つ目の問題から派生するもので、多言語プログラムのデバッグが単一言語プログラムのデバックより難しくなる可能性が高いということです。これは、1 つではなく、2 つ (またはそれ以上) の言語について開発者が理解する必要があるということによります。たとえば、私が紹介した例の場合、デバッグを行うには、C# コード内にブレークポイントを設定し、F# コード内でもステップ実行を行い、再び C# コードに戻るという作業が必要になります。多くの点で、これは新しい問題ではありません。F# が、理解する必要がある言語のリストに新しく追加されるだけのことです。

開発組織によっては、C# コードで作業している開発者が、F# コードはアドバタイズされたとおりに機能すると (宣言によって、または単体テストへの信頼を通じて) 信用し、F# コードにステップインせず、ステップオーバーしている場合もあります。これは、多数の Visual Basic 開発者や C# 開発者が、C または C++ のアンマネージ ライブラリ内のメンバを呼び出すコードをステップ実行する場合と似ています。

今後のコラムでは、多言語プログラマの皆さんに、多言語アプローチによってプログラミングがより簡単になる方法をさらに紹介すると共に、他の言語とその利点についても取り上げます。

ご意見やご質問は、polyglot@microsoft.com まで英語でお送りください。

Ted Neward は、信頼性の高いアジャイル エンタープライズ システムを専門とする国際的なコンサルタント企業である ThoughtWorks のプリンシパル コンサルタントです。多数の著書があり、Microsoft MVP アーキテクト、INETA の講演者、PluralSight のインストラクタでもあります。Ted の連絡先は、ted@tedneward.com です。また、blogs.tedneward.com にブログを公開しています。