オブジェクトは目的語

オブジェクト指向プログラミング言語ではメソッドを呼び出したいとき、

<インスタンス名>.<メソッド名>

インスタンスを示す変数の名前のあとに、そのオブジェクトに属するメソッドの名前を示します。このプログラミング言語の構文は、誤解も招く良くない構文であったと思います。 <インスタンス名>.<メソッド名> の構文は英語の SV 文型を連想させるので、インスタンスが主語でメソッドが動詞であると勘違いしてしまいます。

しかし、実際にはオブジェクトを目的語とした方が自然な――人間に優しい――プログラムとなるように思います。 Java の標準ライブラリを見ても、オブジェクトが目的語となっているものが多いです。例えば Callable インターフェイスは call() メソッドを持ちます。 "callableObject.call();" とプログラム上は書きますが、英語で書くと "call the callable-object." を意味していると考えるのが自然です。他にも、 String 型の split メソッドも説明に "Splits this string around matches of the given regular expression." と書かれています。プログラミング言語では文は目的語の次に動詞が来るような形になります。主語が明らかではないかと思われるのですが、これは自明でメソッドの呼び出し元です (プログラムを書く人でも計算機でもいいですが)。

オブジェクトが主語になってしまうプログラムはあまり良いプログラムとはいえません。例えば、FooManager のような名前のクラスを作って、 "manager.doSomething();" と書くこともできるのですが、別にこれなら static メソッドで記述すれば十分なわけでクラスにする意味がありません。そもそもクラスとはあくまでデータ構造であって、そのデータに対する操作がメソッドです。操作の対象とはつまり目的語のことです。

プログラミング言語は決して英語ではないのですが、英語らしいプログラムが正しいプログラムだとしばしば思われているように思います。 というより私がそのような勘違いをしていました。 <インスタンス名>.<メソッド名> の構文は、そのような考えを持っていたときに、適切でない設計を招いてしまいます。これはプログラミング言語の欠陥だと思います。

例外は真偽値を返すメソッドです。Java の String 型だと、 contains() や isEmpty() が挙げられます。 "string.contains(sequence)" だとか "string.isEmpty()" という形で使われ、これらの文はオブジェクトが主語となった命題として振る舞います。オブジェクトが目的語となる場合の文は命令であり、オブジェクトが主語となる場合の文は命題です。既存のプログラム言語は、命令と命題が区別がなく、この点も良くないと思っています。

参考

ソフトウェアの設計など不要では?

ソフトウェアの設計技法は数多く提唱されてきました。構造化設計から始まり、オブジェクト指向設計デザインパターンドメイン駆動設計、など。私も趣味でこれらについてかなり勉強してきたように思います。問題を綺麗に解くことが好きだからでしょう。

しかし、単に私が不幸なだけかもしれないのですが、現実世界では人類が発明してきた設計技法は無視されているように思えます。設計工程と呼ばれるものはありますが、単にそれは書類をつくる仕事であって、設計などされていないようです。そもそも、設計ができる人はほとんどいないし、設計の良し悪しを評価できる人も限られています。こう書くと、私はできるのに他の人の程度が低いように聞こえますが、私もちゃんとした設計はできない; というか思想もなく一貫もしていない雑なものしか触れず、設計ができるかどうかを確かめる機会に恵まれなかった。

現実は美しくもない雑な仕事ばかりなのではあるが、それらに価値がないかと全くそんなことはない。美しさなんてのは、仕事の価値とは全く関わりのないものです。社会という巨大なシステムに組み込まれて、日々動いていることが価値でしょう。なんとなく作りたいから作って、使われてもいないようなものは、無価値でしょう。

