エンジニアにとって「コミュニケーション能力」とは何を意味するのか?

世の企業も求むる「コミュニケーション能力を持つ社員」といふものを、エンヂニアリングの現場でも求めてみむとてするなり。

「コミュニケーション能力」という言葉は実に曖昧で、10人いれば5〜6パターンぐらいは異なる解釈が出てきそうな代物だ。この文章では、ソフト屋さんを含む「エンジニアリングの現場にいる技術者」に求められるコミュニケーション能力の実体について、基本的なところを述べてみたい。

(ただし私はプログラマなので、ソフト屋寄りの見解となる)

コミュニケーション能力を「社会生活を営む人間の間で知覚・感情・思考を伝達するための能力」であると仮定した上で、掘り下げてみよう。

コミュニケーション能力を獲得するための第一歩は、経験の言語化だ。一般に、人間はことばという共通のフレームを用いて情報を伝達する。経験を言語化できなければ、経験を相手に伝えることができないし、また相手の言葉を理解する(ことばを経験にマッピングする)こともできない。

一般的に、経験の言語化は幼少期に家族などの特定の親しい人との会話によって形成され始めるらしい。ただ、発達障害(とくに自閉症)の場合、会話によってこの辺の能力を形成していくのが難しい傾向にあるようだ(ではどうすればよいか、という点については色々と研究されている模様)。

経験を言語化できていると仮定すると、次の論点は、発達心理学でいう一時的ことばと二次的ことばだろう。

一時的ことばは、同じコンテキストを持つ親しい相手と1対1でコミュニケーションする際に用いることばだ。経験の言語化が、幼少期に家族などの特定の親しい人との会話によって培われるために、一時的ことばは「お互いについての知識・経験を共有している」という前提のものとなる。だから、不完全な表現であっても相手は共有する知識・経験から補完して理解することができる。

二次的ことばは、未知の人も含む不特定多数の人とコミュニケーションする際に用いることばだ。この場合、「お互いについての知識・経験を共有している」という前提は成立しない。そのため、情報を発信する側は、発信する情報を理解するために必要な情報を全て言語化し、整理した上で、表現する必要がある。同時に、情報を受信する側は、内容を理解するために必要な情報を全てことばの文脈から抽出し、個々の情報の関連付けを行うことで、理解する必要がある。

二次的ことばは、スピーチのように不特定多数にたいして語る場面だけでなく、チームミーティングでの進捗報告や、業務手順書の作成のように、「ある程度共通するバックグラウンドを持つが、『家族などの特定の親しい人』ほどはお互いについての知識・経験を共有していない、特定少数の人々」との情報伝達でも用いられる。お互いに共有している知識・経験が限られているため、情報を発信する際には、相手が内容を正しく理解するに足るだけの情報を提供しなくてはならない。

社会人に求められるコミュニケーション能力は、「二次的ことばによる情報の伝達がスムーズに行える」ということである。つまり、自分が伝えたい内容について相手側の知識・経験が乏しい場合でも正しい情報を伝達できることであり、その逆に相手が発信する場合にも会話・文章ということばだけから正しい情報を理解できることである。

要するに「話が面白い」とか「会話が弾む」などの面で評価が高い人であっても、それが「一時的ことばによる身内間での情報伝達」に留まるのならば、それは「社会人に求められるコミュニケーション能力」を兼ね備えていることを意味しない。

さて、二次的ことばによるコミュニケーションでは、情報を全てことば(相手の話した内容や、文章に書かれている内容)だけから理解していく必要がある。これを実現するには、ことばに含まれる単語を正確に理解し、単語を連結している文法を正確に理解する必要がある。また自分自身が発信側となる場合には、単語を正確に用いつつ、単語を連結する文法を正確に用いなくてはならない。

社会人(というか職場)でのコミュニケーションで用いられる単語には、日常でも用いられる単語だけではなく、その環境に応じた専門用語・業界用語の類も含まれる。エンジニアのコミュニケーションにおいては専門用語が多用される傾向にあるので、各専門用語の意味を正確に理解して用いる必要がある。

なぜ専門用語が多用されるのか? お互いに「相手はその専門用語を知らない」という前提でコミュニケーションしようとすると、内容が冗長になりすぎて、全てを伝えるのに時間がかかりすぎてしまうからだ。だから、専門用語として厳密に意味を定義した上で、双方とも定義された意味を理解しているという前提で情報伝達を行うのである。

例えるなら……バリカタを知らない人にバリカタという言葉を使わずに「あのラーメン屋のバリカタは柔らかすぎでバリカタじゃない」ということを伝えようとするようなものである。バリカタのラーメンを食べたことがない人は、バリカタを「通常よりもかなり硬い麺」に置き換えて説明されたとしても、そもそもバリカタの硬さを理解していないので、「あのラーメン屋のバリカタという名の『バリカタよりも柔らかい麺』」がどの程度の硬さなのか類推できない。だから、単に「通常よりもかなり硬い麺」に置き換えるのでは駄目で、相手が食感を覚えている食べ物のうち、「バリカタに近い硬さのもの」と「あのラーメン屋のバリカタという名の『バリカタよりも柔らかい麺』に近い硬さのもの」を探り出して提示しなくてはならない。しかし身をもってバリカタを知っている人同士ならば、バリカタで十分通じるものだ。だから時には「話をする前に、今から『本当にバリカタのラーメン』と『バリカタという名のバリカタよりも柔らかい麺のラーメン』を食べ比べに行こうぜ!」という方法も有効だ。ことばで説明するよりは早く済むだろう。でも食べ比べの時間は余分にかかるから、結局は、バリカタを知っている人同士の方が情報伝達が早く済むことになる。

エンジニアが用いる専門用語は、その職場・チームといった環境に特異のものだけでなく、他の環境のエンジニアを含むもう少し広い範囲(例えば業界)にて共通言語として用いられる専門用語もある。例えばソフトウェア・エンジニアの場合、基本情報技術者試験に出てくるような用語は、専門用語の中でも広い範囲で用いられるものだ(スレッドという単語が通じるソフト屋さんは少なくないはず)。そういった公知の専門用語をどれくらい正しく理解して使用できているか――という点もエンジニアのコミュニケーション能力に含まれてくる。

