【書評】A Philosophy of Software Design 1章-2章

はじめに

本記事はソフトウェアの Complexity (複雑さ) について扱っているA Philosophy of Software Designを読んだ際の個人的な備忘録です。170Pほどの洋書で読みやすいので、読みながら学びになったことをまとめていきます。読みやすいとは言え英語なので和書の5倍くらいの時間がかかりますが、頑張って読んでいきます。

www.amazon.co.jp

1章:イントロダクション

ソフトウェアを開発していくとソフトウェアは次第に複雑になっていき、ソースコードの修正や機能追加が難しくなっていく。結果として機能追加にかかる時間が増大したりバグを引き起こすこととなるが、これはソフトウェアの複雑さに起因すると著者は語る。なんらかのツールによって複雑さを抑えることも可能ではあるが、限界はある。従ってソフトウェアデザインにおいて、ソフトウェアの複雑さを最小にすることで、ソフトウェアをシンプルに保つことが重要である。

よりプログラムをシンプルにするにはどうすればいいか、一般的には以下の二つの手法がある。

  • コードをよりシンプルにすることで複雑さを減らすこと
    • 特殊ケースを消す
    • 統一性のある識別子を利用すること
  • 複雑さをカプセル化し、意識すべき複雑さを減らすこと=moduler design

....とだいたい上記のような内容でした。目から鱗の大発見!!とかはまあなくて、ソフトウェアの複雑性を抑えるにはどうすればいいのだろうかを考える本だよってこと、そしてそれがいかに重要であるかを述べています。introなんで、まあ「ソフトウェアの複雑さを最小化すること」が本書の目的だってわかればまあそれでよしって感じで流し読みしました。次!

2章:複雑さの性質

本書の目的は複雑さを最小化するための原則を知ることにあるわけですが、そもそも複雑さとはなんだろう?2章では高いレイヤーから議論を行っており、より低いレイヤーの議論は後続の章で語られている。まずは敵を知ることから。

Complexity defined | 複雑さの定義

自分が直感的に感じていたことを数式モデルで表現しており、割と良いな!って思った定義が以下です。


C = \sum_{p}c_pt_p \\
C...Complexity of System(ソフトウェアの複雑さ)\\

c_p...part(p) weigit(そのpartの複雑さ)\\

t...developerがそこに触れる頻度や時間 \\

ある関数がとても複雑な処理をしていて、中身がもはやブラックボックスになっていたとします。これはあなたの開発しているソフトウェアの中で複雑さに寄与しているでしょうか? 答えはそこに触れる時間による、です。

普段我々が利用しているライブラリは、モノによっては色々な最適化が施されていたりして、一見するとなんのこっちゃよくわからん複雑なソフトウェアです。例えば僕はtensorflowが裏側でやってくれていることを事細かに把握はしてないですし、知らんけど多分複雑です。そういうソフトウェアをコンポーネントとして扱って開発を行っているわけですが、我々の開発とは隔離されたものと言えるでしょう。どんだけ複雑でも開発から隔離されているものはソフトウェアの複雑さに勘定として入れなくてもいいよね、我々が意識すべき複雑さは開発の中でより長く触れる複雑なコンポーネントですよね。

従って、あるコンポーネントの複雑さを、developerが費やす時間で重み付けしてsumを取ることでソフトウェアの複雑さを測ろう。そういう意図のモデルです。

Symptoms of complexity | 複雑性の症状

複雑さがもたらす症状には大きく分けて3つの症状があります。

Change amplification | 変化の増大

コードを改変する際により多くの箇所を修正する必要が出てくること。本の中で出てくる具体例としてはwebpageの背景色の例が挙げられています。 以下、二つの例を取ってみましょう。

  1. それぞれのページでbackgroundカラーが定義されている
  2. ある一つの変数を経由してそれぞれのページのbackgroundカラーを定義している(変数は一つだけ)

仮に前者のソフトウェアデザインで背景色を変更しようと思うと、全てのページの背景色を変えにいかなければなりません。ページが数千もあったら溜まったもんじゃないです。反して後者ならば変数に代入するパラメータを変えれば全てのページの背景色が切り替わります。複雑なソフトウェアとは、より多くの変更を要求します。

