スレッドの停止する日

並行処理を実現する機能として最初に触れたのがOSのスレッド(あとメモリ保護の無い環境のリアルタイムOSのタスク)である。そのせいか、スレッドの生成・終了のコストが気になってしまい、ワンショットの処理をスレッドで行わせる(≒頻繁にスレッドを生成して終了させる)ことについて、未だに抵抗がある。

(軽量スレッドの類を使える環境にない、ということもあるが……)

そんな訳で、一度生成したスレッドを使い回すコードを書くことが多い。スレッドの中でループさせておいて、外部から「実行する処理」を依頼する構造だ。

クラスの中に閉じ込めるのならば、コンストラクタでスレッドを生成して、デストラクタで終了させる、といった感じになる。

ここで、デストラクタにおいて「どうやってスレッドを終了させるか?」という問題が生じる。頻繁にスレッド外からスレッド内に処理を依頼するケースでは、高確率でスレッド間通信の仕組みを用意することになるから、それに乗っかって「スレッドを終了しろ」というメッセージを送ればよい。でも、そのような仕組みが用意されていないのなら、どうだろうか?

伝統的なスレッド・プログラミングに不慣れな開発者が書いてしまいがちなコードは、こんな感じだろうか?

#include <chrono>
#include <iostream>
#include <thread>

using namespace std::literals::chrono_literals;
using std::this_thread::sleep_for;

class Foo final {
private:
    bool m_stop { false };
    std::thread m_thread;

public:
    Foo() {
        using std::cout, std::endl;

        m_thread = std::thread([&]{
            cout << "start thread" << endl;
            while (!m_stop) {
                cout << "loop" << endl;
                sleep_for(1ms);
            }
            cout << "stop thread" << endl;
        });
    }

    ~Foo() {
        m_stop = true;
        m_thread.join();
    }
};

スレッドの終了を通知するためにbool型の変数m_stopを用いている。スレッド内部で、周期的にこの変数の変化を監視する訳だ。

このコードには問題がある。C++の仕様に少しばかり詳しいならば、あるメモリ上のオブジェクトにたいして排他制御無しで「値を参照するスレッド」と「値を変更するスレッド」が存在するコードは未定義の動作(データ競合)を引き起こす、ということに気づくだろう。

安直に直すなら、例えば変数m_stopをbool型ではなくstd::atomic_bool型にすればよいだろう。

#include <atomic>
#include <chrono>
#include <iostream>
#include <thread>

using namespace std::literals::chrono_literals;
using std::this_thread::sleep_for;

class Foo final {
private:
    std::atomic_bool m_stop { false };
    std::thread m_thread;

public:
    Foo() {
        using std::cout, std::endl;

        m_thread = std::thread([&]{
            cout << "start thread" << endl;
            while (!m_stop.load()) {
                cout << "loop" << endl;
                sleep_for(1ms);
            }
            cout << "stop thread" << endl;
        });
    }

    ~Foo() {
        m_stop.store(true);
        m_thread.join();
    }
};

もう1つ気になることがある。スレッドの中でwhile (!m_stop)といった感じでm_stopを監視している訳だが、スレッドの中にも、スレッドを生成しているコンストラクタの中にも、変数m_stopを書き換えるコードが1つも存在しない。

そのため、もしかしたらコンパイル時に次のようなコードに最適化されてしまうかもしれない、という疑念が生じる。

cout << "start thread" << endl;
if (m_stop) {
    cout << "stop thread" << endl;
    return;
}
while (1) {
    cout << "loop" << endl;
    sleep_for(1ms);
}

このような最適化を邪魔するにはvolatileを使えばよいだろう。

#include <chrono>
#include <iostream>
#include <thread>

using namespace std::literals::chrono_literals;
using std::this_thread::sleep_for;

class Foo final {
private:
    volatile bool m_stop { false };
    std::thread m_thread;

public:
    Foo() {
        using std::cout, std::endl;

        m_thread = std::thread([&]{
            cout << "start thread" << endl;
            while (!m_stop) {
                cout << "loop" << endl;
                sleep_for(1ms);
            }
            cout << "stop thread" << endl;
        });
    }

    ~Foo() {
        m_stop = true;
        m_thread.join();
    }
};

std::atomicとvolatileは別の機能で、かつ併用することが可能だ。今回のケースでは変数m_stopをvolatile std::atomic_bool型にすればよさそうだ。

#include <atomic>
#include <chrono>
#include <iostream>
#include <thread>

using namespace std::literals::chrono_literals;
using std::this_thread::sleep_for;

class Foo final {
private:
    volatile std::atomic_bool m_stop { false };
    std::thread m_thread;

public:
    Foo() {
        using std::cout, std::endl;

        m_thread = std::thread([&]{
            cout << "start thread" << endl;
            while (!m_stop.load()) {
                cout << "loop" << endl;
                sleep_for(1ms);
            }
            cout << "stop thread" << endl;
        });
    }

    ~Foo() {
        m_stop.store(true);
        m_thread.join();
    }
};

これで終わりだろうか? 問題なく動作するように見えるコードだが、同時に、ちょっとばかり泥臭いコードにも見える。もう少しスマートな方法はないだろうか?

最近気づいたのは、1回限りのスレッド間通信にはstd::futureとstd::promiseを使えばよい、ということだ。

#include <chrono>
#include <future>
#include <iostream>
#include <thread>

using namespace std::literals::chrono_literals;
using std::this_thread::sleep_for;

class Foo final {
private:
    std::promise<void> m_pr_stop;
    std::thread m_thread;

public:
    Foo() {
        using std::cout, std::endl;

        m_thread = std::thread([&, fu_stop = std::move(m_pr_stop.get_future())]{
            cout << "start thread" << endl;
            while (fu_stop.wait_for(0ms) == std::future_status::timeout) {
                cout << "loop" << endl;
                sleep_for(1ms);
            }
            cout << "stop thread" << endl;
        });
    }

    ~Foo() {
        m_pr_stop.set_value();
        m_thread.join();
    }
};

元のコードの構造を保持するために、while文の判定式ではwait_for(0ms)でスリープさせず、while文の最後でsleep_for(1ms)している。初回の処理のタイミングが多少遅れても構わないのならば、wait_for(1ms)にして末尾のsleep_for(1ms)を取り除いてしまえばよいだろう。

詰めプログラミング的教育

今の職場は――いや、今の職場も、新人教育にC言語を採用している。

実際の業務としてはC++も多い上に、C#、Dart、JavaScript/TypeScript、Kotlin(とごく稀にJava)、Python、Swift(と時々Objective-C)と色んな言語が飛び交う環境なのだが、歴史的事情によりC言語色の濃いC++のコードも多いし、C言語の仕事も無くはない。いにしえのObjective-CコードにてC言語的側面に直面することもある。

そんな訳でC言語なのだが。

しかし周知の通り、C言語は最近の他の高水準言語と比べると低水準である。平たく言えば、便利ではない。この点が、初級者向けのプログラミング教育の際に問題となる。

職場でのプログラミング教育においては、言語やらライブラリを覚えることは、課題を解く上ではごく一部の要素でしかない。実際には、(暗黙のうちに)次のようなことを学ぶ。

  1. 解くべき課題を分析・整理して、内容を把握する。
  2. 課題を解く方法――解決策を考える。
  3. 解決策を抽象機械*1向けにアレンジする。
  4. アレンジした解決策をプログラミング言語で記述する。
  5. コンパイルエラーや実行時エラーなどを中心に、処理系との付き合い方を身に着ける。

(現実には、複数の人が関わるプロダクトでは、プログラミング言語で記述する際にキレイに書けるか否かも重要になってくる。あとGitなどのツールの扱い方を覚えるとか、報連相を含めて「実際のプロジェクトでの作業の進め方」を体得するとか、プログラミング以外の要素も入ってくるものだが、ここでは割愛する)

言語やライブラリの知識は、上記で言う項目 (4) に直接関わる内容であり、間接的に (3) に影響を与える。しかし (1) や (2) には効果がない。

課題の難易度が上がるにつれて、課題を解く上で (1) や (2) の割合が大きくなる。また、課題の抽象度が高くなるため、それを抽象機械の水準にブレイクダウンする (3) の作業の負担も増加する。

プログラミング教育においてC言語を使う場合の問題は、比較的難易度の低い課題のころから (3) や (4) の負担が大きい上に、難易度が上がるとたちまち (3) の負担が上昇してしまうため、学習者が (3) や (4) の作業で手一杯になってしまうことだ。この影響で、なかなか (1) や (2) の教育にたどり着けない。

例えば、素のC言語でGUIプログラミングは無理ゲーなのでコンソールアプリを扱うにしても、1行分の入力文字列を安全かつ比較的確実に取得することすら難しいのがC言語だ。他の言語では、言語処理系の実装上の制約その他の要因があるにせよ、教育用課題の範囲においては、1行の長さを気にせずに文字列を取得することが比較的容易だ。しかしC言語の標準ライブラリではそれがままならない*2。何らかのルーチンを自作するか、便利なライブラリを探すしかないのだが、受講者はまだそれらの作業が可能なレベルからは程遠い「スタート地点」にいる。

このような傾向は、課題の抽象度が高くなるにつれて酷くなる。

(単純な入力に関しては、初期の段階では騙し騙しscanf(3)を使うものだが、課題の難易度上昇に伴いscanf(3)以外で何とかする必要が生じる。避けられない問題だ。というかそもそもscanf(3)を騙し騙し使うのだって、講師役のベテランは普段scanf(3)を使わないので、例えば変換指定の空白文字絡みの微妙な仕様に講師ともどもつまづくとか、そういう非生産的な光景が毎回繰り広げられるのである)

正直なところ、実際の業務の都合を無視して、教育の実施者と受講者双方のレベルを度外視するならば、プログラミング初級者にはPythonを使って『Python言語によるプログラミングイントロダクション』片手に教育するか、OCamlを使って『プログラミングの基礎』片手に教育した方が、中~長期的にはよいのではないか?

とは言いつつも、諸事情により現実にはC言語を使うしかない。

では、どうすればよいか?

最近思うのだが、自分が書いたプログラムが動いたときの楽しさとか無視しちゃって良いのなら:

  1. 予め、ちょっとしたアプリを書いておくが、ある関数のみ中身を空にしておく。
  2. その関数の仕様を明示した上で、受講者に中身を書かせる。