ただし専門用語の知識の有無は人によって異なるので、情報を発信する側は、文脈によって専門用語を用いるか否かを切り替える必要がある。例えば、新入社員と会話する時と同期の社員と会話する時では、前者よりも後者の方が専門用語の割合が多いはずだ。

また、エンジニアのコミュニケーションでは、抽象度の高い事象や、大きくて一度に理解できない事象を取り扱うことが多い。そのため、同じ情報を後から何度でも参照できるように、会話だけでなく「文章を用いた二次的ことばによるコミュニケーション」が多用される傾向にある。文章によるコミュニケーションでは、会話でのコミュニケーションでは可能な「その場のニュアンスで不完全な表現を補完する」ということが不可能なので、単語の内容や、単語を連結する文法を、会話の場合よりも正確に用いなくてはならない。

その上で、文章だけでは抽象度の高い事象を正確に伝達できない可能性があるため、時には適切な図表を用いる必要もある。事象によっては数式で表現することもあるだろう。ソフト屋の場合は、時にソースコードという人工言語にて他のエンジニアとコミュニケーションを図ることもある。

つまるところ、文章と図表を用いて情報を正確に発信できること、文章と図表から情報を正確に読み取ることができること、この2点である。この辺りは、大学教育でのレポートや論文の作成にて発信側を、レポート・論文作成の際の文献調査にて読み取り側を研鑽するように思う(で、その前提となる「普通の文章の読み書き能力」は義務教育で学ぶ……と書いてしまって大丈夫だろうか?)。

まとめると、エンジニアに求められる「コミュニケーション能力」とは、以下を兼ね備えていることである。

  1. 二次的ことばによる情報の正確な伝達をスムーズに行うことができる。
  2. 文章と図表による情報の正確な伝達をスムーズに行うことができる。
  3. 専門用語を正しく理解して使用することができる。

基本的に、エンジニア同士のコミュニケーションでは「正確な情報をスムーズに」という点が重視される。エンジニアが取り扱う事象の大半は、抽象度の高いものや、大きくて一度に理解できないものなどの、複雑な事象だ。複雑なものを取り扱うには正確な情報が必要であるし、複雑なものを取り扱うことに集中するためにも余分なコスト(「情報が不正確かもしれないので再検証する」という時間的ムダなど)を払いたくないものだ。

特に、簡単な事象が過去に解決されてきた積み重ねの結果、エンジニアリングの現場にて取り扱う事象が高度化している昨今では、複雑な事象にチームで取り組むことが多い。『人月の神話【新装版】』のブルックスの法則にて言及されているように、チーム内で相互コミュニケーションが求められる場合、コミュニケーションの労力は人数nの時「n(n - 1) / 2」に比例する。このような本質的問題を抱える状況において、「情報が不正確である」とか「情報は正確だが、読み解くのに苦労する」などの偶有的問題まで発生したら、進むものも進まない。それゆえに、エンジニアは「正確な情報をスムーズに」という点を重視する傾向にある。

だから、世間一般で言われるコミュ障(雑談が苦手・苦痛なタイプ)でも、業務上必要な情報を正確かつスムーズに伝えられるのなら、大抵は問題ない。もっとも、コミュ障ゆえに対人関係の構築に不慣れな場合、業務上必要な情報を伝達する際に相手が把握している情報量をうまく推測できないために、「どこまで情報を付与すれば相手が理解できるか/どこまで省略しても相手が理解できるか」を見誤ることはある*1

まあ、最初から上記3項目を全て高いレベルで身に着けている新卒社員は少ないだろうから新人教育やOJTで最低限鍛えるぞ――というのがジャパニーズ・トラディショナル・カンパニーの伝統行事なのだが、しかし会社にも「許容できる教育コスト」に上限がある。だから、新卒採用にて「コミュニケーション能力のある人=上記(1)と(2)がある程度の水準に達している人」を求めるのだ、その辺の能力を正しく計測できて採用できているか否かは別として*2

*1:難しいよね。

*2:多分できていない。

実はオブジェクト指向ってしっくりこないんです?

――いや、自己分析するに、「オブジェクト指向」ではなく「特定のオブジェクト指向プログラミング言語」がしっくりこないことがあるんだな、これが。

具体的には、触ったことのある言語ではJavaC#だ。この2つの「しっくりこなさ」具合からすれば、C++の方がマシだ(もっとも、C++は別の部分で好きになれない面があるのだが*1)。

C#は経験が浅すぎるので、Javaについて。Javaは、ここ数年の間にちょっとしたコンソールアプリの実装とAndroidアプリの開発で使用した。興味深いことに、コンソールアプリを書いた際は「しっくりこなさ」全開だったのだが、Androidアプリを書いた時にはその手の違和感はなかった。

この違いは何だろうと考えて、ふと気づいた。おそらく私は、クラスの定義を強要されることに違和感を感じる体質なのだ。

私のプログラマとしての土台(第一プログラミング言語)はC言語だ。C言語にはクラスはない。次に使うことが多いC++は、クラスを定義するも定義しないも活殺自在な言語だ。JavaScript/JScriptVBScriptも、クラスを定義する/しないは比較的自由だ。小ツール実装で時々使うRubyPythonは、ちょっとしたスクリプトであれば構文の見た目として「クラス定義なんて知らないよっメソッド定義だけだよ!」風に記述できる。

これらの言語のうち、C言語C++のアプリケーションメインエントリは関数だ。JavaScript/JScriptVBScriptRubyPythonでは、必須構文としてのアプリケーションメインエントリ的なものはない*2

つまり、C言語を除けば、コードの見た目として「必要になったらクラスを定義するが、必要なければ使わない(関数/メソッドを単独で定義して用いる)」というスタイルで記述することが可能な言語に慣れている(そしてC言語にはそもそもクラスが存在しない)。

しかしJavaC#は、例えばC++にてmain関数と2つのサブ関数で記述できるような小ツールであっても、クラスを定義した上でmainメソッドを記述しなくてはならない。この時点で、何というか「この程度のことでクラスを持ち出すなんて……」と思ってしまうのだ。

