日々常々

ふつうのプログラマがあたりまえにしたいこと。

技術から入ってもいいと言う話

システム開発の分野は技術の移り変わりが早く(これも他の分野と比べたことないので「早い気がする」と言うだけなのだけど)、なんらかの成功を収めた企業などの採用しているものがバズワードとなって一気に広まったりします。この時、その技術だけを追ってしまう現象がよく観測されます。だからバズワードになるんだけど。

批判的な意見

バズワードに飛びつくのは往々にしてアンチパターンです。例えば「モノリスからマイクロサービスへ」でも、明確な理由もなくマイクロサービスに飛びつくのは避けるよう書かれています。

書籍に限らず経験のある人は技術の螺旋を見通し、「この技術は結局のところ重要な課題を解決しない」と示唆する発言をしたりします。 強く辛辣な言葉もたまに見かけます。「そこに問題はないんだけどな」とか。

こう言う意見を無邪気に聞いてしまう人は、場合によっては「本質」と言う言葉に振り回されるようになります。 「本質はそこじゃないんだ」とか言うものの、見通せているかは危うかったりして。 いっちょ噛みで言う「本質」は「完全に理解した」と同義ですし、そんなすぐに本質を見抜けるのなんて卓越した技能を持った人だけです。居ないとは言わないけど、多分その一部ではないんじゃないかな。 そして結局何をすればいいのか途方に暮れているようなのも散見されます。

強そうな人の強い発言を目にしてしまうと、その技術を習得することに抵抗を覚えるでしょう。 これ自体は自然な気持ちだと思いますが、強い発言は広大な背景を持ってなされるものです。その背景は当人にとって当たり前のことなので、わざわざ語られたりもしません。 強い発言は往々にして正しく、説得力があります。経験から発せられる一本筋の通った発言に惹かれるのは当然ですが、その意見を取り入れられる土壌がない状態ではおそらく劇薬でしょう。

技術から入ってもいい

と言うことで、掲題の通り。技術から入ってもいいと思っています。

特定の技術は、特定の課題を解決するための模範解答の一つです。 短い記事やブログでなく、ある程度のボリュームの書籍や実用に耐えるツール、テンプレートなどまで昇華された技術には学ぶ価値があります。

対象の問題領域に対して知識が不足している状態で、問題だけを見てコアを掴めるのは一握りのモデリング能力を有した人だけでしょう。少なくとも私にはできない。 その問題を捉え、ある側面で切り取ったものが技術だと思います。 その特定の技術に精通すれば、解決しようとしている問題領域が、少なくともその技術の実装者が捉えたモデルが習得できます。 他の捉え方を身につけるのは、その視点を得てからでも遅くはありません。遅くはないはずです。早い遅いが何で決まるかは知らないけど。

とにかく その技術を最終到達点と認識しなければ大丈夫です。 逆に言えば、その技術があらゆる問題を解決する銀の弾丸(「銀の弾丸」は狼男にしか効かないので、万能ではないと思うんだけど、なんか万能の代名詞として使われますよね。なんでだろ。)やゴールデンハンマーのように捉えたとしたら大丈夫ではありません。先にあげた「批判的な意見」はこれを嗜めるものだと思っています。当人にはそんなつもりもなく、思ったこと言ってるだけだったりしますが。

繰り返します。知らないのならば、技術から入る選択肢を取っても構いません。 ただし入り口です。なので「技術 から 入ってもいい」です。 その技術を通して問題の輪郭を掴めたら、同じ問題に対した他の技術で照らし直してみたり、直接問題領域に飛び込んだりしてみることです。 次のステップがあることを意識するだけで、だいぶ違うと思います。

特定の技術に固執する癖があるならば、少なくともそこは改めることをお勧めします。 固執して突き進めるのは、技術ではなく問題を選べる場合です。卓越した技術があれば問題の方を選べます。問題を選べるようなら、技術への固執云々はたぶん気にしなくていいと思います。 私は問題にあった技術を選ぶのが好きなんで、その気持ちはわかりません。

コード、区分、フラグ、IDと名前。

システム開発ではよくコードや区分、フラグと呼ばれるものを扱います。

これらが混同していたり、曖昧になっていたりすることは多いので、その辺りを整理しておきます。 IDや名前なんかもこの文脈に登場するので、ついでに。

コード

コードはエンコード/デコードできるものです。 桁数ごとに意味があったり。 一定のルールで読み書きできるのがコードかなと思います。

区分

種類が固定されたコードを、特別に区分と呼ぶことにします。 値ごとに意味が決まるので、コードの一種としています。