――こんなスタイルで、出題者がテーマに応じた空の関数を用意しておき、それを1つずつ解かせて、レビューして、再度解かせて……と繰り返していく方法ならば、やり方次第では先に挙げた項目 (1) や (2) に踏み込んだ教育も可能ではないだろうか?

例えば、文字列の内容を解析する関数なら、状態遷移図やジャクソン構造図を用いたデータ構造の解釈と、そこからプログラムに変換する方法に的を絞って教育できる。解析する内容次第だが、関数1つに収まるボリュームで済む。

解かせるのが関数1個ならば、例えばその関数にテストデータを突っ込んでテストする処理をmain関数に書いておくことで、IDE上で特別な設定無しでデバッグ実行するだけで、ブラックボックス的に関数の動作を検証して受講者に結果を見せることも可能だろう。また、そういう課題を用意する労力は、同じようなことをアプリ単体に適用する場合よりは少なく済むはずだ。

そういう、詰めプログラミング的な教育も、ある程度は有効ではないか? もちろん、全面的にそれを採用するわけにはいかないのだが……。

*1:抽象機械:要はコンピュータのこと。ただし、プログラミング言語という「色眼鏡(抽象化層)」を通して見たコンピュータの姿であり、実際のコンピュータその物とは差異がある。ちなみに、例えばJavaを使っているのにCOBOL向け抽象機械を念頭にコードを書いてしまうのが、コボルの暗黒面に落ちた悪いコボラーである。善きコボラーは、COBOL案件でCOBOL向きのコードをCOBOLで書いている。

*2:gets(3)を使ったらアカンのは当然として、しかしfgets(3)だって「バッファサイズを超える長さの入力があったらどうするか?」とか「改行コードをどうするか? 入力元がファイルの場合、ファイル末尾には改行コードが存在しない可能性があるから、注意しないと……」とか、色々あるのです。

中高年プログラマの生き残り戦略 基礎の基礎編

最低でもあと10年は職業プログラマとして働き続けようと思っているのだが、働き続けるにあたり重要なファクターをあれこれ考えた結果、結局のところ根本的には「心身の健康」と「体力」と「お金」だろうという結論にたどり着いた。

……これだけでは話が広がらないので、もう少し深掘りしてみる。

個人的な意見だが、成功*1というものは、「環境」「努力」「才能」の3つの絵柄がうまい具合に揃った時に、確率的に生じるものだと思っている。

3要素が揃わないと何も得られない。3要素が揃った場合も、その揃い方次第で、当たるのは小役かもしれないし、ボーナスかもしれない。時にはリプレイかもしれない。パチスロと違うのは、当人には何も見えない(当たったことすらも!)ことと、何をもって役とするか(そして何が小役で何がボーナスか等)が人それぞれ異なることだろう。

このように考えた時、何らかの成功を得るためには、「環境」「努力」「才能」のそれぞれの絵柄を見定めることを前提としつつ、「試行回数を増やす」ことが重要となる。

研究によれば、何かしら成果をあげる確率は年齢に左右されないようだ。20代から70代まで、世にインパクトを与えるようなアウトプットを出す確率は概ね同じらしい。

ただし、加齢によって試行回数は減る。30代以降は徹夜できなくなるし、疲労回復も遅くなり休息時間が増える。集中力も衰えてくる。試行に費やすための可処分時間が減るうえに、1回の試行に費やす時間が増える傾向にある。試行回数が減ることで、一定の期間において成功を得られる可能性は低くなる。

では試行回数をなるべく減らさないためにはどうすればよいか? そう考えた時、結局は「心身の健康」と「体力」が重要だということになる。あと「お金」は、それで直接的に「心身の健康」と「体力」が得られる訳ではないが、「心身の健康」と「体力」を効果的かつ効率的に維持・増進するための諸々を間接的にサポートするツールとして用いることが可能だ。

付け加えると、就職氷河期世代として未だに「失業して収入ゼロ! 来週ぐらいまでは何とかなるけど、来月以降の食事と家賃をどうしよう……次の仕事、見つかるかなあ……」という夢を1年に1回ぐらい見るので、「心の健康」という観点で、究極的には、経済的自立が可能なぐらいの資産があると心置きなくコードを書けるのかもしれない。そんなものないけど。

ところで、加齢によって衰える能力がある一方で、加齢によって成熟する能力もある。だから若い頃と同じアプローチはうまくいかなくなる。「環境」「努力」「才能」のうち「才能」の絵柄が変化しているのだから、それに見合った「環境」と「努力」の絵柄を見定めなくてはならない。

世に出回っている「40代エンジニアの生存戦略」みたいなアレコレは、実のところ「環境」と「努力」の絵柄の話だ。残念ながら「才能」の絵柄とその変化は人それぞれ異なるし、肝心の記事にはターゲットとしている「才能」の絵柄が描かれていないことが多いので、大抵は赤の他人が読んでも意味がない。あとコの業界は意外と幅広く、今の自分がよく目にする「環境」の絵柄も人それぞれなので、「環境」の絵柄を固定しない総花的な記事は毒にも薬にもならず、「環境」の絵柄を固定した記事は自分自身のそれとのミスマッチで役に立たないことが多い。

「環境」の絵柄を探るには、時に転職のような「心身の健康」と「体力」と「お金」がかかる札を切ることになるだろう。「努力」の絵柄も、手を変え品を変え試行するには「心身の健康」と「体力」が重要となるし、自己投資の元手として「お金」を使うこともあるだろう。

そう考えると、結局は「心身の健康」と「体力」と「お金」が重要なのだろう。どれも欠けているけど。

*1:何をもって「成功」とするかはさておくとする。

自作NAS時代の終焉

Mini-ITXの自作PCにSambaを入れてNASとして運用してきたのだが、次のNASは自作せずにNASキットを購入することにした。

理由は2つ。1つ目はOSのアップグレードが面倒になってきたことで、2つ目は業務を見据えた人柱となるためだ。

Ubuntu Server LTSを使ってきたのだが、16.04・18.04・20.04とアップグレードしてきて、微妙に調子が悪くなってきた。再インストールすればよいのだが、あまり再インストールのことを考えていないパーティション構成なので、作業が面倒である――ということで放置していた。

そうこうしているうちにハードウェアも古くなってきたので、新しいNASを調達してデータを移行することにしたのだ。

ここで、LinuxベースのNASキットならば、製品サポート中は「新しいバージョンのOS」へのアップグレードに対応しているだろうし、いざとなれば再インストールも比較的手軽に実行可能ではないか、と考えたのである。

(個人的には、LAN内でのファイル共有サービスに限って言うなら、Linux + Sambaの構成のNASよりもWindows Server IoTモデルのNASの方が信頼できるのだが、残念ながらOSをアップグレードできないので……)

まだ何を買うかは検討中だが、ちょっと調べた範囲でも、QNAPやSynologyのNASキットはNASという枠組みを超えてアレコレとできそうな代物だ。

ひとまずQNAPなりSynologyのNASキットを触って評価してみようと考えている。評価結果は業務にバックポートするつもりだ。

実は仕事でWindows Storage ServerモデルのNASを使っていて、サポート期限の関係でそろそろ移行先を考えなくてはならないのだが、よい移行先が見つからないのである。

多拠点化や在宅勤務の導入などにより、クラウドでデータをやり取りすることが主流となったが、かといって全てのデータをクラウドに置くわけにもいかないので、NAS自体は廃止できない。

オンプレミスでActive Directoryを運用してきたような組織ならば、Entra IDと連携させて云々――みたいな理由でWindows Server IoTのNASに移行するメリットはあるかもしれない。でも小所帯でADも使ってなくてクラウド利用率が多くて――みたいな環境で、NASの利用頻度も低いとなると、わざわざ高額なWindowsモデルのNASを選ぶのもちょっとなあ……といった感じである。

QNAPやSynologyのNASキットが「今の仕事でのNASの使い方」に耐えうるならば、移行先として検討してもよいだろう。とりあえず、実際に触ってみなくては……。

我々は非公開の関数やメソッドをどうやって個別に単体テストするか?

前回からの続きである。

eel3.hatenablog.com

「非公開の関数やメソッドを個別に単体テストするか否か」という観点では、現状では「組み込みソフトウェア(特にファームウェア等のハードウェア寄りのもの)の開発なら有りでしょ」という見解に至ったのであった。

では次に、どうすれば非公開の関数やメソッドを個別に単体テストすることが可能となるのだろうか? 要は「どうやって実現するか?」というテクニカルな話である。

組み込み開発で定番のC言語やC++を使っているならば、話は割と簡単である。プリプロセッサを悪用すればよい。

例えばソースファイルfoo.cに定義されている関数群について、テストコードをファイルtest_foo.cに書こうとしているものとする。

test_foo.cにて以下のようにプリプロセス指令を悪用することで、foo.cのファイルスコープに隠ぺいされている関数・変数・型定義などに直接アクセスすることが可能となる。

// test_foo.c

#include "foo.c"

// これ以降、foo.cのファイルスコープ中の諸々にアクセス可能!

C言語やC++のプリプロセッサの役割が本質的に「C/C++にチューニングされたテキスト処理」であることを理解しているならば、これがどういう詐術なのか分かるだろう。あと単体テスト用のプロジェクトの「コンパイル対象のファイル」にfoo.cを含めてはならないことも。

C++でクラスを用いていて、プライベートなメンバ変数・メンバ関数に直接アクセスしたい場合にも、もう少し悪用度を上げることで対応できる。

例えばソースファイルbar.hに定義されているクラスBarのプライベート関数を直接実行したいなら、テストコードを書くファイルtest_bar.cppにおいて:

// test_bar.cpp

#define private public
#include "bar.h"
#undef private

// これ以降、bar.hに定義されたclassのprivateな諸々にアクセス可能!

非常に乱暴な力技だが、これで何とかなる。

問題は、言語仕様に含まれる機能にてこんな無茶をできる現役のお仕事用言語がCとC++ぐらいしかない、ということである。よりモダンな言語が組み込み開発の現場に下りてきた時にどうなるのか(そもそも、そんな未来においても内部品質に踏み込んだ単体テストが必要とされるのか否か)、思案のしどころである。

我々は非公開の関数やメソッドを個別に単体テストするべきなのか?

非公開の関数やメソッドにたいして独立した単体テストを実施するか否かについては、正直なところ「ケースバイケース」と答えるしかない。ただし、ある種の傾向は見られるように思う。

