oinume journal

Scratchpad of what I learned

Chapter 1 - Intruduction / A Philosophy of Software Design

A Philosophy of Software Design の第1章を読んだのでそのまとめ。

Intro

  • プログラムは機能が増えるごとに複雑さが増していく。複雑さが増えると、開発スピードが遅くなりバグが増える
  • 開発ツールは複雑性に対処するに役立つが、これには限界がある。一方、シンプルなソフトウェアのデザインはより大きくてパワフルなプログラムを導いてくれる。
  • 複雑性に対処するには2つのアプローチがある。
    • 1つ目はコードをシンプルかつ明らか(わかりやすく)すること。例えば複雑性は特殊ケースを削除することで減らすことができる。
    • 2つ目のアプローチはカプセル化でmodular design とよばれている。
      • Modular designではソフトウェアをモジュールに分割して管理して(OOPだとクラスとか)それぞれのモジュールは他のものに依存しない。
      • そのため、プログラマーはあるモジュールの開発をするときに、他のモジュールの詳細を知らなくて済む。

ウォーターフォールモデルの紹介

  • ウォーターフォールだと基本的には設計フェーズではすべてを設計し、開発のフェーズでは設計をしない。ソフトウェアは物理的なシステムより複雑で目に見えないので、特に大きなシステムであるほど全体を細部まで理解することは難しい。
  • 開発フェーズで初めて問題がわかるケースもよくあり、場合によっては設計のやり直しが発生する。ウォーターフォールモデルだとこれは大きな手戻りになってしまう。
  • この問題があるため、最近のソフトウェア開発ではアジャイルのようなインクリメンタルなアプローチが使われている。

How to use this book

ソフトウェアの設計スキルを向上させるための良い手法の一つは、"red flags"という複雑なソースコードの断片のサインを認識することである。この本ではその"red flags"を、メジャーな設計の問題を通じて説明する。

A Philosophy of Software Designを読み始めた

タイトル通りで、最近第2版が発売されたのと、いろんなところでオススメされていたのでこの本を読んでいる。やっと第6章まで読み終わったので、それぞれの章のまとめをブログにアップしていく予定。

以下は読んだ章のINDEX.

JavaScriptの記号の演算子と構文

JavaScript / TypeScript では一見すると「これなんだ?」という記号の演算子や構文がよく出てくるので、自分用の備忘録としてメモしておく。式と演算子 - JavaScript | MDNには演算子の一覧のページがあるため、わからない記号が出てきたら以下のページを見るとだいたい載っているはず。

?? - Nullish coalescing operator

  • Null 合体 (??) - JavaScript | MDN
  • Null 合体演算子 (??) は論理演算子の一種です。この演算子は左辺が null または undefined の場合に右の値を返し、それ以外の場合に左の値を返します。
  • 例えば以下のコードだと、aがnull or undefinedの場合は a is null or undefined がvalに代入される
const val = a ?? 'a is null or undefined';

!! - Double not operator

  • Logical NOT (!) - JavaScript | MDN
  • 複数の否定演算子を連続して使用することで、明示的にあらゆる値を対応する論理型プリミティブに変換することができます。
  • 否定の否定なので肯定ということになり、元の値をbooleanに変換するということらしい
const val = !!a; // val is always boolean

?. - Optional chaining operator

  • オプショナルチェーン (?.) - JavaScript | MDN
  • ?. 演算子の機能は . チェーン演算子と似ていますが、参照が nullish (null または undefined) の場合にエラーとなるのではなく、式が短絡され undefined が返されるところが異なります。関数呼び出しで使用すると、与えられた関数が存在しない場合、 undefined を返します。
const val = a?.b?.c; // aがnull or undefinedの場合 val が undefinedになる

... - Spread syntax

  • スプレッド構文 - JavaScript | MDN
  • スプレッド構文 (...) を使うと、配列式や文字列などの反復可能オブジェクトを、0 個以上の引数 (関数呼び出しの場合) や要素 (配列リテラルの場合) を期待された場所で展開したり、オブジェクト式を、0 個以上のキーと値のペア (オブジェクトリテラルの場合) を期待された場所で展開したりすることができます。
  • 一番多用されるのは配列やオブジェクトをコピーするときなのではないかと思われる
const arr = [1, 2, 3];
const arr2 = [...arr]; // arr.slice()みたいな

! - Non-null assertion operator

これはJavaScriptではなくてTypeScriptにしかない演算子。

// Compiled with --strictNullChecks
function validateEntity(e?: Entity) {
  // Throw exception if e is null or invalid entity
}

function processEntity(e?: Entity) {
  validateEntity(e);
  let s = e!.name; // Assert that e is non-null and access name
}

TypeScriptのDestructuring assignment

最近仕事でfrontendの開発を少しずつやるようになったのだけど、TypeScriptはGoに比べて演算子や記号を使う記法が多くて読むのに一苦労する。なので「これなんだろう?」と思ったやつをメモしておく。

TypeScriptやっていて一番最初に「ん?」と思ったのは、以下のような構文だった。

const obj = { title: 'hello' }
const  { title: myTitle } = obj // ★
console.log(myTitle) // "hello"