その一方で、Androidアプリのような複雑な*3ソフトウェア、特にGUIアプリケーションというオブジェクト指向プログラミング的アプローチに向いているソフトウェアでは、クラスを用いる意味もメリットもあるので、違和感を感じることがない。とはいえ、時としてクラスに属しないメソッドを書きたくなることはある。C++ならば名前空間に直接属する関数として、Objective-CならC言語流のファイルによるモジュール分割を用いたデータ結合の関数として定義するだろう、ちょっとした汎用のユーティリティ・メソッドだ。

別の視点から考察すると、どうも私は「構造体(=レコード型)」の影響を強く受けていて、「何らかの関連がある複数のデータ+それらを操作する専用のルーチン=オブジェクト」という意識が強い。C言語で構造体を使用してややオブジェクト指向プログラミングっぽいアプローチをすることもあるが、その場合、「何らかの関連がある複数のデータ」を一まとめにする必要があるならば構造体にまとめるが、必要なければまとめない。

つまり、心理的に「クラス≒抽象データ型≒レコード(複数のフィールド)と操作用ルーチン」という意識がある。抽象データ型として扱うのが妥当ならばクラスにするが、それ以外の用途――例えばJavaJavaScriptのMathクラスのような使い方――でのクラスの採用に消極的というか、クラス以外の代替機能を使おうとする個人的傾向がある。

仮にMathクラスのようなものをC++で再実装するならば*4、個人的にはクラスではなく名前空間を使用して、関数や定数をひとまとめにするだろう。

このようなアプローチは、例えばC++で採用するには問題ない。しかし、慣れ故にJavaC#でも採用しようとしてしまい、メソッドをクラス内でしか定義できないことに気づき、ついついもやもやしたものを感じてしまう。

現実には、プログラミング言語には「言語仕様」という制約がある訳で、その制約の元では、例えば「『C++名前空間』のような機能がないので、代わりにクラスを使う」などのアプローチは合法的だろう。だから、もやもやしたものを感じてしまう必要など全くないのだが……。

結局のところ、慣れの問題なのだ。クラスのない世界での生活が長く、また必要に応じてクラスを定義するか否かを切り替えられる言語に慣れてしまっていて、クラスが必須な言語には慣れていないので、違和感を感じるのだろう。

蛇足:クラスが必須な言語に違和感を感じる一方で、CやC++のような関数が必須な言語(ステートメントを関数の中にしか記述できない言語)には違和感を感じないあたり、どう考えても慣れの問題以外の何者でもない。

*1:好きにはなれないが、better Cとして使い方をわきまえている分には便利な言語だと思う。

*2:もしかしたら、私が知らないだけで、アプリケーションメインエントリ的なものがあるかもしれない。しかし、そうだとしても、必須ではない(何らかの構文として記述する必要がない)だろうから、その言語のユーザからすれば「無いも同然」だ。

*3:テキストフィルタよりもGUIアプリの方が複雑だ、という意味での「複雑な」である。

*4:標準ライブラリに用意されている、C言語由来のcmathの数学関数は無視するものとする。

仕様書・設計書の必要論と不要論の不毛な議論を避けるためのヒント

仕様書や設計書の「要る/要らない」で議論になることがあるが、往々にして不毛なのでここに記しておく。

実のところ「仕様書や設計書は必要だ」という意見も「仕様書や設計書なんて不要だ」という意見も、根本的には同じ問題を出発点としている。

それは「必要な仕様書・設計書を、必要なときに、必要なだけ」というジャスト・イン・タイムの原則から外れている、ということだ。

「仕様書や設計書は必要だ」という人は、「必要なドキュメントが存在しない」「必要なときに手に入らない」「内容が不足している」という経験をした人であることが多い。

「仕様書や設計書なんて不要だ」という人は、「不要なドキュメントを作らされた」「まだ必要ではないのに作らされた/不要になった後に作らされた」「必要量を超えて作らされた/不要な内容まで書かされた」という経験をした人であることが多い。

結局はどちらも「必要な仕様書・設計書を、必要なときに、必要なだけ」が成立していないのだ。

問題の根っ子は同じなのに、適正レベルからの逸脱の方向の違いというコンテキストの差異にしか目を向けていないために、必要論と不要論に分かれてしまうのだ。不毛だと言わざるを得ない。

そろそろ「美しいコード」って言うの止めようぜ

――と題名の通りの感想を抱いたのだが、「美しいコード」の代替となる上手い言い方が思い浮かばないのである。

いや、記事の内容に異論はないというか、私自身は汚いコードに拒否反応を示す人であるし、未だに「もっと良いコードを書きたいなあ」という欲を持っている。

なんだけど、こう、何というか、「美しいコード」の「美しい」という言葉ゆえに誤解されてしまっている側面があるように思うのだ。

どうも「美しさなんか関係ないだろ!」と否定する人は、「美しい」という言葉ゆえに脊髄反射的に「気難しい職人のこだわり」みたいな解釈をしているように感じられる。どことなく「常のものではない」という意識がある。だから「(常の側にいる私たちには)関係ない」と切り捨ててしまう。

でも実のところ、「美しいコード」の初級〜中級というのは、製造現場に例えると「4S5Sが定着している状態」だと思うのだ。目的は「安全性・効率化・品質向上」で、手段の1つが「4Sないし5S」。これがプログラミングでは、目的は「RASIS」であり、手段の1つが「美しいコード」、という感じだろうか。

「3M(ムダ・ムラ・ムリ)を解消する手段の1つとしての4S/5S」からのアナロジーとして「システム開発の3Mを解消する手段の1つとしての『美しいコード』」と捉えると、「美しいコード」は割と基礎レベル寄りの手法であるし、日常的に定着していないとマズい代物だということになる。

つまり「美しいコード」は常のものだ。それどころか4Sや5Sのような土台・基礎であり、出来てないと「え、そんなこともできないの?」と言われて恥をかくレベルのモノだ。