そもそも関数やメソッドにたいする単体テストには、大まかに以下の観点がある。

  • 関数やメソッドの外部品質を検証する。
    • 関数やメソッドの外部仕様を明確化して、仕様に基づいてテストを実施し、仕様を満たしているか否かをテストする。
  • 関数やメソッドの内部品質を検証する。
    • 関数やメソッドの内部実装を元にテストケースを作成して、テストを実施し、中身が健全で問題ないか否かをテストする。

外部品質や内部品質に関するテストをどこまで実施するかは、以下の要素を勘案して決めることになる。

  1. 要求される品質
  2. 開発に使用する言語・フレームワーク・OS等の抽象度
  3. そのソフトウェアが頻繁かつ継続的に書き換えられるか否か

先ほど「ケースバイケース」と書いたのは、これらの要素が可変のパラメータだからである。一方で、開発するソフトウェアの分野に応じて各要素の値に偏りが生じることにより、俯瞰視点では「ある種の傾向」が見えてくる。

要求される品質

要求品質について、少なくとも組み込みソフトウェア(ファームウェアや、ファームウェアに組み込まれる可能性のあるミドルウェア)の開発ぐらいに高い品質が求められるケースにおいては、単体テストにおいて外部品質と内部品質の両方が検証対象となる。

組み込み開発では、コードカバレッジの基準として命令網羅や分岐網羅が挙げられることがある。内部品質が検証対象に含まれているからこそ、網羅条件に関する用語が登場する訳だ。それも、単純に網羅することを目的とするのではなくて、「このテスト条件では、この経路を通過するはずだよね?」という仮説と検証を繰り返した結果としての「命令網羅100%」や「分岐網羅100%」のような基準が設けられている世界である。

外部に公開されている関数やメソッドについて、外部品質だけでなく内部品質も検証するのなら、それらの中身の一部を構成する非公開の関数ないしメソッドについても、詳細にテストする必要がある。詳細にテストするにあたり、非公開の関数・メソッドを個別にテストできると、色々と都合がよい。

開発に使用する言語・フレームワーク・OS等の抽象度

開発に用いる言語については、「C言語ないし『C由来のAPIを頻繁に利用するC++』」とそれ以外の言語の間に大きな壁があるように思う。

C言語では手動でリソース管理する。malloc(3)したらfree(3)するし、open(2)したらclose(2)する。このような環境においては、ある関数において「内部で動的に確保したリソースを、関数終了時に解放する」という仕様を満たしていることを保証するためにも、実行時に関数内で通過する式や文の経路を検証することでリソース解放関数が呼ばれていることを確認する――つまり内部品質の観点での検証を取り入れることになりがちである。

C++においても、ベタにC言語由来のAPIを叩いているなら状況は同じだ。std::mutexにたいするstd::lock_guardのようにRAIIを取り入れる、みたいな工夫も考えられるが、それが面倒なことも多い(しっかりと設計しようとすると、意外と時間や手間がかかるものだ)。

C言語やC++以外で業務で使われることが多いモダンな言語では、ガベージコレクションやObjective-C/SwiftのARCのような仕組みが導入されている。それらにも苦労するポイントは無くはないのだが、しかし手動でリソース管理しなくてもよくなることで、リソース管理に関して内部品質の観点での検証が不要となる傾向にある。C言語やC++と比べて、単体テストにおける網羅条件のプレゼンスは低い。

そのソフトウェアが頻繁かつ継続的に書き換えられるか否か

アジャイルなどのモダンな開発スタイルにおいては、コード・ベースは頻繁かつ継続的に書き換えられる。このような環境においては、単体テストにおいて内部品質に踏み込んだテストコードを書いてしまうと、コード・ベースの書き換えによってテストコードの書き換えも発生してしまう。

このコストをどうとらえるべきだろうか? 律儀にテストコードも書き換えるコストと、ソフトウェアの内部品質が保たれるメリットは、釣り合いがとれているだろうか? 要求品質は開発するソフトウェア次第だ。もしかしたら、単体テストのレイヤーにて内部品質に踏み込むよりも、例えば「単体テストによる外部品質の検証」に「他の何かしらの手法」を併用することで、より低コストで要求品質をクリアできるかもしれない。

そもそも、全てのソフトウェア開発において、コード・ベースが頻繁かつ継続的に書き換えられるものだろうか? 少なくともファームウェア開発においては、一旦リリースされたコード・ベースが頻繁かつ継続的に書き換えられることは非常に稀だろう。

キーポイント:組み込みソフトウェア開発か否か

2024年現在においては、おそらく「組み込みソフトウェア開発か否か」がキーポイントだと言えるのではないだろうか?

組み込み開発ぐらいの高い品質が要求される場合には、単体テストにおいて外部品質だけでなく内部品質の観点での検証も行う必要がある。

特に組み込み開発においては、今でもC言語が多用されている。C++を用いる場合にも、「組み込みC言語」相当の標準ライブラリ機能しか使えない、ヒープ領域を用いる機能を使えない等の制約があり、C言語と同じように手動でリソース管理することも多い。リソース管理の観点においても、内部品質に踏み込んだ検証が有効である。

そして組み込み開発で書かれたコードはあまり書き換えられる事がない。コード・ベースがFixされる環境においては、内部品質に踏み込んだ単体テストを書いても、テストコードのメンテナンスのコストは低い。

以上を勘案すると、組み込みソフトウェア開発においては、単体テストにおいて内部品質に踏み込んだ検証を行うことは有効だろう。その一環として、非公開の関数やメソッドにたいして独立した単体テストを実施することは、悪くないアイデアだと言える。

一方で、一般的なアプリケーション開発においては、単体テストにおいて内部品質も検証することは、目標品質にたいして少々オーバースペックとなる可能性がある(より高水準なテストにおいては、依然として内部品質の検証も重要であるが……)。

開発においては、C言語やC++よりもモダンな言語とフレームワークが用いられる。この結果として、大半のシチュエーションにおいて手動でのリソース管理が不要となる。C++を用いる場合にも、モダンC++流のリソース管理を導入することにより、リソース管理の複雑さは低減する。内部品質に踏み込んでリソース管理に関する検証を行う必要性は低減する。

アジャイル等のモダンな開発スタイルが導入されることも多く、コード・ベースは頻繁かつ継続的に書き換えられる。そのため、内部品質に踏み込んだ単体テストを書いてしまうと、テストコードをメンテナンスするコストが高くついてしまうだろう。

色々と考えると、一般的なアプリケーション開発においては、単体テストにおいて内部品質も検証することは、コストとメリットの釣り合いが取れているとは言い難い。単体テストでは外部品質の検証をメインとして、単体テスト以外の手法(テストだけでなく内部設計も含む)を併用することで品質を確保する、という方針をとるべきだろう。内部の「詳細な実装」の一部である非公開の関数やメソッドについては、独立した単体テストを実施するべきではない。

今はこんな感じだが、環境の変化によって今後変わる部分も多々あるだろう。特に組み込みソフトウェア開発においては、C言語やC++に代わるモダンな言語(今はRustが注目されているのだろうか?)の採用や、組込みLinuxなどでユーザランドで動かすソフトウェアを書く機会の増加など、単体テストの実施方針に影響を与えそうな要素が見え隠れしているように思う。

using namespace stdしたくなった時に検討すること

using namespace stdしたくなった時、そこに確固たる理由があるならば、次の2点について検討すること。

  1. スコープを限定する。
  2. 対象とする識別子を限定する。

ある程度C++と戯れたことがある人ならば、using namespace stdについて理解しているだろう。理解した上で、それでもなおusing namespace stdしたいとなれば、そこには確固たる理由があるはずだ。

そんなこと考えずに軽々しくusing namespace stdしようとしている人や、あるいはC++初学者でサンプルコードを真似てusing namespace stdと書こうとしている人は、using namespace stdの効果と副作用について学んだ後に、再度using namespace stdするか否かを検討すること。

スコープを限定する

using namespace stdのようなusing指令(usingディレクティブ)は、名前空間や関数の中に記述することができる。この時、名前空間の中に記述したならその名前空間のブロックの中でのみ、関数内なら当該関数の中でのみ、using指令が有効となる。

旧来のファイルスコープの考え方も有効だ。コンパクトなソースファイルならば、必要なヘッダファイルをひとしきりインクルードした後にusing namespace stdしても大丈夫なケースもありうるだろう。

using namespace stdの効果を無分別に広範囲にばら撒くのは危険だが、スコープを限定することで制御可能な範疇に留めることができるかもしれない。

なお、ヘッダファイル中のグローバルなスコープにusing namespace stdと書く奴は、5分の1の確立でイスの脚に右足の小指をぶつける呪いにかかると風の噂に聞いたことがある。

対象とする識別子を限定する

using指令ではなくusing宣言を用いて、ターゲットとなる識別子を限定するのも悪くないだろう。

例えば、ターゲットとしてstd::coutとstd::endlだけで十分ならば、using namespace std;よりもusing std::cout; using std::endl;の方がマシだろう(C++17以降ならばusing std::cout, std::endl;と書くことも可能だ)。

using宣言にもスコープの概念が適用されるので、必要最小限の範囲でのみusing宣言が適用されるようにするとよい。

なお、ヘッダファイル中のグローバルなスコープにusing宣言を書く奴は、週に1回ほど自宅から50m離れたころに玄関を施錠したか思い出せなくて猛烈に気になる呪いにかかると、その筋の専門家が話しているらしい。

using namespace stdの副作用についての私見

using namespace stdの意味と効果については他所に解説を譲るとして、using namespace stdの悪影響は、以下の2点の合わせ技から生じているように見える。

  1. std名前空間の中に、標準ライブラリの「結構な分量の識別子」が、割とフラットに存在する。
  2. 標準ライブラリの識別子に「比較的一般的なワード」が多く含まれている。

モダンなプログラミング言語の標準ライブラリとは異なり、C++の標準ライブラリの機能は比較的フラットな感じに公開されていることが多い。例えば<algorithm>ヘッダで公開されているテンプレート関数の多くはstd名前空間の直下に定義されている。

そのため、using namespace stdすることで、予想外に多い識別子と干渉しうる状態になってしまう。

(後発のプログラミング言語の流儀に従うならば、例えばstd::algorithmという名前空間を定義して、その直下にテンプレート関数を定義する、みたいなアプローチがとられるように思うのだが、どうだろうか?)

