言語問わずだが、気になるコードの書き方があるので、いくつか備忘録がてら書き記すことにした。ここではPythonを前提に、必要であれば別の言語のコードも含めて話を進める。

否定は膨張する

NOT A ≠ B かもしれない

否定は大きな力を持つ。ある集合にAとBしか要素がなければ、「Aではない」は「Bである」と同義だが、集合の要素が多くなるにつれて、「Aではない」が表現する範囲が増えていく。集合にさらなる要素Cが追加されたとき、「Aではない」という判定が「BまたはCである」ということになり、ロジックによっては容易に破綻することが想像できる。

ここで言う集合とは、SetやHashSetのことではなく、一般的な集合のことで、あるデータ型の持ちうる値のことを示している。例えばbool値であればtrueとfalseの二値だし、列挙体であればその要素数分だけ値がある。

import enum
 
class Item(enum.Enum):
    SWORD = 0
    SPEAR = 1
    AXE = 2
 
x = Item.SWORD
if x != Item.AXE:
    # Item.HAND_AXEが追加されたとしても正しい?
    ...

自分は何者かを定義する

否定が膨張するというのであれば、肯定形で判定すればいいのか?という反論を考えなければならないが、それも必ずしも正解とも言い切れない。状況によってはコード量が増えることに繋がるし、そもそもこの判定はなんのためにあるのか?と意味も希薄になってくる。

そういうときには、集合にメソッドを追加して、なにに対して真なのか?をメソッド単位に切り出して明確にすることで解消できる。

import enum
 
class Item(enum.Enum):
    SWORD = 0
    SPEAR = 1
    AXE = 2
    
    def is_short_range(self) -> bool:
        return self in [self.SWORD, self.AXE]
    
    def is_middle_range(self) -> bool:
        return self in [self.SPEAR]
 
print(Item.SWORD.is_short_range())

期待された代入

本当に代入されるのか?

条件の真偽によって、ある変数に代入する値を変えたいことがしばしば発生する。以下のようなコードだ。

result = None
if condition:
    result = 100
else:
    result = 200
# ここではresultに値が入っていることが期待されている
# resultの型は int | None だが、Pythonではそれが希薄

そのためにnullable値あるいは初期値を用意して、if-elseの条件に応じて変数に代入することになるが、ここでいくつかの問題が生じる。

  1. 初期値としてnullや初期値といった第三の値が入る
    • そしてそれらの値はしばしば検証されない
  2. 代入される変数がMutableであり、不変性がない
  3. ロジックがベタ書きされることで、見通しが悪くなる

1.はよく発生する状況だ。我々はnullに散々苦しめられてきたのに、過ちを繰り返し続けている。今や言語によってはnullableサポートがあるにも関わらず、自らnullableを許す状況を望んでしまっているので是正しなければならない。

2.については変数がImmutableであろうとMutableであろうと、再代入するコードを書くようなことはあまりなくなってきたように感じられるが、それでも不変性を担保できることの意義はある。

3.はこういった冗長で危険性を孕むコードが、有限の表示領域しかないディスプレイの何割かを占めていることに怒りさえ感じる。unsafeなコードと同じで、危険なことをするのであれば、その影響範囲は最小限に抑えておくべきで、メソッドとして切り出すことも検討すべきだ。

メソッド化を面倒がらない

期待された代入を解決するのは簡単で、今述べたように単にこの処理をメソッド化するだけでよい。そうすれば真偽値に応じた結果を返すだけでよく、変数もImmutableにできるし、第三の値が入り込む心配もない。各判定でそのまま return すればいいし、変数もいらない。

def _get_result(self) -> int:
         :
         :
    if condition:
        return 100
    return 200

else節の強要

elseに意義を感じているか?

どうしてもifに対してelseを書きたがる人がいるようだ。あらゆる条件に対して網羅性を得たいということなのだろうが、個人的には冗長さを感じてしまう。

組込みプログラミングのコーディング規約では、else節を設けよというスタイルもあるようで、私も組込みプログラミングをしていた時期があったが、この主張には懐疑的な立場である。

if-else if文は、最後にelse節を置く。

独立行政法人情報処理推進機構(IPA) 技術本部 ソフトウェア高信頼化センター(SEC), 組込みソフトウェア開発向けコーディング作法ガイド [C言語版], pp.62 (信頼性3.5)
https://www.ipa.go.jp/archive/publish/secbooks20180629.html

確かにプログラマの熟練度を無視すれば有効なものかもしれないが、ある程度設計に長けた人が書くのであれば、いくつかの問題がある。

  1. 常にelse節があることによってネストが深まり、見通しが悪くなる
  2. if-elseが入れ子になると想定ケースを網羅しにくくなる
  3. else節を置くことが必ずしも問題の解決に結びつかない

2~3レベルのif-else文を書けば、厳しい状況になると容易に想像できる。私の前職のとあるプロジェクトではif-elseのネストが10レベルに及ぶコードもあり、それが故に検証もできなくなっていた。elseは万能ではないし、それ自体がコードの質を上げてくれるわけでもない。

発想を変える

結局見通しのいいコードを書け、という話に帰着するのだが、ひとまず設計の話は置いておいてif-elseの話だけをすると、私の考え方は「基本的には偽のときの動作をさせて、真だったときには例外ケースの動作をさせる」である。もちろん条件は逆でもよい。Objective-Cのインスタンスの初期化でも似たコードが見て取れる。

- (instancetype)init {
    if (self = [super init]) {
        // Initialize self
    }
    return self;
}

このコードは基本的にはNILを返す。ただし、親クラスの初期化やメモリの確保に成功したときはそのインスタンスを返す。無駄なelseを省くことでネストも最低限で済む。

null値をエラー値として扱うことが一般的な言語であれば、このように書けるし、そうでないならエラー型の返却や例外の送出を基本的な動作としておけばよい。

早期リターン

以前私は早期リターンを是としていたが、これも使いどころが難しい。あらかじめ特定のケースを枝刈りしておけば効率もよくなるし、不正な値が入り込む余地もなくなる。しかし、無策の早期リターンはその後の処理でどの状況が担保されているのかわかりにくくなる。

これは早期リターンがしばしば否定形で捕捉されることと、その条件を後続のロジックの前提条件として把握していないといけないからで、コードを読む際に脳への負担がかかるからだ。今の結論としては「いい感じにしろ」ではあるが、美的センスやコーディングのセンスに関わるところなのだろう。

これについては別途早期リターンについてでも自分の意見をまとめている。

なんでもDictionary

辞書や連想配列、ハッシュマップと呼ばれるデータ形式はとても便利だ。キーと値の集合として扱うことができ、配列と異なり連続性がなくてもよく、任意のキーが与えられても柔軟に対応できる。多くの場合キーはHashableでなければならないが、あまり問題にはならないだろう。

ここで、よく犯しがちな失敗はデータをなんでも辞書として扱うことだ。確かにHTTPヘッダーのように辞書で扱ったほうがいいものもあるが、最小公倍数として辞書を使うことが適切なのであって、ほとんどの状況ではクラスを作ったほうが値も制限できるし、どういったプロパティを持つのかも明瞭になる。

JavaScriptの場合は少々難しい議論となる。オブジェクトは軽量な複合データとしての役割を期待されているので、単純にクラスにすればいいというものでもなく、どこかにある境界を見定めることが難しい。ひとつ思いつくのはDTO (Data Transfer Object)を超える役割を求めるかどうかだろうか。