だから「『美しいコード』って、実は製造現場でいう4Sや5Sなんですよ。基礎中の基礎ですよ」と明確化すると同時に、今後似たような誤解が起こらないように、対外的には「美しいコード」という言葉を封印して、別の言い方をすべきだと思う。

問題は「美しいコード」に代わるパワーワードが思い浮かばないことだ。「良いコード」だとインパクトに欠けるしなあ。

2〜3世代前のOS X/macOSの実行環境を整えるためのメモ

最新版を含む過去4世代のOS X/macOSをbootできる環境を整えているのだが、環境構築の過程で得られた知見を残しておく。

なお、このメモはOS X Mavericks(10.9)以降を前提としている。執筆時点での最新のOSはmacOS High Sierra(10.13)である。

どこまで古いOSをインストールできるか?

Windowsの場合、例えばWindows 10プリインストールPCであっても、Windows 7に対応したデバイスドライバが公開されているなら、Windows 7でも問題なく動作する可能性が高い。しかしMacはそうではない。

現在のMacはハードウェアとOS双方を含めてクローズドなシステムだ。そのため、Mac本体の各デバイス用のドライバを独自に入手する術がない。OS付属のデバイスドライバを使用するしかないのである。

そのため論理的には、あるMacの機種に、その機種にプリインストールされていたものよりも古いOSを入れようとした場合、必要なデバイスドライバが古いOSに含まれていないために正常に動作しない、という可能性がある。

(もしかしたら、OSのインストーラ側にてガードがかけられていて、古いOSをインストールできないようになっているかもしれないが、試していないので不明である)

少なくとも、当該機種の発表時に搭載されていたOS以降のものを入れるべきだろう。例えば、購入時にOS X El Capitan(10.11)がインストールされていたMacには、El Capitan以降のOSのみ入れるべきだ。間違ってもMavericksを入れようとするべきではない。もしMavericksの動作環境を構築したいなら、古いMacを入手してインストールするか、ParallelsVMwareなどの仮想環境にゲストOSとしてインストールするべきだろう。

古いOSのインストール先

用途次第ではあるが、過去数世代のOSを入れるとなると、各バージョンごとにそこそこのディスクスペースが必要となる。なので内蔵ディスクではなく外付けHDDなどに入れるべきだろう。*1

古いOSのインストーラの入手方法

El Capitan以前のOSのインストーラは、過去にApp Storeからダウンロードしたことがあるのなら、App Storeの「購入済み」タブから再ダウンロードできる。

過去にダウンロードしたことがないために「購入済み」タブから再ダウンロードできない場合は、OS X El Capitan へアップグレードするには - Apple サポートに書かれているように、このリンク経由でApp StoreOS X El Capitanのページを開いて、そこからダウンロードすることができる。MavericksOS X Yosemite(10.10)のページは既に存在しないようなので、過去にダウンロードしたことがない場合、通常の手段で入手することはできなさそうだ。

macOS Sierra(10.12)以降は、App Storeからダウンロードしても、「購入済み」タブに表示されなくなった。

Sierraインストーラを入手したい場合は、macOS Sierra にアップグレードする方法 - Apple サポートに書かれているように、このリンク経由でApp StoremacOS Sierraのページを開いて、そこからダウンロードすること。

High SierramacOS High Sierra にアップグレードする方法 - Apple サポートに書かれているようにこのリンク経由でダウンロードできるが、El CapitanないしSierra上のApp Storeからダウンロードしないと、全コンポーネントが含まれた状態のインストーラを入手できないようだ。

MojaveはmacOS Mojave にアップグレードする方法 - Apple サポートに書かれているようにこのリンク経由でダウンロードできる。

なお上記のいずれか場合も、過去にダウンロードしたインストーラがディスク上に残っていると、再ダウンロードができない可能性がある。この場合は、ディスク上のインストーラを削除してから再ダウンロードを試みること。

特にありがちなのは、例えばSierraの公開直後に10.12のインストーラをダウンロードしていて、1年後のHigh Sierra公開前後にSierraの最終版である10.12.6のインストーラをダウンロードしようとして、10.12のインストーラが残っていたために再ダウンロードできない――というパターンである。注意すること。

インストール用メディアの作成

ダウンロードしたインストーラmacOS上でアプリケーションとして実行可能だが、インストーラに付属するcreateinstallmediaを使用してインストール用メディアを作成することも可能だ。対象メディアはUSBメモリや外付けHDDだ。

作成方法についてはmacOS の起動可能なインストーラを作成する方法 - Apple サポートを参照のこと。

(前述のサポート情報にも書かれているが)High Sierraインストーラは、El CapitanないしSierra上のApp Storeからダウンロードしないと、全コンポーネントが含まれた状態のものが入手できず、結果としてインストール用メディアを作成できないので、注意すること。

*1:Windowsユーザ向け補足:Windows PCでは、外付けディスクにWindowsを入れることは不自然というか、Microsoftのポリシーの関係で普通にはできない所業なのだが、一方でMacでは割と普通なことである。

書籍購入:『Optimized C++』

あとで書く。

Optimized C++ ―最適化、高速化のためのプログラミングテクニック

Optimized C++ ―最適化、高速化のためのプログラミングテクニック

書籍購入:『micro:bitではじめるプログラミング』

あとで書く。

micro:bitではじめるプログラミング ―親子で学べるプログラミングとエレクトロニクス (Make:PROJECTS)

micro:bitではじめるプログラミング ―親子で学べるプログラミングとエレクトロニクス (Make:PROJECTS)

ALSAでMIDIをループバックさせたい――ただしALSA RawMidi APIで!

ALSA C libraryのAPIを使用してMIDIの入出力を行うLinuxアプリがあるのだが、出力したMIDIをループバックさせて入力として読み込みたい――という話である。

macOSなら標準のIACドライバを、Windowsならサードパーティの仮想MIDIケーブル*1を使うパターンである。

今回のポイントは、ALSAのRawMidi interfaceのAPIを使用しているアプリである、という点だ。

ALSAMIDI APIは、RawMidi interfaceとSequencer interfaceの2種類が存在する。RawMidi interfaceはベタにMIDIバイスのポートを読み書きするためのOSS互換のAPIだ。一方のSequencer interfaceは高水準のAPIで、MIDIバイスだけでなくTimidity++のようなアプリケーション間でもMIDIの入出力を行えるようにデザインされている。そのため、MIDIシーケンサという抽象化されたオブジェクトのポートを読み書きするようになっている。