ただ動くものを作るだけであれば、はたして設計なんて必要だろうか。別になくてもいいじゃないかと思っている。だって世の中それで動いているじゃないですか。モジュールに分割されていなくたって、システムとしては動く。逆に、モジュール単体で動くようにつくるのは結構難しい。Java であれば頑張ればできるが、 C や COBOL だったらほぼ無理だ。単体で動かないようなものにシステムを分割する意味ってあるのだろうか? どうせ結合テストシステムテストしかやらないのだったら、それに合格するだけの仕事で十分だろう。そして、昔やっていなかったのに、Java だとかの登場でモジュール単体の試験ができるようになったからといって、細かく設計をしなければならない理由はない。「作れたからなんとなく作っちゃいました」という人は捻り殺したくなるが、それと同様にできるようになったからやりましたもありえない思う。

設計にはコストつまり時間がかかる。設計なんかせずに、フラットな一つのルーチンにコピペしながら、ダラダラと書いていくのが最も生産性は高い。動くだけだったらそれで十分だ。また、設計なんてしたら性能が下がる。モジュールに分割したら、モジュール間で通信のコストが必要になる。完全にモノリシックなシステムの方が性能は高い。設計をしなければ、安くて速いものが手に入るのだ。

できる人もいなくて、やる意味も大してないなら、もうやらなくていいんじゃないかな?

有効数字で浮動小数点数を丸める

浮動小数点数の時系列データを圧縮するのに xor encoding という手法があるそうだ。

値が時系列に 15.5, 14.0625, 3.25, 8.625 というように並んでいたとする。

それぞれを倍精度浮動小数点で表すと、ビット列は下のようになる。

decimail double precision
15.5 01000000 00101111 00000000 00000000 00000000 00000000 00000000 00000000
14.0625 01000000 00101100 00100000 00000000 00000000 00000000 00000000 00000000
3.25 01000000 00001010 00000000 00000000 00000000 00000000 00000000 00000000
8.625 01000000 00100001 01000000 00000000 00000000 00000000 00000000 00000000

時系列データというのは、同じ値が続いたり、前後の値が近いという特徴がある。そのとき、近い数字はビット列の並びも近いとすると、前の値ビット列と XOR を取ると、ビットが立っているところが集中するようになる。

00000000 00000011 00100000 00000000 00000000 00000000 00000000 00000000
00000000 00100110 00100000 00000000 00000000 00000000 00000000 00000000
00000000 00101011 01000000 00000000 00000000 00000000 00000000 00000000

ビットが立っているところのデータだけ記録すれば、保存に必要なディスク容量が少なくなるだろうというアイデアです。ビットが立っているところだけ記録する方法は Gorilla の論文を参照してください。

しかし、この手法は本当にいいのだろうか?

浮動小数点数のバイナリデータをダラダラと吐き出すシステムもないわけではないのだろうが、ほとんどの場合10進数のテキストデータが渡されるのではなかろうか。10進数を浮動小数点数に厳密に変換することはできません。例えば、 3.14 を倍精度浮動小数点にしたときのビット列は

01000000 00001001 00011110 10111000 01010001 11101011 10000101 00011111

となります。これを10進数に戻すと 3.14000000000000012434… となります。

3.13 を倍精度浮動小数点にしたら、

01000000 00001001 00001010 00111101 01110000 10100011 11010111 00001010

となるのですが、3.14 と XOR をとったら

00000000 00000000 00010100 10000101 00100001 01000000 01010010 00010101

です。2進数でキリが悪い数字となっているので、値は近いにも関わらず、ビット列はそこまで一致していません。

そもそも、3.14, 3.13 は有効数字は3桁であり、倍精度浮動小数点は10進数で約16桁の精度なので、10進数をそのまま浮動小数点数に直すと無効な桁も表現してしまっていることになります。有効数字だけ残して、無効な桁を表現するビットを丸めてしまえば、もっと効果的にデータを圧縮できそうです。

3.14 の場合は、下のように有効数字を示す右側のビットを落としてしまってよいでしょう。

丸めなし 01000000 00001001 00011110 10111000 01010001 11101011 10000101 00011111
丸めあり 01000000 00001001 00100000 00000000 00000000 00000000 00000000 00000000

有効数字3桁で丸めた浮動小数点を10進数に戻すと、 3.140625 となります。これが2進数ではキリのよい数ということです。記事の最後にこのような丸めを実現するプログラム例を載せておきます。

