多言語プログラマ

Cobra の活用

Ted Neward

目次

はじめに
信頼せよ、しかし検証せよ
"デザートのトッピング? 床のワックス? いえ、両方です!"
まとめ

このコラムの最初の記事では、複数のプログラミング言語、特に複数の種類のプログラミング言語を理解することの重要性について説明しました。他のプログラマに差をつけるには、C# や Visual Basic だけでは十分ではありません。道具箱に入っている道具が増えれば、もっと良い大工になれるというわけです (これはある程度は真実ですが、あらゆる道具が揃っていても使いこなせなければ良い大工とは言えません)。

この記事では、使い慣れたオブジェクト指向言語との違いがあまり大きくない言語で、いくつかの新機能を提供するものについて説明します。その新機能は、新しい考え方を提案しているだけでなく、現在持っている道具箱の中身に今までとは別の方向から光を当てます。

ここで、Cobra を紹介します。Cobra は Python の子孫です。Python の主な特徴は、動的および静的に型指定されたプログラミング モデルの組み合わせ、組み込みの単体テスト機能、スクリプト機能、およびいくつかの Design by Contract (契約による設計) の宣言です。

はじめに

ルイス キャロルの有名な言葉のように、"初めから始める" ことにしましょう。Cobra は .NET 言語です。Web ブラウザで cobra-language.org にアクセスするか、または同じドメインで Subversion クライアント経由により、Cobra を入手できます。ソースから構築する場合は、ソース ディレクトリの install-from-workspace.bat スクリプトを使用して Cobra コンパイラを構築し、必要なアセンブリをグローバル アセンブリ キャッシュ (GAC) にインストールします。構築済みのバイナリも利用可能であり、必ずしもソースから構築する必要はありませんが、Cobra で記述された Cobra コンパイラのソースを見ることは興味深い作業です (ある言語のコンパイラ自体がその言語で記述されるのは大きな成果です)。重要なプログラムで動作している Cobra 言語の機能の (すべてではありませんが) 多くを見ることができます。

コンパイラを構築した後、最初に行う手順は、皆さんが好きな "コンパイラのテスト" (または "プログラマのテスト") のプログラムとしてよく使用される Hello World で Cobra 言語をテストすることです。

class Program
        def main is shared
                print 'Hello, world.'

Cobra では、ソース コードとのやり取りに 2 つの異なる方法が使用されます。スクリプトに似たアプローチでコードを直接実行するか ("cobra hello.cobra" をコマンド ラインから実行)、またはコードをコンパイルしてから C#/Visual Basic や C++ で行われる従来の方法で実行します (最初にコンパイルし ("cobra –compile hello.cobra")、その後に .NET 実行可能ファイルの場合と同じように実行)。ただし、いずれの方法でも "cobra" 実行可能ファイルは同じで、Cobra\Source ディレクトリに含まれたファイルです (観察力の優れた読者の方々は、.cobra ファイルの "スクリプト" を終えた後でディレクトリの内容を確認したときに、コンパイルされた "hello.exe" が実行後に残されていて、Cobra の "スクリプト" モードが単なる "2 パス コンパイル後に実行" シーケンスであることに気が付くでしょう)。

Cobra ソースを調べていくと、C# 言語や Visual Basic 言語に非常によく似ている点、逆にまったく異なる点が見えてきます。実際、Python (または CLR 用の同等の IronPython) を熟知している方であれば、Cobra が主に Python 構文を基にしている言語であることにすぐに気が付くでしょう。Cobra を作った Chuck Esterbrook 氏は、Python 構文に魅力を感じ、この構文を基にして独自の言語を開発しました。