その上で、標準ライブラリで用いられている識別子の多くは、比較的一般的なワードだ。一般的である分だけ、開発者が使おうと思ったワードと被る可能性が高くなる。

合わせ技の(1)と(2)のどちらか一方だけでも現状と異なっていたならば、using namespace stdの悪影響はもう少し小さくなっていたのではないだろうか?

まあC++は意外と歴史がある上に、より古いC言語の諸々(仕様もライブラリも資産も!)を引き継いでいる訳で、using namespace stdの悪影響については「結果としてそうなっちゃった……」という部類の代物だろうと思っている。

この辺の状況はC++20以降のモジュールの導入によって徐々に変化するだろう、と期待している。同時に、C++23で導入されたのがstdモジュールとstd.compatモジュールである点を鑑みるに、標準ライブラリ全体のモジュールの再編にはまだまだ時間がかかりそうだと感じている(何しろ再編が大変だから「最小限のモジュール化」としてstdモジュールが導入された訳なので)。

シェルスクリプトにて無駄に「いい感じの見た目」で変数展開されるように頑張った話

Super-microDXにインストールした河豚板をfiupdateするためにISOイメージが必要となるが、毎回URLを調べてダウンロードするのが面倒なので、ダウンロード用のシェルスクリプトdl-fuguita.shを作成して使い回すようにした。

#!/bin/sh
# -*- coding: utf-8-unix -*-
# vim:fileencoding=utf-8:ff=unix
# @(#) Download a FuguIta ISO image file.

set -u
umask 0022
IFS=$(printf ' \t\n_'); IFS=${IFS%_}
PATH=/bin:/usr/bin
export IFS LC_ALL=C LANG=C PATH

readonly progname="${0##*/}"
readonly version=1.0.0

# usage <exit-code>
usage() {
    echo "usage: $progname [-hv] <revision>" 1>&2
    exit "$1"
}

# version (no parameter)
version() {
    echo "$progname $version" 1>&2
    exit 0
}

# main routine

readonly url=https://jp2.dl.fuguita.org/LiveDVD/
readonly arch=i386
readonly ver=7.5

opt=
while getopts hv opt; do
    case $opt in
    h)      usage 0 ;;
    v)      version ;;
    \?)     usage 1 ;;
    esac
done
shift $((OPTIND - 1))

[ $# -eq 1 ] || usage 1
rev=$1

ftp $url{SHA256,FuguIta-$ver-$arch-$rev.iso.gz}

普段curl(1)やwget(1)を使っているLinuxユーザとしては、ftp(1)でHTTPサーバからファイル取得するのは、ちょっと不思議な感じだ。軽く調べた感じでは、BSD系のftp(1)はHTTPによるファイルダウンロードにも使えるらしい。

dl-fuguita.shを作ったことで、fiupdateする際に、次のような感じのワンライナーを実行すればよくなった。

# バージョン202408021にアップデートする場合:
( rev=202408021; ./dl-fuguita.sh $rev && fiupdate $rev )

このdl-fuguita.shに「ダウンロードが中断した場合に、後で続きからダウンロードできる」ための仕組みを追加したくなった。OpenBSDのftp(1)では、オプション-Cを付ければよいらしい。そこで、シェルスクリプトのオプションに-Cがあったらそのままftp(1)に引き渡すようにしてみた。

実装はこんな感じ。今後、似たような「引数なしオプション」を増やしたいときに流用できるように、ほんの少しだけ凝った書き方をしている。

# 前略

ftpopt=

opt=
while getopts Chv opt; do
    case $opt in
    C)      ftpopt="$ftpopt -$opt" ;;
    h)      usage 0 ;;
    v)      version ;;
    \?)     usage 1 ;;
    esac
done
shift $((OPTIND - 1))

[ $# -eq 1 ] || usage 1
rev=$1

ftp $ftpopt $url{SHA256,FuguIta-$ver-$arch-$rev.iso.gz}

オプション-Cを2回以上指定されたケースを考慮していないが、自分用のスクリプトなので、手抜きでもOKとしている。

さて、こんな感じの実装で十分に機能するのだが、しかし個人的に最終行(ftp(1)を叩いている行)で$ftpoptを展開した結果が気になったのだ。

例えば、$ftpoptが空の場合、ftpと$urlの間の空白が2文字になる。またオプション-Cを1回だけ指定した場合も、ftpの直後の空白が2文字になる。

この部分を、もっと、こう、無駄に「いい感じの見栄え」で展開されるようにできないか考えた結果、こんな感じで実現できることが分かった。

# 前略

ftpopt=

opt=
while getopts Chv opt; do
    case $opt in
    C)      ftpopt="${ftpopt:-}${ftpopt:+ }-$opt" ;;
    h)      usage 0 ;;
    v)      version ;;
    \?)     usage 1 ;;
    esac
done
shift $((OPTIND - 1))