テキストの 10進数 → 有効数字で丸めた浮動小数点 → xor encoding

という風に変換していけば有効数字を維持したまま時系列データを圧縮できるのではないかと考えました。

実際にやってみました。Gorilla の論文に載っている符号化は実装が面倒すぎたので、代わりに前の値との xor 値を並べたバイト列を圧縮してどの程度効果があるのか試した。

sar でとった 1秒おきの PC の CPU 使用率のデータで試してみたら、以下のような結果になりました。

サイズ [bytes]
もとのテキストデータ 70062
倍精度浮動小数点数に変換後 XOR 値をとって gzip 圧縮 29357
有効数字で丸めた浮動小数点数に変換後 XOR 値をとって gzip 圧縮 26887

一応、効果はあるようです。しかし、普通にテキストファイルを gzip 圧縮したら 18431 bytes となって、最も有効だった。PC の CPU の使用率は変動が激しすぎて、そもそも xor encoding が有効でないのだろう。滑らかな変動をするようなデータで試した方が良かったのかもしれない。


import argparse
import math
import re
import struct
import sys


class DoublePrecisionFloat:
    def __init__(self):
        self.bits = None

    @staticmethod
    def from_float(f):
        instance = DoublePrecisionFloat()
        instance.bits = struct.unpack('>Q', struct.pack('>d', f))[0]
        return instance

    @staticmethod
    def from_string(string):
        f = float(string)
        return DoublePrecisionFloat.from_float(f)

    @staticmethod
    def from_bits(bits):
        instance = DoublePrecisionFloat()
        instance.bits = bits
        return instance

    @property
    def raw_float(self):
        return struct.unpack('>d', self.bits.to_bytes(8, byteorder='big'))[0]

    def to_binstr(self):
        bb = self.bits.to_bytes(8, byteorder='big')
        return ' '.join(['{:08b}'.format(b) for b in bb])

    def to_hexstr(self):
        bb = self.bits.to_bytes(8, byteorder='big')
        hex2bytes = ['{:02x}{:02x}'.format(bb[i], bb[i + 1])
                     for i in range(0, len(bb), 2)]
        return ' '.join(hex2bytes)

    @staticmethod
    def __significant_bits(significant_figures, base):
        return int(math.ceil(
            significant_figures * math.log2(base)))

    # 11111111 11110000 00000000 ... 00000000
    # |<----------><------  fraction  ------>
    # |  exponent
    # sign
    __FILTER = int.from_bytes(
        b'\xFF\xF0\x00\x00\x00\x00\x00\x00',
        byteorder='big',
        signed=True)

    def round_off(self, significant_figures, base=10):
        # 0         0.1         1
        # |<-------------------||
        #        round off
        sig_bits = self.__significant_bits(significant_figures, base)
        # left side is padded with 1
        filt = self.__FILTER >> sig_bits
        new_bits = self.bits & filt
        return DoublePrecisionFloat.from_bits(new_bits)

    def round_up(self, significant_figures, base=10):
        # 0         0.1         1
        # ||------------------->|
        #        round up

        sig_bits = self.__significant_bits(significant_figures, base)
        # left side is padded with 1
        filt = self.__FILTER >> sig_bits

        # TODO: the case of overflow
        new_bits = (self.bits + (~filt)) & filt
        return DoublePrecisionFloat.from_bits(new_bits)

    def round(self, significant_figures, base=10):
        # 0         0.1         1
        # |<--------||--------->|
        #  round off   round up

        sig_bits = self.__significant_bits(significant_figures, base)
        filt = self.__FILTER >> sig_bits
        round_up_bit = 0x0010000000000000 >> (sig_bits + 1)
        new_bits = (self.bits + round_up_bit) & filt
        return DoublePrecisionFloat.from_bits(new_bits)

    def xor(self, other):
        return DoublePrecisionFloat.from_bits(self.bits ^ other.bits)


