emahiro/b.log

Drastically Repeat Yourself !!!!

TRPL の数当てゲームをやってみる

Overview

The Rust Programing Language のチュートリアルにある「数当てゲーム」* をやってみました。

doc.rust-jp.rs

やった内容は以下にまとめてます。

github.com

*数当てゲームですが、日本語訳だとサンプルコードのシグネチャが古かったのでサンプルコードは英語版の方を参照することをお勧めします -> https://doc.rust-lang.org/stable/book/

Memo

クレートについて

クレートのエコシステムについてはまだ自分の中では色々手探り状態ではありますが、バイナリクレートとライブラリクレートがあって、ライブラリを追加する(Goでいう 3rd package を go get してきて使う)というのは ライブラリクレートを追加する 、という風に使いますし、クレートを追加する、という記述があるとだいたいこの ライブラリクレート のことを指すようです。

どんなクレートがあるかは https://crates.io/ を参照すればわかります。pkg.go.dev や npmjs.com みたいなものですね。

Result 型を返り値に持つメソッドとパターンマッチについて

io::std::read_line メソッドは Result 型を返すので match でパターンマッチでも書くことが可能です。

元々使用していた expect() は Result 型の失敗の可能性を扱うのに使います。

このio::ResultオブジェクトがErr値の場合、expectメソッドはプログラムをクラッシュさせ、 引数として渡されたメッセージを表示します。read_lineメソッドがErrを返したら、 恐らく根底にあるOSによるエラーに起因するのでしょう。 このio::ResultオブジェクトがOk値の場合、expectメソッドは、Ok列挙子が保持する 返り値を取り出して、ただその値を返すので、これを使用することができるでしょう。 今回の場合、その返り値とは、ユーザが標準入力に入力したデータのバイト数になります。

io::stdin().read_line(&mut guess).exprect("Failed to ...") と書くと、Result の結果が Ok だったらそのまま guess に結果が入って Err の場合に expect で指定したエラーメッセージが出力される、というデザインはいいですね。

新しい型での覆い隠し(Shadow)

Rust は変数の再代入があった場合にコンパイラがエラーを吐きますが、これとは別に別の型にして Shadow することは許可されてました。

Rustでは、新しい値でguessの値を覆い隠す(shadow)ことが許されているのです。

let mut guess = String::New() // 文字列型で定義
let guess: u32 = guess.trim().parse().expect("Please type a number!"); // uint32 で別型で覆い隠す (≠再代入)

一見再代入のように見えないこともないですが、これは再代入とは違います。

ちなみに変数の変換に利用されてる parse() メソッドについてですが、parse も Reulst 型を返すのでパターンマッチで書くことが可能です。

※ ここでは変換不能な文字列( ex. aaa とか)が入ってきた場合でもエラーハンドリングせず無視して続行する実装にしてます。

読書 Note -『Righting Software』- 第一部 -「構造」と「組み立て」

Overview

Righting Software を少しずつ読み進めていく中で第一部の 3~5章に書かれているソフトウェアの設計の「構造」と「組み立て」の内容が、今まで目にしてきた設計系の書籍やブログを読んできた中で一番腹落ちした内容 (*) だったので書籍に記載された構造についての自分なりのメモや考えたこと、及び Go で具体的に実装したサンプルをログとしてまとめておくためのエントリ。

*個人的には名著『Clean Architecture』よりもわかりやすい内容だと思った。

今回の学習内容は以下にまとめてある。

github.com

読書 Note

まず Righting Software に構造と組み立ての章に関して気になったところを簡単にまとめる。

ユースケースと要件

  • 要件は必要な機能ではなく、必要な振る舞いを明らかにするものではなければならない。
  • システムが何をすべきかではなく、システムがどのように動作しなければならないかを規定する。

必要な振る舞い

  • 必要な振る舞い(= システムがなんらかの仕事を達成してビジネスに付加価値を与えるためにどうしなければならないのか?) を表現したものが ユースケース
  • ユースケースはシステム内のアクティビティの特定のシーケンスになる。
  • ユースケースはエンドユーザーとシステムのインタラクションやシステムと他のシステムとのインタラクション、バックエンドの処理を記述できる。

表現方法

  • ユースケースは文章でも図でも記述できる。
  • 文章によるユースケースは簡単に作れ、そのことは大きなメリットだが、表現方法として図よりも劣っている。
    • 人間は文章をわざわざ読もうとしない笑

ユースケース図とアクティビティ図

ユースケース

f:id:ema_hiro:20210811162611p:plain

このレベルのユースケース図は簡単に記述できるし、図じゃなくても文章で簡単に記述できる。ただ、最初から図を書いた方がいい。

アクティビティ図

こちらを書籍で推奨しているのはこちら。
アクティビティ図は振る舞いの時間的な側面を捉えることができる(待ちや並列実行など)点もユースケース図よりも表現の幅がある。

f:id:ema_hiro:20210811174457p:plain

階層化されたアプローチ

  • ザ・メソッドでは階層構造を重用している。
  • カプセル化も階層構造である。
  • 個々の階層はそれぞれの変動性と下位階層の変動性をカプセル化して上位階層から見えないようにしている。

サービスの活用

階層を超える方法として望ましいのは サービスの呼び出し である。
サービスを使えば以下のようなメリットが得られる。

  • スケーラビリティ
    • サービスインスタンスは呼び出しごとに生成できる。
    • 進行中の呼び出し数と同じ数だけサービスインスタンスがあればよい。クライアントの数が多くても、比例してバックエンドリソースに負荷をかけずに済む。
  • セキュリティ
    • サービス指向プラットフォームはどれもセキュリティを重視している。
    • サービス間呼び出しにも認証認可が必要になり、なんらかの ID 伝播メカニズムを使うことになる。
  • スループットと可用性
    • サービスはキューを通じた呼び出しを受けつけることができることで、負荷の上限をオーバーする分の処理はそのままキューに貯めておく、という方法で膨大な数のメッセージを処理することができる。
  • 応答性
    • サービスであれば呼び出しをバッファリングして、システムが限界を超えることを避けることができる。
  • 信頼性
    • クライアントとサービス間では到達の保証、ネットワーク接続障害の処理、呼び出し順序の指定などに信頼性の高いメッセージングプロトコルを使うことができる。
  • 整合性
    • 結果整合性を保証する協調的なビジネストランザクションを使えば1つの作業に複数のサービスを参加させることができる。
    • 呼び出しチェーンの過程でエラーが発生したら、作業全体をなかったことにする。
  • 同期
    • クライアントが複数の並行スレッドを使っていたとしても、サービスの呼び出しは自動的に同期される。

階層の種類

本書には以下の階層(レイヤー)があると書かれている。

  • クライアント層
    • プレゼンテーション層とも呼ばれる。
    • エンドユーザーのユーザーアプリケーションを指すことが多い。
  • ビジネスロジック
    • ビジネスロジックの変動性をカプセル化する。
    • このレイヤーがシステムに必要とされる振る舞い(=ユースケース)を実装している。
    • ユースケースは顧客と時間の両方の変動性を孕んでいて、ユースケースの変更はユースケースを構成するためのシーケンスの変化、またま、シーケンス内の各アクティビティの変化の二種類しかない(*1)
    • 実装としては Manager と Engine がこのレイヤーでそれぞれカプセル化する。
      • Manager はシーケンスの変動性をカプセル化する。
        • ex. ビジネス都合で並行処理だったプロセスを直列に変更する etc...
      • Engine はアクティビティの変動性をカプセル化する。
        • ex. あるシーケンスの中に存在する通知アクティビティにおいてユーザーに通知していた内容を社内向けにも通知する etc...
      • Engine は Manager よりも関心の範囲が狭く、ビジネスルールやアクティビティそのものをカプセル化している。

*1. ユースケースの変動性は以下のように並行処理で実現されたユースケースが、ビジネスの変化により直列の処理が必要になった、というようなものがあげられる。

f:id:ema_hiro:20210811181857p:plain

  • リソースアクセス層
    • 文字通りリソースアクセスにまつわる変動性をカプセル化する。
      • なぜならリソースが永続的なストレージか一時的なインメモリストレージ化によってアクセス方法は変動するから。
    • ミドルウェアを類推させるような名前をインターフェースの名前に使わない。
      • インターフェースに select() や insert() があると裏には RDBMS があることを類推させてしまい、そんなにないと思うがリソースの変更に弱い。なぜなら、リソースを変更するとリソースアクセス層も変更しないといけなくなるから。
      • 同様に Open() や Close()、Write() といったインターフェースは裏側にファイルシステムがあることを類推させてしまい同上。
  • リソース層
    • 実際にDBやファイルシステム、キャッシュにアクセスするレイヤー
    • memo
      • ミドルウェアのクライアント部分に当たるので ifra 層などと呼称されるケースもあるレイヤー。
  • ユーティリティ層
    • ロギング、認証認可、pubsub など全てのシステムが横断で利用するものをまとめておくレイヤー。