[ $# -eq 1 ] || usage 1
rev=$1

ftp ${ftpopt:-}${ftpopt:+ }$url{SHA256,FuguIta-$ver-$arch-$rev.iso.gz}

${ftpopt:-}${ftpopt:+ }は、$ftpoptが未定義か空文字の場合には空文字に展開されて、$ftpoptに1文字以上の文字が設定されている場合には「$ftpoptの中身 + 1文字の空白文字」に展開される。

例えば最終行は、$ftpoptが空なら「ftp $url」のような感じになり、$ftpoptの中身が-Cなら「ftp -C $url」のような感じになる。空白が2文字にならずに済むのだ。

こんな感じに、いい感じの見た目に変数展開されるようになったが、特に意味はない。全くもって無駄な行為である。

C++で妙なリンクエラーに遭遇した話

最近、自作ライブラリに機能を追加したのだが、実装中に妙なリンクエラーに遭遇して右往左往したので、メモを残しておく。当初、へなちょこC++使いの私には原因がつかめず、とりあえず回避策でお茶を濁していた。

追加したのはstd::this_thread::sleep_for()のラッパー関数だ。見ての通り、非常に簡単な内容である。

これらの関数は、当初は引数の型をconst参照にしていたのだが、謎のリンクエラーが発生したため、ひとまず参照ではなく実体を渡すようにしたバージョンをコミットした、という経緯がある。例えば関数cun::sleep::millis()の引数の型は、前掲のソースコードではconst std::chrono::milliseconds::repになっているが、初期実装ではconst std::chrono::milliseconds::rep&だった。

// 初期実装時のプロトタイプ宣言
extern void millis(const std::chrono::milliseconds::rep& r) noexcept;
// コミットした版のプロトタイプ宣言
extern void millis(const std::chrono::milliseconds::rep r) noexcept;

この関数を含むライブラリcunは、ユーティリティ類をまとめた静的ライブラリlibcunをビルドした上で、個々のテストアプリにてビルド済みライブラリをリンクして使用する構成になっている。

問題は、テストコードをコンパイルした後、静的ライブラリをリンクする時に発生した。Ubnutu 22.04のGCC 11.4.0がこんなリンクエラーを出力したのだ。

/usr/bin/ld: test_sleep.o: in function `main':
test_sleep.cpp:(.text.startup+0x182): undefined reference to `cun::sleep::millis(long const&)'

興味深いことに、Visual Studio 2022でもリンクエラーが発生した。

test_sleep.obj : error LNK2001: 外部シンボル "void __cdecl cun::sleep::millis(__int64 const &)" (?millis@sleep@cun@@YAXAEB_J@Z) は未解決です
test_sleep.exe : fatal error LNK1120: 1 件の未解決の外部参照

最初は関数名などのスペルミスを疑ったのだが、ミスはなかった。というか、そもそもVisual Studio Code上で関数の定義元に正しくジャンプできるので、名前は正しいはずだ。

一体、何が起きているのだろうか? 疑問はオブジェクトファイル内のシンボル名を見たら半分だけ氷解した。

cun::sleepにはsecs()、millis()、micros()、nanos()の4個の公開関数が定義されているのだが、そのうちmillis()のみ、ライブラリ側とテストコード側とでシンボル名が食い違っていた。

$ nm ../../../build/linux/libcun.a | grep -F sleep
sleep.o:
0000000000000000 T _ZN3cun5sleep4secsERKl
0000000000000000 t _ZN3cun5sleep4secsERKl.cold
00000000000001e0 T _ZN3cun5sleep5nanosERKl
000000000000002d t _ZN3cun5sleep5nanosERKl.cold
0000000000000130 T _ZN3cun5sleep6microsERKl
000000000000001e t _ZN3cun5sleep6microsERKl.cold
0000000000000080 T _ZN3cun5sleep6millisERl
000000000000000f t _ZN3cun5sleep6millisERl.cold
                 U nanosleep
$ nm test_sleep.o | grep -F sleep
                 U _ZN3cun5sleep4secsERKl
                 U _ZN3cun5sleep5nanosERKl
                 U _ZN3cun5sleep6microsERKl
                 U _ZN3cun5sleep6millisERKl
$ _

シンボル名の末尾を見てみると、テストコード側は全てERKlであることを期待しているのだが、ライブラリ側にてなぜかmillis()のみERlとなっている。これでは確かにリンクエラーとなるはずだ。

Visual Studio 2022でも同様に、シンボル名が食い違っていた。エラーメッセージから推測するに、テストコード側は?millis@sleep@cun@@YAXAEB_J@Zというシンボル名を期待していたようだが:

> dumpbin /NOLOGO /LINKERMEMBER ..\..\..\build\msvc\libcun.lib | findstr /l sleep
    C71F4 ??$sleep_for@_JU?$ratio@$00$00@std@@@this_thread@std@@YAXAEBV?$duration@_JU?$ratio@$00$00@std@@@chrono@1@@Z
    C71F4 ??$sleep_for@_JU?$ratio@$00$0DLJKMKAA@@std@@@this_thread@std@@YAXAEBV?$duration@_JU?$ratio@$00$0DLJKMKAA@@std@@@chrono@1@@Z
    C71F4 ??$sleep_for@_JU?$ratio@$00$0DOI@@std@@@this_thread@std@@YAXAEBV?$duration@_JU?$ratio@$00$0DOI@@std@@@chrono@1@@Z
    C71F4 ??$sleep_for@_JU?$ratio@$00$0PECEA@@std@@@this_thread@std@@YAXAEBV?$duration@_JU?$ratio@$00$0PECEA@@std@@@chrono@1@@Z
    C71F4 ??$sleep_until@Usteady_clock@chrono@std@@V?$duration@_JU?$ratio@$00$0DLJKMKAA@@std@@@23@@this_thread@std@@YAXAEBV?$time_point@Usteady_clock@chrono@std@@V?$duration@_JU?$ratio@$00$0DLJKMKAA@@std@@@23@@chrono@1@@Z
    C71F4 ?micros@sleep@cun@@YAXAEB_J@Z
    C71F4 ?millis@sleep@cun@@YAXAEA_J@Z
    C71F4 ?nanos@sleep@cun@@YAXAEB_J@Z
    C71F4 ?secs@sleep@cun@@YAXAEB_J@Z
        3 ??$sleep_for@_JU?$ratio@$00$00@std@@@this_thread@std@@YAXAEBV?$duration@_JU?$ratio@$00$00@std@@@chrono@1@@Z
        3 ??$sleep_for@_JU?$ratio@$00$0DLJKMKAA@@std@@@this_thread@std@@YAXAEBV?$duration@_JU?$ratio@$00$0DLJKMKAA@@std@@@chrono@1@@Z
        3 ??$sleep_for@_JU?$ratio@$00$0DOI@@std@@@this_thread@std@@YAXAEBV?$duration@_JU?$ratio@$00$0DOI@@std@@@chrono@1@@Z
        3 ??$sleep_for@_JU?$ratio@$00$0PECEA@@std@@@this_thread@std@@YAXAEBV?$duration@_JU?$ratio@$00$0PECEA@@std@@@chrono@1@@Z
        3 ??$sleep_until@Usteady_clock@chrono@std@@V?$duration@_JU?$ratio@$00$0DLJKMKAA@@std@@@23@@this_thread@std@@YAXAEBV?$time_point@Usteady_clock@chrono@std@@V?$duration@_JU?$ratio@$00$0DLJKMKAA@@std@@@23@@chrono@1@@Z
        3 ?micros@sleep@cun@@YAXAEB_J@Z
        3 ?millis@sleep@cun@@YAXAEA_J@Z
        3 ?nanos@sleep@cun@@YAXAEB_J@Z
        3 ?secs@sleep@cun@@YAXAEB_J@Z
> _

ライブラリ側のシンボル名は?millis@sleep@cun@@YAXAEA_J@Zだった。

さて、シンボル名が食い違っていることまでは分かったのだが、なぜシンボル名が食い違う状態に陥ったのか、へなちょこC++使いの私には分からなかった(C++のベテランなら、おそらくERKlとERlの違いの意味とかを調べる手立てを知っていると思われるのだが)。

そこで、この問題の回避策として、コミット済みコードから分かるように、引数の型として参照を使うのを止めた。これでシンボル名の食い違いは発生しなくなった。

$ nm ../../../build/linux/libcun.a | grep -F sleep
sleep.o:
0000000000000000 T _ZN3cun5sleep4secsEl
0000000000000000 t _ZN3cun5sleep4secsEl.cold
00000000000001e0 T _ZN3cun5sleep5nanosEl
000000000000002d t _ZN3cun5sleep5nanosEl.cold
0000000000000130 T _ZN3cun5sleep6microsEl
000000000000001e t _ZN3cun5sleep6microsEl.cold
0000000000000080 T _ZN3cun5sleep6millisEl
000000000000000f t _ZN3cun5sleep6millisEl.cold
                 U nanosleep
$ nm test_sleep.o | grep -F sleep
                 U _ZN3cun5sleep4secsEl
                 U _ZN3cun5sleep5nanosEl
                 U _ZN3cun5sleep6microsEl
                 U _ZN3cun5sleep6millisEl
$ _

この現象は異なるコンパイラ(そして異なる標準ライブラリ実装)で発生した。処理系の不具合とかではなく、もっと別の要因、ハッキリ言うと自分自身のヘマに起因しているような気がしたが、明確な根拠はなかった。

あと、どの関数も似たようなコードなのに、なぜかmillis()だけシンボル名の食い違いが発生した――という点もヒントになりそうだった。他との違いは型(std::chrono::milliseconds::rep)だけだ。しかしライブラリ側もテストコード側も同じヘッダファイル(≒同じプロトタイプ宣言)を参照していて、かつコンパイル時に警告すら出ていない。それなのに生成されるシンボル名が食い違うとは……。

原因は2日後に分かった。関数millis()だけ、ライブラリ側のプロトタイプ宣言と関数定義とで引数の型が食い違っていたのだ。

// ヘッダファイルに書かれていたプロトタイプ宣言
extern void millis(const std::chrono::milliseconds::rep& r) noexcept;

// ソースファイルに書かれていた関数定義。
void millis(milliseconds::rep& r) noexcept
{
    delay<milliseconds>(r);
}

プロトタイプ宣言では、引数の型はconst参照だった。なので、ヘッダファイル中のプロトタイプ宣言を参照したテストコード側は、const参照型を前提としたシンボル名を生成した。

一方でライブラリ側は、ソースファイルに書かれている通りに普通の参照型を前提としたシンボル名を生成した。

これにより、両者の間でシンボル名の食い違いが発生した。

なるほど、確かに理屈は通る。実際に、関数定義側の型をconst参照にしてみたら、リンクエラーは発生しなくなった。

「妙なリンクエラー」だと思っていたものは、案の定、自分が埋め込んだバグだった訳だ。

原因は分かったものの、なお個人的に納得できなかったのは、ライブラリをビルドする時に仮引数の型の食い違いが検出されなかったことだ。ソースファイルsleep.cppではヘッダファイルsleep.hppをインクルードしている。宣言と定義とで仮引数の型が異なることを検出できなかった(それも異なる2つのコンパイラで!)だなんて……。

――と、まあこんなことを考えたあたりに、C++に慣れていない(そして予想以上にC言語の影響を受けている)プログラマの後ろ姿が透けてみえるだろう。

多分、C言語なら*1、宣言と定義とで仮引数の型が異なることを検出できた可能性がある。なぜならば、C言語では関数を多重定義(オーバーロード)できないからだ。関数名が同じならば、引数や戻り値の型も一致していなくてはならない。

でもC++では関数の多重定義が可能だ。宣言と定義とで仮引数の型が食い違っているのか、それとも「const参照を引数にとるバージョン」の関数宣言と「普通の参照を引数にとるバージョン」の関数定義が存在するだけなのか、コンパイラには見分けがつかない。だから警告しないというか、警告できないというか。

うーん、C++では「宣言と定義の食い違い」にどう対処すればよいのだろうか? この問題、絶対にすでにつまづいた人がいるはずなんだよなあ。先人たちの解決策を知りたい。

やっぱり、なるべくヘッダファイルのみで完結させる(ソースファイルとヘッダファイルに分けない)ようにするべきなのだろうか? でも、それはそれでビルドにかかる時間が長くなりそうだ。

*1:もちろんC言語の言語仕様に参照は無いので、これは思考実験の類だと思ってほしい。

今までどのくらいプログラミング言語を触ってきたか(3秒で挫折したものものも含む) Ver.16

2024-06-23現在のステータス。昨年(2023-06-18)から1年経て、こうなっている。

eel3.hatenablog.com

なおCSS、HTML、XMLはひとまず除外する。人工言語ではあるけれども「プログラミング言語」という括りに含められるか否かは議論が分かれる気がする。*1

まあでも、XMLについてはメタ言語だから――XSLTみたいに、XML上に構築されたものがプログラミング言語的なことはあるよね。MSBuildのXMLスキーマもプログラミング言語っぽい部分があったりするし。

よく使っている

AWK (Gawk)
単純なテキストレコードの処理はAWKで十分間に合う。今の時代、自作ツールをnawkやGNU awk単体で実装するのは苦行すぎて*2皆無なものの、シェルスクリプトやMakefileにAWKのコードを埋め込むなどして他のコマンドと組み合わせて使う機会は依然として多い。シェル上でワンライナーでテキスト処理する時にも重宝している。これはこれで十分AWKらしい使い方ではないだろうか? ところで、はやくオプション--csvが使える処理系が広まってくれないかなあ……。
C++
最近のお仕事の主力言語で、C言語のコードをC++に移行する作業も多いのだが、しかし未だに本職のC++使いのレベルに到達できていない。まだまだC++17止まりではあるが、C++11以降は非常に便利で、better Cでも使う価値があると思っている。C言語使いからすると、C++03時代よりも充実度が進んだ標準ライブラリや、ラムダ式やautoによる型推論に始まるモダンな言語機能は、便利で羨ましい限りだ*3。あと、Swift時代のクロスプラットフォームC++ライブラリ作者は、どうあがいてもARCから逃れられないので、C++11以降のスマートポインタは必須だ*4。正規表現とスレッドとファイルシステムが標準ライブラリに加わったので、あとはソケット(低水準ネットワークAPI)をサポートしてくれないだろうか。低水準の処理を行いつつも便利な機能で実装時間を短縮できる点は便利で、少なくともシステムプログラム言語としての利点だと思う。だけど機能多すぎ/複雑すぎなところはなんとかならないものか。強力な反面、使い手を選ぶ言語だ。
C言語
お仕事での主力言語だった――ここ最近は使ってないなあ(C++のコードの一部にC言語寄りのコードを埋め込むことはあるけど)。シンプルかつ低水準の世界が垣間見れるところが割と好きだが、同時にどうしようもなく面倒にも感じる。最近の他の言語と比較すると、シンプルすぎて安全機構が欠けていたり、標準の便利機能が少なかったりするので、入門用の言語としては薦められない。にもかかわらず、かつてはプログラミング未経験者向けのC言語の本が盛んに出版されていた――あれ、何だったのだろうか? 謎だ。クロスプラットフォームなモジュール屋としては、2023年1月にVisual Studio 2012がEOLに到達したことで、ようやく大手を振ってC89からC99に移行できることを喜びたい。あとVisual Studio 2019 version 16.8以降にて本格的にC11/C17のサポートが始まったことも。一方で、macOSのXcodeでthreads.hが見つからない問題はまだ直らないのかいな? ま、まあ、まずはC89時代からの手癖をC99向けにアップデートするところから始めたいと思う。
DOSバッチファイル
プログラミング言語に含まれるかどうか不明だが、含めてしまう。ちょっとした自動化や、複数ツールを組み合わせて使うときのラッパーとして、今でもよく使う。コマンドプロンプトはシバン(shebang)に対応していないので、スクリプト言語で書いたツールを起動するラッパーとしても多用している。意外と色々なコマンドが用意されているので、単純にそれらを叩く分には十分だが――言語機能がショボいので、バッチファイルでifやforのような制御構文系コマンドが必要になってきたら、何か決定的に間違えていないか、考え直すようにしている。
make (Makefile)
プログラミング言語に含まれるかどうか不明だが、DSL扱いで……いやGNU Makeはそこそこプログラミング言語的か。GNU Make 4.0はさらにプログラミング言語的だな、特にGNU Guileなところが。GNU MakeとNMAKEが主力。昔は稀にNetBSD Make(pmake)を使うこともあった。3者いずれも独自拡張アリアリで使っている。もう素のmakeではMakefileを書けない :) 最近はLinux向けツールのインストール・スクリプトを書く時にシェルスクリプト(特にBashアリアリとか)や高水準なスクリプト言語(Pythonとか)が選ばれがちで、なかなかMakefileを見かけることが少なくて、ちょっとだけ寂しさを感じている。
Objective-C, Objective-C++
時代はSwiftだと言われて久しい――どころか場所によっては「Flutter + Dartがメインで、Swiftはサブ」みたいなこともありそうだけど、どっこいObjective-CとObjective-C++は生きている。というかSwiftのコードにC++で書かれたライブラリを直接組み込むことができない以上、両者を繋げるグルー言語として生き残ることになるよね。一定以上のリアルタイム性が求められるアプリをSwiftだけで書くのは厳しくて、どうしても部分的にC言語やC++を使うことになり、グルー言語としてObjective-Cが召喚されることになる。最近流行の言語と比べると良くも悪くも80年代的だが、アプリケーションプログラミング用としてはC言語よりマシだし、C++ほど複雑怪奇*5ではない。そしてC言語やC++で書かれた既存のライブラリをそのまま使える。Objective-Cのハイブリッドな所は好きだが、Objective-C++はハイブリッドすぎて――C++のクラスとObjective-Cのクラスを、C++のラムダ式とObjective-Cのブロック構文を同時に使うのは大変だ。便利ではあるんだけどね。
Python
最近、ようやくドキュメンテーション文字列と型ヒントを覚えた。型ヒントは便利で興味深い機能だと思う。Pythonではlazyなスタイルでのコーディングが許されず、整然とコードを記述する必要がある。その辺は、Perl 5やRubyとは随分と雰囲気が異なるように思う。少し気になるのは、インデントが必須な言語仕様であるために、シェルスクリプトに埋め込んで使うのが苦痛な点だ。Pythonだけでコードを書く分には気にならないのだけど……。
Ruby
自作ツールを実装する時、最近はPythonを使うことが多いのだが、時々Rubyを選択することもある。多言語化(文字エンコーディング)が絡むテキスト処理や、処理速度は考慮しなくてよいが桁あふれが気になる数値計算を行う場合だ。あとirb(1)も時々使っている。to_s(16)やto_s(2)で基数変換して表示できるところが割と好き。
シェルスクリプト (/bin/sh)
プログラミング言語に含まれるかどうか不明だが……いや、私的にはシェルスクリプトは立派なプログラミング言語だ。基本的な用途は、バッチファイルと同じくちょっとした自動化や複数コマンドを組み合わせて使うときのラッパーだが、実現できる内容は遥かに多い。言語本体(?)がバッチファイルよりも高機能だし、Unixユーザランドはコマンドが充実している。その意味では、WindowsではMSYSよりもCygwinで――いやむしろWSL(Windows Subsystem for Linux)で環境構築すべきだろう。Cygwinでは、主要な処理をシェルスクリプトで記述しておき、bashからはシェルスクリプトを利用し、コマンドプロンプトではラッパーのバッチファイル経由でシェルスクリプトを叩く使い方をしている。ただWindows上では処理速度が妙に遅くなる点が不満だ。まあしかし、Unixのシェルは言語設計もシステム開発技法も未成熟だった大昔に「プアな環境でも問題なく動作する、プログラマブルな対話型コマンドインタプリタ」として開発された代物なので、言語設計の研究が進んでから作られたプログラミング言語と比較してはならない。なお自分自身が落とし穴に嵌らないようにShellCheckを活用すべし。