Linux上でのMIDIの取り扱いについての解説資料――例えばThe Linux MIDI-HOWTOとかLinuxビンボ道〜明日がALSA設定編とかArchLinuxのWikiに書かれているTimidity++の話題などは、Sequencer interfaceを使用しているアプリ向けの話が大半だ。

Sequencer interfaceを使用しているアプリなら、カーネルモジュールsnd-seq-dummyをロードすることで使用可能となるMidi Throughという仮想MIDIシーケンサを使用して、MIDIをループバックさせることができる。「Midi Through」という名前の通り、入力ポートに書き込まれたMIDIをそのまま出力ポートから読み出せるMIDIシーケンサで、macOSのIACドライバやWindowsの仮想MIDIケーブルと似たような要領で扱うことができる。

# snd-seq-dummyの組み込み方。
# 末尾の「ports=1」は、ループバック用のポートの数を指定する。
sudo modprobe snd-seq-dummy ports=1

UbuntuやRaspbian(Raspberry Pi)ではデフォルトで組み込まれているので、再度ロードする必要はない。

$ # 手元のRaspberry Pi 3 Model B(Raspbian 9.3 stretch)の場合。
$
$ aconnect -i   # 入力ポートを表示
client 0: 'System' [type=カーネル]
    0 'Timer           '
    1 'Announce        '
client 14: 'Midi Through' [type=カーネル]
    0 'Midi Through Port-0'
$
$ aconnect -o   # 出力ポートを表示
client 14: 'Midi Through' [type=カーネル]
    0 'Midi Through Port-0'
$ _

上記の例では、入力・出力ポートともに14:0を指定すればよい。

しかしRawMidi interfaceを使用しているアプリはMidi Throughを使えない。RawMidi interfaceで扱えるのはMIDIバイスだが、Midi Throughは仮想MIDIシーケンサだ。厄介なことに、RawMidi interfaceのMIDIバイスとSequencer interfaceのMIDIシーケンサは全く別の概念で、APIからして全く異なるのである。

$ # 手元のRaspberry Pi 3 Model B(Raspbian 9.3 stretch)の場合。
$
$ # aplaymidi -lでは、Sequencer interfaceで扱うことが可能な、
$ # MIDIシーケンサの入力ポートが表示される。
$ aplaymidi -l
 Port    Client name                      Port name
 14:0    Midi Through                     Midi Through Port-0
$ # Midi Throughが見つかる。
$
$ # amidi -lでは、RawMidi interfaceで扱うことが可能な、
$ # MIDIデバイスの入力ポートが表示される。
$ amidi -l
Dir Device    Name
$ # Midi Throughはおろか、何のMIDIデバイスの入力ポートも見つからない。
$ _

ではどうすればよいか? カーネルモジュールsnd-virmidiを組み込んで仮想MIDIバイスを使用できるようにした上で、aconnect(1)で仮想MIDIバイスの入力ポートを出力ポートを接続すればよい。これで、仮想MIDIバイスの入力ポートに書き込んだMIDIコマンドを出力ポートから読み出すことが可能となる。

# snd-virmidiの組み込み方。
#
# 末尾の「index=1」は、仮想MIDIデバイスに付与するカード番号である
# (RawMidiのデバイスは「カード番号,デバイス番号,サブデバイス番号」で指定する仕組み)。
# カード番号は0から始まる通し番号で、
# システムが認識したMIDIデバイスごとに自動的に付与されている。
# 「amidi -l」などでシステムが認識しているMIDIデバイスを確認したうえで、
# それらと重複しない番号を割り当てること。
sudo modprobe snd-virmidi index=1

snd-virmidiを組み込むと、既定では4個の仮想MIDIバイスが使用可能となる。

$ # 手元のRaspberry Pi 3 Model B(Raspbian 9.3 stretch)の場合。
$
$ # snd-virmidiをロードする前。
$ amidi -l
Dir Device    Name
$
$ # snd-virmidiをロード。
$ sudo modprobe snd-virmidi index=1
$
$ # snd-virmidiをロードした後。
$ amidi -l
Dir Device    Name
IO  hw:1,0    Virtual Raw MIDI (16 subdevices)
IO  hw:1,1    Virtual Raw MIDI (16 subdevices)
IO  hw:1,2    Virtual Raw MIDI (16 subdevices)
IO  hw:1,3    Virtual Raw MIDI (16 subdevices)
$ # 「index=1」を指定したのでhw:1に割り当てられている
$ # (もし「index=2」だったらhw:2,0〜hw:2,3が割り当てられただろう)。
$ # 「16 subdevices」なので、例えばhw:1,0,0〜hw:1,0,15が使用できる。
$ _

興味深いことに、snd-virmidiの仮想MIDIバイスは、RawMidi interfaceで扱えるMIDIバイスであると同時に、Sequencer interfaceで扱えるMIDIシーケンサとしても振る舞う。

$ # 手元のRaspberry Pi 3 Model B(Raspbian 9.3 stretch)の場合。
$
$ aconnect -i   # 入力ポートを表示
client 0: 'System' [type=カーネル]
    0 'Timer           '
    1 'Announce        '
client 14: 'Midi Through' [type=カーネル]
    0 'Midi Through Port-0'
client 20: 'Virtual Raw MIDI 1-0' [type=カーネル,card=1]
    0 'VirMIDI 1-0     '
client 21: 'Virtual Raw MIDI 1-1' [type=カーネル,card=1]
    0 'VirMIDI 1-1     '
client 22: 'Virtual Raw MIDI 1-2' [type=カーネル,card=1]
    0 'VirMIDI 1-2     '
client 23: 'Virtual Raw MIDI 1-3' [type=カーネル,card=1]
    0 'VirMIDI 1-3     '
$
$ aconnect -o   # 出力ポートを表示
client 14: 'Midi Through' [type=カーネル]
    0 'Midi Through Port-0'