その他

第3章前半で構造についての説明やガイドライン書かれていて、後半では拡張性やマイクロサービスのことも触れられている。
第4章では第3章で説明された階層や各コンポーネントの組み合わせ方(組み立て)について記載されている。 後半に記載してる Go のサンプルではその組み立て方もある程度参考にしている。 第5章では 3, 4 章で書かれている内容をベースにしたケーススタディになっている。

今回は構造についてまとめておきたかったので第3章の前半をベースにまとめているが、設計については第一部を通読するのは復讐も兼ねて勉強になると思う。

階層についての個人としての解釈・考察

各階層について

  • クライアント層
    • ユーザー及び、システムを使う側の最前面に露出するところなのでもっとも変動性が大きい。
  • ビジネスロジック
    • ここが一番腹落ちした。DDD とかで語られるレイヤーわけでよく見かけるユースケース層というものの本当の意味がわかった気がする。
    • マネージャーはユースケースを表現している。
      • マネージャーはユースケースを管理する。
      • Engine はアクティビティ図における各アクティビティノードを表現している。
      • マネージャーは Engine およびリソースアクセスレイヤーをインスタンス化し、それらをオーケストレーションする役割を担う。
      • ユースケースの変更があっても本来的にはマネージャーのみの変更で済むはずで変更が局所的になり、システムの振る舞いの変更に強くなる。
      • マネージャにはユニットテストが欲しくなる。なぜならビジネスロジックを実装として表現してるものだから。
  • リソース層
    • ミドルウェアのクライアント部分に当たるので ifra 層などと呼称されるケースもあるレイヤー。infra レイヤーと書かれると馴染みのあるレイヤーだなと思う。

ユースケースとアクティビティ図の考え方について

各アクティビティがドメインの受け持つロジックに関心事があり、マネージャーはそのドメインロジックを協調させてユースケース(=ある要件に対してのソフトウェアの振る舞い)を実現するものと捉えると、階層構造をとるアプリケーションの捉え方が少し変わった。
概要で参照してる PullRequest にて実装した設計は Righting Software で言及されている階層と組み合わせを、 ユーザー登録のユースケースをベースにして 以下のようなシーケンスをイメージして実装している。

これを図に起こすと以下のようなイメージになる。

f:id:ema_hiro:20210811191025p:plain

まとめ

階層構造を取るアーキテクチャについてずっとわかったようなわからなかったようなところを行ったり来たりしていたが、『Righting Software』に書かれている構造と組み分けの章を読んでみて、アクティビティ図とセットで考えることでようやく雰囲気で理解していたカプセル化の意味を少し実感できた気がする。
後半のプロジェクトデザインの章については、時間があればまた別のエントリでまとめたいと思う。

Rust で grep コマンドを実装する

Overview

以下のハンズオンの内容にある Rust で grep コマンドを再実装する部分を再度自分でやってみました。

speakerdeck.com

やった内容は以下にまとめてあります。

github.com

Memo

いくつか工夫したりメモしたりしたポイントがあるのでまとめます。

検出した文字列が何行目にあるのか?を追加

grep コマンドなのでファイルの "何行目に" あるのかを表示したいと思って行数を表示する変更を追加しました => https://github.com/emahiro/il/pull/32/commits/3faac486add2181287a8ed5321cb95581e2a62b8

structopt について

github.com

このクレートを追加して struct の引数にマクロを定義すると以下のように簡単にコマンドラインオプションをデフォルトで実装してくれます(めちゃくちゃ便利...!!!)

use structopt::StructOpt;

#[derive(StructOpt)]
#[structopt(name = "rsgrep")]
struct GrepArgs {
    #[structopt(name = "PATTERN")]
    pattern: String,
    #[structopt(name = "FILEPATH")]
    files: Vec<String>,
}

実行サンプル

cargo run # 引数なしで実行する

cargo run
   Compiling rs_grep v0.1.0 ($PATH/src/github.com/emahiro/il/rs_sandbox/rs_grep)
    Finished dev [unoptimized + debuginfo] target(s) in 1.68s
     Running `target/debug/rs_grep`

# 以下が自動的に付与される。
error: The following required arguments were not provided:
    <PATTERN>

USAGE:
    rs_grep <PATTERN> [FILEPATH]...

For more information try --help

ちょっとマクロを読むのは慣れないと難しいな、という感想はありますが。

クレートについて

Rust のパッケージやライブラリ、モジュールについてはまだ全然理解できてないですが、Go の package 単位で依存関係を追加するのは クレートを追加する というようです。

パッケージとクレートの関係については TRPL に記載してました。

doc.rust-jp.rs

Cargo.toml の中身を見ても、src/main.rs については何も書いてありません。これは、Cargoは src/main.rs が、パッケージと同じ名前を持つバイナリクレートのクレートルートであるという慣習に従っているためです。

どんなプロジェクトでもそれ自体はクレートなんですね。

ちなみにクレート (Crate) は木箱という意味なんですね。なるほど。

ja.wikipedia.org

TRPL のクレートとパッケージの章をざっと見ててとりあえず以下のところを最初は覚えておくととっつきやすいのかなと思いました。

doc.rust-jp.rs

ビルドした成果物の格納場所

Cargo.toml があるディレクトリルートの target/debug 配下にバイナリファイルとしておいてあります。

./Cargo.toml
./target/debug/$BuildResult 

サンプルとして rust で grep を実装したコマンドは以下のように表示されます。

./target/debug/rs_grep
error: The following required arguments were not provided:
    <PATTERN>

USAGE:
    rs_grep <PATTERN> [FILEPATH]...

For more information try --help

rayon を利用して並行処理

https://github.com/rayon-rs/rayon を利用してファイルの読み込みを非同期に動かすようにしましたが、その際に関数型っぽく書く書き方に変更しました。
ref: https://github.com/emahiro/il/pull/32/commits/d97d35a14b0f2f55b7100d5e85c41d04f539e5cb

これには理由があって、元々手続き型っぽく書いていたんですが、rayon のクレートを追加した際に変更した iter() -> par_iter() の部分で返り値が Iterator 型から String の Vector 型に変更されていたので Iteration を回す for ... in ... 構文が使えなくなりました。
そのため関数型っぽく for_reach の中でラムダ式を書いて同じような実装を実現してます。

See Also

doc.rust-jp.rs

Cookie に格納された情報を使っている Handler のテストを実装する

Overview

タイトルの通りです。Go で API を書くときなど HTTP の Handler を実装していくと思いますが、ここでログインなどの Cookie にセッションを持たせるような認証機構がセットになってる実装をする場合、この認証機構(特に管理画面でいうところの持ってる権限の判定など) を突破しないと Handler のテストが動かない、と言うケースがあると思います。もしかしたら認証機構を Middleware として切り出して Handler の処理本体では認証を行わない、と言う実装方針も考えることができますが、今回は Handler のなかで認証を実装してるケースにおける Cookie をセットした状態でテストを書くことを念頭におきます。最終的には Handler でチェックしてる認証機構を Middleware に切り出したり、と言ったことも考えられるかなと。

前提: そもそも Go で Cookie を操作するには?

Go の HTTP リクエストの処理において Cookie を操作するにはリクエストとレスポンスで扱うそれぞれのケースがあり、これらは全て net/http package の内部にあります。

Cookie に何を入れるかにもよりますが、おおよそ社内機能のようにアクセスが制限されてる環境であれば、セッション情報を埋め込んでログインだったりに利用することが多いと思います。

Cookie をログインセッションのために使うのはお手軽ですが、万能ではないのでちゃんと使われるサービスの特性を元に検討してください。ここでは Cookie に埋め込んだセッション情報をどう安全に取り扱うかについては言及しません。

Cookie を使った認証機構がある Handler のテスト

Cookie を取り出してその情報を何かしら検証してるテストにおいてはテストする時に Cookie をセットします。

サンプル

以下のような Handler の実装を考えます。

func (h *Handler) Get(w http.ResponseWriter, r *http.Request) {
    if r.Method != http.MethodGet {
        w.WriteHeader(http.StatusMethodNotAllowed)
        w.Write([]byte(http.StatusText(http.StatusMethodNotAllowed)))
        return
    }
    ck, err := r.Cookie("test")
    if err != nil {
        w.WriteHeader(http.StatusUnauthorized)
        w.Write([]byte(http.StatusText(http.StatusUnauthorized)))
        return
    }

    w.WriteHeader(http.StatusOK)
    w.Write([]byte(fmt.Sprintf("OK. cookie value is %#v", ck)))
}

これの Handler のテストにおいてテストを追加します。

Cookie を追加していない時

go test -v .
=== RUN   TestRouter
    main_test.go:37: Unauthorized
--- PASS: TestRouter (0.00s)
PASS
ok      github.com/emahiro/il/cookiesessiontest 0.239s