あまり使っていない

Scheme
GaucheのWindowsネイティブ環境用バイナリは実験版だが、私が触る分には何の支障もない*6ことに気づいて久しい今日この頃。『Scheme手習い』と『Scheme修行』を購入したので、とりあえずCommon LispではなくGauche(Scheme)の勉強をする方向に転換しようか検討*7しているうちに何年たったのやら。Gaucheはフィルタ・ライクな小ツールの実装用としても良い感じだ。しかし最も多い利用方法はREPLを電卓代わりにすることだ*8。うーん、作業環境がmacOSかLinuxに移ったなら、大手を振ってGaucheでフィルタを書くのだが。
sed
プログラミング言語に含まれるかどうか不明だが、DSL扱いで*9。テキスト処理用。シェルスクリプトやMakefileにて他のコマンドと組み合わせて使う。というか正規表現でのテキスト置換以外の機能を使った記憶が……あったな、dとiとpと=とブレースによるグループ化ぐらいだが。私の技術レベルではsedでFizzBuzzを書けないので、sedで難しい処理を記述しないようにしている。
Windows PowerShell
時代はPowerShell Coreらしいが、現行のWindows 10でデフォルトで利用できるv5.1に留まったままである。スクリプト言語としてのPowerShellは、オブジェクト指向で.NET Frameworkを叩けてダイナミックスコープでスクリプトブロック(という名の無名関数)と、無茶でピーキーで完全にプログラマ向けな代物だ。Microsoftもよくもこんなエライ代物を出したものだ。残念なことに、コマンドプロンプトの代替という観点では、外部ツールとの親和性が微妙にイマイチだ(特に文字コードとか)。でもPowerShell内で閉じている分には問題ないので、私の手元では「Windows専用のGUI付き小ツールを作るためのスクリプト言語」か「Excel COMとか叩く用のスクリプト言語」か「Windows Serverの管理スクリプトを書くためのスクリプト言語」扱いしている。ところで、いい加減『Windows PowerShell イン アクション』並みの言語解説書の最新バージョン対応版を出版してくれないだろうか。

最近使ってないが、縁は切れてない

bash
最近はデフォルトシェルがbashな環境も多いので、自分用のツールぐらいは素の/bin/shではなくbashで書いても大丈夫な気がしてきた。shよりbashの方が遥かに便利だからなあ――PerlやRuby等には負けるけど。bashでスクリプトを書くときの唯一の欠点は、メジャーバージョンごとの差異や各ディストリでのビルドオプションの違いにより、同じbashという名前でも実は千差万別なところだと思う。PerlやRubyのバージョンは気にするけど、これがシェルになると意外とバージョンに無頓着になってしまう。なんでだろう?
C#
かつて、勉強を兼ねてC# 2.0を少し触ろうとするも未完に終わり、数年後にあらためてVisual Studio 2013をインストールして少しだけ触った*10けどほんの少しだけで終わった過去をもつ私。変数の型推論・ラムダ式・LINQ・デフォルト引数は便利だなあと思っていたら、いつの間にかC# 8.0になってKotlinやSwiftに見られる流行を取り入れてますな。おっちゃん、付いてくのが大変だよ。.NET Frameworkの機能数は反則ものだが、所々に微妙に抽象化が行き過ぎたAPIが見られるのは気のせいだろうか? それにしても、クラスが必須ではないC言語やC++に慣れてしまった弊害か、アプリケーション・メインエントリすらclass内に定義しなくてはならないC#には、なかなか慣れない。
Free Pascal
お試しで触っているのだが、微妙にDelphi/Free Pascal初心者(ただし他言語の経験者)向けの良い資料が少なくて難儀している。玉石混交なのだ。いっそのこと『OBJECT PASCAL HANDBOOK: マルチデバイス開発ツ-ルDelphiのためのプログラミング言語完全ガイド』を買ってしまおうかしら……と思っていたら絶版っぽい。
Go
寡作ながらもいくつか小ツールを書いてみたが、標準ライブラリが充実しているコンパイラ型言語っていいっすね。C言語に比べればC++の標準ライブラリも充実しているが、どちらかといえばプリミティブな機能が中心だ。PythonやRubyばりの標準ライブラリを抱えているGoには及ばない。その辺は、やはりCプログラマ(特にCでフィルタやデーモンの類を書く層)には受けそうな言語だと思う。並列処理周り(goroutines)とかARM対応とかが気になる。ソフトリアルタイム限定だが「組み込みLinux + Goで書いたデーモン」とかどうだろう? ただメモリを食うらしいという噂がどうなったか気になる――64bit環境では解消されるという話だったようだが、32bit環境でも解消されるようになったのだろうか? 組み込みでは現時点では逆立ちしたって64bit CPUはありえないからなあ、スマホやタブレット以外では。
Java
生まれて初めて触れたプログラミング言語その2。実のところ、職業プログラマとして本格的に使用することは一生ないと思っていた。Androidアプリ開発も、Kotlin採用後に本腰入れて関わるようになったので、Kotlinメインだ。だが、なぜかぬるい感じに時々Javaのコードを触っている。先にコレクションの操作方法が充実した他の言語を学んでからJavaを本格的に触るようになったので、Java 8以降のStream APIが使えないと身体が拒否反応を示す。少なくとも、構文の見た目こそ保守的なオブジェクト指向プログラミング・スタイルで書かれたC++に似ているけど、中身はC++とは似ても似つかない代物だということは体感している。
JavaScript(クライアントサイド)
ものすごく久しぶりにクライアントサイドJavaScriptのコード触った――いまどき珍しい、DOM直叩きスタイルだけど。収穫は、最近のECMAScriptのスタイルに触れたぐらいだろうか? フレームワークもTypeScriptも触っていないので、クライアントサイド開発のスキルは依然として賞味期限切れのままだ。
JavaScript(サーバサイド?)
初めてお仕事でNode.js向けのJavaScriptのコードを触った。Web開発の外の人からみた印象としては、ブラウザ以外のJavaScript処理系はNode.jsに収斂しちゃった感があるなあ――Denoもあるけど、エコシステム的に今後どうなるんだろう?
Kotlin
本格的にAndroidアプリ開発に関わるようになったのがGoogle I/O 2017直後の過渡期なので、JavaよりもKotlinでの経験値の方が多い。モダンな「強い静的型付け」の、割とええ感じの言語やね。ただ、使い始めが「Swift 3をつまみ食いして半年以上経ってからKotlinをつまみ食いした」みたいな経緯だったこともあり、未だに両者の概念・機能が頭の中でごった煮になっている。それと、NDK絡みの作業が多いので、C++17・Java 7/8・Kotlinを行ったり来たり。泣けるぜ。Swiftもそうだが、最近のメジャーな「強い静的型付け」の言語は「開発環境込み」で高い生産性とコードの安全性を両立させる方向に進んでいる気がする。
Lua
Wiresharkのパケット解析スクリプトを書いたことも、C言語で書かれたUnixデーモンの設定ファイル用に処理系を組み込んだこともあった*11。あれから数年経ったが、今はどんな感じなんだろう?
Perl 5
時々、やむをえない事情で触ることがある。だが基本的によく分からない。何というか、あの記号の羅列っぽさに中々慣れないというか、自分は余りに自由度が高すぎる言語は苦手だと気づいたというか。(言語仕様に慣れているなら)半ば使い捨てなテキストフィルタとかをさっと書くに分には悪くない言語だと思うのだけど。
SQL
生まれて初めて触れたプログラミング言語その3ぐらいに位置する。組み込みの人なのでSQLとは無縁だと思っていたが、まさかTransact-SQLを少しだけ触ることになるとは。最近はAndroidアプリ絡みでSQLiteに触れることもあるが、AndroidXのRoom経由だったり、ContentResolverのqueryだったりと、フルセットのSQL文ではなく局所局所でDSL的に使う感じである。
Swift
「コンパイラによる強力な型推論と型安全性のチェック」がお仕事用のメジャーな言語にまで降りてきたという点で、Swiftは静的型付け言語界のJavaScript*12だと思っている。でもユーザ数的には、Kotlinが「静的型付け言語界のJavaScript」ポジションなのかもしれない。割と好感が持てる言語だが、知識が中途半端にKotlinとごった煮になっているので、ついうっかりif式を書こうとしてコンパイルエラーになったり、「varとval」と「varとlet」の振る舞いの差異につまづいたりしてしまう*13。
Tcl/Tk
Tclは書き方を忘れた頃にテキスト処理ツールを書いている気がする。Tclは結構独特な言語だ。構文がシェルスクリプトばりに全てコマンドだったり、値が全て文字列だったり、実はリスト構造だったり、意外とTCPソケット通信が得意だったり……。それでも慣れれば結構使いやすい。意外とプロトタイピングに向いている気がする。8.6以降ではオブジェクト指向プログラミングもOKだが、それよりも例外処理用のtry末尾呼び出しの最適化用のtailcallの方が興味深い。しかし、これからメジャーになる可能性は低そうだ。Tkは……小規模なGUIツールをさくっと構築できるところは便利だが、Webアプリ全盛の時代にどれだけ訴求力があるのやら。
Visual Basic .NET
Visual Basic .NET 2003で書かれたコードを時々メンテ中。流石に開発環境はVisual Studio 2013移行したけど。
XSLT
よく考えてみたら生まれて初めて触れたプログラミング言語その4ぐらいに位置する言語だった。縁が切れたと思いきや、仕事でXHTMLから特定要素を抜き出す作業に使うことがあったり……。XMLからテキストレコードに変換してしまえば、後はUnix流テキストフィルタの世界が待っている。餅は餅屋というもので、定型的なXMLの変換はXSLTで記述するべきか。唯一気に入らないのは、xsl:sortでアルファベットの大文字と小文字を区別してソートすることができないこと。ぐぬぬぬ。