Cognitive load | 認知的負荷

コードを改変するときに必要な情報を整理するのに多大な時間を要するようになる。これはわかりやすい複雑さですね。例えばC言語は認知的負荷が高いです。C言語を書いたことがない人に説明すると驚かれますが、動的配列とかが他の言語のようには提供されてなくて、malloc/freeを使ってメモリ管理を自分でしなければなりません。仮に確保したメモリをfree(開放)し忘れるとメモリリークが起こってメモリが枯渇したりします。こういう要素に気をつけながらコードを書くのは骨が折れるし認知的負荷が高いです。RubyPythonで同等の処理を記述するよりも多くの時間がかかる、と言って否定する人はあまりいないでしょう。

他にも以下のようなことが認知的負荷を引き起こすと指摘されています。

Unknown unknowns: 何を知らないのか分からない

変更対象が分からなかったり、変更に際して何を考慮しなければならないのかがわからないこと。これが症状の中で最悪のものです。苦し紛れに書いたコードが、結果としてどのような問題をもたらすことになるのかもよくわかっておらず、問題が起きてからそれを知ることとなる...知見の溜まってないOSSなどを利用していると起こったりしますね...

Cause of complexity | 複雑さの原因

これまでは複雑さがもたらす症状について触れてきましたが、そもそもなぜソフトウェアが複雑になるのかについて、最後にまとめます。筆者は複雑さの原因を2つに分類しています。

  • dependency | 依存
  • obscurity | 曖昧であること

dependency | 依存

クラス間の依存が増えて行ったり、いろんなところで変数が共通して利用されだすと、途端にコードを追うのが辛くなります。クラスのレイヤーの話でなくとも、他のソフトウェアと依存していることもあります。例えばnetworkのプロトコル。senderの仕様を変更したら、receiverも同様に変更しなければなりません。他にもあるmethodのパラメータを増やした結果、呼び出しもとも同様に変更をしなければならなかったり、rest APIとかもクライアントサイドが仕様を変更するとバックエンドもそれに合わせて仕様を変えなければならなかったり...など、依存しているものが増えるとソフトウェアは複雑になっていきます。こっちは構造のお話

obscurity | 曖昧であること

こっちはコードの可読性というか命名のお話。名が体を表すコードならいざ知らず、よくわからない変数名を定義していて、他の人にはなんのこっちゃよくわからんソースコードは複雑さの原因になります。ソースコードから曖昧さを排除することでソフトウェアはシンプルになります。

1章2章まとめ

後続の章で述べられているDeep Moduleという概念が大変クリアで美しかったため購入した書籍なのですが、この章も個人的には有意義でした。自分が意識していなかったことが述べられているわけではないですが、暗黙知を形式値に昇華してくれたこと、バラバラの知識・経験的直感をまとめ上げて自然言語にしてくれたこと、それだけで大変価値のある文章でした。引き続きのんびり読んでいきます。

OSI参照モデルとRFC1122

はじめに

先日、非CS学部出身のソフトウェアエンジニアの友達にネットワークのプロトコルについて軽く説明してた時に、OSI参照モデルに則って説明をしていました。 機械工学科から頑張って情報系にジョブチェンジする際にマスタリングTCP/IPを読み込んだのを思い出しながら、喋ってて、内容は以下のようなものです。

  • インターネットプロトコルを構成する要素を以下の7層に区切って考えるモデルである
  • 送信側では、パケットは7層で作られて下の層に降りていきながらラップされていき、受信側では下の層からラップを剥がしながら通信の内容を読み取る。

最後、雑ではありますが、大体正しいこと言ってる気がします。ざっくりしたイメージを共有したところで、具体的な層ごとの説明をしようとしたところで違和感を感じます。

あれ...?プレゼンテーション層とセッション層を代表するプロトコルってなんだ...?