以下のように Cookie を追加した時

req, _ := http.NewRequest(http.MethodGet, ts.URL, nil)
req.AddCookie(&http.Cookie{
    Name:  "test",
    Value: "test",
})
resp, err := http.DefaultClient.Do(req)
if err != nil {
    t.Fatal(err)
}

この時のテストの結果は以下

go test -v .
=== RUN   TestRouter
    main_test.go:41: OK. cookie value is &http.Cookie{Name:"test", Value:"test", Path:"", Domain:"", Expires:time.Time{wall:0x0, ext:0, loc:(*time.Location)(nil)}, RawExpires:"", MaxAge:0, Secure:false, HttpOnly:false, SameSite:0, Raw:"", Unparsed:[]string(nil)}
--- PASS: TestRouter (0.00s)
PASS
ok    github.com/emahiro/il/cookiesessiontest 0.933s

雑な実装としては上記のとおりです。

SecureCookie を使って Cookie で Session を扱う

単純な Cookie ではなく、Session とセットで扱うために gorilla が提供してる https://pkg.go.dev/github.com/gorilla/securecookie を紹介します。

個人的には go-chi も好きですが、gorilla はさらにいろんなツールセットが整っていて好みなフレームワークです。Routing の仕方が特徴的ですが、ne/http の ServerHTTP を満たすインターフェースをしてる点も go-chi と同じで標準に乗っ取っていて好みです。

net/http 互換なので gorilla のパッケージを go-chi から利用することも可能です。

securecookie を使って Session をやりとりする実装をします。

まず最初に Cookie をセットする箇所は以下のようになります。

req, _ := http.NewRequest(http.MethodGet, ts.URL, nil)
codec := securecookie.CodecsFromPairs(keyPairs)
ckstr, err := codec[0].Encode("test", map [interface{}]interface{}{
    "test": "test",
})
if err != nil {
    t.Fatal(err)
}
req.AddCookie(&http.Cookie{
    Name:  "test",
    Value: ckstr,
})

取り出す側は以下です

store := sessions.NewCookieStore(keyPairs)
store.Options = &sessions.Options{}
session, err := store.Get(r, ckName)
if err != nil {
    // refresh session
    session = sessions.NewSession(store, ckName)
}

テストする側では以下のように securecookie を設定します。

store := sessions.NewCookieStore(keyPairs)
store.Options = &sessions.Options{}
session, err := store.Get(r, ckName)
if err != nil {
    // refresh session
    session = sessions.NewSession(store, ckName)
}

securecookie では codec 後の encode 処理をかける際に上記 interface の map を渡します。key も interface なら value も interface を渡すのでこれは本当の意味でなんでも渡せる君になってます笑。

map を渡せるようになってるので受け取る側では key を指定して SessionStore に格納された値を指定して以下のように取り出すことが可能です。

store := sessions.NewCookieStore(keyPairs)
store.Options = &sessions.Options{}
session, err := store.Get(r, ckName)
if err != nil {
    // refresh session
    session = sessions.NewSession(store, ckName)
}

w.WriteHeader(http.StatusOK)
w.Write([]byte(fmt.Sprintf("OK. cookie name is %s value is %#v", session.Name(), session.Values["test"]))) // map[interface{}]interface{} で指定した key で値を取り出せる

少しややこしいな、と思ったのは net/http の Cookie の実装と違い、ここは Cookie をセットする時は SecureCookie として扱うものの、Cookie からセッション情報を取り出した場合には Session として以下扱っているところです。

ちょっとだけリファクタリングする

最後に handler で Cookie の情報を取り出していた箇所を Middleware にしました -> https://github.com/emahiro/il/pull/31/commits/3ee20b9ead7fc24b8238360dee4e40f06ba87cbb

まとめ

このエントリーでざっと流した内容は https://github.com/emahiro/il/pull/31 にまとめてあります。

Memo

http: Request.RequestURI can't be set in client requests が出る

httptest.NewRequest で httptest.Newserver でインスタンス化したサーバーにリクエストを送ると http: Request.RequestURI can't be set in client requests というエラーが出ます。

=== RUN   TestRouter
    main_test.go:23: http://127.0.0.1:49592
    main_test.go:29: Get "http://127.0.0.1:49592": http: Request.RequestURI can't be set in client requests

httptest で立ち上げたサーバーの URL を指定するとURI が指定されていない、というかクライアントではセットできないと言われてリクエストが不通です。

req, err := http.ReadRequest(bufio.NewReader(strings.NewReader(method + " " + target + " HTTP/1.0\r\n\r\n")))

パッと調べた感じ、httptest の中でより LowLevel な関数を Call しており、この辺が怪しそうでした。

// ReadRequest reads and parses an incoming request from b.
//
// ReadRequest is a low-level function and should only be used for
// specialized applications; most code should use the Server to read
// requests and handle them via the Handler interface. ReadRequest
// only supports HTTP/1.x requests. For HTTP/2, use golang.org/x/net/http2.
func ReadRequest(b *bufio.Reader) (*Request, error) {
    return readRequest(b, deleteHostHeader)
}

この readRequest 関数の内部で parseRequestLine という処理を読んでおり、この中で Request.RequestURI が空になって返ってそうだったので、Request.RequestURI がないことによるリクエストの普通が発生してると考えてます。時間がある時に深掘りしてみようと思います。

Go で作る自作 Linter 開発入門

Overview

Go の静的解析、というか x/tools/go/analysis package を利用して簡単にコードを検査する自作 Linter の実装に入門します。

今回使ったサンプル実装は以下に置いてあります。

github.com

準備

以下のツールを使います。

  • GoAst Viewer
  • 以下の Go のパッケージ
    • golang.org/x/tools/go/analysis
    • golang.org/x/tools/go/analysis/passes/inspect
    • golang.org/x/tools/go/analysis/singlechecker
    • golang.org/x/tools/go/ast/inspector

自作 Linter を作る手順

GoAst Viewer で Ast の構造を視覚的に確認する

僕もそうですが、初心者の一番のハードルって Ast の構造を頭の中でトレースすることができないことだと思います。いまだに慣れません。