どんな値が入りうるかわからないものは区分とは呼べません。 それは多分コードと呼んだ方がいいです。

フラグ

種類が2つ(on/offやtrue/false)に固定された区分、特別にフラグと呼ぶことにします。 3つ目が出てきたらフラグとは呼べません。 それは区分って呼んだ方がいいんだろうなと思います。

フラグと区分の関係を示したいのは、フラグはしばしば増えるからです。 削除フラグに入ってる9とか2とかのことです。君はもうフラグじゃない。

ID

IDはidentifier、識別子です。 大事なのは一意であることだけです。 値の体系は関係ありません。文字として読めなくてもIDはIDです。

体系の決まっているIDはコードの側面も持ちますが、だからと言ってIDはコードではありません。 こう言うのは「IDのコード体系」と捉えるのがいいかなと。

コードをそのままIDとして使うこともあります。 こう言う文脈ではコードとIDは混同されがちですし、その文脈に閉じれば、同じと看做しても問題ありません。 でも広い文脈ではコードとIDは異なります。

IDは識別する範囲内で一意であればいいので、区分はフラグもその範囲内では一意だとかなんとか言えます。それに意味はない気はしますが。

名前、番号

これらは機械ではなく人が主に使用します。 名前や番号自体に一意性はありません。読み書きのルールもあるようなないような感じです。

名前や番号として固有の値を付けられることもありますが、しばしばコードやIDがそのまま使われます。 「名前で一意になる」は名前がIDの側面を持っている状態です。 「シリアル番号から型番がわかる」は番号がコードの側面を持ち、そのコードの一部に区分が使用されている状態です。 こう言う時、名前や番号とそれらが混同されがちです。

番号は主に数値で構成され、順序性があったりします。 番号を名前として使用することはあるかもしれませんが、名前を番号としては使用しないでしょう。 番号は名前よりは一意性を持つ傾向にありますが、一意性はたいして重要じゃないこともあります。 番号に一意性が必要であれば、IDを番号に使用するのがいいかと。

大雑把な関係

こんな感じ。

f:id:irof:20211026180017p:plain

破線は「コードを名前として使ったりする」みたいな読み方で。

混同していて問題のないことも多いので、常に厳密に区別する必要はありません。迷った時の参考程度に。

ローカルクラスとしてrecordを使う

irof.hateblo.jp

こちらで書いた「Local record classes」を使う時の注意。

触ってると案外使う機会が出てきそうな感触があったので、気をつけるところを書いておきます。

ローカルクラス

そもそもローカルクラスってあまり使わないものなんで存在も認識されてなかったりします。メソッド内に書くクラスなんですが……

class Hoge {

    void fuga() {
        class Piyo { } // これ
    }
}

書いたら「こんなとこにクラス書けるの?」な反応もしばしば。 用途は特殊だし代替手段もあるので無理に使う必要性はないです。知らなくても生きてけるし、知ってたからと言って 知識マウントに使うにも微妙で 嬉しいことってそんなにない。 サンプルコード書く時にちょっと楽なくらいです。

ローカルレコードクラスの使い所

と言う感触だったんですが、これがStream APIに加えてrecordで若干変わりました。recordのネスト、ついでに他も でコンストラクタ全部出力するコードでも使ってます。

  • 名前付きのPair(やタプル)として作成できる
  • classだからメソッドも持たせられる

あたりが素敵です。 ローカルクラスでもできたんですけど、やろうとするとフィールドとコンストラクタを作らなきゃなので、メソッド内に書くには大袈裟でした。なのでまともにクラスを作るか、いろんなことに目を瞑って Object[] を使ってきました。record だとこの辺要らないので、メソッド内でもノイズがない手触りが良いです。

クラスを作りたくない理由は、そこまで広いコンテキストで使いたくないから。パッケージプライベートでも広いし、クラスにネストしたprivateクラスでもまだ広いかなって思うことがある。名前汚染みたいなことも気になっちゃう。 メソッドのローカルに閉じられると名前もメソッド内に特化したもので良くなるし、多少雑にやっても他を汚さないから取り扱い易いんです。

ローカルクラスとの違い

recordはクラスではあるんですが、classとイコールではないです。 特にrecordのネスト、ついでに他も で書いたstaticの扱い。 これがローカルの場合も入ってきます。

void method() {
    record LocalRecord() { }
    class LocalClass { }
}

コンパイルは通ります。同じように見えますが、LocalRecordstatic で、LocalClassは違います。 これが何を意味するかと言うと、「それぞれから外側にアクセスできるか」です。

