空想犬猫記

※当日記では、犬も猫も空想も扱っておりません。(旧・エト記)

Clean Architecture

色々な場所で話題に上がっていた Clean Architecture(原著)を読んだ。

自分自身、実は「自称・アーキテクト」として10年以上、とあるソフトのアーキテクチャを請け負っているので、自分の中に言語化されていないノウハウがそれなりにあった。私にとってうれしかったのは、それらの言語化できていたかったノウハウが多少言語化されて整理されていたことと、この本自体が割と広く有難がられていることである。個人的に役立ったのは Chapter 13 の Component Cohesion についてだった。

プログラムの設計を議論する場合、正論と正論が矛盾するという厄介なことがしばしば起こる。経験の浅い頃は、なぜそういうことが起こるのか分からず、結局最後までネチネチ煙に巻く人の意見が通ってしまい、後で苦労することになることがあった。あるいは、正論を盾に責任を果たそうとしない口達者にも手を焼いた。経験を積んでからは「ごもっともだけど、それは今重要じゃない」と言えるようになった。この本を読めば、REP-CCP-CRP 原則のテンション・ダイアグラムを持ち出して、何をすべきかをより明確に説明できることだろう。何よりも、3つの原則がそもそも緊張関係にあることが前提なところから話が始められるようになったこと、また、アーキテクトの仕事が一つの完全な正解を導くのではなくて、与えられた境界条件から、開発効率を最大化するためにパラメータを調整することであることを、共通の言葉で説明できるようになったことが大きい。

振り返ると、基盤中の基盤ライブラリは、社内向けであっても REP の原則を最初から厳守していたし、派生プロダクトのコードをフレームワークに追加するときには CCP の原則で外に括りだすことをお願いしたこともあった。そのような数々の技術的決断を、後から答え合わせする形で再定義できた。

新たな製品をデザインするときに、コアのロジックの議論に集中したいときに「バックエンドデータベースはどうしますか?」みたいな話が出たら、「The Database Is Detail. この本読んで」。みたいな使い方ができて捗るかもしれない(しないけど)。

あ、そうそう、この本できっちりと「a software architect is a programmer; and continues to be a programmer. (p.136)」と言い切ってくれたのも、この本が使える信用できると思った理由の一つだ。

告白すると、一応、タイトルに冠されてる Clean Architecture の章まではちゃんと読んだが、以降は斜体で強調されているステートメントと結論以外は斜め読みした。その他、コンポーネントの依存関係で、なぜ circular dependency が良くないかは、それらのコンポーネント群が、結局一つの塊としてコンポーネントになってしまって管理コストが上がることは、子供のおもちゃを使ってもっと簡単に説明できると思った。コールバックメカニズムを用いて依存性問題を解消する実装上のテクニックを dependency inversion と言って仰々しくするとってつけたようなコンサルタント用語の類も、頭の片隅に入れておけば十分である。それらは言語化されているという点で便利な概念だけど、まだ本質を掠めている程度であるからだ。蛇足を承知で言うと、導入の章でソースコードの行数をもって生産性の指標にしているのは単純化しすぎと感じた。なぜならば、コーディング技術が向上したりプログラミング言語そのものがバージョンアップして改良されたりすることで行数が圧縮されるのは、生きているソフトウェアでは起こりうることだからだ。そしてそれはアーキテクチャとは関係がない。

個人的には仕事柄この本はとても有益だった。一方で、この本を読んでポカンとして何も響かなくても、別にそれがダメとも思わないし、むしろ本書に出てくるキーワードでマウントをとるアーキテクトにならないようにしようと心に刻んだのだった。

Clean Architecture 達人に学ぶソフトウェアの構造と設計 (アスキードワンゴ)

Clean Architecture 達人に学ぶソフトウェアの構造と設計 (アスキードワンゴ)

Clean Architecture: A Craftsman's Guide to Software Structure and Design (Robert C. Martin Series)

Clean Architecture: A Craftsman's Guide to Software Structure and Design (Robert C. Martin Series)

Firefox設定メモ(2018年1月版)

Firefox設定メモ ~MS PゴシックをMeiryo UIに置換~ - 空想犬猫記 を久々にアップデート。