client 20: 'Virtual Raw MIDI 1-0' [type=カーネル,card=1]
    0 'VirMIDI 1-0     '
client 21: 'Virtual Raw MIDI 1-1' [type=カーネル,card=1]
    0 'VirMIDI 1-1     '
client 22: 'Virtual Raw MIDI 1-2' [type=カーネル,card=1]
    0 'VirMIDI 1-2     '
client 23: 'Virtual Raw MIDI 1-3' [type=カーネル,card=1]
    0 'VirMIDI 1-3     '
$ _

上記の例では、RawMidi interfaceのデバイスhw:1,0がSequencer interfaceのポート20:0に、hw:1,1が21:0に――という具合に対応している。

仮想MIDIバイス自体はMIDIをループバックする機能を持たないが、aconnect(1)を使用してSequencer interfaceの入力ポートと出力ポートを連結させると、RawMidi interface側でも有効となる。

例えばaconnect(1)で入力ポート20:0を出力ポート20:0に連結すると:

aconnect 20:0 20:0

入力ポート20:0に書き込んだMIDIを出力ポート20:0から読み出せるだけでなく、デバイスhw:1,0の入力ポートに書き込んだMIDIをhw:1,0の出力ポートから読み出せるようになる*2

あとは、RawMidi interfaceのAPIを使用するアプリケーションにて、デバイスhw:1,0を使用すればよい。同じサブデバイス番号同士(例えば入出力ともにhw:1,0,0)にてMIDIがループバックされる。

*1:昔はMIDI YokeHubi's Loopback MIDI Driverを使っていたものだが、64bit Windowsを使うようになってからは縁遠くなった。最近はloopMIDIを使えばよいのだろうか?

*2:試してはいないが、おそらく入力ポート20:0に書き込んだMIDIをhw:1,0の出力ポートから読み出すことや、hw:1,0の入力ポートに書き込んだMIDIを出力ポート20:0から読み出すことも可能なはずだ。というのも、The Linux MIDI-HOWTOを読む限り、元々snd-virmidiはRawMidi interfaceのMIDIバイスとSequencer interfaceのMIDIシーケンサを橋渡しするために開発されたと推測できるからだ。

低速で無駄のある小規模ツール開発を実現する方法

「低速で無駄のある」はツール本体なのか開発手法なのか、はたまた保守フェーズか。

例1:外部コマンドを何度も実行するシェルスクリプト/バッチファイルとして実装する

シェルスクリプトもバッチファイルも個人的に愛憎相半ばする気持ちを抱いてしまう言語というかツールだけど、その有効性は否定しない。自分もよく使っているし。

個々の小さなコマンドを組み合わせて目的を達成する――大抵のテキスト処理はその方法で何とかなるし、私が普段扱うデータ量は高が知れているので実行速度が問題となることもない。そして今の所はハードウェア性能は足りている。

とはいえコマンドを実行すれば(それが組み込みコマンドやシェル関数でなければ)その度にプロセスが生成されるし、各コマンドが個別にファイルにアクセスすればその度にファイルIOが発生する。気をつけないとこれらのコストが嵩んで信じられないほど低速なツールに仕上がってしまうことがある。というか覚えている限り2回ほど痛い目にあった。Windowsはセキュリティツールの影響もあってプロセスの生成コストが高いからなあ。

ループの中で外部コマンドを実行していたり、更にそこで毎回ファイルを引数指定していたりしたら要注意だ。テストデータでは気にならない時間で終了しても、現実的なデータ量ではウンザリするほど遅くなることがある。ループ数が増加すればするほど遅くなる。

該当する箇所をシェル組み込み機能に変えたり、テキストフィルタに置き換えることで済むなら良いが、それが駄目なら他の言語で書き直す羽目になるかもしれない。

例2:本格的に内部構造を変更するリスクを避けて無茶苦茶小手先の手法で既存ツールを改造する

作業コストや自分の力量の兼ね合いで、時として小手先の手法に頼らざるをえないこともある。しかし大抵は後でしっぺ返しをくらう気がする。何でだろう。

MVC (Model View Controller) や、それに微妙に似たDocument/Viewアーキテクチャなんて言葉がある。これに基づくと、例えばデータの画面表示に関して、

  • 表示したい元データはModelが保持している。
  • Modelからデータを取得し、表示用に加工してViewに突っ込む。
  • Viewには加工後のデータが存在する。

――こんな感じになると思う。実はデータの加工をどこでやるべきか分かっていない。

ある時遭遇したプログラムはViewerの類で、Model側に元データが残らない設計になっていた。Viewに加工後のデータが残っているだけだが、表示方法が1種類だけだったので何の問題もなかった……当初は

これがある時、表示方法を2種類に拡張して動的に切り替えられるように改造することになった。Model側に元データが残っているなら簡単だけど、残っていない! ピンチッ!

ここでModel側に元データを残すように修正すればよかったのに、何故か「Viewに残っている加工後のデータを使う」という力任せな方針を採ってしまった。表示方法を切り替える際にModelからデータを取得するのではなく(残っていないので不可)、Viewに残っているデータを使うのだ。この方法で何とかなってしまったのが悪かった。

その後、更に表示方法が増えたのだ! 私はこの対応の時に初めて駆り出された……。

表示を切り替える際に「Viewに残っている加工後のデータを使う」という実装の為、例えば次のような問題が噴出してしまった。

  1. 表示を切り替える度に、異なる加工済みデータが後に残る。
  2. 表示方法が3種類以上になると、表示を切り替えるときに複数の異なる種類のデータが元データとなる。つまり一つの表示方法に対して2種類以上の変換処理を実装する必要が生じる。
  3. データによっては、表示方法を色々と切り替えていくうちに誤差が生じる可能性がある。

ああ、こういうのが積み重なって「誰も弄りたがらない負債コード」と化してコールタールの沼のように踏み入れたプログラマを引きずりこんでいくのだろうなあ。

例3:よく分からんけどネット上の色々なサンプルをざっくり切り貼りして実装する

別にネット上のサンプルコードを使うことは……ライセンス絡みで問題ないなら構わないと思っている。