これから(また)使うかもしれない

Alloy
形式手法の中では比較的カジュアルに使えそうなので期待中。入門書も処理系も入手した。私の場合、先に何か論理型の言語をかじった方がよいのかも。
bison (yacc)
プログラミング言語に含まれるかどうか不明だが、DSL扱いで。やっぱり構文解析系統のコードを自作するのは割に合わない――だなんてうそぶきつつ、LALR法とか全く知らないままに、既存のyaccのコードを切り貼りして遊んでみた。簡易電卓レベルだが便利さを体感しつつ、さっそくtypo 1文字で痛い目(shift/reduce)に遭った。とりあえず、flexと組み合わせた上でのエラー処理(エラーメッセージの改善)が課題だ。
Common Lisp
2009年に勉強しようと思い立ったものの、未だに進んでいない。階乗とかハノイの塔とかiotaぐらいは書いたが、目標は「ちょっとしたツールを自作する」だ。まだ道は遠い。最近は時々CLISPを簡易電卓代わりにしている。
Coq
ソフトウェアの基礎が気になるので、処理系だけ入手。
F#
OCamlは「Windows上で日本語を扱う」という視点では処理系がちょっと微妙なので、いっそのことF#に乗り換えようかと……。『実践F#』が積読状態になっている。
flex (lex)
プログラミング言語に含まれるかどうか不明だが、DSL扱いで。字句解析用のツールという印象が強かったのだが、よく考えてみたら、flexは「sed(1)のよくある使い方」と同様に「正規表現でパターンマッチング --> 何らかのアクション」という内容を記述するためのツールだった。ただ単に、「何らかのアクション」をC言語で書けることと、flex自体ではパターンマッチングをせずに「パターンマッチングするC言語のコード」を生成することが少々風変わりなだけ。grep(1)やsed(1)その他で小ツールを実装して運用しつつ、性能が求められたらflexで専用ツール化する――とか考えたけど、普通にgrep(1)やsed(1)を使う方が高速だった。
Forth
pForthをMinGWでビルドしたので処理系は手元にある。スタック指向の言語はいつか勉強したい。
Io
プロトタイプベースである点を除けば、何となくSmalltalk的であるような――公式ドキュメントらしきIo Programming Guideでも影響を受けた言語として真っ先にSmalltalkが挙げられているから、あながち思い違いでもないだろう。今更ながら『7つの言語 7つの世界』のIoの章を読み終えたので、ちょっとしたコード片を書いているが……Windows版のバイナリが古いためか、リファレンス通りなのに動作しないコードに直面している。
LOGO
そういえばLOGOを触ったことがない。とりあえずUCBLogo(Berkeley Logo)だろうか? Windows上でUCBLogoばりにGUI無しで動作する処理系はないだろうか?
Object REXX
思うところがあって処理系とIBM謹製のドキュメントを入手したものの、そこから先の進展は無いまま。ReginaでClassic REXXっぽい感じで触っているからなあ。
OCaml
Common Lispを勉強するはずが、いつの間にか触っていた言語。一応、階乗ぐらいは書いた。時間が取れたらもうちょっとしっかりと勉強したいが、面倒なのでF#に移行しようか検討中。
Oz
『Scheme手習い』の次はCTMCP片手にOzで勉強かなあ。道は遠いな……。
PostScript
これかForthか、どちらに手を出すべきか? 悩ましい。
Processing
入門書も処理系も入手して、あとは弄る時間をつくるだけ。
Prolog
『7つの言語、7つの世界』の地図の色分けプログラムには衝撃を受けた。何というか「正しい問い」を見つけられるか否かが肝なのか。この辺は、根底の部分でAlloyに通じる何かがあるように思う。ひとまず、Prologで論理プログラミングと宣言的なスタイルに慣れておけば、形式手法にて「論理で宣言的に」記述するときに戸惑いが減るのではないかと期待している。
Rust
仕事柄「C/C++の次のシステムプログラミング言語」はそれなりに興味の対象で、今のお仕事的にはGo言語やD言語よりもRustがド直球の本命に近いはず……まあ、本筋とは外れたところでGo言語を使うことになるとか、そういうパターンはありそうだけど。ちなみに、これら3言語と同列にSwiftが挙げられることもあるようだが、個人的見解としては、システムプログラミング言語としてのSwiftには全く期待していない。あれは、Appleというしがらみからは逃れられないでしょうな。
VBA (Visual Basic for Applications)
今までVBAから逃げ回っていて、今のところ追いつかれてはいないのだが、どこかで使う(というか解読する)ことになりそうな予感。たぶん、Excel VBA 8割にAccess VBA 2割ぐらいかなあ。

今は全く使っていない