そこでこの GoAst Viewer って言うツールはとても重宝してて、検査したい Bad コードに似たサンプルコード*(コンパイル通らなくても良いを載せて、自作検査ツールを作る前にまずどう言う構造をしてるのかをチェックします。

※ プロダクションコードなんかの検査をしたいケースはそのまま form に商用利用してるコードを叩き込むのも不安かなと思うので。機密情報ですし。

追記

x/tools/cmd/gotype

pkg.go.dev

gotype -ast $File を使うと GoAst Viewer の Dump と同じ結果が得られるので、商用利用してるコードの静的解析を行いたい場合はこれを使うと良さそうです。
Dump を読むのはちょっと骨が折れますが。

go get -u golang.org/x/tools/cmd/gotype
gotype -ast ./testdata/src/a/a.go

singlechecker を使って検出する

go/analysis を使ってコードの検査をするには singlechecker package を使うのが簡単です。

analysis.Analyzer を定義してそれを singlechecker に放り込むだけでファイル及びファイルの中を ast の Node に従って検査をしてくれます。

var a = &analysis.Analyzer{
    Name:     "samplechecher",
    Doc:      "sample code checker",
    Run:      run,
    Requires: []*analysis.Analyzer{inspect.Analyzer},
}

func main() {
    singlechecker.Main(a) // run の中が実行されます。
}

func run(pass *analysis.Pass) (interface{}, error) {
    i := pass.ResultOf[inspect.Analyzer].(*inspector.Inspector)
    filter := []ast.Node{}
    i.Preorder(filter, func(n ast.Node) {})
    return nil, nil
}

ref: https://github.com/emahiro/il/pull/29/commits/1eada5667a829f5f2761faec21d7140ea4fefdab

あとはひたすら ast の Node を掘っていってチェックしたい文字列に対して pass.Report で出力をするだけです。

nodeFilter で階層を指定する

これちょっとハマったんですが、検査対象の ast.Node をフィルタすることができます。例えば GoAst Viewer で確認すると、階層としては FuncDecl > CallExpr と言う順序になって CallExpr の方が 階層が深いのでここで指定した ast.Node 以下のみを検査対象とすることができますが、逆に言うとフィルタリングしてしまった Node より上層の Node については検査できません。

// 以下は FuncDecl 以下の Node を検査対象にできる。
filter := []ast.Node{
        (*ast.FuncDecl)(nil),
}

// 以下は CallExpr 以下の Node を検査対象にできるが、FUncDecl は対象にならない(っぽい)
filter := []ast.Node{
        (*ast.CallExpr)(nil),
}

Tips

SuggestedFixes がめちゃくちゃ便利なので絶対に使う

同僚の daisuzu さんのブログがめっちゃ詳しいですが、コードの修正がコードから簡単に行えます。

daisuzu.hatenablog.com

使い方

サンプルコードは以下です。これは Smaple と言う文字列を Example に変えたいと言う場合に SuggestedFixied を使った例です。

pass.Report(
    analysis.Diagnostic{
        Pos:     ident.Pos(),
        End:     ident.End(),
        Message: "change Sample to Example",
        SuggestedFixes: []analysis.SuggestedFix{
            {
                Message: "Sample -> Example",
                TextEdits: []analysis.TextEdit{
                    {
                        Pos:     ident.Pos(),
                        End:     ident.End(),
                        NewText: []byte("Example"),
                    },
                },
            },
        },
    },
)

このように書いておいてあとは go run . -fix $File のように自作ツールに -fix オプションを設定して実行することで、自作ツールの検査で引っかかったところを直したい文字列に自動で修正してくれます。

ハマったところ

Pos/End の位置

というかよく使ってて間違うんですが Position を取り違えると意図せず丸ごと変換してしまいます。サンプル PR だと以下のコミットの前後です。

このコミットを積むまでは例えば func Sample() error という関数名を func Example() error と変更したくても、position が意図とは違った位置で書き換えを行なってしまい Sample と含まれる全ての行そのものを Example に変換してしまいます。このため、より範囲を限定した ast.Ident を使ってる箇所を書き換えることで意図する挙動にしました。

こればっかりは動かしてみながら調整する他ないかなと思いますが、どの範囲(Ast Viewerで確認できる)を今操作してるのかをみながら実装するのがいいと思います。

検査の出力結果をテストする

検出されるべき文字列をテストすることができます。

自作ツールの動作(警告文など)をテストするツールとしてanalysis パッケージは analysistest を提供しており、その中の https://pkg.go.dev/golang.org/x/tools/go/analysis/analysistest#Run 関数において自作 Linter で検出されるコードが書かれてる行に以下のように want 文を書くと example テストのように検出結果のテストができます。

// ./testdata/src/a/a.go
func Sample() error { // want `change Sample to Example`
    return nil
}

SuggestedFixes を使ってる場合は https://pkg.go.dev/golang.org/x/tools/go/analysis/analysistest#RunWithSuggestedFixes が用意されており、これを使ってテストする場合、検出 & 修正後の結果になっている golden ファイルを要求されます。(これで出力結果だけでなく、文字列修正した場合の結果もテストできます。)

ちなみにテストは go.mod と同階層に testdata ディレクトリを用意してその配下で通常のパッケージ構成のようにテスト用の Go のファイルを用意することで analysistest から簡単にテストを実行できます。

// ディレクトリ構成
go.mod
testdata
    src
        a
            a.go

テストを実装するときは以下のように実装します。

testdata := analysistest.TestData()
analysistest.RunWithSuggestedFixes(t, testdata, a, "a")

※ このとき a package 内に Fixed した後の結果を出力してある golden ファイルがあることが必要です。じゃないと test を回したときに以下のようなエラーが発生します。

analysistest.go:171: error reading $PATH/src/github.com/emahiro/il/go_sandbox/codeanlyzersample/testdata/src/a/a.go.golden: open $PATH/src/github.com/emahiro/il/go_sandbox/codeanlyzersample/testdata/src/a/a.go.golden: no such file or directory

ハマったところ

大きな話でもないんですが、記号(ex. () など)を pass.Report の中で使っている場合、警告メッセージを出す want のないのバッククオテーションのなかで目地的に \(\) のようにエスケープすることが求められます。これをしないと警告文が違うと判定されてテストが延々とおりません。

ReviewDog 🐶 と組み合わせてみる

ReviewDog には検査で引っかかったところを GUI 上で Commit を積んで修正できる suggester の機能があります。これをつかうと自作チェッカーで解析して検査結果を出力したところを手元でコミットを積むことなく GitHub の PR の GUI 上から検査できて良いので自作チェッカーと組み合わせて使いたいとします。

ハマったところ

go run . ./testdata/src/a/a.go
$PATH/src/github.com/emahiro/il/go_sandbox/codeanlyzersample/testdata/src/a/a.go:10:1: change Sample to Example
$PATH/src/github.com/emahiro/il/go_sandbox/codeanlyzersample/testdata/src/a/a.go:14:1: change SampleWithContext to ExampleWithContext
exit status 3

上記の終了時のステータスコードに着目すると exit status が 3で、正常終了してません。

静的解析結果で引っかかったところがあるとツール内(checker の実装内)で正常終了で終わってくれず、例えばツールで引っかかった結果に対して Suggestion Fix をかけたいケースなどでは正常にその先に Suggestion Fix に結果が伝播しない、と言うことがありました。

以下のコードをみるとわかります。

上記にも記載しましたが、GitHub Actions で ReviewDog の Suggester を動かしたい場合は検査結果と変更の提案(diff形式)を検出した後 checker の実行は正常終了として扱い、検出された diff 形式を Reviewdog の Suggester まで渡さないといけません。つまり exit code 3 では困ります。

そこで actions の定義 yaml の中で以下のように Run を定義することで解決しました。

- name: Run reviewdog
        env:
          REVIEWDOG_GITHUB_API_TOKEN: ${{ secrets.YourGHSecretToken }}
        run: go run checker -fix $FilePath | exit 0 // パイプでつないで checker の結果の exit code 3 を正常終了(= exit code 0) にねじ曲げる。
- uses: reviewdog/action-suggester@v1
        with:
          github_token: ${{ secrets.YoutGHSecretToken }}
          level: "$Severity"
          fail_on_error: false // true にすると CI の検査結果が Fail になります。

まとめ

GoAst Viewer と singlechecker を使うと小難しいことをとりあえず抜きにして、とりあえず簡単に静的解析に入門できます。構造さえわかってしまえは Linter 作成はほぼ筋力で解決できるのでやろうと思えば誰でもできます。個人的には SuggestedFixes が本当に便利なのと、ReviewDog と組み合わせるとレビューの生産性はいくらでも向上すると思っています。

現職でも不具合や障害を起こした実装を繰り返さないようにいくつか有志で自作ツールが実装されて実際に CI の中で動いています。プロダクト固有の実装ルールなどはいくらでもあると思いますし、Go を書く上での一般的な Linter(golangci-linter や staticcheck) でカバーできないものがある場合に非常に有用なのでぜひ実装してみてください。

僕も慣れるまでは何個か時間見つけて作ってみます。ちなみにテンプレはこのコミットを作ったので参考にしてみてください → https://github.com/emahiro/il/pull/29/commits/1eada5667a829f5f2761faec21d7140ea4fefdab

See Also

  • https://github.com/yuroyoro/goast-viewer
  • 今回のサンプルでは使用してませんが、ast の構造上各 Node ごとにより深ぼって検査したい (ex. ast.BlockStmt から CallExpr を取り出して別の検査を走らせたい)場合には ast.Inspect を使うと便利です。具体的には以下のように CallExpr の中に入って検査をすることができます。全部 main で処理しちゃうと複雑なので関数に分けたいケースなどに使います。
func InspectMore(b *ast.BlockStmt) {
    ast.Inspect(ident, func(n ast.Node) bool {
            call, ok := n.(*ast.CallExpr) // BlockStmt -> CallExpr の中に入る.
            if !ok {
                    return true
            }
            // Other lint operation.
            return true // true にすると検査を次に進める.
    })
}

ref: https://pkg.go.dev/go/ast#Inspect

Go の string についてなんとなく調べた

Overview

ちょっと興味があって Go の string の実装周りについてふらっと調べたので備忘録。
そのほかにも何かわかれば追記するかも。

Memo

string 本体について

string is the set of all strings of 8-bit bytes, conventionally but not necessarily representing UTF-8-encoded text. A string may be empty, but not nil. Values of string type are immutable.

ref: https://pkg.go.dev/builtin#string

Go の文字列そのものは 8-bit の byte 配列の文字列の集合。

実行環境での string の実体については以下

// StringHeader is the runtime representation of a string.
// It cannot be used safely or portably and its representation may
// change in a later release.
// Moreover, the Data field is not sufficient to guarantee the data
// it references will not be garbage collected, so programs must keep
// a separate, correctly typed pointer to the underlying data.
type StringHeader struct {
    Data uintptr
    Len  int
}

ref: https://golang.org/src/reflect/value.go

データ = 文字列の中身を表す参照と長さの情報を持っている。GC から守られてるわけではない(GCで回収されうる)

fmt.Fprintf で ``(バッククオート)で囲った文字列をformatしたときにハマった話

Overview

タイトルの通りです。
Go で文字列の中に改行コードが含まれていた場合 "" なら \\n のようにバックスラッシュを使ってエスケープしますが、そもそもの改行コードを含む文字列をバッククオートで出力した場合、自動で改行コードがエスケープされる、という挙動について fmt の使い方でハマるところがあったので備忘録。

改行コードをエスケープする

シンプルなエスケープは以下

func main() {
    s := "ハロー\\n私は太郎です。"
    str := fmt.Sprintf("%s", s)
    fmt.Println(str)
}

// Output: ハロー\n私は太郎です。

https://play.golang.org/p/JlSVd363_sc

バッククオートを使った場合は以下

func main() {
    s := `ハロー\n私は太郎です。`
    str := fmt.Sprintf("%s", s)
    fmt.Println(str)
}

// Output: ハロー\n私は太郎です。

https://play.golang.org/p/cUJKw6wKOdm

ダブルクオテーションで囲った文字列の場合、そのままでは改行コードがエスケープされません。

fmt package で罠にハマる

バッククオートで囲ってる改行コード付き文字列を fmt を使って%sでフォーマット出力するとエスケープされない、という挙動に出会いました。

func main() {
    s := "ハロー\n私は太郎です。"
    str := fmt.Sprintf("%s", s)
    fmt.Println(str)
}

// Output: ハロー
私は太郎です。

https://play.golang.org/p/yKBsxsoJwRD

これは""で囲われてる文字列の改行コードがそのまま評価されて出力結果が改行されます。

func main() {
    s := `ハロー\n私は太郎です。`
    str := fmt.Sprintf("%s", s)
    fmt.Println(str)
}


Output: ハロー\n私は太郎です。

https://play.golang.org/p/cUJKw6wKOdm

バッククオートで囲った時は改行コードは評価されずに文字列として出力されます。

func main() {
    s := `ハロー\n私は太郎です。`
    str := fmt.Sprintf(`"%s"`, s)
    fmt.Println(str)
}

// Output: "ハロー\n私は太郎です。"

https://play.golang.org/p/i6WgbSnT7Mt

`` で囲われた format の中において %s を使って出力のフォーマットを行う場合 "" で囲われた文字列の中に改行コードがエスケープされずにそのまま出力された文字列になります。
ここで困るのは上記のようなエスケープされていない文字列が出力されてしまう場合、これを JSON も Key などに当ててしまったら JSON の内部では "\n" が評価されしまい、意図しない改行が入ってしまいます。

このため、バッククオートで本来エスケープされるはずの改行コードをエスケープした状態にするには以下の2つの方法があります。

  1. %q を使う -> シングルクオートで出力してくれる -> バッククオートで囲った文字列についてエスケープ済みで出力してくれる。
    ref: https://play.golang.org/p/uFaY3w1d1fG

  2. https://pkg.go.dev/strconv#Quote を使う。
    https://pkg.go.dev/strconv#example-Quote に大体使い方は書いてます。

まとめ

%q とても便利だなと思いました。

追記

String literals

"" と `` はそもそも文字列リテラルが違うので出力が異なるのは仕様。(生の文字列リテラルと解釈された文字列リテラル

ref: https://golang.org/ref/spec#String_literals

特に以下の部分

Raw string literals are character sequences between back quotes, as in foo. Within the quotes, any character may appear except back quote. The value of a raw string literal is the string composed of the uninterpreted (implicitly UTF-8-encoded) characters between the quotes; in particular, backslashes have no special meaning and the string may contain newlines. Carriage return characters ('\r') inside raw string literals are discarded from the raw string value.

生の文字列リテラルは暗黙的に UTF-8エンコードされてる、と仕様で書いてある。

Rust のパターン記法 ( Pattern matching )

Overview

Rust の Pattern matching がとても便利だなと思ったので備忘録です。

TRPL には以下のように記載されています。

doc.rust-jp.rs

これを読めばパターン記法に関して必要なことは大体網羅されてるので本当にドキュメントが充実してるいい環境だなと思います。Rust。

単純なパターンマッチング

let x = 10;
match x {
    1 => print!("one\n"),
    2 => print!("two\n"),
    _ => print!("none\n")
};

switch ケースの default 的などこにもマッチしなかった場合には match 記法では _ で表現します。
わかりやすいなーと思います。

Some/None というパターンマッチング

個人的にいいな思ったパターンマッチングのデザインがこれです。

let x: Option<i32> = Some(10);
match x {
    Some(n) => print!("found {}\n", n),
    None => print!("none")
};

サンプルは上記の通りなんですが、パターンマッチさせる対象の型がはっきりしない場合(= Option 型の場合)に match 記法を使うとエディターの補完サポートなどもあって、マッチして取り出した型がなんなのかがわかります。

これで実装するときに有用だなと思ったのが、コマンドラインツールを実装するときで、cat コマンドを自作するときに引数の2つ目(index=1) に何が入るのかを取り出すのに使えます。

use std::{env::args, fs::read_to_string};

fn main() {
    match args().nth(1) {
        Some(path) => print!("path is {}\n", path),
        None => print!("No path is specified\n")
    }
}

サンプルコードは上記です。実行するとこんな感じで指定したファイルを標準出力に吐きます。

cargo run ./src/main.rs
   Compiling rs_cathandson v0.1.0 ($HOME/src/github.com/emahiro/il/rs_sandbox/rs_cat_handson)
    Finished dev [unoptimized + debuginfo] target(s) in 0.39s
     Running `target/debug/rs_cathandson ./src/main.rs`
use std::{env::args, fs::read_to_string};

fn run_cat(path: String) {
    match read_to_string(path) {
        Ok(content) => print!("{}\n", content),
        Err (err) => print!("error: {}\n", err)
    };
}

fn main() {
    match args().nth(1) {
        Some(path) => run_cat(path),
        None => print!("No path is specified\n")
    }
}

if let 記法

Option 型は値がない可能性があることを表す型で Some で値を取り出し None は値がなかったことになります。
パターンマッチングにおいて値があるときだけ処理を続けたい、というケースを実装する場合 None が冗長だったりします。そこで使うのが if let です。

fn main() {
    if let Some(path) = args().nth(1) {
        run_cat(path)
    }
}

シンプルですね。ただこれだけで match Some/None を表現してるかと思うと表現力の高い言語だなーと思います。

タプルのパターンマッチング

パターンマッチングしたいターゲットが2つある場合タプルが使えます。サンプルは以下です。match 記法に対して (A, B) というタプルを取ることができます。

let arg1 = args().nth(1);
let arg2 = args().nth(2);

match (arg1, arg2) {
    (Some(arg1), Some(arg2)) => println!("arg1: {}, arg2: {}", arg1, arg2),
    _ => println!("Pattern or path is not specified.\n"),
};

この場合 None のとりうる値が増えます。上記のサンプルでは default を表現するために _ を使ってますが、(Some(A), Some(B)) のようにタプルでパターンマッチをかける場合、以下のように Some に対応した None をそれぞれカバーしないとコンパイラに怒られます。

non-exhaustive patterns: `(None, Some(_))` and `(Some(_), None)` not covered patterns `(None, Some(_))` and `(Some(_), None)` not covered
  1. Some(A), Some(B)
  2. None, Some(_)
  3. Some(_), None
  4. None, None

_ は 2,3,4 全部のケースを1つにまとめることができる記法です。

まとめ

Rust をぼちぼち書き始めてますが、パターンマッチやResult型でのエラーハンドリングなど普段使ってる Go にはない要素が多数あって描いてて新鮮です。

Rust で制御構文を書く

Overview

4連休なので Rust のお勉強をしてる記録です。いつまで続くかはわかりません。

ema-hiro.hatenablog.com

Rust のハンズオンを開催してもらって以降、開発できる環境を整備したりしてましたので今回は改めて制御構文について単純な Fizzbuzz を実装した内容をまとめます。

Rust の制御構文

Rust の制御構文は式なので変数にアサインすることができます。しばらく制御構文が式と評価される言語を触っていなかったので割と新鮮でした。
制御構文が式なので以下のように書くことができます。

let fizzbuss = if num % 15 == 0 {
    "FizzBuzz".to_string()
} else if num % 5 == 0 {
    "Buzz".to_string()
} else {
    "Fizz".to_string()
};
  • 式なので制御構文の中で値を必ず返します。
  • else if を使った場合は最後に必ず else をつける必要があります。

その他やったこと

Iterator で loop を実装する

Go でいうところの for 文で値を回してみます。

fn iter() {
    for num in 0..100{
        println!("{}", num)
    }
}

シンプルですね。これは index 0 から 99 までの loop を回すことができます。

コマンドライン引数を取り出してみる

シンプルな FizzBuzz を実装しただけでは面白くなかったのでコマンドライン引数を取り出して指定した値の評価する実装をします。
コマンドライン引数の取り出し方は TRPL にそのまま書かれてます。

doc.rust-jp.rs

※ 標準ライブラリの std::env ライブラリで標準入出力関連で使うライブラリなのでしばらくお世話になりそうかなと思いました。

String を Int32 に変換する

以下を参考にしました。先に変換先の型で定義した変数に対して parse と unwrap をかけるとその型に変換してくれるのスマートでいいなと思いましたね。

stackoverflow.com

実装

というわけでこんな風に実装しました。

use std::env::{self};

fn main() {
    let args: Vec<String> = env::args().collect();
    let numstr = &args[1].to_string();
    let num: i32 = numstr.parse().unwrap();
    let fizzbuss = if num % 15 == 0 {
        "FizzBuzz".to_string()
    } else if num % 5 == 0 {
        "Buzz".to_string()
    } else {
        "Fizz".to_string()
    };
    println!("{}", fizzbuss);
}

これを実行してみると以下のようになります。

cargo run 15
   Compiling rust_handson_20210716 v0.1.0 (~/src/github.com/emahiro/il/rs_sandbox/rust_handson_20210716)
    Finished dev [unoptimized + debuginfo] target(s) in 0.93s
     Running `target/debug/rust_handson_20210716 15`
FizzBuzz

まとめ

制御構文とiteratorコマンドライン引数についてざっと触ってみました。

byte 文字列を Int に変換する

Overview

ちょっと凝ったユースケースですが、byte 文字列を Int に変換したいユースケースがあったときに使えるテクニックについてまとめます。

以下のようなユースケースを考えます。

func main() {
    b := []byte("123456")

    for idx := range b {
        fmt.Println(b[idx])
    }

}

// OUTPUT:
// 
// 49 <- 1 
// 50 <- 2
// 51 <- 3
// 52 <- 4
// 53 <- 5
// 54 <- 6

ここで byte 配列に格納された書く byte 文字列を自然数の配列に変換したいとします。
byte 文字列で 49(ASCIIコード上) は 1 なのでこの与えられた byte 配列を 1,2,3,... という Int の配列に変換することを目指します。

ASCII コードの 0 を使う

要は各 byte 文字列から 48 を引いた値が Int に変換したときの数字に一致するので 48 を引いてあげると良さそうです。

文字としての 0 は ASCII コード表 の上では 48 番目で対応する ASCII コードは 0x30 です。 なのでこれを与えられた数から引きます。

func main() {
    b := []byte("123456")

    for idx := range b {
        fmt.Println(b[idx] - 0x30)
    }

}

これで目的とする byte 文字列を Int に変換することができます。

Atoi の実装を参考にする。

文字列から int に変換するときによく使う strconv.Atoi の実装で似たようなことをしています。
ref: https://golang.org/src/strconv/atoi.go?s=5658:5690#L241

// Atoi is equivalent to ParseInt(s, 10, 0), converted to type int.
func Atoi(s string) (int, error) {
    const fnAtoi = "Atoi"

    sLen := len(s)
    if intSize == 32 && (0 < sLen && sLen < 10) ||
        intSize == 64 && (0 < sLen && sLen < 19) {
        // Fast path for small integers that fit int type.
        s0 := s
        if s[0] == '-' || s[0] == '+' {
            s = s[1:]
            if len(s) < 1 {
                return 0, &NumError{fnAtoi, s0, ErrSyntax}
            }
        }

        n := 0
        for _, ch := range []byte(s) {
            ch -= '0'
            if ch > 9 {
                return 0, &NumError{fnAtoi, s0, ErrSyntax}
            }
            n = n*10 + int(ch)
        }
        if s0[0] == '-' {
            n = -n
        }
        return n, nil
    }

    // Slow path for invalid, big, or underscored integers.
    i64, err := ParseInt(s, 10, 0)
    if nerr, ok := err.(*NumError); ok {
        nerr.Func = fnAtoi
    }
    return int(i64), err
}

ch -= '0' に注目します。一つ前では文字列 0 を表す ASCII コードを目的とする byte 文字列から引いて目的とする Int を取り出してましたが、 Atoi の実装ではUnicodeコードポイントを持つ rune で 0 を表現してそれを byte から引くことで実現してます。

'0' の中身を出力してみるとわかります。

func main() {
    fmt.Println('0')
}

// OUTPUT:
//
// 48

つまり '0' を使うと byte 文字列からそのまま目的とする int に変換することができます。

func main() {
    b := []byte("123456")
    for idx := range b {
        fmt.Println(b[idx] - '0')
    }

}

これでも同様に動作させることができます。

まとめ

byte -> int という実装で以下のことがわかりました。

  1. byte文字列と ASCII コードはセットで計算できる
  2. byte文字列と Unicode コードポイントはセットで計算できる。

文字列の変換、奥が深い...。

Go でテキストを1行ずつ読み込む

Overview

Go でテキストファイルを1行ずつ読み込んでいく方法を忘れてたので思い出しついでに備忘録です。

bufio を使う

bufio を忘れてました。

こんな感じで処理できます。

package main

import (
    "bufio"
    "bytes"
    "fmt"
)

// text ファイルに書かれてる内容だとする
const s = "aaaaaa\nbbbbbb\ncccccc\n"

func main() {
    buf := bytes.NewBufferString(s)
    scanner := bufio.NewScanner(buf)
    for scanner.Scan() {
        fmt.Println(scanner.Text())
    }
}

// output:
// 
// aaaaaa
// bbbbbb
// cccccc

ref: https://play.golang.org/p/1uRGl8Ox_jK

VSCode で Rust の開発環境を整備する

Overview

VSCode での Rust の開発環境を整備する手順にまとめます。PR としては以下にまとめました。

github.com

Install Rust

以下に書いてある手順そのままです。

doc.rust-jp.rs

Rust ではこの rustup というツールという公式のツールをつかって色々とインストールするんですね。

ツール

公式の Rust の Extension + rust-analyzer を入れれば完了です。

marketplace.visualstudio.com

marketplace.visualstudio.com

rust-analyzer を動かす

標準の Rust の Extension を入れた状態だと rust-analyzer が動いてくれませんでした。

f:id:ema_hiro:20210720025430p:plain

これは静的検査に必要なツールが入っていないからだったので、これらを入れます。必要なのは以下の3つのツールです。rustup コマンドを使ってインストールします。

 rustup component add rust-src
 rustup component add rust-analysis
 rustup component add rls

これで VSCode をリロードして main.rs を開くと自動補完始め、開発しやすい状態が整います。

その他

play.rust-lang.org

See Also

あとは以下を読みながら少しずつ進めることにします。

blog-dry.com

Rust ハンズオンを開催したブログを書きました

会社のブログで金曜日に開催した Rust のハンズオンに関するエントリを書きました。

link.medium.com

登壇者からずっと Rust やらないんすか?とプレッシャーをかけられていて、本まで貰っていたのですが、ようやく踏ん切りついてはじめました。
ハンズオンは非常に盛り上がって色々脱線してもしまって最後まで行けなかったので、まずは題材となってる grep コマンドの実装を最後までやってから Web API 書いてみる、くらいのことはしようかなと思っています。

前提となる知識もそれなりに必要なソフトウェアなのでとっつきづらさはありますが、エコシステム自体は非常に成熟してるので使う上では安心して使えそうな印象を持ちました。やっていくぞ!

Kalidoface3D × OBS を使って Mac でアバター配信に入門する

f:id:ema_hiro:20210716040025p:plain

Overview

Kalidoface3D というブラウザから3DモデルをトラッキングできるツールとOBS (Open Broadcast Software) を使って Mac 使いでもお手軽にアバターを纏ってオンラインMTGに参加できることが判明したのでその手順についてまとめます。

www.moguravr.com

僕自身アバターを纏って Zoom に参加することにはずっと興味がありましたが、如何せん現状の VR 事情だと必要機材が多かったり、メインは Windows世界線だったりしてそもそも始めるのにハードルが高いなーと感じてたのですが、この Kakidoface はブラウザで提供されてるツールなので OS を選ばずに使うことができます。
ブラウザなのでスマートフォンタブレットからもアバターを纏うことが可能になります。

必要機材

  • Mac ( 202201現在M1Max推奨 )
  • ブラウザ(なんでもいいですが Safari だと ボディトラッキングは許可されてないそうです)
  • Kalidoface 3D
  • Open Broadcast Software
  • 配信したいツール(今回は Zoom を想定しています。)

手順

Kalidoface 3D にいってお好きなアバターを選ぶ

Kalidoface 3D - Face & Full Body Tracking にいってお好きなアバターを選びます。

f:id:ema_hiro:20210716040005p:plain

OBS をDL して入力ソースの設定を行う

OBS の DL & Install

Open Broadcast Software から Mac 版を DL してきて起動します。

起動すると最初に使用用途を聞かれるのでどれか適当に回答してください。今回は仮想カメラを使って配信したいだけなので一番下を選択しました。

f:id:ema_hiro:20210716040252p:plain

入力ソースの設定

慣れてないとまず OBS の使い方でつまづきます。
OBS はあくまで仮想カメラなのでカメラに何を入力するのか、という入力ソースを指定する必要があります。今回はブラウザのツールを仮想カメラで表示させたいのでブラウザをキャプチャするのでソースにブラウザを指定します。

ソースタブの中で右クリックして追加 > ウィンドウキャプチャを選択します。

f:id:ema_hiro:20210716041054p:plain

Mac で画面収録が許可されていればウィンドウの一覧で Chrome が出てくるはずです。繰り返しですが、 OBS に画面収録の権限を OBS に渡しておく必要がある ので、もし Chrome がでてこない、という方は Preference > セキュリティとプライバシー > 画面収録で OBS を許可しておきましょう。許可したら OBS 再起動してくださいね。

f:id:ema_hiro:20210716041142p:plain

ウィンドウキャプチャをソースに設定するときの画面が出てくるのでこの画面で Chrome を選択します。
プレビュー画面で Chrome が表示されない場合は空の名前でウィンドウを表示 にもチェックを入れておきます。(最初これがやらないとブラウザの画面がキャプチャされませんでした。何か更新処理でも走ってるのかもしれないです)

ここまでくるとブラウザが OBS にキャプチャされるようになるはずです。キャプチャされたのを確認したら OBS でキャプチャされてる画像が投影されてる画面で右クリックして「プレビュー有効化」を選択します。

f:id:ema_hiro:20210716041746p:plain]

これで OBS 側の基本的な設定は終わりです。Kalidoface3D で動いてるアバターのキャプチャが表示されていることを確認したらコントロールタブの「仮想カメラ開始」をクリックして入力ソースを仮想のカメラとして認識させ始めてください。

f:id:ema_hiro:20210716042007p:plain

うまくいくと以下の画像のようになります。

f:id:ema_hiro:20210716042212p:plain

※ OBS ではブラウザをキャプチャしてるだけなのでブラウザのフォーカスを他のタブにしたらアバターは表示されなくなるのでタブの移動は注意してください。

以下の記事も詳しいです。

create.anigameinfo.com

Zoom で OBS の仮想カメラをカメラのソースに指定する

OBS 側で配信開始をしたら、一度 Zoom を再起動してください。そしたら再起動後にビデオの入力ソースにOBSの仮想カメラあることを確認して、選択してください。

Zoom の設定画面で以下のようになると思います。

f:id:ema_hiro:20210716043016p:plain

ここまできたらもう Zoom MTG でビデオを ON にした段階でアバターを纏った姿が配信されます。

f:id:ema_hiro:20210716043600p:plain

配信時の Tips

クロマキー設定して OBS の背景を透過させる。

アバターの背景を透過させたい場合はグリーンバックにする必要があるので、OBS 側でその設定を行い、Zoom 側で背景とフィルター >「グリーンスクリーンがあります」を選択します。

www.alloneslife-0to1work.jp

このブログが詳しいですが、やりかたとしては OBS のキャプチャしてるシーンの箇所でプレビュー有効化した時の要領で右クリック > フィルタを選択。

以下の画面のエフェクトフィルタ > +ボタン押下 > クロマキーを選択します。

f:id:ema_hiro:20210716043957p:plain

これで背景を透過できるようになります。

Zoom の解像度に合わせて OBS の出力を調整する

ブラウザで Kalidoface3D 開いて OBS でキャプチャして、Zoom でそのソースを参照する + 開発用途でクジラをわんさか動かしてたりすると PC が悲鳴をあげて、性能が著しく劣化します。

焼け石に水感ありますが、すこしでも負荷を下げるために Zoom での配信目的、というだけであれば OBS 側のキャプチャ結果の解像度を Zoom に合わせて下げたりすることもできます。
OBS の設定 > 映像 で最適なものに変更します。今回は Zoom で MAX が HD なので 1280x720 に出力を合わせてしまっても問題ありません。

f:id:ema_hiro:20210716044423p:plain

感想

ツールのセットアップ周りはまだコツがいる、というか知らないとどうにもならない(僕も現職の VR チーム勢にめっちゃ質問してました)ということはありますが、今回の手順はあまりに簡単でもう Mac だからバ美肉できない、なんて言い訳は通用しなくなったなと思います。
とにかくこんなに簡単にできるとは思いもしませんでした。

そして一度アバターを纏うと全ての場所にアバターで露出したくなる衝動に駆られることがわかりました。プライベートな情報を秘匿しつつ、カメラOFFのときより動きがあるからコミュニケーションに齟齬が生じる可能性が少なさそうでとてもいいですね、アバター

宿題系

Bluetooth との噛み合わせがイマイチ

Mac で kalidoface3D を OBS 経由で Zoom に配信する場合 Zoom 側で Bluetooth 接続機器が丸ごと死ぬという事象が発生しました。
これはなんで起きるのかまだわかってませんが、とりあえず Bluetooth で接続してたスピーカーからは相手の声が一切聞こえてこなかったです。
現状とりあえず有線接続の機器だと正常に動くので Zoom で配信するときは有線のヘッドホンをしばらくは使おうと思います。

~どこの噛み合わせが悪いのかまだ分かってません。むしろ上手くいった方いらっしゃったら教えてください。~

=> これはおそらく CPU 使用率が高すぎて Bluetooth が不安定になってる、という状況になってました。

追記

20220125

Kalidoface3D を Mac で使用する最大のボトルネックが PC のパフォーマンスで、アバターかぶって Zoom で配信してる最中は、めちゃくちゃPCが重たくなってしまうのがかなりストレスだったのですが、2022年になって M1Max を手に入れて再度同じ構成で試したところ、気持ち悪いくらいサクサク・ヌルヌルで最高の体験を手に入れてしまいました。
現状手元の M1Max の性能は以下ですが、この性能では Docker を立ち上げた状態で OBS を起動し、仮想カメラ経由で Zoom に配信するという Intel Mac であればファンが悲鳴をあげ、CPU使用率も常に天井に張り付いてしまい、他のアプリケーションの動作に影響を与えてしまうような状態にしても M1Max では何の問題もありません。

f:id:ema_hiro:20220125214847p:plain

ただ、バッテリー長持ちと言われてる M1Max でさえそこそこの速度で電力を消費していきます。
とはいえ気になるといえばそれくらいです。
必要機材には M1Max 推奨と記載しましたが、もし快適なアバター生活を Mac で楽しみたいなら M1Maxは必須 、いや M1Max は人権 と言っても過言ではないでしょう。

2021年上半期をふりかえる

Overview

7月も上旬が終わってしまったけど、2021 年半分が終わったので振り返りをしてみます。

Good

仕事忙しい

いいのか悪いのかわかりませんが、引き続き仕事は忙しいです。
まぁ暇よりはずっといいです。

2020年年末以下のようなエントリーを書いたがありがたいことにこのブログに載せてたことや、そのあとにやったことが評価されていい感じにお賃金も伸ばしてもらいました(多分キャリアの中で1番伸びたんじゃないかなと思います。)
辞めちゃった前の上長にはめちゃくちゃ死ぬほどこれでもないってくらいバチクソ感謝してます。

ema-hiro.hatenablog.com

ただ、コードを書く時間は相変わらず増えてないですし、さらに減った感あります。
コードを書く前作業に時間を使うことは多かったり、プロジェクトをまとめつつ自身はプロジェクト外の案件をワンマンアーミーでやってたりして、2020年と比べると、いろんなコンテキストを持つ案件が常時3~4 つくらい並行して走ってて、それら全部の技術的な取りまとめをしてることがほぼ、みたいな感じでした。
おかげで日中は案件関連に関わる業務をこなして自分の時間が取れたと思ったらもう17時!!みたいなことが日常茶飯事になってました。

さすがにしんどかったのでプロジェクトのメンバーに技術的なところだけでなく、要件周りも関係者とコミュニケーションとって決めてもらうことは増やしました。その代わり、自分はその大前提となる背景だったり、「なんでそれするんだっけ?したいんだっけ?」みたいなところを調整することにフォーカスしてました。

これはこれで仕事の進め方としてはよかったですが、これは楽しいところは手を離し、よりしんどいところばっかりにフォーカスするようにしたことと同義なので、ストレスは増しました。

あと現職だと僕は結構ドキュメント警察なんですが、コンフルだとやっぱりしんどいのかなーと思って、開発メンバーは全員 Figma でやりとりしてもらったりとか、「それをみる人」のスコープに合わせて、ツールの選定を変えたりとかしました。
あと、なんかずっと気になってた Slack のスレッド使いについて実験をしてみたりとか、この手のプロセスやコミュニケーションに関わることを小さくはじめて色々検証してみる、みたいなことをしてました。

ema-hiro.hatenablog.com

僕は結構こういう開発プロセスとかに興味があるので、ここはもし機会があれば色々議論できる人がいたらしてみたいな、と思いました。

家電の入れ替え

2021 上半期でちょうど一人暮らしをして 10 年が経過したので、いい機会だと思って白物家電(冷蔵庫と洗濯機)を買い替えました。
冷蔵庫は大型に、洗濯機はドラム型洗濯乾燥機にしました。

これでわかったのは、白物家電QOL に直結し、暮らしのレベルを根底からグレードアップさせてくれる素晴らしいツールだったということです。

自動製氷は水出しコーヒーをやるには絶対必要だし、乾燥機があると洗濯の回数が段違いになり、いつでも清潔な衣服を着れます。
特に洗濯乾燥機にしたことで、寝具の洗濯頻度が激増したので清潔な寝具で寝れるようになったのがめっちゃよかったです。男の1人暮らしだとどうしても寝具の選択は後回しにしがちなので...。

ちなみに白物家電で何を買うか検討してるの、技術選定に似ててめっちゃ楽しかったです。
特に入れ替え周期が10年スパンなので、仕事でやる技術選定より寿命が長くて頭使いましたね。10年後自分がどうなっているのか?を前提に逆算で何を買うのか決めてるあたりが楽しい。

次は掃除機と炊飯器を買い換えようと計画中です。(掃除機はもう今月か来月に買い替えるかもしれない)

積読消化

無限に積まれていった積読たちの一部消化し始めました。
社内の輪読会にも参加し始めたので、読むペースがついたのはよかったです。
一時期は1週間1冊ペースを維持してました(本当は2,3日で一冊読みたい)

読んでる本としては Product Management や技術は技術でも設計やモデリングやら、その辺の本を読むことが増えました。あとお金関連の本(決算の読み方とか)
特に Product Management 系の書籍は最近のものを読みますが、技術に関連する本は割と古典とかも読んでます。

ちょっとここ最近また忙しくて読むペースが落ちてたのでそろそろ復活させたいなと思います。

ちなみに頑張って紹介しても積まれるペースの方が早くて困ってます。
積読の金額計算したらびびりそうなので計算してません。なぜ人は読むかもわからない本をポンポン買ってしまうのか。

More

生活リズムの崩壊

「仕事忙しい」の反動として生活リズムは崩壊しました。
海外サッカーが好きなのでたびたび朝方までサッカーを見てることはありましたけど、この上半期は退勤即仮眠 -> 夜寝れなくなる -> 作業したりしつつ、サッカーの試合を見て朝方寝る、みたいな生活がデフォルトになってしまいそれが海外サッカーシーズンが終わった今も継続中です。

Slack の自分のチャンネルに AM 3時とか4時台に登校とかしてて、いつ寝てるんだ疑惑が浮上したこともあります(退勤後即です)

有給残り少ない

これはこれでめちゃくちゃいいことだけど、この間ふと有給残日数を確認したら1.5日 とかになっててびっくりしました。笑

ここまでちゃんと有給を消化してしまったことに驚きです。元々使わずに溜め込みがちでリモートなのでなおさら休まなくなったのもあって有給使わないかなーと思ってたんですが、疲れたのでとりあえず休むか、くらいのノリで使ってたり、あと現職は長期休暇シーズンに飛び日があると有休消化推奨期間になってて、まぁここまではよくある話だと思うんですが、経営層はじめ上司や周りがボンボン休むので僕も釣られて休んでしまい、気づいたら消えてました。

ちなみに現職は有給とは別にコロナ特別休暇みたいなものがここ1年くらい突如与えられたり(※)、福利厚生で家族の記念日に休暇が取れたり、気づくと有給とは別に休む日がめちゃくちゃ増える福利厚生があるので休みに困ったことはないです(これはちょっとだけ宣伝)

※ コロナ特別休暇のおかげで来たる五輪休暇は有給とは別計算で2日増えて6連休です。

英語学習たりない

今年のテーマに置いてますが、あんまり進捗はよろしくないです。

英単語とリスニングはかろうじて続けられてますが、英会話(Speaking & Listening) はてんでダメですね。
同僚に週一でレッスンつけてもらってますがこればっかりは量が勝負なのでなかなか進捗がでないっす。。。

一応進捗してるところもあって英単語と Reading & Listening は続けてるので、綺麗な英語で話すスピードが速くなければ、何を言ってるのか?わからないみたいなことは無くなってきました。あと英作文(チャットでよく使う)も、外国籍の方がどんな表現使ってるのか観察しながらほぼ見様見真似で使いつつ、ニュアンスや定型文をちょっとずつ学習して使うようになりました。これだけでも英作文考える時間が減るのですごくよかったです。
こういう生の英作文を勉強できるのは当たり前のように外国籍の方がいる環境ならではだと思います(一種の福利厚生ではこれ)

とないえ、幼稚園児レベルの英語が小学2年生くらいになっただけなのでせめて下半期で中学生くらいまではいきたいなと思って日々精進です。
(というか、筋トレと同じなので、やればやっただけ結果出るはずなんですけど、やっぱりたまにだれちゃうんですよね...)

Try

生活リズム直す

やりたいです。しかしこのエントリを3時ごろ書いてる時点でそもそも直す気ない気もしてます。

英会話やる

せっかく福利厚生でオンライン英会話レッスンがあるのでそろそろまじで始めたいです。
最近ガチでタイムゾーンが違う人たちと仕事をしないといけない時が来てしまい、それはそれでめっちゃありがたいんですが、初回のMTGで何を話しているのか、言語レベルでさっぱりわからなかったのでヤバみを感じました。

同僚にも週一でレッスンを完全ボランティアでつけてもらってるので少しでも成果を出したい...。

がんばろう...。

その他

上半期買ったものリスト

上半期も色々買ったのでその散財記録です。

洗剤自動投入付きドラム式選択乾燥機

振り返りでも書きましたが上半期一番買ってよかったと思います。
これは元同僚の IE11 だきしめおじさんの話を聞いて自動洗剤投入付きのものにしましたが、洗剤容器を置く場所も削減できたので一石二鳥でした。


冷蔵庫

念願の野菜室 & 肉魚専用チルド & 自動製氷機を手に入れたおかげで自炊や食材の保存がめちゃくちゃ改善しました。
買うときにもう一回り小さいやつも検討しましたが、現在の住んでる家が専用のダイニングがあって、キッチンも広いので思い切ってデカイ方を買いましたが、これが正解でした。
白物家電の原則は予算の範囲内で大は小をかねますね。
あと自炊派なら冷蔵庫に金を惜しむな、は言いたいと思います。


cores コーングラインダー

在宅になってからというもの、コーヒーが趣味になってしまったのでお試し電動ミルを買ってから半年でちゃんとした電動ミルを買ってしましました...
水出しコーヒーもつくれるようになったし、自粛が加速してしまってます。

ちなみに同価格帯で Wilfa やもうちょっと言ったところの NextG とも悩みましたがお手入れのしやすさで cores にしました。


RICHO GRⅢ

念願のカメラをようやく買い替えました。
コロナで外出減っちゃいましたが、ちょっと外に出たときとかに持っていけるのでいい買い物でした。
尊敬してるカメラマンたちもこぞって使って、SONYVlog カメラとも悩みましたが、やっぱりカメラやるなら GR かなと思って GR にしました。

CalDigit TS3 Plus

デスク周辺機器を増やすにつれてコードが増えまくってしまったのと、Mac に接続しないといけない機器が増えてしまったので Dock をもともと買おうと思ってて、変な拡張を買うくらいならガチなやつがいいかなーと思って berkin のやつと悩んでこっちを買いました。
違いは分かりませんが、CalDigit は Apple の公式アクセサリーにもなってるので互換性も問題ないのかなと思って CalDigit を選択しました。
デスクの配線がスッキリしたのでとても満足してます。

Anker PowerWave 3in1

最近Nomadの充電器が壊れたので安心と信頼の Anker に乗り換えました。
無線充電器は便利ですが、どこまでいっても有線には敵わんな、というのが個人的な感想です。これは Apple Watch の Dock も備えてるので便利です。

Anker PowerConf C300

洗濯機に次ぐ上半期ベストバイは多分これかなと思います。多分年間通しても買ってよかったものリストに入ると思います。
これも安心と信頼の Anker。
とにかくコストと品質が全く釣り合ってない(コスパが最高)現時点で WebCam 買うならこれ一択、という商品かと。

Anker Magnetic Cable Holder

これも Anker。
ガジェットの Anker の生活用品の山崎実業は EC で外れないブランド二大巨頭な気がします。

デスク状のコード類をすっきりさせたくて買いました。何度も貼れるシール式なので机を傷つけることもなく配置を変えられるところもお気に入りです。
これもこのままいくと今年買ってよかったものリストに多分入ります。

CIO モバイルバッテリー

たまたま見つけたんですが、高速充電対応してる小型の充電器です。
コロナで出かけなくなったのでモバイルバッテリーが必要な機会が減ったような気もしますが、もともと PC も充電できるごついやつをいつも持ち歩いててめっちゃ重たくて疲れたので、これくらい小さくて容量もあるやつを探してたのでベストマッチでした。