HTTPやSMTPなどは一番上位のレイヤーに位置するという認識はありましたが、ぶっちゃけ5層6層と分離して考えられるのか...?あれ...? トランスポート層TCPUDPプロトコルの違いや輻輳制御などを学んだ記憶があるので説明できる。ネットワーク層IPv4/IPv6、ルーティングアルゴリズムについて勉強した。データリンク層物理層は...CSMA/CD、CSMA/CAとかあの辺のパケットの衝突検知や回避、macアドレスEthernet周りが定義されてるはずだが...うん?ここってEthernetとかは1層と2層跨ってるよな?なんて思いながら言葉に詰まりました。

OSI参照モデルは実装されていない

いろんな文献を参照してるとOSI参照モデルは頻繁に登場します。僕が初めてネットワークをしっかり学んだマスタリングTCP/IPでもOSI参照モデルを利用してネットワークを階層化し、説明をしていました。もちろん4層モデルの存在は知っていましたが、捉え方が違うだけだろうとか思っとりました。全然違いました。OSI参照モデルRFCで採用されていません。RFCで採用されているのは4層モデルであり、OSI参照モデルはあくまで学習用くらいで捉えておいた方がいいようです。wikipediaにも冒頭にそんな説明がありますね*1

ややこしいのは、中途半端に基礎知識みたいな形で広まってるのと、L2スイッチやL3スイッチみたいにOSI参照モデルに則った命名してる機器が普通に存在してたりするので面倒ですね。

RFC1122を読む

ネットワークのレイヤの定義ってどこにあんだ??って思ったら、RFC1122がそれでした。めっちゃ長い。全部読めたもんじゃねぇ。 細かい話は置いておいて、RFC1122*2 はネットワークのレイヤを以下の4階層に分けることを定義してます。

これらの層が果たす役割だったり、要件を定義していくのが、図とか使わず自然言語でずーーーーーーっと続いてます*3。凄まじい。RFCはあまりしっかりと読んだことなかったですが*4、Introductionを読んでいると仕様にもいくつか厳しさのレベルがあって、絶対やんなきゃいけないもの、やった方がいいもの、やっても良いものという風に、言葉使いを定義しています。丁寧ですね。

     *    "MUST"
          This word or the adjective "REQUIRED" means that the item
          is an absolute requirement of the specification.
     *    "SHOULD"
          This word or the adjective "RECOMMENDED" means that there
          may exist valid reasons in particular circumstances to
          ignore this item, but the full implications should be
          understood and the case carefully weighed before choosing
          a different course.
     *    "MAY"
          This word or the adjective "OPTIONAL" means that this item
          is truly optional.  One vendor may choose to include the
          item because a particular marketplace requires it or
          because it enhances the product, for example; another
          vendor may omit the same item.

他にも、要件の追加説明にはDISCUSSIONやIMPLEMENTATIONなんてラベルの項目があって、こういうことが起こるとこんな問題が生じるから、実際に実装するにはこうするといいよ、みたいな実装者への提案もあったりしてます。こんな感じなのか。当たり前ですが実装と1対1で対応するほど詳細な定義はされてなくって、ある程度含み持たせてんだなぁとなりました。この定義をメールでやりとりしながら決めて、で、スーパーハッカーみたいな人たちがその実装作って、実際それが動いてThe Internetを支えてるって、めっちゃcoolですな....

ただ自然言語で説明されてもイメージがつきにくいところがたくさんあって、世に出回ってる教科書や資料なんかを見た方が普通に時間効率はいいですね。大学院の授業で使ったComputer Networkingは相当に丁寧でよかった記憶しかないのでリファレンスにええっすよとだけ推しとこ。

終わり

自分はネットワークが専門でもないですし、ネットワーク関連のR&Dプロジェクトに大学院を出た後アサインされる予定も今のところないです。仕事はアプリケーションよりのエンジニアリングがメインなので。実務の中ではRFCを直接参照するよりも他の媒体で情報収集することが多そうですし、APIを叩くインターフェースが綺麗に簡単に使えるように中身をラップしてくれているので、ぶっちゃけRFCで実装されてようがOSIで実装されてようが気になりませんでした。ただ、他人に何かを説明する場面で嘘つきそうになったのに気づけてよかったっす。知識の整理にもなったし。

あと低レイヤーオタクなのでこういうの読んでると、いつかエンジニアとして成長してネットワークのプロトコル実装するお仕事とかできるくらい強くなりたいなって思いました。誰かスカウトしてください。