Python を使用した経験のない方でも、すぐにわかる違いが 1 つあります。コードのブロックを周りのコードと区別して設定する場合、Cobra ではキーワード ("begin" や "end") またはトークン ("{" and "}" など) を使用せず、Python 同様に空白文字に意味を持たせます。Cobra では (発明者の好みで) インデントされたブロックを使用して、つまり字句のインデントによってコードのブロックが区別されます。言い換えると、意思決定ブロック ("If" ブロック) の最後を示すために、閉じ中かっこを使用するのではなく、次の行のインデントを前の行よりも 1 レベル分下げます。図 1 では、"if" テストの結果にかかわらず最後の "print" 行が実行されることを Cobra は認識できます。意思決定を行う "if" ステートメントと "print" 行のインデント レベルが等しいからです。

図 1 Cobra コードのインデント構造

class Program
        def main is shared
                if 1==1
                        print "Oh, goody, math works!"
                        print "I was beginning to get worried there."
                print "Math test complete"

1 つのファイル内でインデントを制御するためのタブと空白が混在していると問題が発生するおそれがあるため、Cobra では、タブと空白のいずれかを使用する必要があります。2 つが混在している状態が検出されると、コンパイル エラーが生成されます。

さらに、コードをわかりやすくするために、Cobra では形式的な面が体系化されています。

  • class、struct、interface、enum などの型は、先頭を大文字にする必要があり、アンダースコアで始めることはできません。Microsoft が Base Class Library で確立したパターンに似ています (String、Control、ApPDomain など)。
  • 引数およびローカル変数は、小文字で始める必要があり、先頭にアンダースコアは使用できません。例としては、x、y、count、index などです。
  • 式の中では、オブジェクトまたはクラスのメンバへのアクセスは、ピリオド (.foo) では明示的に、メンバがアンダースコア ("_foo") で始まる場合は暗示的に行われます。
  • 強制的には適用されませんが、通常はオブジェクト変数の先頭にアンダースコアを付与します。これは可視性の protected を意味しています。たとえば、_serialNum、_color、_parent のようになります。

このように、Cobra コードのどのようなフラグメントについても、Cobra プログラマはフラグメントまたはスニペットに関連する要素を簡単に区別できます。たとえば、次の Cobra ステートメントについて考えてみましょう。

    e = Element(alphabet[_random.next(alphabet.length)])

以下のことは明らかです。

  • e および alphabet はローカル変数または引数である。
  • _random は (おそらく) 可視性の protected のオブジェクト メンバまたは変数である。
  • Element は型である。

Cobra はこのように Python の基礎に従っています。コードの名前付け規則や書式の規則など、どのシナリオに対しても方法は 1 つだけです。

信頼せよ、しかし検証せよ

信頼せよ、しかし検証せよ。ロナルド レーガン元大統領が (軍縮交渉の中で) 用いた言葉として有名ですが、プログラミングの世界でもこの方針を貫くことは非常に重要です。仕事先で共に働くプログラマを信頼し、NullReferenceException が生成されることが明確に文書化されているメソッドに null 参照を渡すことはあり得ないと信じるのは簡単です。しかし、一般的に繰り返し実証されてきたことですが (特に、顧客または上司の前で行うデモのときなどに)、これらの前提を明確にしておいた方が安全です。今、レーガン元大統領が生きていてプログラミングに興味を持っていたなら、このように言い換えたでしょう。「信頼せよ、しかし、文書化し、固執し、検証せよ」と。

プログラミングの世界で、これは 2 つの基本的なことを意味します。"固執する" というのは、防衛的にプログラミングすることです。多くの場合、アサート ステートメントまたはアサート メソッドを使用することにより、渡された値が特定の条件を満たしていることを確認します (実行時の例外発生の可能性は承知のうえです)。"検証する" というのは、メソッドの単体テストを記述することです。これは、どうしても制限に違反しようとするプログラマにメソッドの実装で対応できることを確認するためです。