class FloatString:
    def __init__(self, string):
        self.string = string

    @property
    def significant_figures(self):
        # TODO: This code is ugly

        string = self.string

        # 0.0
        f = float(string)
        if f == 0.0:
            return 0

        # 0.0123
        pattern1 = r'[+-]?0\.0*(\d+)'
        match1 = re.search(pattern1, string)
        if match1:
            return len(match1.group(1))

        # 123.4 or 123.40
        pattern2 = r'[+-]?0*([1-9]+\d*\.\d+)'
        match2 = re.search(pattern2, string)
        if match2:
            return len(match2.group(1)) - 1

        # +1E+10 or -1e-10
        pattern3 = r'[+-]?(\d+)[eE][+-]\d+'
        match3 = re.search(pattern3, string)
        if match3:
            return len(match3.group(1))

        # +1.23E+10 or -1.23e-10
        pattern4 = r'[+-]?0*(\d\.\d+)[eE][+-]\d+'
        match4 = re.search(pattern4, string)
        if match4:
            return len(match4.group(1)) - 1

        # 1234
        pattern5 = r'[+-]?0*(\d+)'
        match5 = re.search(pattern5, string)
        if match5:
            return len(match5.group(1))

        raise ValueError("could not get significant figures: '%s'" % string)

    def to_rounded_float(self):
        d = DoublePrecisionFloat.from_string(self.string)
        sigfig = self.significant_figures
        return d.round(sigfig)

    def to_nonrounded_float(self):
        d = DoublePrecisionFloat.from_string(self.string)
        return d


def main():
    parser = argparse.ArgumentParser()
    out_formats = parser.add_mutually_exclusive_group()
    out_formats.add_argument('-b', action='store_true',
                             help='print results with binary strings')
    out_formats.add_argument('-x', action='store_true',
                             help='print results with hex strings')
    out_formats.add_argument('-f', action='store_true',
                             help='print results with decimal number')
    out_formats.add_argument('-n', action='store_true',
                             help='not round results')
    args = parser.parse_args()

    if args.b is True:
        write_func = lambda f: print(f.to_binstr())
    elif args.x is True:
        write_func = lambda f: print(f.to_hexstr())
    elif args.f is True:
        write_func = lambda f: print(str(f.raw_float))
    else:
        write_func = lambda f: sys.stdout.buffer.write(
            struct.pack('>d', f.raw_float))

    def stdin():
        try:
            while True:
                yield input().strip()
        except EOFError:
                pass

    for line in stdin():
        if args.n is not True:
            rounded = FloatString(line).to_rounded_float()
        else:
            rounded = FloatString(line).to_nonrounded_float()
        write_func(rounded)

if __name__ == '__main__':
    main()

プログラミングと英語

英語ができなかったら、プログラミングはできるとは言えないのではないか。英語ができなければ近いうちにソフトウェアエンジニアと名乗れなくなるのではないかと思う。私は英語ができない側の人間だから、こんなことを考えるのだと思いますが。

日本社会として今は空前の英語ブームです。本屋に行けば英語教材の棚の占める面積は大きい。昇進や入社に TOEIC テストの点数を課している企業も多いので、多くの人々が英語の勉強をするべく、教材を買っている。従業員に英語力を課す企業は英語が必要になる方に賭けるという経営判断をしたのだろう。日本人の会社員の皆が英語を読んだり書いたり話したりできる必要があるのかは、私には分からない。英語を課した企業が賭けに勝つのかどうかは、おいおい結果が出るだろう。

企業として英語化が良いのかは分からないが、個人としては良いことなのは間違いないと思っている。これだけ英語ブームだったら、英語ができたら給料が上がる。従業員に英語力を課した企業は当然英語ができる人の給料を上げなければならない。*1一方で英語なんてどうでもいいと思っている企業は安い給料で英語ができない人を雇えるのだから、人件費の削減になる。企業として英語ができる人を比較的高給で雇うか、できない人を安く雇うかのどっちが得かは、私には分からないが、個人としては英語を覚えた方が得だろう。