void method() {
    var object = new Object();

    record LocalRecord() {
        void method() {
            object.toString();  // コンパイルエラー
        }
    }
    class LocalClass {
        void method() {
            object.toString();
        }
    }
}

これが本稿の主題です。ローカルクラスと同じ場所でローカルレコードクラスを使った場合、ローカルクラスと同等には使用できません。 「ローカルクラス」としてではなく「メソッド内に閉じたrecord」と認識する必要があります。

当然、メソッドの一時変数だけでなく、外側のクラスのインスタンスメンバにもrecordの内側からはアクセスできません。 recordで使用するものはレコードコンポーネントとして明示的に渡してあげる必要があります。

「ローカルレコードクラスでメソッドを使わなければ問題ない」ではありますが、メソッドを持てるのはただのデータ構造に対するせっかくのメリット。 取りまとめて名前までつけた情報の組み合わせには固有のメソッドがある。はず。たぶん。

もうちょい詳しく

エラーメッセージは Non-static variable 'object' cannot be referenced from a static context です。 static なんて書いてないけど、recordstatic context ということ。

この辺は javap で出てきたりします。以下は javap -v の抜粋。

SourceFile: "Sample.java"
NestMembers:
  Sample$1LocalClass
  Sample$1LocalRecord
InnerClasses:
  #20= #15;                               // LocalClass=class Sample$1LocalClass
  static final #21= #17;                  // LocalRecord=class Sample$1LocalRecord

同じように書いてるのに LocalRecord のほうは static final がついてます。

明示したい

「暗黙に任せずに明示したい」と思うかもしれませんが、以下はいずれもコンパイルエラーです。

void method() {
    static record LocalRecord() { }
    static class LocalClass { }
}

ローカルクラスにstaticはつけれないので、 static record は書けません。「デフォルトがどっち」とかではなく「強制的にこっち」なので明示云々じゃないんですね。

たぶんrecord はレコードコンポーネント以外を持たないのを優先したんでしょう。外側にアクセスするためには外側からインスタンスを受け取るようにコンストラクタを変えなきゃですし。 recordとしては正しいと思うけど、インナークラスとかに対する半端な知識があると混乱ポイントかなと思います。

蛇足: Stream APIのLambda内レコード

これってどうなんだ?と思いつつ。

Stream.of("abc", "defg")
        .map(s -> {
            record Pair(String value, int length) {}
            return new Pair(s, s.length());
        })
        .forEach(System.out::println);

StreamAPIの途中でrecordを宣言もできます。staticなんで後続処理にインスタンス渡してもインナークラスインスタンスを渡した時のようにメモリリークの原因にもならないはず。 後続処理(ここではforEachSystem.out::println)がPairコンパイルレベルで必要としなければ普通にPairとして動作します。

Pair を以下のように書けば、当然出力も変わります。

record Pair(String value, int length) {
    @Override
    public String toString() {
        return "きゃー";
    }
}

もちろんですが、Pair 独自のメソッドは後続ではコンパイルできません。

Stream.of("abc", "defg")
        .map(s -> {
            record Pair(String value, int length) {}
            return new Pair(s, s.length());
        })
        .map(Pair::value)  // コンパイルエラー
        .forEach(System.out::println);

record定義を前に出せば当然いけます。

record Pair(String value, int length) {}
Stream.of("abc", "defg")
        .map(s -> {
            return new Pair(s, s.length());
        })
        .map(Pair::value)
        .forEach(System.out::println);

けどこれは面白くない・・・あ、そうだ。java.util.functionのインタフェースを使おう。

Stream.of("abc", "defg")
        .map(s -> {
            record Pair(String value, int length) implements Supplier<String> {

                @Override
                public String get() { return "さぷらい"; }
            }
            return new Pair(s, s.length());
        })
        .map(Supplier::get)
        .forEach(System.out::println);

いけた。よし! toStringが呼べたからObjectとかの識別はできてたんだろけど、前段のPairSupplierってのをmapの時にわかるんですね。すごいなぁ。

……できるのは分かったけど、どう見てもわかりづらいし、お蔵入りかなー。

おまけ: ローカルenum

書けたのか……いやこれこそ書いてどうすんだ?

void method() {
    enum MyLocalEnum { HOO }
}

説明は省きますが、これもローカルレコードクラスと同じくstatic-contextになります。 「書いてどうすんだ?」とか書いたけど、ちょっと思いついてしまった。いつか試す。