システム内の人物を表すクラスを作成するという (比較的単純な) タスクを考えてみましょう。一般的に、人物を表すには、姓、名、および年齢が必要ですが、他の人物との結婚も想定されます。通常、このような種類のクラスを記述する場合は、いくつかの不変条件 (invariant)、つまりクラスを使用するときに必ず守るべき基本原則を確立する必要があります。たとえば、Person クラスで、姓に null を指定できないようにします。通常、この設定は Person を記述する開発者によって強制的に適用されますが、その一般的な方法として、図 2 の C# コードのようにクラスの property-set コンストラクトが使用されます。

図 2 不変条件で制限を強制する

public class Person
{
  public Person(string fn, string ln, int a)
  {
    this.firstName = fn;
    this.lastName = ln;
    this.age = a;
  }
  public string FirstName
  {
    get { return firstName; }
    set { firstName = value; }
  }
  public string LastName
  {
    get { return lastName; }
    set { if ((value != null) || value.Length > 0) lastName = value; }
  }
  public int Age
  {
    get { return age; }
    set { age = value; }
  }
  public override string ToString()
  {
    string ret = String.Format("Person: 
  }
}

ここで問題なのは、property-setter が使用される場合のみ不変条件が強制されることです。たとえば、後から別の開発者が Person で使用した新しいメソッドによって補助ストア (プロパティによってカプセル化されたフィールド) が直接参照された場合、不変条件の違反が発生します。この事態はあまり目立たないので、だれもすぐには気が付かず、気が付いたときには遅すぎるというわけです (これは、クラスで内部的にプロパティを使用するか、またはフィールドを直接操作するかを議論する場合の重要な論点です)。もちろん、この問題は、プロパティとメソッドの両方に当てはまります。プロパティが複数の補助フィールドにアクセスすることは、この言語で禁止されているわけではないので、頻繁ではありませんが定期的に発生します。また、さらに重大な問題は、すべての不変条件 (たとえば、姓および名を null または空にすることはできない、年齢を負の数字にすることはできない、など) は、内部状態を変更する可能性のあるすべてのメソッド、プロパティ、またはコンストラクタで強制する必要があります。

Cobra では、基本的な契約による設計システムを提供し、クラスまたはメソッドがクラスに関する特定の要件を宣言できます。Cobra コンパイラは、対象となるデータを変更する可能性のあるすべてのメンバ (またはプロパティやメソッド) に対して暗黙的に検証ステートメントを追加します。この方法では、クラスをどのように使用するかにかかわらず、名に null が指定されないことが保証されます。インスタンスのコンテンツを変更する可能性のある各メソッドで、明示的にコード化する必要はありません (図 3 を参照)。

図 3 検証ステートメント

class Person
    invariant
        .firstName <> ""
        .firstName.length > 0
        .lastName <> ""
        .lastName.length > 0
        .age >= 0

    var _firstName as String
    var _lastName as String
    var _age as number

    pro firstName from var
    pro lastName from var
    pro age from var

    cue init(first as String, last as String, age as number)
        ensure
            first.length > 0
            last.length > 0
            age > -1
        body
            _firstName = first
            _lastName = last
            _age = age

    def toString as String is override
        return 'Person: [.firstName] [.lastName], [.age.toString] years'

具体的にどのような頻度でどの程度まで、これらの検証チェックを Cobra 言語で挿入するかについては、比較的簡単に確認できます。IL 逆アセンブラ (ILDasm.exe) を起動し、結果の Person クラスを調べます。調べてみると、Cobra アプローチの長所がわかります。すべての "変更" メソッドは、不変条件を検証するコードによって追加されます。これはクラス全体で行われます (不変条件の生成方法を変更するためのコンパイラ オプションがあります。選択できる方法は、まったく生成しない、メソッドでのみ生成する、すべての操作に追加する (既定) です)。

Cobra の契約による設計は優れた機能ですが、エラーのないプログラムを保証するには不十分です。また、適切なデータと不適切なデータが両方とも渡される状況で、目的の動作が確実に行われるようにするには、ほとんどの場合、コードに対して一連の単体テストを記述する必要があります。Cobra では、ソフトウェア開発プロセスにおけるテストの重要性を考慮し、テストを言語内部のファーストクラス コンストラクトにします。言語の "テスト" コンストラクトです。これにより、Cobra はコンパイル プロセスの一部としてテストを実行できます。たとえば、Person クラスの場合、単純な単体テストのスイートは図 4 のようになります。

図 4 Person の単体テスト

class Person
    invariant
        .firstName <> ""
        .firstName.length > 0
        .lastName <> ""
        .lastName.length > 0
        .age >= 0
    test
        p = Person('Neal', 'Ford', 29)
        assert p.firstName == 'Neal'
        assert p.lastName == 'Ford'
        assert p.age == 29

