oinume journal

Scratchpad of what I learned

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だと思うけど、現時点でもモリモリ実装されているのでとても楽しみである。

Apple Silicon向けDocker Desktop for Macでコンテナがハングしないようにする

TL;DR

  • Apple Silicon(M1) Docker Desktop for Macでgoogle/cloud-sdkのDocker Imageを使ってコンテナ内でCloud Datastore Emulatorを立ち上げると、CPUが100%で張り付いてハングするという問題があった。
  • 解決方法として、該当のDocker Imageをarm64v8アーキテクチャでビルドして、そのイメージでコンテナを立ち上げるようにしたら問題は起きなくなったので、その方法の紹介

問題の詳細

Apple Silicon版のDocker Desktop for MacはRosetta2を使いインテルアーキテクチャ(amd64 / x86_64)をエミュレーションして実行されている。エミュレーションが行われているため、ネイティブでarm64v8のコンテナを実行するよりCPUを使ってしまうという問題がある。これはKnown Issuesとして以下のように書かれている。

However, attempts to run Intel-based containers on Apple Silicon machines under emulation can crash as qemu sometimes fails to run the container. In addition, filesystem change notification APIs (inotify) do not work under qemu emulation. Even when the containers do run correctly under emulation, they will be slower and use more memory than the native equivalent.

In summary, running Intel-based containers on Arm-based machines should be regarded as “best effort” only. We recommend running arm64 containers on Apple Silicon machines whenever possible, and encouraging container authors to produce arm64, or multi-arch, versions of their containers.

自分が遭遇した現象としては、google/cloud-sdkのイメージを使ってgcloudコマンドでDatastore Emulatorを立ち上げようとすると、3回に1回ぐらいの確率でCPUが100%になり、コンテナが全く応答しなくなるという問題だった。

We recommend running arm64 containers on Apple Silicon machines whenever possible

とDocker Desktop on MacのKnown Issuesに書かれているので、これにしたがってgoogle/cloud-sdkのarm64v8のDocker Imageをビルドしてみた。

google/cloud-sdk arm64v8 Imageをビルドする

公式には提供されていないので、自分でDockerfileを作ってイメージをビルドする必要がある。 https://hub.docker.com/u/arm64v8 には様々なarm64v8のDocker Imageがあるので、debian:buster-slimをベースにして以下のようなDockerfileを作る。ちなみに今回はgoogle-cloud-sdk-datastore-emulatorしか使わないのでこれだけをインストールしているが、他のEmulatorが必要であれば公式のDockerfileを参考にインストールすれば良い。

FROM arm64v8/debian:buster-slim

ARG CLOUD_SDK_VERSION=341.0.0
ENV CLOUD_SDK_VERSION=$CLOUD_SDK_VERSION

RUN groupadd -r -g 1000 cloudsdk && \
    useradd -r -u 1000 -m -s /bin/bash -g cloudsdk cloudsdk

RUN mkdir -p /usr/share/man/man1/ && \
    apt-get update && \
    apt-get -y install \
        curl \
        gnupg \
        sudo \
        python3 \
        python3-crcmod \
        bash \
        openjdk-11-jre-headless

RUN echo "deb [signed-by=/usr/share/keyrings/cloud.google.gpg] http://packages.cloud.google.com/apt cloud-sdk main" | sudo tee -a /etc/apt/sources.list.d/google-cloud-sdk.list
RUN curl https://packages.cloud.google.com/apt/doc/apt-key.gpg | sudo apt-key --keyring /usr/share/keyrings/cloud.google.gpg add -
RUN apt-get update && \
    apt-get install -y \
    google-cloud-sdk \
    google-cloud-sdk-datastore-emulator
RUN gcloud config set core/disable_usage_reporting true && \
    gcloud config set component_manager/disable_update_check true && \
    gcloud config set metrics/environment github_docker_image_emulator

そしてDocker Imageをビルドする。

$ docker build -t cloud-sdk-emulators-arm64v8:341.0.0 .

最後に

今回はGoのユニットテストからory/dockertestを使ってDatastore Emulatorを立ち上げていたのだが、コンテナを何個立ち上げても全くハングしなくなりとても安定した。今回はDatastore Emulatorだったけど、他のImageでも起こりうる問題だとは思うので、もしApple siliconのマシンでDockerを動かしていて同じような症状で困っている人は参考にしてもらえると良いと思う。