上記は会社員一般についての議論だが、ソフトウェアエンジニアもしくはプログラマに関しては、英語ができなかったらそもそも仕事にならないのではないかと思ったりする。理由は二つで、一つは英語が読めなかったらマニュアルが読めない;もう一つは英語の読み書きができなかったらソースコードの読み書きができないことです。

インターネットがこれだけ普及した現在、我々日本人がアクセス可能な情報であっても、ほとんどの情報は英語で書かれている。知っているか知らないか――エンジニアは専門職なのだから専門知識を有していることが職業的な優位性を担保している。同じ問題に直面している人は世の中には多いものなので、既に世の中には問題の解決策が存在していることがしばしばある。普通にマニュアルを読めば答えが書いてあったりするし、Stack Overflow にヒントが載っていたりすることもある。それを知っていれば問題は素早く解決できる。エンジニアってそのために雇われているのでしょう? もちろん、Stack Overflow や Blog ポストに載っていることが正しいこととは限らないので、裏取りが必要なのは言うまでもない。隣に座っている人の情報の信憑性も同程度ではあるので、正しい情報源としては結局マニュアルを当たる必要があり、マニュアルが英語だった場合はそれが読めなければ困ったことになってしまう。いわゆるオープン系のシステムは、知らない人が作った部品を使うことがその定義であり、ほとんどの場合英語で書かれているマニュアルを読めなければ、仕事にならない。

マニュアルに限らず、ソースコードだって英語で書くものだ。英語ができなくても coding つまり機械に読めるような符号を書くことはできるが、例えば整数に全て i をつけるようなことはできるが、それはプログラミングではない。符号化はコンパイラの仕事であって、プログラマがやることではない。私は、「設計=名付け」と考えていて、名付けができていなければ、設計をしていないと思っている。英語ができなければ名付けができないので、英語ができないならプログラミングができないという考えになる。ただ動くだけのものだったら、別に英語力なんて要らないが、ただ動くだけのソフトウェアが極端に組織の足を引っ張るのは、誰しも経験して知るところだろう。

私がソフトウェアエンジニアを雇うなら、英語ができることを課す。自分が英語を出来なきゃ、英語ができる人のマネジメントなんてできないので、ちょっとは勉強しなければならないなと強く思う今日このごろ。

*1:英語のできないおじさんの給料は下げられないが、若い人には語学力を求めるような、おかしな会社もあるかもしれないが、そんな会社はさっさと辞めた方がいいと思う。

Bloom Filter を作ってみた

Bloom Filter を実装してみた。簡単な実装なので、速度や空間効率は悪いです。

Bloom Filter というのは、確率的データ構造の一つで、ある要素が集合に含まれるかどうかを試験するものです。空間効率が非常に良いのが利点で、偽陽性、つまり集合に含まれない要素もあるとしてしまう場合があるという欠点があります。偽陰性はありません。

以下が実装例です。

import hashlib
import math