    var _firstName as String
    var _lastName as String
    var _age as number

    # ... everything else as-is ...

コマンド ラインで "-test" オプションを使用して実行する場合、Cobra はクラス内に含まれた単体テストを実行します。ここで、すべての動作が予測どおりであった場合、Cobra はテストに合格したことを報告し、コードを実行可能ファイルにコンパイルします (ライブラリは "-t:lib" コマンド ライン スイッチを使用してコンパイルされます)。

これは、従来とは別の方法による単体テストの構築ですが、従来の NUnit ベースの単体テスト手法に比べていくつかの利点があります。テストが別個のファイル内にあるとコードの保守や更新を行う開発者にとって不便ですが、Cobra ではテストはコードの近くに保持されます。単体テストの記述および保守を簡単に行うことは、通常は良いことだと見なされます。また、単体テストをテスト対象のクラス内に適切に保持することは、何年も前に Java キャンプで教えられたヒントです。

残念ながら、十分に包括的な (理想的な) テストをこの手法で実施すると、コードが膨張します。配置するコードの一部としてコントラクトによる検証および単体テストが含まれていると、コードのサイズが 2 倍、3 倍、または 4 倍にまで膨張し、完成した製品の展開を簡素化する計画がこれによって阻まれてしまいます。さいわい、Cobra 言語では、"-turbo" オプションを使用した解決策も提供されています。その解決策により、個別の最適化だけでなく、契約による設計および単体テストのコードの削除も行います。これは、一般的に展開直前の最適化手順を目的としています。

"デザートのトッピング? 床のワックス? いえ、両方です!"

随分前に、「サタディ ナイト ライブ」という番組のコントで、デザートのトッピングにも床のワックスにもなるという万能な製品が出てきて、"両方に使えるよ" というのが話の落ちでした。私はおおいに笑いました。70 年代の話です。

ところで、最近、プログラミング言語のコミュニティで新たに議論されていることがあります。"静的な入力" 対 "動的な入力" です。かつて、(古典的な) Visual Basic とその実行時の (IDispatch ベースの) バインドおよびバリアントに対する批判が、C++ とそのコンパイル時のテンプレート機能を使用する開発者のコミュニティによって行われましたが、今また、同じような議論が行われています。ただし、今回は、厳密かつ静的に型指定するコミュニティが守る側で、厳密かつ静的に型指定された言語の安全性のメリットを主張しています。これに対して、Ruby および Python (または IronRuby および IronPython) のユーザーは生産性に関する利点を主張しています。この種の議論がたいていそうであるように、多くのレトリックが登場し、多くの根拠に乏しい事柄が主張され、多くの統計が引用されます (が、ほとんどはその場で作り上げられたものです)。

実のところ、この議論はやや見せかけにすぎない面があり、その裏にはメソッド呼び出しの事前バインドと遅延バインドの根本的な違いが隠されてしまいがちです。"従来の" コンパイル型言語 (C++/CLI または C#) では、一致するメソッドの有無に基づき、メソッド呼び出しが許容可能かどうかをコンパイラがコンパイル時に決定します。ターゲット クラスにメソッドが 1 つ見つかると、生成されたアセンブリに必要な命令が出力されます (CIL では、これは対象のメソッドのメタデータ トークンを含む "callvirt" 命令)。これに対して、遅延バインドの呼び出しが実際に解決されるのは実行時です。通常は、メタデータベースの API に似たリフレクションを使用します。この時点で、ランタイムがメソッドを検索し、見つかった場合はそれを起動します。

ここでの違いを見つけるのは難しくありません。最初のケースでは、コンパイル時にメソッドを検出できる必要があります。場合によっては、コンパイラが適性に動作するように、プログラマは型のナビゲーションを組み込む必要があります (これが、汎用性のある "if obj is Person" です。その後にはダウンキャストおよびメソッドの呼び出しのイディオムが続きます)。この言語ステートメントのシーケンスは、ストレスの元になりがちです。状況のコンテキストが広く知られていることから、対象のオブジェクトがその型でなければ呼び出しが成功しないことをプログラマがわかっている場合は特にイライラします。コンパイラはそこまで理解できないので、開発者は、コンパイル プロセスが適切に行われるように一連の手順を実行する必要があります。

ただし、2 番目のケースでは、プログラマは自分が "知っている" ことが現実の世界とは合致しないことを発見します。それは、大規模なデモ、製品の出荷、IPO の日の実行時など、考えられる限りの最も厄介な場面で実行時の例外が生成されるからです。想定された不変条件を別の開発者が偶発的に壊したのかもしれません。または、単体テストでテスト対象とならなかったコード パスが原因かもしれません。それがどのように発生したとしても、プログラムが停止したという現実に変わりはなく、プログラマは悔しい思いをします。

議論はこのように続きます。生産性の向上を重視して、コンパイラで確認されたコードの正確性を信用するか。または、プログラマの単体テスト スイートを信用してコンパイラに停止を命令するか。どちらを選択すればよいでしょうか。

Cobra は、この議論をうまくかわすことができます。静的でもあり、動的でもあることがその方策です。つまり、Cobra はいずれかの動作をそれが可能な場合に行います。C# で行われるように、メソッドの呼び出しを事前にバインドします。何らかの理由でそれが不可能な場合は、遅延バインド セマンティクスを選択し、実行時にメソッド呼び出しを解決します。さらに、C# 4.0 では同種の機能を保証していますが (Visual Basic も .NET 言語となってからは同様です。Option Explicit フラグおよび Option Strict フラグを使用)、Cobra では、事前バインドまたは遅延バインドをそれぞれいつ行うかを決定するために、プログラマが構文を追加する必要はありません。開発者が事前に呼び出しを行わなくても、コンパイル時に決定されます (図 5 を参照)。

図 5 動的バインド

class Person

    get name as String
        return 'Blaise'


class Car

    get name as String
        return 'Saleen S7'


class Program

    shared

        def main
            assert .add(2, 3) == 5
            assert .add('Hi ', 'there.') == 'Hi there.'
            .printName(Person())
            .printName(Car())

        def add(a, b) as dynamic
            return a + b

        def printName(x)
            print x.name  # dynamic binding

おわかりのとおり、Cobra では、"動的な修飾子" (特定の型またはメソッドがその引数および型を動的として扱うことを示す)、および "printName" メソッドで使用される "動的なインターフェイス" (名前を静的にバインドできないことを Cobra が認識して実行時にそれを解決する) の両方をサポートします。

まとめ

非常に優れた固有の機能セットを持つ Cobra は、"選択の集合" 言語と言えます。事前バインドと遅延バインドとを静的に切り替えることができるため、Office の Automation モデルなど、遅延バインド API を多用する .NET コンポーネントと共に使用するのに適した言語です。一方、可能な限り、事前バインドの安全性とパフォーマンスの利点も保持されています。これだけでも、Cobra は検討に価する言語です。特に Office (および他の COM ベース) のAutomation プログラミングには最適です。

さらに、スクリプト言語とコンパイル型言語の両方のツールとして動作する Cobra の能力を利用すると、Cobra の利点を最大限に活用できます。

多言語プログラミング実験での皆さんの健闘を祈ります。

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

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