しかしですね、何の整理もせずに大雑把に切り貼りされると困るのですよ。低速になるか否かは運次第でもソースコード自体は確実に無駄の多い代物になりますから。ええ私も経験がありますあの全体の3分の1ぐらいが全く不要なコードでバラバラなコーディングスタイルがキメラのように組み合わさっていてライブラリ関数を使わずに自前で処理を書いてたりして当然のごとく関数化なんて申し訳程度にしかされてない一枚岩でこれまたバラバラな命名スタイルの変数が山のようにあるアレですよあのたった400行足らずのコードの整理に暇を見てこつこつ実施したとはいえ2週間近く掛かったのは災難なのかそれともその程度で済んだ幸運をかみ締めるべきなのかは未だに分かりません。

世間からすればまだまだ軽い事例だとは思うけど、嫌なものは嫌だ。

ある程度まとまった量のコードを外部から持ってくる――例えば関数単位やクラス単位でサンプルコードをコピーするのならともかく、ステートメント単位で切り貼りする時に何の整理もしないのは死亡フラグだ。

  • ネット上のサンプルコードを切り貼りする時はコードのスタイルを直すべし。
    • 大概のサンプルコードのスタイルは組み込む先のコードのそれと異なる。
    • コードの一貫性を保つためにも、スタイルは直すこと。まあ後で整形ツールにかけるという前提があるのなら話は別かもしれない。
  • ネット上のサンプルコードを使う時は、自分が欲しい部分以外は徹底的に取り除くべし。
    • 残っていると、後でそのコードを解読する羽目になった人が発狂します。

あとステートメント単位でコピーする場合もまとまった量のコードをコピーする場合も、何も考えずに使うのはダメだ。

  • ネット上のサンプルコードを使う時は、その内容の是非を問うこと。必要なら書き換えるべし。
    • バグもあれば質の悪いコードだってあるさ。
    • 例えばVBScriptのサンプルコードにて文字列を格納した配列をForループで舐めて中身を連結していることに深く高度な理由があるのか、それとも単に作者が組み込み関数Joinを知らなかっただけなのか?
  • ネット上のサンプルコードを使う時は、そのコードが前提としている実行環境・開発環境・依存ライブラリ等に留意すること。
    • サンプルコード中で新しく便利なAPIを使っていて、しかしターゲット環境が古いためにまだそのAPIは使えなかった……なんてことがある。
    • あと「このクールなライブラリを組み込めば一発だぜ」という記事を鵜呑みにして実装したが、実はライブラリのライセンスが……なんてこともある。

結局のところ、ネット上のサンプルコードを読んで内容を理解してからでないと、適切に切り貼りすることは不可能なのだ。

まとめ

  1. IOの頻度に注意。
    • 例ではファイルIOを挙げたが、TCPのようにタイムアウトが起こりうる通信処理などでも注意すること。
  2. 急がば回れ。その方法は「小手先の対応」ではないか、手を付ける前に考えること。
    • ただし、本質的な対応を実施するに足るコストを賄えるか否かは別の話。
  3. ネット上のサンプルコードは、よく読んで内容を理解した上でコピペしてプログラミングすること。
    • コピペプログラミングで楽できるのは「ゼロから考えなくて済む」という点だけ。
    • 何も考えずにコピペプログラミングで一定以上の品質のモノが出来上がるはずがない。出来上がるのはウンコなコードだ。

リンク切れしたシンボリックリンクを探す

id:eel3:20121112:1352651058 でシンボリックリンクを張りなおすMakefileを書いたのだが、デッドリンク(リンク切れしたシンボリックリンク)がそのまま残ってしまう問題があった。

この問題に対処するために、デッドリンクを探す方法を――『覚えて便利 いますぐ使える!シェルスクリプトシンプルレシピ54』から持ってくることにした。ただ、ちょっと理由があって、デッドリンクを消すのではなく列挙することにした。単純に列挙するだけなら、他のツールと組み合わせて色々なことができるからだ。

Version 1

初期バージョン。

#!/bin/sh
# deadlink: select dead symlinks from arguments
# bug:
#   ----
#   xxxx -> yyyy (File xxxx is symlink to yyyy.)
#   yyyy -> zzzz (File yyyy is deadlink! file zzzz is not exist.)
#   ----
#   In above case, this script assumes that file xxxx is deadlink.

PATH=/bin:/usr/bin

for i; do
    [ -L "$i" -a ! -e "$i" ] && echo "$i"
done

考え方は単純で、シンボリックリンクかつ存在しないファイルを探せばよい。-hや-Lではファイルがシンボリックリンクか否かのみチェックするだけで、シンボリックリンクを辿ることはない。一方でファイルの存在をチェックする-eはシンボリックリンクを辿る。もしデッドリンクだったなら、-hや-Lは真を返すが、-eはシンボリックリンクを辿るのに失敗して偽を返す。

ただ、この方法は「シンボリックリンクを指し示すシンボリックリンク」では意図しない挙動となるかもしれない。xxxxがyyyyを指し示すシンボリックリンクで、yyyyがzzzzを指し示すシンボリックリンクで、zzzzが存在しなかったと仮定する。zzzzが存在しないのでyyyyはデッドリンクだ。xxxxは、yyyyが存在するのでデッドリンクではないはずだが、シンボリックリンクを辿った先の最終地点であるzzzzが存在しないので、このスクリプトアルゴリズムではデッドリンクと見なされてしまう。

もっとも今回の最終目的はあくまで「デッドリンクを消す」だ。デッドリンクであるyyyyが消された時点でyyyyを指し示していたxxxxはデッドリンクになる――削除対象となるのだから、最初の時点でxxxxまでデッドリンクとして列挙されても問題はないだろう。

ちなみに使い方はこんな感じ。

$ deadlink *
xxxx
yyyy
$ deadlink * | xargs -d '\n' rm
$ _

引数として渡したファイル名に対してデッドリンクか否かチェックを行い、1行1ファイル名の形式で標準出力に表示する。この例ではカレントディレクトリのファイルをチェックした結果、デッドリンクとしてxxxxとyyyyが列挙されている。もしデッドリンクを削除したいのなら、xargs(1)を使ってrm(1)の引数に渡せばよい。