class BloomFilter:

    def __init__(self, num_item, false_positive):
        assert 0 <= false_positive <= 1
        # Optimal number of bit array size and hash function
        # https://en.wikipedia.org/wiki/Bloom_filter#Optimal_number_of_hash_functions
        log2 = math.log(2)
        logp = math.log(false_positive)
        size = - num_item * logp / (log2 * log2)
        num_hash = size / num_item * log2

        self.size = int(math.ceil(size))
        self.num_hash = int(math.ceil(num_hash))

        self.array = self.BitArray(self.size)
        self.hash_functions = [self.HashFunction(i, self.size)
                               for i in range(self.num_hash)]

    def add(self, item):
        for hash_func in self.hash_functions:
            i = hash_func(item)
            self.array[i] = True

    def __contains__(self, item):
        return self.contains(item)

    def contains(self, item):
        for hash_func in self.hash_functions:
            i = hash_func(item)
            if self.array[i] is False:
                return False
        return True

    class BitArray:

        def __init__(self, length):
            # Allocate an object per a bit for simplicity
            self.array = [False for _ in range(length)]

        def __getitem__(self, index):
            return self.get(index)

        def get(self, index):
            return self.array[index]

        def __setitem__(self, index, bit):
            return self.set(index, bit)

        def set(self, index, bit):
            if bit > 0 or bit is True:
                self.array[index] = True
            else:
                self.array[index] = False

    class HashFunction:

        def __init__(self, seed, maximum):
            self.seed = seed.to_bytes(
                seed.bit_length() // 8 + 1, byteorder='big')
            self.maximum = maximum

        def __call__(self, byte_buffer):
            return self.call(byte_buffer)

        def call(self, byte_buffer):
            # MD5 is enough because I don't want to oneway hash function
            b = hashlib.md5(byte_buffer + self.seed).digest()
            i = int.from_bytes(b, byteorder='big', signed=False)
            return i % self.maximum

要素の数と偽陽性の確率を指定します。

>>> bf = BloomFilter(num_item=1000, false_positive=0.01)

アイテム数 1000 で偽陽性率が 1% で、たったの 9856 bit = 1232 bytes にデータが収まります。

>>> bf.size
9586

要素を入れて、入っているかどうかが判定できる。

>>> bf.add(b'1')
>>> b'1' in bf         # 必ず正しい
True
>>> b'2' in bf         # たまに間違う
False

面白いですね。

異常時にちゃんと止まるシェルスクリプト

自動化は大切ですが、自動化するときは異常時にちゃんと止まるようにしなければなりません。トヨタ自動車でいうところのニンベンのついた「自働化」である。なぜ自働化が必要かというと、エラー時に止まってくれない機械は稼働中に人が張り付いて見守っていなければならないからです。また、エラーが起こったのちに機械が動き続けるならば、異常なアウトプットが出力され続けることになり、異常なアウトプットを処理しなければならないというムダが発生します。自動化のためのスクリプトを書くときは、エラー時には止まるように作らなければなりません。

しかしながら、シェルスクリプトは恐ろしいことにエラーが起きても既定ではそのまま処理を続けていきます。異常時にちゃんと止まる自働化スクリプトとするためには以下をスクリプトの先頭に書くと良いでしょう。

set -eu -o pipefail
trap 'echo "ERROR: line no = $LINENO, exit status = $?" >&2; exit 1' ERR
set -e
set -e
echo 1
false
echo 2

上記のシェルスクリプトを実行すると、以下のように false のところ、つまり 0 以外で exit したところで止まります。

$ bash sete.sh
1
$ echo $?
1
trap

しかし、上記ではどこで止まったのかが分からないので、どこで止まったのかを trap を用いて表示するようにします。

set -e
trap 'echo "ERROR: line no = $LINENO, exit status = $?" >&2; exit 1' ERR
echo 1
false
echo 2

trap に第1引数で指定されたコマンドがエラー発生に実行されます。 LINENO 変数には現在の行番号が格納されています。実行と以下のようにエラー発生時の行番号と exit status が標準エラー出力に表示されて止まります。

$ bash traperr.sh 
1
ERROR: line no = 4, exit status = 1
$ echo $?
1
pipefail

set -e だけだとコマンドをパイプでつないだ場合に上手く止まらない場合があります。

set -e
trap 'echo "ERROR: line no = $LINENO, exit status = $?" >&2; exit 1' ERR
echo 1
false | true
echo 2

を実行すると、 false コマンドが失敗しているはずなのに、止まりません。

$ bash pipefail1.sh 
1
2
$ echo $?
0

set -o pipefail をつけるとパイプの途中のコマンドが失敗しても、止まります。

$ cat pipefail2.sh 
set -e -o pipefail
trap 'echo "ERROR: line no = $LINENO, exit status = $?" >&2; exit 1' ERR
echo 1
false | true
echo 2
$ bash pipefail2.sh 
1
ERROR: line no = 4, exit status = 1
$ echo $?
1
set -u

set -u を指定すると宣言してない変数が使用されるとエラーになります。

$ cat setu.sh 
set -u
echo 1
echo $AAA
echo 2
$ bash setu.sh 
1
setu.sh: line 3: AAA: unbound variable

自働化は運用時の話なので、タイポなんてのは運用前になくなっているはずなので、運用時にはなくてもいいですが、間違っているのに動いちゃっているという状態にならないためにつけておくべきでしょう。

プログラムのレイヤーは降りれても登れない

プログラムにはレイヤーというものがあります。レベルとも呼びます。プログラムというのは人間の理解を超えて複雑なものなので、全容を知るということは人間には出来ません。理解できない複雑なものを理解するために、人間には便利な思考法が備わっています:抽象化という思考法です。抽象思考がどういうものなのかは、説明が長くなるので、ここでは省きますが、プログラムの世界もレイヤー化という一種の抽象思考によって驚くほど複雑なものが作られてきました。一般に、カーネルなど生の機械を触るプログラムを低レイヤー(低レベル)と言い、アプリケーションの画面など人間に近いプログラムを高レイヤー(高レベル)と言います。

ソフトウェアエンジニアとざっくり呼ばれますが、レイヤーごとに仕事をしている人が異なります。高レイヤーのエンジニアはアプリケーションエンジニアと募集されていることが多いですし、比較的低レイヤーのプログラミングは最近ではシステムプログラミングと呼ばれます*1。同じプログラミングではあるけれども、求められている知識やスキルが微妙に異なるので、レイヤー毎にエンジニアの棲み分けが行われています。

低レイヤーのエンジニアは、高レイヤーのエンジニアより自分は偉いと思っていることがしばしばあります。私が知っているカーネルの開発をしている人は、職業欄に “Kernel Developer” とわざわざ書いています。俺は特別だと思っているのでしょう。鼻には付きますが、自分の仕事に誇りを持つことは悪いことではないです。

しかしながら、そのよく分からない高慢がエンジニアのキャリアや企業の収益の可能性を制限してしまっているのではないかと思います。

低レイヤープログラマは「低レイヤーができたら高レイヤーもできるが、逆はできない」と言うのですが、これは嘘です。使う人の気持ちになれないからです。作ったけど何に使うかは分からないと、恥ずかしげもなく言う程度の低い人がいる。そういう人々は、自分の仕事は難しいのだと偉そうにしているけれども、低レイヤーのプログラミングすら実はまともにできないのだ。

さらに、現在では、低レイヤーのソフトウェアを作ることが大して金にならなくなっている。Linux のように OSS として公開されているものが、選ばれるようになっているからだ。それに、例えば Google みたいな低レイヤーの製品で利益を得ていない企業が、自分らが使うために作ったが、隠し持っていても競争力に繋がらないし、広く使われるほうが有利だと OSS を公開している。

使い道を分かっている人が作って無償で公開しているものがあるのに、どうして使い道も考えずになんとなく作っている製品を買う人がいるのだろうか。無償で配布されているものにすら及ばないものしか作れないなら、その仕事に1円の価値もないのではないかと思う。

そんな事業辞めて、もっと高いレイヤーに移れば良いのにと思うが、それができない。売るレベルのものが作れないとしても、一回覚えたことにこだわってしまうものだ。それに程度の低いプログラマが高レイヤーのプログラムを書くとそれはそれはひどいものができる。なんとなく動くし、性能はいいのかもしれないが、抽象度が整っていない難読でメンテ不可能な代物になる。また、使い道が分かっていないというのは変わらないので、作ったものに商品性はない。レイヤーを移るのは無理なので、仕事を維持するための仕事をして1円の価値もないものを作り続けるしかないのだろう。

お前らはそれしかできないかもしれないが、俺は他のことができるのに、どうしてお前らの仕事を守るためだけのことをしなければならないのか――と思っている人は、さっさと金になるレイヤーに移った方がいいと思う。使い道が分かるようになったら、再びレイヤーを降りてプロダクトマネージャー的なことをやったら良い。その方がキャリアもそうだし、社会のためにもなると思う。

*1:システムプログラミングをする人のことは何と呼ぶのだろう?インフラエンジニア?