*1:wikipediaなんて参考にすんなって人が多いですが9割5分くらい正しいこと書いてる(分野による)んで論文書いたり公式な声明書いたりしない限りはかなり参考になると思ってます。

*2:日本語版の方が読みやすい

*3:長い論文だと100P近いものってありますし、見慣れないわけでもないんですが、フォーマットがcssとか知らなそうなアレでアレがアレで長く感じるのかな

*4:このRFCも隅から隅まで読んだりはしてません

GCS上のオブジェクトを一部だけダウンロードする

はじめに

自分用メモ。欲求は以下

  • GCS上にある数GBとかの大きめのデータの中身を確認したい
  • でもローカルに丸ごと引っ張ってくるのは嫌
  • linuxのheadコマンドみたいに一部だけみたい

comand line(gsutil)

catコマンドのオプションを見てみたら2秒で解決した

$ gsutil cat --help
〜〜略〜〜
OPTIONS
  -h          Prints short header for each object. For example:

                gsutil cat -h gs://bucket/meeting_notes/2012_Feb/*.txt

              This would print a header with the object name before the contents
              of each text object that matched the wildcard.

  -r range    Causes gsutil to output just the specified byte range of the
              object. Ranges are can be of these forms:

                start-end (e.g., -r 256-5939)
                start-    (e.g., -r 256-)
                -numbytes (e.g., -r -5)

              where offsets start at 0, start-end means to return bytes start
              through end (inclusive), start- means to return bytes start
              through the end of the object, and -numbytes means to return the
              last numbytes of the object. For example:

                gsutil cat -r 256-939 gs://bucket/object

              returns bytes 256 through 939, while:

                gsutil cat -r -5 gs://bucket/object

              returns the final 5 bytes of the object.

-hでheader(先頭)をよしなにとってきてくれる。  -rでrangeを指定できる。指定はバイトで指定する

Python

from google.cloud import storage

def gcs_head(bucket_name: str, file_path: str, character_code: str) -> None:
    """
    GCS上のcsvの先頭1MBだけをダウンロードして出力する
    
    input
    bucket_name: str
        gcsのbucket
    file_path: str
        オブジェクトへのパス
    character_code: str
        オブジェクトの文字コード
        downloadしたものはバイト型なので標準出力しようと思うとdecodeする必要がある
    """
    gcs_blob = storage.Client().bucket(bucket_name).blob(file_path)
    head: bytes = gcs_blob.download_as_bytes(end=100000)
    print(head.decode(character_code))


gcs_head(bucket_name="proj_hoge", file_path="sample.txt", character_code="shift-jis")

Blobsのドキュメントを見てると、ダウンロード系のメソッドにはstartとendの引数があって、読み込みの開始位置と終端をバイトで指定可能。

しっかり学ぶ数理最適化・第2章3.1節 - 双対問題

双対問題

線形計画問題において、最適値を求めるのが困難な際に重要となるのが、最適値の上界と下界を求めることです。最大化問題において、ある実行可能解が得られた時、それは必ず最適値以下であるはずです。つまり得られた実行可能解を下界として捉えることができます。では上界を求めるにはどうすれば良いのでしょうか?上界を求める一つの手段として2.3.1節で述べられる双対問題による手法があります。双対問題を考えることで最大化問題を最小化問題に置き換えることができ、最適値の上界を求めることができるというなんとも不思議な求め方です。賢いですね。まあただ実行可能解を1つ得ることが容易でない問題もあるということで、世の中そんな甘くはなさそうですが...

本節では、やや天下り的な具体例から手法の流れを把握し、その後に一般化を行うことで双対問題を用いた最適値の上界・下界を求める手法に関してみていきます。

数式が非常に多くアレルギーが出そうになりましたがよくよく見てみると別に大した式じゃありません。数式ばかりに目がいきがちですが、著者の梅谷先生はこの節で行っていることを簡潔にまとめてくださってます。まとめると以下です。

制約条件の1次結合を用いて線形計画問題の最適値の良い上界を求めた