Firefox Quantum では使えなくなる可能性はあるものの、現状の Firefox 57 では、まだ userContent.css によるフォントの置換テクニックが有効であった。(参考: Modifying the Default Skin - Mozilla | MDN

最新の設定では、置換先のフォント名は明示的には書かず、すべて sans-serif にしておき、一方で sans-serif のフォント設定を、自分が一番読みやすいフォントに設定しておくという方式にした。個人的には Yu Gothic UI がお気に入り。

@charset "UTF-8";
@font-face {
  font-family: "MS Pゴシック";
  src: local("sans-serif");
}

@font-face {
  font-family: "MS PGothic";
  src: local("sans-serif");
}

@font-face {
  font-family: "MS P明朝";
  src: local("sans-serif");
}

@font-face {
  font-family: "MS PMincho";
  src: local("sans-serif");
}

@font-face {
  font-family: "MS UI Gothic";
  src: local("sans-serif");
}

Rust雑感

年初から、また少しだけ Rust(The Rust Programming Language)というプログラミング言語を覚え直していた。リンク先のサイトには以下のような特徴が挙げられている。

  • zero-cost abstractions
  • move semantics
  • guaranteed memory safety
  • threads without data races
  • trait-based generics
  • pattern matching
  • type inference
  • minimal runtime
  • efficient C bindings

平たく言うと、メモリ安全で、データ競合も起こらず、C言語並みに軽くて速いという。さらに調べてみると、ジェネリクスはもちろん、trait objects を使った仮想関数テーブルによるポリモーフィズムに加えて、メンバ変数を持てる enum と match 文でパワータイプによるポリモーフィズムもサポートされているという尖りまくりの言語であることがわかった。

しかしながら、

trait objects は、なんかちょっと欲張って取ってつけた感が否めない。
途中から出てきた ref キーワードも、なんか必然性を感じない。
ところどころでシンタックスが美しくない(例:多次元配列の定義 [[u8;9];9])

このように現時点では C/C++ に取って代わりうる言語の中で、頭一つ抜き出ているものの、少し不安な要素もある。VSCodeプラグインや、開発支援ツールの充実度でも、某G言語にはまだまだ遠く及ばない感じ。Go言語のような巧みなシンタックスのデザインと Rust の厳密で突き抜けた部分を合わせると最強のシステム記述言語になり得るのではないだろうか。現状はまだまだ人柱を絶賛募集中といったところか。

return throw 問題

今日ハマったC++の落とし穴。次のコードの出力結果を予想してください。

#include <iostream>
#include <exception>

using namespace std;

static void assertZero(int i)
{
  if (i == 0)
    return

  //
 // I'm sure it is not a zero!!
  //
  throw exception("Not a zero");
}

int main()
{
  try
  {
    assertZero(0);
    cout << "0 is zero.\n";
  }
  catch (...)
  {
    cerr << "0 is not zero.\n";
  }

  try
  {
    assertZero(1);
    cout << "1 is zero.\n";
  }
  catch (...)
  {
    cerr << "1 is not zero.\n";
  }

  return 0;
}

これは

0 is zero.
1 is not zero.

となって欲しいところだけど、実際には以下のようになる。

0 is not zero.
1 is zero.

これ見て紅茶を噴きかけたのだが、こうなった原因は、assertZero 関数の return 直後にセミコロンをつけ忘れたことにある。C++の仕様によれば return の後には式を書くことができて、throw は void 型の式だったんですな。つまり返り値が void 型の関数において return throw exception("..."); と書くのは仕様上セーフであるらしい。結果として assertZero 関数は

static void assertZero(int i)
{
  if (i == 0)
    return throw exception("Not a zero");
}

と解釈され、めでたく全く意図とは正反対の挙動をするというわけでした。Go や TypeScript など、セミコロンの要らないモダンな言語に侵食されると、この手のミスが増えることが予想されます。これを機に中括弧を省略しない宗派に切り替えることも検討中。

(再現環境:Visual Studio 2017)

一時オブジェクトの寿命

先輩にバグじゃないの?と指摘されたコード。

class MyString
{
public:
  MyString();
  MyString(const std::string& s);
  :
};

extern const std::string& GetName(int idx);

int main()
{
  for (int i = 0; i < N; ++i)
  {
    const MyString& s = GetName(i);
    // この後、forのスコープ内で文字列 s を使うのは安全か?
  }
  
  return 0;
}

このコードは、以前、レガシーな std::string を使ったコードを国際化対応した文字列クラスに一括置換したときに生じたものだ。まだプロダクションには使われていないが、一瞬、嫌な汗をかいた。結論から言うとこれは合法だった。コンパイラも完全にスルーしていたので気付かず。

何が起こっているかというと、GetName() で std::string の参照が返った後、MyString の変換コンストラクタ MyString(const std::string&) が呼ばれ、一時オブジェクトが作られる。一時オブジェクトをconst参照で受けた場合、一時オブジェクトはconst参照に束縛されるというルールがあり、const参照変数のスコープまで延命される。したがってfor文のスコープ内で使うことができる。

まぁ、参照で受けてコピーを作らないという意図も叶っておらず、単にややこしいだけなので、早々に直したほうが良いだろう。

VC++のunordered_set/unordered_mapが遅い件

正確に言うと、insertが同種のset/mapに比べて圧倒的に遅い。

最近、メモリの節約のために辞書的なオブジェクトを作成して、同値のオブジェクト(例えば文字列)を再利用するようなコードを書いたところ、unordered_set/unordered_mapで思うような性能が得られなかった。

コンテナ以外のアロケーションの影響を受けないように、あらかじめ文字列を用意して、それを辞書に登録する以下のサンプルコードを書いて、Visual Studio 2015でプロファイルをとってみた。

#include "stdafx.h"

using namespace std;

namespace
{
  struct char_cmp : public binary_function<const char*, const char*, bool>
  {
    result_type operator()(first_argument_type l, second_argument_type r) const
    {
      if (!l)
      {
        if (!r) return false;
        return true;
      }
      else
      {
        if (!r) return true;
        return strcmp(l, r) < 0;
      }
    }
  };

  struct char_hash : public unary_function<const char*, size_t>
  {
    size_t operator()(argument_type key) const
    {
      return hash_value(key);
    }
  };

  struct char_equal_to
  {
    typedef const char* first_argument_type;
    typedef const char* second_argument_type;
    typedef bool result_type;

    bool operator()(first_argument_type pL, second_argument_type pR) const
    {
      return strcmp(pL, pR) == 0;
    }
  };

  template <typename Set> __declspec(noinline) void TestSet(const vector<string>& samples)
  {
    Set set;
    for (auto& s : samples) set.insert(s.c_str());
  }
}

int main()
{
  vector<string> samples;
  samples.reserve(100000);

  default_random_engine generator;
  uniform_int_distribution<int> distribution(33, 126);

  for (int i = 0; i < 100000; ++i)
  {
    samples.emplace_back();
    auto& str = samples.back();
    str.reserve(300);
    for (int j = 0; j < 300; ++j)
      str += distribution(generator);
  }

  TestSet<unordered_set<const char*, char_hash, char_equal_to>>(samples);
  TestSet<set<const char*, char_cmp>>(samples);
  return 0;
}

で、結果がこちら

f:id:xoinu:20161210085710p:plain

サンプルの要素数が増えれば増えるほど差は広がるばかり。確認のためTDM-GCCで同等のコードを書いてみたところ、むしろunordered_set/unordered_mapの方がinsertは高速だったので、これはVisual StudioSTLの実装特有の問題っぽい。

Visual Studio上でunordered_set/unordered_mapを旧来のset/mapの代わりに使う際には、必ずベンチマークをとって、挿入性能劣化に見合う検索性能の向上の恩恵に与れるかどうかを確認したほうが良さそうだ。

やれやれ……

追記:
xoinu.hatenablog.com
2年前にint型のコンテナで挿入のパフォーマンスのチェックをやってた。そのときの結果と少し矛盾している。これを見るにVC++のxhashヘッダにあったstd::hash_value()関数がよろしくないのかもという気がする。