Active Basic
VBScripを触りだした影響で、時々思い出しては弄くっていた。ほんの少しだけ触って放置して、すっかり忘れてからまた触る――これを繰り返していた気がする。なので毎度初めて触るのと同じ状態だった。String型をバシバシ使用 :)
bc
その昔、Windows標準の電卓アプリの代わりに使おうとして色々あって挫折した。今はirbかclisp/goshで計算しているからなあ。
CASL II
生まれて初めて触れたプログラミング言語その1。何だかんだで、後でCプログラマになってからも低水準での思考ツールとして微妙に役に立っている。まあ考えるための言語であって実用言語ではない。仮に実用的な処理系*14があったとしても余りに命令がシンプル過ぎて悶絶するなあ、なんてFizzBuzzしてみて思った。
Clojure, Scala
JDKがなくてもJava APIを叩くスクリプトを書けるので非常に便利。Scalaの型推論とか、便利っすね。言語仕様はJavaよりも好みだ。とはいえ、IoT時代にJava VMベースでどこまでメインストリームに居残ることができるのか? ちょっと興味深い。サーバサイドに活路を見出すのだろうか?
COBOL
FizzBuzzするためだけにOpenCOBOL 1.0をWindows上に用意して触ってみた。なんというか、COBOLの名前と生まれた時代が示すように基幹業務(というかお金や帳簿が絡んでくるところ)向けの言語だよなあ、といった感じ。COBOL 2002のフリーフォーマットを採用するだけでも使い勝手が変わる気がしたが、世の中にはまだ広まらないのだろうか。
CoffeeScript
仕事で使う予定はない。RubyやPythonその他の影響を受けているだけあり、その手のスクリプト言語っぽい感じでコードを書けるので、慣れれば素のJavaScriptで直接コーディングするよりは楽だ。しかし標準ライブラリ回りや処理系絡みの機能やサードパーティのライブラリなど、結局はJavaScriptを知らないとCoffeeScriptでコードを書けないと思う。それに生成されたJavaScriptのコードを見て「うわぁ、これあまり効率的でないなあ」と感じる時もあって、高速化が必要な部分では生成されるコードを気にしながら記述したりCoffeeScriptを諦めてJavaScriptで書くことになるので、やはりJavaScriptを知らないとマズイ。とはいえ便利なのは確かだ。CoffeeScriptのコードは即Node.jsで実行できるので、その辺りから「CoffeeScriptでテキストフィルタ」的な文化が生まれると面白いかも。気になるのはECMAScript 6の存在で、今までCoffeeScript独自の機能だった部分の多くがES6で取り込まれるので、今後ES6対応が進むにつれてCoffeeScriptの立場がどうなっていくのか、少々興味深い。
D言語 2.x
仕事柄「C/C++の次のシステムプログラミング言語」はそれなりに興味の対象で、Go言語ほどではないが、D言語も気になる存在だ。D言語はシンタックスがC・C++に近いだけでなく、コーディングしている時のアプローチ・判断についても、CやC++での流儀がそこそこ通用しやすい気がする。少なくとも、Go言語でコーディングするよりは、文化的背景の違いによるモヤモヤは感じにくい。あと、標準ライブラリを使ってテキストフィルタを書いたところ、エラー処理を1~2ヶ所のtry - catchにスッキリまとめることができて、ちょっと驚いた。throwされる例外のメッセージ文字列が、ちょうどよい塩梅の内容だったため、メッセージを変更する(いったんcatchして、再throwする)必要がなかった。ちょっと残念なのは、マルチバイト対応だが……。
Emacs Lisp
「.emacsにコピペ」限定で。Common LispやSchemeを触ったためか、何となく内容を追えるようになってきた気がしていたが、勘違いだった。
Fortran
Fortran 90やFortran 95あたりは結構近代的な言語だと思う。用途次第ではC言語よりもFortranの方が遥かにマシな選択だろう。配列がらみの処理はFortranの方が得意だし、言語機能としてのモジュール化の方法はC言語には存在しない。可変長な文字列の扱いに微妙な制限がある点はマイナスな気もするが、まあ基本的に数値計算プログラム用の言語だからなあ。
GDB (GNU Debugger)
……いやGDBはデバッガとして使っているが、GDBのスクリプトを書く機会は(FizzBuzz以外に)ない。勉強不足なだけかもしれない。
Groovy
JDKがなくてもJava APIを叩くスクリプトを書けるので非常に便利。動的型付け言語っぽくいくもよし、@CompileStaticや@TypeCheckedで型推論するもよし。言語仕様はJavaよりも好みだ。コンソールアプリを書く人としては、オプション引数解析用の機能を標準で持っている点で、GroovyはClojureやScalaよりもポイントが高い*15。個人的には、IoT時代に「Java VMベース」の言語としてどこに活路を見出すのが、興味深く見守りたいところ。やはりサーバサイドだろうか?
HSP (Hot Soup Processor)
FizzBuzzで楽しんでみたが、何というか他言語経験者には受けが悪そうな命令体系だと思う。もっとも初心者がプログラミングという行為に深入りせずにWindows用のGUIな何かを作る分には、あの命令体系でも十分な気がしないでもない。ところで元々は「HSPで職業プログラマ的な良いコードを書くと、どんな感じになるか?」というネタを思いついて処理系を用意したのだけど、そちらは全く進展がないまま。
JScript on WSH
他人が使うテキスト処理ツールの実装に使って以来、時々触ってきた。Windows用の配布可能な小ツールを実装する時の定番言語だった。でもそろそろ潮時だろう。HTAと組み合わせてクライアントサイドJavaScriptなノリで簡易なGUIツールを実装できる点も、PowerShell + WPF + XAMLで代替できそうだ。他のメリットは「JavaScript(ECMAScript)でフィルタを書ける」だったが、WSHのなかなか目的にたどり着けないオブジェクト階層にイライラするよりも、Node.jsやPhantomJSを使ったほうが精神衛生的にマシだ。
m4
その昔テキスト処理用に触ろうとして、Windows用のどの処理系も日本語の置換に何かしらの問題を抱えていたので泣く泣く諦めた。思うところがあって改めて少し触ってみたが――なるほど、確かに中毒性のある言語*16だ。
QML
宣伝文句のとおり、QMLはGUIの記述に非常に向いている。それも、単に標準のUI部品(エレメント)を使うだけでなく、少し改造して使うとか、オリジナルのUI部品を作って使うとか、それらを別のアプリケーションに使いまわすとか、そういう時に威力を発揮する。あと、プロパティバインディングやレイアウトのアンカー指定など、画面サイズの変更に追随するUIを作りやすい機能も揃っている。JavaScriptでちょっとした処理も記述できる――とはいえ、やりすぎるとパフォーマンスの罠が……。少なくとも、JavaScriptでゴリゴリコードを書くのはQML的ではない。QMLは宣言的に、シンプルに書くものだ。力技でロジックでゴリ押しすると、色々と罠に嵌る言語だ。
REXX
Open Object REXXの処理系を入手したのに、何故かReginaを入れてClassic REXXっぽい方向に走っていた。何というか、COMコンポーネントや.NET Frameworkと無関係でいられるのなら、バッチファイルの代替としてはREXXあたりがほどよい塩梅だと感じる。しかし最近流行の言語とは随分と勝手が違うし、日本語の情報も少ない。メインフレーム以外の世界で流行る可能性は少ないだろう。
Smalltalk (Squeak, Pharo)
Smalltalkは有名な古典的プログラミング言語だというのに、触ったことがない。ということでSqueakとPharoの処理系のみ準備完了。うーん、「環境」付きなのが気になる――言語を弄くる基準が「コンソール上でテキストフィルタ」という変な人種な私だからなあ。
Smalltalk (GNU Smalltalk)
個人の思想信条による理由よりSqueakとPharoにわだかまりを感じてしまう変人なので、邪道だと思いつつもコンソールでテキスト処理もOKなGNU Smalltalkも用意してみた。これで言語としてのSmalltalkの勉強に集中できる……か?
T4 Text Template
「へえ、こんなものがVisual Studioに入っていたのか。機能多すぎで色々と便利なツールを見逃しているんだな、やっぱり」と思いつつ触ってみた。テンプレート変換の用途ではピカ一だと思う。ただ処理系を手に入れる方法が「Visual Studioをインストールする」or「MonoDevelopをインストールする」なので、何となく「単体で手軽に使えるツール」ではないというイメージが……。まあC#やVBで処理を記述するので、それらの処理系が必要だという面での制約なのだろう。
VBScript on WSH
JScriptほどではないが「Windows上で他人も使えるツールを書くためのLL」扱いしていた言語。Windows Server管理の関係で触っていた。というかWebで入手可能なWSHのサンプルの大半がVBScriptで書かれていたり、ADSI関連のコレクションをJScriptで舐めれなかったりして、結局は必要に駆られて使用することに。明快に記述できる文法は評価に値するが、スクリプト言語としては少々冗長だ。配列は自動拡張しないし、組み込み関数はプリミティブ気味だし、冗長気味な文法との合わせ技でコードがさらに冗長になっていく……。文法や言語仕様の詳細なドキュメントが見つからないのだが、どこにあるのだろうか?*17
Vim script
少し触ってみた分には、exコマンドの拡張(=コマンドの羅列)とは気づかない程度にはプログラミング言語らしいと思う。とはいえ妙なところで嵌ったり微妙に一貫性のない部分があったりするので、その辺りで好き嫌いが別れる気がする。
秀丸マクロ
7年ほど秀丸エディタを使っていたが、マクロを書く機会はなかった。一念発起してFizzBuzzしてみて感じたのは、最近の便利な言語に慣れた身としては色々とモヤモヤ感がある言語仕様だということ(歴史的経緯的に仕方ないのだが)。とはいえちょっとした拡張ツール的なものを手軽に作れそうではあった。

*1:「HTML5 + CSS」の組み合わせなら、チューリング完全の疑惑があったり、JavaScript使わずにCSSでWebチャットを作った猛者がいたりと、色々と怪しいのだけど。

*2:「独立性の高い単体のツールを実装する」という視点では、現代ではAWKよりも便利な言語が山ほどある。

*3:しかし標準ライブラリの充実度をJavaやC#(.NET Framework含む)と比較したり、型推論まわりをKotlinやSwiftと比較してはいけない。以前よりも随分と便利になったのだけど、だけど、隣の芝生を見てしまうと、うーん……。

*4:SwiftではC++の機能を直接呼び出すことができないので、Objective-Cでラッピングして利用することになる(インタフェースはObjective-Cで、内部実装はObjective-C++)。この時、Objective-Cクラスのインスタンス変数として「C++クラスのインスタンスを保持するスマートポインタ」を持つ構成にしておくと、Objective-Cクラスのインスタンスがdeallocされる時に、スマートポインタ経由でC++クラスのインスタンスもdeleteされる。

*5:少なくともC++の言語/ライブラリ仕様は私の手には余る。自分が把握している範囲の機能でコードを書くのは好きなのだけど。

*6:支障がある部分を触るほど深入りするには、あと20年ぐらい掛かるのではないか?

*7:Schemeの勉強というよりも、再帰の勉強なのか?

*8:現状はirbかclispかgoshの3択だ。

*9:これでもsedはチューリング完全な言語だと証明されているらしい。

*10:言語仕様的にはC# 5.0の環境だが、ライブラビまわりはC# 4.0相当だったはず。

*11:Windowsのことを考えなければ、自前でライブラリをビルドしてアプリに組み込むのは結構簡単だった。

*12:私の認識では、JavaScriptは、第一級関数やクロージャがお仕事用のメジャーな言語に組み込まれて、少なくない人が使う契機となった言語だ。

*13:Kotlinのvalは「再代入不可の変数」だ(定数はconstで定義する)。Kotlinのプリミティブ型以外のデータ型はclass(つまり参照型)なので、valで定義した変数を破壊的操作する行為は割と普通だと思う。一方でSwiftのletは定数であるし、値型が中心の言語仕様である影響かstructやenum(つまり値型として振る舞うデータ型)が多用されるので、letで定義した変数を破壊的操作できるケースとできないケースが生じる。

*14:――といってもシミュレータだけど。

*15:ClojureやScalaに関しては、同様の機能を私が見逃している可能性も高いのだが。

*16:m4はマクロプロセッサなのでプログラミング言語ではないはずだけど……。

*17:MSDNの資料では物足りない。もうちょっと掘り下げた内容のものが欲しい。