理解する前にこの文言を見ても「?」だったでしょうが、理解した後に聞くとなんとも簡潔な表現に聞こえます。後の2.3.2節では緩和問題に触れており、双対問題の導出にはこちらを利用することの方が多いようです。気が向いたらまとめます(多分向かない)

しっかり学ぶ数理最適化輪読回・第2章

数理最適化勉強会も始めました

僕は数学があまり得意ではないので、こういう勉強には気が乗らないのですが、数学から逃げずに少しずつでも工学をやる上で必要な素地を身につけていかなければならないな、と最近ひしひしと感じています。同期もみんなひしひしと感じているようで、数学ゼミを開講する運びになりました。

集合と位相、圏論あたりのマジの基礎からやっていた時期もあったのですが、M2になったしより実用的な分野の勉強会をしたいねってことで数理最適化を学ぶことになりました。個人的には、MLPシリーズの機械学習のための連続最適化をやりたかったりしたのですが、多数決の結果ラボの勉強会で扱う書籍は「しっかり学ぶ数理最適化」に決定しました。カバーしてる範囲がどのくらい違うのかもあまり把握してないですが、とても重厚で名著感が凄まじいため、どうせいつか読む本だと思ってます。なので気合入れて読みたいですね。

「気合入れて読みたいですね」と言ったそばから「最後の生産計画問題サボっとるやんけ」とツッコまれそうですが、疲れたので仕方ないっす。人間限界あるんで。

線形計画問題

目的関数も、制約条件も線形な関数で表現できるものを線形計画問題と呼ぶようです。この章では定式化を行っているだけに留まっていて、それをどのように解くのかまでは議論していません。線形計画問題を解く手法は線形計画法と呼ばれており、後の章で述べられる単体法などに代表される手法を用いることで多くの場合効率的に大域的な最適解を得られるようです。 僕は担当ではなかったので資料を作ってないですが、2.1.2節以降では線形でない問題も近似を行うことで線形計画問題に落とし込めるよ、という話をしており(全てではない)、実用上、様々な問題を線形計画で解けちゃうようです。

この章は読んでいて、確かに、まぁ、そうなるな、みたいな感想をもった章で特別難しい概念や理解に苦しむ定式化なんかはありませんでした。ですがよくよく問題を見てみると「この問題も線形関数だけで表現できるのか!」みたいな発見があって楽しかったです。次回は緩和問題と双対定理に関して発表するのでがんばります。

経済・ファイナンスデータの計量時系列分析 勉強会(1)

時系列解析のお勉強会を始めました

ハミルトン先生の時系列解析本を読めるくらい賢かったらよかったんですが、残念ながら僕はそこまで賢くないので、まだ頑張ったら読めそうな沖本先生の本を読むことになりました。ハミルトン本は日本語版が絶版になってるっぽいのも相まって学習難度が高いっすね。英語版は話に聞く限り無料のPDFがインターネッツに落っこちてるらしい。

ところで期待値の表記に違和感がある。僕の理解不足か?

僕のスライドでいうところの8Pです。分散、標準偏差のところ見てください。大括弧っていらないんすか?(本書がそのような表記になってたためそのまま載せてます)。自己共分散に関しては大括弧ついてるんすけども違いは何なんでしょう。よくわからん。時系列解析以前に基本的な確率統計をもっとガッツリ深くまで勉強した方がいい気もしてますが、時間は有限なのでね.... それと有識者から聞いた話によるとPRMLの13章あたりで(時)系列データが扱われているとのことなのでそちらもセットで読み込んでいきたいっすね。8章のグラフィカルモデルの途中で読むの止まってる。頑張ろう。

並行処理・並列処理(1)

友達と毎週10分程度のライトニングトーク的なことを定期的にやることになりました。知ってることの言語化って大事だよね、お互い知らない知識を共有しあいながら言語化の練習やろーぜみたいな流れでやることになって喋ることをまとめてたのですが、アレ?これブログにすればよくね?ってことに気がついてしまいました。完全に天才です。てことで気合入れていきます。心は4月病なので意識の高さがATHです。

並行処理と並列処理