ところで、なぜ「引数に指定したフォルダの中身をチェックしてデッドリンクを列挙する」みたいな仕様にしなかったのか? 理由は、それをやろうとすると面倒だから。再帰的にチェックするか否かとか、再帰レベルを調整できるように云々とか、そんな機能は他のツールに任せれば十分だ。具体的にはfind(1)とか。

Version 2

最初に書いたdeadlinkの実装では、チェック対象のファイルを引数として渡す仕様になっていた。

仮にあるディレクトリ以下にて再帰的にデッドリンクをチェックしたい場合、どうするのか? find(1)とxargs(1)を使えばよい。

find ./work -type l -print0 | xargs -0 deadlink

シンボリックリンクか否かのチェックを二重に行っているが、気にしない方向で。find(1)は列挙したファイルを標準出力に書き出すので、xargs(1)を使ってdeadlinkの引数に渡すようにする。

ただxargs(1)を使う方法はファイル数が多くなったときに効率の面が気になる。というのも、引数として許される上限までファイルが列挙されないとdeadlinkが実行されないのだ。なので下手をすればfind(1)によるファイルの列挙が完了してからdeadlinkが実行されるだろうし、そうなる可能性は高いと思う。

しかし、もしdeadlinkが標準入力からファイル名を読み込みつつデッドリンクか否かのチェックを行うのなら、find(1)などによるファイルの列挙とdeadlinkによるチェックが並行して実行される。マルチコア環境では高速化が期待できる。実際にはfind(1)もdeadlinkもファイルシステムにアクセスするので、効果は低いかもしれないが。

そんな訳で標準入力からもファイル名を読み込むことが可能なように改造することにした。ただ元々の仕様も捨て難い。元の仕様を残すのなら「引数が無かったら標準入力から読み込む」という仕様では都合が悪い(メタキャラクタであるアスタリスクを引数に指定したら、ファイルが無かったので引数ゼロになった――なんて可能性があるので)。そこで引数が `-' の1つのみだった場合にのみ標準入力から読み込むようにしてみた。

#!/bin/sh
# deadlink: select dead symlinks from arguments
# bug:
#   ----
#   xxxx -> yyyy (File xxxx is symlink to yyyy.)
#   yyyy -> zzzz (File yyyy is deadlink! file zzzz is not exist.)
#   ----
#   In above case, this script assumes that file xxxx is deadlink.

PATH=/bin:/usr/bin

if [ $# -eq 1 -a "$1" = '-' ]; then
    # Trims IFS character from the beginning and end of the $i.
    while read i; do
        [ -L "$i" -a ! -e "$i" ] && echo "$i"
    done
else
    for i; do
        [ -L "$i" -a ! -e "$i" ] && echo "$i"
    done
fi

実はチェック対象のファイルが1つだけで、且つファイル名が `-' だった場合に困るのだが……そこは運用でカバー、ということで。

Version 3

標準入力からもファイル名を読み込むことが可能なようにしたものの、今度はwhile readを使っている点が気になる。高速化のために追加した部分なのに、低速な機能を使うというのも妙な話だ。

そこでwhile readによるループの部分をスクリプト言語で書くことにした。

最初はawkを使おうとしたのだが、system()を使ってtest(1)を呼び出す部分の外部コマンドの組み立てが面倒だった(特にファイル名のエスケープ処理が……)。そこでPerl5にしてみた。Perlが入っている環境は結構多いはずなので、問題になることは少ないだろう。

#!/bin/sh
# deadlink: select dead symlinks from arguments
# bug:
#   ----
#   xxxx -> yyyy (File xxxx is symlink to yyyy.)
#   yyyy -> zzzz (File yyyy is deadlink! file zzzz is not exist.)
#   ----
#   In above case, this script assumes that file xxxx is deadlink.

PATH=/bin:/usr/bin:/usr/local/bin

if [ $# -eq 1 -a "$1" = '-' ]; then
    perl -n -e 'chomp; -l $_ && ! -e $_ && print "$_\n";'
else
    for i; do
        [ -L "$i" -a ! -e "$i" ] && echo "$i"
    done
fi

うん、非常に短い。もういい加減sayを使うべきなんだろうけどprintを使用している。

最近はデフォルトでPerl5が入っていない(!)事態も起こりそうなので、頑張ってPythonでも書いてみた。何が「頑張って」なのかというと、インデント位置である。

#!/bin/sh
# deadlink: select dead symlinks from arguments
# bug:
#   ----
#   xxxx -> yyyy (File xxxx is symlink to yyyy.)
#   yyyy -> zzzz (File yyyy is deadlink! file zzzz is not exist.)
#   ----
#   In above case, this script assumes that file xxxx is deadlink.

PATH=/bin:/usr/bin:/usr/local/bin

if [ $# -eq 1 -a "$1" = '-' ]; then
    python -c '
import os, sys
for line in sys.stdin:
  i = line.strip()
  if os.path.islink(i) and not os.path.exists(i):
    print i'
else
    for i; do
        [ -L "$i" -a ! -e "$i" ] && echo "$i"
    done
fi

インデント位置で怒られたので、仕方なくPythonのコードを行頭から書き始めることにした。うーん、見栄えが良くない……。なおPython 2.7にてそれっぽく動作することは確認したが、Python 3で動作するか否かは不明だ。

(念のため書いておくと、Pythonのインデントに関する仕様は特に気にしていない。少なくとも、Python単体でコードを書く分には好ましいと思っている。シェルスクリプトに埋め込むという使い方が邪道気味なのだ)

個人的にはRubyが好みなのだが――PerlPythonは入っていないがRubyは入っている、という環境は考えにくい。むしろ逆にRubyが入ってない環境の方が多そうなので、Ruby版は書いていない。

しかし、ここまでくると、シェルスクリプトPerlPythonのコードを埋め込むのではなく、全てPerlPythonで書いてしまった方がスッキリするような気が……。

終わりに

最終的には、上記のPerl5版に手を加えて、こんな感じになった。

https://github.com/eel3/eel3-scripts/blob/master/bin/deadlink