★のコードでは

  1. obj というオブジェクトから title という要素を取り出して
  2. myTitleという定数を宣言
  3. myTitleに 1. で取り出した要素を代入する

ということをやっている。myTitleが定数宣言であるのに対して、キーのtitleは単純にオブジェクトのキーなので特に何かが宣言されているわけでない、ということが初めはわかっていなかったのでこの構文を見るたびに頭が混乱していた。

調べてみると、これはJS由来の構文でDestructuring assignmentと呼ぶらしい。

上の例ではオブジェクトから要素を取り出す例だったが、当然配列から特定の要素を取り出すこともできる。

const array = [1, 2]
const [ first, second ] = array
console.log(first) // 1

さらにスプレッド構文 ... を使って以下のような代入も可能。

[a, b, ...rest] = [10, 20, 30, 40, 50];
console.log(rest); [30, 40, 50] 

Goと違って多値が返せないからこういう構文が必要になるのかなと思ったのだけど、どうなんだろう?

Go1.17beta1でGenericsに触れてみた

Go1.17beta1がダウンロードできるようになったので、Generics(Type Parameters)でStackを書いて軽く遊んだメモ。

Generics (Type Parameters)について

最新の仕様のProposalは以下を見ると良い。

実際にどんな感じのコードになるのかは、GitHubのdev.typeparams branchのコードを見ると良さそう。

Go1.17beta1のダウンロード

普段使っているgoコマンドから以下のようにGo1.17beta1をダウンロードできる。

$ go get golang.org/dl/go1.17beta1
$ go1.17beta1 download

これで ~/sdk/go1.17beta1 にインストールされる。

FYI: Goは標準の機能で複数のバージョンをインストールできる。より詳しくは以下のドキュメントを読むと良い。

Genericsのサンプルコード

Go1.17beta1に付属している以下のディレクトリに結構ある。

$ ls $(go1.17beta1 env GOROOT)/test/typeparam
absdiff.go      fact.go         issue45738.go   min.go          slices.go       tparam1.go
adder.go        graph.go        list.go         ordered.go      smallest.go     typelist.go
append.go       importtest.go   list2.go        orderedmap.go   smoketest.go    value.go
chans.go        index.go        lockable.go     pair.go         stringable.go
combine.go      interfacearg.go map.go          pragma.go       stringer.go
cons.go         issue45547.go   maps.go         sets.go         struct.go
double.go       issue45722.go   metrics.go      settable.go     sum.go

GenericsでStackを書いてみる

こんな感じになった。現時点ではType Parametersの型をexportすることができないっぽいので、stackと小文字で定義してmainパッケージに書いている。

// run -gcflags=-G=3
package main

import "fmt"

var (
    ErrEmptyStack = fmt.Errorf("stack is empty")
)

type stack[T any] struct {
    data     []T
    capacity int
}

func newStack[T any](capacity int) *stack[T] {
    if capacity <= 0 {
        panic("must be 'capacity' > 0")
    }
    data := make([]T, 0, capacity)
    return &stack[T]{data: data, capacity: capacity}
}

func (s *stack[T]) Push(v T) {
    s.data = append(s.data, v)
}

func (s *stack[T]) Pop() (T, error) {
    if s.Size() == 0 {
        var zero T
        return zero, ErrEmptyStack
    }
    v := s.data[len(s.data)-1]
    s.data = s.data[:len(s.data)-1]
    return v, nil
}

func (s *stack[T]) Peek() (T, error) {
    if s.Size() == 0 {
        var zero T
        return zero, ErrEmptyStack
    }
    return s.data[len(s.data)-1], nil
}

func (s *stack[T]) Size() int {
    return len(s.data)
}

func main() {
    s := newStack[int](10)
    s.Push(1)
    s.Push(2)
    s.Push(3)
    fmt.Printf("s = %+v\n", s)

    _, err := s.Pop()
    if err != nil {
        panic(err)
    }
    fmt.Printf("s = %+v\n", s)
}

コード書いてて少し悩んだのは、func (s *stack[T]) Pop() (T, error) のようなメソッドでerrorを返す時にT型のゼロ値をどうやって返すのか、という点だった。どうやら以下のように var zero T を定義してそれを返すのが良いらしい。return nil, ErrEmptyStackという風に書くと、Tはポインタ型ではないのでコンパイルエラーになる。

func (s *stack[T]) Pop() (T, error) {
    if s.Size() == 0 {
        var zero T
        return zero, ErrEmptyStack
    }
    v := s.data[len(s.data)-1]
    s.data = s.data[:len(s.data)-1]
    return v, nil
}

実行

main関数があれば普通にgo1.17beta1 runコマンドで実行できる。-gcflags=-G=3を付けないとgenericsのコードはコンパイルできないので注意。

$ go1.17beta1 run -gcflags=-G=3 ./typeparam.go
s = &{data:[1 2 3] capacity:10}
s = &{data:[1 2] capacity:10}

感想

Type Parametersの機能が入るリリースは2022年の2月のGo 1.18だと思うけど、現時点でもモリモリ実装されているのでとても楽しみである。