OSがめっちゃ頑張ってお仕事してくれているが故に我々はYoutubeを見ながらWordで文章を書き、slackでメッセージをやりとりできたりするわけです。コンピュータが複数のことを「同時」にやってるように見えますね。こういう複数の処理を「同時」にこなすシステムのことを、マルチタスクシステムって呼びます。複数の処理を捌く際にどういう意味で「同時」なのかで並行か並列かが変わってきます。

並行処理

まずは並行処理から説明します。「OSがめっちゃ頑張って」って上で書いてますが、物理的な意味で処理を走らせて計算してくれているのはCPUと呼ばれる装置です。OSはソフトウェアです。CPUをどのように使うかを決めてくれてます。2021年現在はこのCPUのコアが複数あるのが当たり前ですが、以前のコンピュータではコアが一つしかありませんでした。CPUは同時に一つの処理しか走らせてくれません。これでは困ります。Word使ってる時にYoutubeが見れません。そこで、ごく短時間で実行タスクを切り替えて複数のタスクを疑似的に同時実行する手法が採用されました。これが並行処理です。CPUは我々人間が思ってるよりずっと早いです。従って人間に知覚できないほどの速さでwordを動かして、youtubeを動かして、っていうのを切り替えています。この切り替えが我々には感じられないので、あたかも「同時」に動いているかのような体験ができているわけです。

並列処理

一方で並列処理はマジの同時に処理が走ってます。最近のCPUはコアが複数存在しているのでこれが実現できるんすね。マジの同時に処理を走らせることができるので並列化可能な処理は並列化させただけ早く処理が終わります。図で示すと以下のような感じ。

f:id:mimatasanmata:20210403230137p:plain
並行処理vs並列処理

タスクを並列化することは基本的に処理の高速化をもたらしてくれます。一方で、タスクを並行で走らせることは大抵の場合処理を高速化させるどころか遅延させます。処理するタスクを切り替えることをコンテキストスイッチと呼ぶのですが、コンテキストスイッチには時間がかかります。従って単独でA, B, Cのタスクを順番に処理するよりも通常多くの時間を要します。まあある程度現実的な時間内でシステムから応答がないと困りますよね。だからオーバーヘッドを作ってでもタスクを切り替えて並行して処理を行ってます。どういう順番でタスクを切り替えるべきかに関してはタスクスケジューリングの楽しい話があるのですが、その話をし出すと日が暮れるのでまたどこかで気が向いたら書きます。

並列処理の限界・アムダールの法則

2個のプロセッサを利用すればどんな処理でも速度が2倍になるだろうか?答えはNoです。アムダールの法則について述べたところで一本目のブログは締めることとしますか。文系でも説明できそうな話しかしてませんが気のせいです。僕は理系の大学院生です。 wikipediaから説明を拝借するとアムダールの法則とは「計算機の並列度を上げた場合に、並列化できない部分の存在、特にその割合が「ボトルネック」となることを示した法則」だそうです。数式で表現すると以下


S(N)=\frac{1}{(1-P)+\frac{P}{N}}

ここでSは性能向上率、Nはプロセッサの個数、Pは並列化可能な部分の割合です。これまじで大したことは言ってなくて、N個のプロセッサ使うと並列化可能な部分はN分の1の時間になるけど、そうじゃない部分は変わんないよね、だからNを無限大に持ってったって無限に早くなるわけじゃないよって言ってるだけです。つまり並列化できる部分を大きくしないとコア数増やしても意味がないって話っすね。

終わり

マジで大した話をしてない気しかしませんがこれでも結構時間を食うことがわかりました。トピックミスったので個人的な学びがなかったのがあれですね。勉強したことをドキュメント化しつつブログとしてまとめる、が正解でした。反省。以下は次回の宿題くらいにしときますか。気が向いたら書きます。

  • 並行プログラムと並列プログラム技法
  • プロセスとスレッド
    • スレッドは危険?
    • スレッド化の4つのステップ
    • Pythonで並列処理を書いてみよう
  • OSの仕組みとタスクスケジューリング
  • マルチスレッドアプリケーションのルール
  • 有名アルゴリズムを並行化してみよう

重くね?ほんまに書くんか?