はじめに
sort
コマンドには重複行を取り除くための -u
オプションを備えています。
$ printf '%s\n' 1 2 3 3 2 1 | sort -u
1
2
3
Unix 哲学的に考えれば、行を並び替える sort
コマンドと重複行を取り除く uniq
コマンドは別のコマンドであるべきなように思えます。しかし sort
コマンドには -u
オプションとして uniq
コマンドに相当する機能が組み込まれています。なぜそうなっている(そうなってしまった)のかを「ソフトウェア作法(さくほう)」を参照しながらこの記事で明らかにしたいと思います。
関連記事
「誰」がuniq機能をsortコマンドに組み込んだ!?
熱烈的な Unix 哲学の信者は「どうせ Unix 哲学を理解しない GNU が便利だと思ってオプションを追加したのだろう」と考えるかもしれません。しかし uniq
機能が組み込まれたのは Version 7 Unix、つまり Unix の開発者が組み込んだのです。これは 1979 年の Version 7 Unix のドキュメントから明らかです。
u Suppress all but one in each set of equal lines.
Ignored bytes and bytes outside keys do not participate
in this comparison.
もしかしたら当時はまだ uniq
コマンドがなかったのでは? と思う人がいるかもしれませんが、sort
コマンドは 1972 年の Version 2 Unix、uniq
コマンドは 1973 年の Version 3 Unix で追加されました。つまりVersion 7 Unix で -u
オプションが追加されたときには uniq
コマンドはすでにありました。
-
sort
: 1972年 Version 2 Unix http://squoze.net/UNIX/v2man/man1/sort -
uniq
: 1973年 Version 3 Unix http://squoze.net/UNIX/v3man/man1/uniq
ちなみに、-u
オプションが sort
コマンドに追加されたのは Version 7 Unix からですが、1975 年のVersion 6 Unix では sort
コマンドに uniq
機能を組み込んだ usort
という名前のコマンドがあります。
したがって 1975 年には sort
コマンドに uniq
機能を組み込んだ方が良いという判断がされていたことがわかります。-u
オプションは本家の機能ですから、当然 BSD Unix にも初期(最初?)の頃からあります。最初の BSD Unix は Version 6 Unix がベースなのでもしかしたらないかもしれませんが。
探せば例外は見つかるかもしれませんが、世の中に Unix が広まった時から -u
オプションは sort
コマンドに組み込まれていたことがわかります。
「効率」のためにuniq機能はsortコマンドに組み込まれた
sort
コマンドに uniq
機能を組み込むメリットは効率です。sort
コマンドによる並べ替え結果を標準出力に出力し、それを uniq
コマンドが読み取って重複行を取り除いて出力するよりも、sort
コマンドが直接重複行を取り除いた方が、無駄な処理をしない分効率が良いのは容易に想像することができます。おそらくプログラミングというものを理解している人であれば、大体の人はこの結論にたどり着くでしょう。
ちなみに現在の高速でマルチコアの CPU と SSD などの高速なストレージでは、大きな負荷とならず複数コアに分散されるからか sort
+ uniq
でも sort -u
でもそう大差はないようです。理屈的に考えると sort
+ uniq
は総 CPU 使用量は多いが実時間は短くなるように思えます。
「ソフトウェア作法」(Software Tools)を読め!
嘘です。別に読まなくていいです。書いてある日本語訳が古くて疲れるし同等の内容は他の本でいくらでも学ぶことができるでしょう。ただし出版された年を考慮すると当時にこのような考え方を持っていたのはさすがとしか言いようがありません。「Software Tools」は 1976 年に出版された本で、日本語訳は「ソフトウェア作法」として 1981 年に出版されました。良い道具として役立つ良いプログラムを書くための方法を示した本です。著者は Unix 開発者としておなじみの Brian Kernighan と Plauger です。ちなみに出版された 1976 年は Version 6 Unix (1975) と Version 7 Unix (1979) の間の年で、つまり usort
コマンドができた後で sort
コマンドに -u
オプションが追加される前、執筆と開発の時期を考えればほぼ同じ頃と考えられます。
同書は良い「道具を作る」ためのプログラミング方法を解説した本です。作法の読み方は「さほう」ではなく「さくほう」です(訳者まえがきより)。つまり訳書のタイトルは「ソフトウェアを作る方法」となります。ここで言う道具とは主に Unix コマンドのことですが「道具を組み合わせる」シェルスクリプトの書き方が書いてあるわけではありません。そもそも原著が出版された時代は Bourne シェルが誕生する前です。Ratfor と呼ばれる Fortran を構造化プログラミング言語に改良した言語(Fortan へのトランスパイラ)が使用されており、記載されているソースコードはシェルスクリプトでは実践できないものばかりです。あくまでシェルスクリプトから呼び出す道具を作る方の本です。保守や変更が容易なプログラムとはどのようなものかを解説しており、構造化プログラミングを前提とした説明になっています。構造化プログラミングの考え方は現在では当たり前のものとなっていますが、同書が出版された頃の 1960 年代後半から 1970 年代前半にかけて話題になったプログラミング手法です。
「ソフトウェア作法を読め!」というのは、sort
コマンドに -u
オプションを加えたやつは Unix 哲学を勉強しろ!と言いたいのではなく(そりゃそうです。著者はUnixの開発者の一人ですから)、Unix 哲学を理解しているはずの人が sort
コマンドに効率のために uniq
機能を組み込んだと書いてあるからです。
「機能を適切に分離する」というコト
この話は同書の196ページ、4章「整列」の「4.7 機能の分離 --- unique」で説明されています。
同じものを 1 箇所に集め、集団として取り扱うことができるようにするために整列作業をする、ということはよくあることである。ときには、その集団からただ一つの要素を取り出し、他は捨ててしまってよいこともある。
(中略)
という追加機能を sort につけ加えることは簡単である。(それをするとしたら、そのための処理はプログラムのどこでやったらよいか?)だがそもそもこれは sort の一部でやるべきことなのだろうか?
これはよい道具の設計に関する一つの重大な基本問題に触れる設問である。その基本問題とは、機能の適切な分離という問題である。
という問題提起を行っています。
「一つのプログラムにはただ一つの機能を持たせておくべきだ」
一つのプログラムにどれだけの機能を組み込んだらよいだろうか? いまの場合、重複する要素を捨てる、という仕事は整列プログラムに組み込んでしまった方が「能率」がよい。というのは、データの通読を 1 回節約できるからである。またもっと重大な問題として、二つの行が「同一」かどうかの判断は使われている比較関数いかんによって定まるものであり(中略)整列機能と重複除去機能をひとまとめにすれば、比較がたしかに首尾一貫した形で、しかも能率よくおこなわれるという利点がある。
sort
コマンドに -u
オプションを組み込む理由が効率(能率)であることは、私が言ってるだけではなくこの本の著者が言っていることです。
少しだけ複雑さが増すことを覚悟すれば一つのプログラムで間に合うのに、わざわざ別のプログラムを二つ用意する必要があるだろうか?一つの有力な理由づけは、どちらか一方の機能だけ使いたい人がいるかも知れないから、というものである。重複除去機能を整列機能から分離すれば、それらを組み合わせてしまうとできなくなるような仕事をすることができる。たとえば行が重複しないことを確かめたいとか、行が重複していることを確かめたいとか隣接する重複の数を数えたいとかいうことは、いかにもありそうなことである。
ここに書かれているように、実は uniq
コマンドが持っている機能は重複行の除去だけではありません。uniq
コマンドは多くの機能を持っています。
$ printf '%s\n' foo foo bar baz baz | uniq -u
bar
$ printf '%s\n' foo foo bar baz baz | uniq -d
foo
baz
$ printf '%s\n' foo foo foo bar baz baz | uniq -c
3 foo
1 bar
2 baz
uniq
コマンドの -u
と -d
オプションは uniq
コマンドが誕生した Version 3 Unix から、-c
オプションは Version 4 Unix からあります。
-
uniq
-u
,-d
: 1973年 Version 3 Unix http://squoze.net/UNIX/v3man/man1/uniq -
uniq
-c
: 1973年 Version 4 Unix http://squoze.net/UNIX/v4man/man1/uniq
整列、重複除去、計数の3機能を全部組み込むとしたら、整列プログラムはずいぶん複雑なものになるし、それに重複した要素を捨てる前に整列されては困ることももちろん十分あり得るのだ!
あまり早い時期に機能を統合するのはまちがいである。少なくともはじめのうちは、一つのプログラムにはただ一つの機能をもたせておくべきだ。やがては多数の追加機能が組み込まれていくかもしれないが、それらの追加機能はお互いに密接に関連したものであるべきだ。
「一つのプログラムにはただ一つの機能をもたせておくべき。」です。しかし著者は一つのプログラムにはただ一つの機能を持たせなければならないと言っているでしょうか?「少なくともはじめのうちは」と言っていますよね?「やがては多数の追加機能が組み込まれていくかもしれないが、それらの追加機能はお互いに密接に関連したものであるべきだ。」と言っていますよね? この部分を勘違いしてはいけません。
「われわれは永年にわたって整列と重複除去を分離していた」
われわれは永年にわたって、整列と重複除去を別のプログラムによっておこなっていた。だがついに効率の問題が重大化し、その結果これらのプログラムは統合された。sort に、隣接する重複を除くための追加機能が組み込まれたのである。(もとの重複除去プログラムがそのまま残され、引き続きさかんに使われて行ったことはもちろんである。)だがはじめのうちは、これがうまい組み合わせだとは誰も気づかなかったのだ。教訓は何か?機能は組み合わせ方がわかるまではばらばらにしておけ、というのがそれである。
sort
コマンドに重複行の除去機能が組み込まれた理由が、効率の問題が重大化したからであることがはっきりと書かれています。この教訓の重要なポイントは「機能はばらばらにしておけ」ではなく「組み合わせ方がわかるまで」という所です。
教訓の教訓『適切な理由があるなら機能を統合しろ!』
ここでの教訓の教訓は私が言っていることに注意してください。
私が言いたいことは「Unix 哲学を間違って理解しているものは、機能をバラバラにして単機能のコマンドを作ることこそが Unix 哲学であり唯一の正しい設計方法だと勘違いしている」ということです。なんでも極端に考えてはいけません。世界はそんなにシンプルではないのです。
実際の Unix コマンドを見れば明らかです。多くのコマンドは「一つの機能だけ」を持っているわけではありません。分離しようと考えれば分離することができる多数の機能を持っているコマンドがいくつもあります。もちろん Unix 開発の初期の頃のコマンドからです。最初のバージョンの機能は一つであったとしても、結局は一つのコマンドは複数の機能を持つように機能追加されているのです。
一つのコマンドが一つの機能であることはたしかに理想的です。しかし現実はそれが良い選択とは限りません。Unix 哲学が言っているのは、シンプルになるように努めようという程度のものです。そもそも Unix 哲学は「一つのことをうまくやる」と言っていますが「一つの機能だけにしろ」とは言っていません(よく読んでください)。一つのことをうまくやるコマンドの多くは複数の機能を持っています。例えば重複除去機能を独立させたはずの uniq
コマンドは複数の機能を持っていましたよね?
複数の機能を一つのコマンドにしてしまうとできなくなることがありますが、逆に複数の機能を一つのコマンドに統合させなければできない(実現しにくい)こともあります。例えば ps
コマンドの出力結果をヘッダ行を残したまま絞り込むのはどうしたら良いでしょうか? これは grep
の機能を内包している sed
や awk
なら簡単にできることです。
$ ps | sed -n '1{p;n;}; /bash/p'
PID TTY TIME CMD
2885036 pts/25 00:00:00 bash
$ ps | awk 'NR==1 || /bash/'
PID TTY TIME CMD
2885036 pts/25 00:00:00 bash
sed
は複数の機能を持っていますが、ストリーミング型のテキストエディタとして「一つのこと」を行うコマンドであり、awk
は複数の機能を持っていますが、パターンマッチングを行い文字列処理と数値計算という「一つのこと」を行うコマンドです。Unix 哲学は単機能のコマンドを作れと言っているのではなくて、コマンドにはそのコマンドとって適切な機能を持たせろと言っているのです。
簡単なことをより簡単に実現するという考えから、単機能もしくは少ない機能のコマンドを「別に用意する」ことは良い考えですが、だからといって複数の機能を持ったコマンドを作ることが Unix 哲学に反するわけではありません。複数の機能を一つに統合すると複雑になりますが、その複雑さをコントロールするプログラミング技術を身に着けていれば問題ありません。つまり一つのプログラムの中を小さい機能の関数の集まりとして実装するわけです。それが Unix 哲学と構造化プログラミングに共通する「モジュール化」という考え方です。重要なことは「機能を適切に分離する」ということであり、適切に分離するというのは「機能は組み合わせ方がわかるまではばらばら」にしますが正当な理由ができれば統合しちゃっていいのです。
さいごに
「Unix 哲学」というと大層なことに思えますが、良いソフトウェア開発の基礎として根付いており、現代のプログラマなら誰でも自然にやっていることです。Unix 哲学は物事を複雑にしないための基本的な考え方であり設計方法ではありません。現実にはさまざまな理由で妥協することが必要なものです。「ソフトウェア作法」で著者が言っていることは構造化プログラミングの原則を忠実に守る、しかし構造化プログラミングを取り入れたからと言って即座に良いプログラムになるわけではない。重要なのは、コードを複雑にしない、移植性のためにシステムを分離し、可読性を高め、保守しやすくすると考えながらプログラムすることであり、プログラムの効率よりも人の効率を重視し、他の人が使いやすい道具を作り、そして他の人が作った道具を利用するということです。
おまけ Unix哲学を学べる本
日本語訳版のタイトル紛らわしすぎ問題(単なる自分用リンク)
- The Elements of Programming Style (1974)
- 著者: Braian W. Kernighan, P.J. Plauger
- ソフトウェア書法 (1976) 木村泉 訳
- Software Tools (1976)
- 著者: Braian W. Kernighan, P.J. Plauger
- ソフトウェア作法 (1981) 木村泉 訳
- The Elements of Programming Style, 2nd Edition (1981)
- 著者: Braian W. Kernighan, P.J. Plauger
- ソフトウェア書法 第2版 (1982) 木村泉 訳
- Software Tools in Pascal (1981)
- 著者: Braian W. Kernighan, P.J. Plauger
- 日本語訳 ないはず
- The Unix Programming Environment (1984)
- 著者: Braian W. Kernighan, Rob Pike
- UNIXプログラミング環境 (1985) 野中浩一 訳 (アスキー版)
- UNIXプログラミング環境 (2017) 野中浩一 訳 (ドワンゴ版・上記と同一内容のはず)
- Program Design in the UNIX Environment (1984)
- 著者: Braian W. Kernighan, Rob Pike
- 4.2BSDやSystem Vの拡張がUnix哲学的じゃないと批判した論文
- https://harmful.cat-v.org/cat-v/unix_prog_design.pdf
- UNIX環境におけるプログラム設計 (1987)
- (「UNIX原典―AT&Tベル研のUNIX開発者自身によるUNIX公式解説書」に収録)
- The Practice of Programming (1999)
- 著者: Braian W. Kernighan, Rob Pike
- プログラミング作法 (2000) 福崎俊博 訳 (アスキー版)
- プログラミング作法 (2017) 福崎俊博 訳 (ドワンゴ版・上記と同一内容)