Posts for: #2024/09

pipe による関心の分離

go の Pipe は go 言語プログラミングのよいところをいくつか同時に実感できる、実用度の高いユーティリティだと思う。go 言語は writer や reader をインターフェースとして定義している。細かいメソッドの違いでいくつかの writer/reader インターフェースを区別していたりもするが、基本的には write/read のメソッドをもっていればよい。

type Writer interface {
	Write(p []byte) (n int, err error)
}

type Reader interface {
	Read(p []byte) (n int, err error)
}

この汎用的な writer/reader のインターフェースに従っていれば、さまざまな要件に対して抽象化ができる。オンメモリでデータを扱うこともあるし、OS のファイルシステム上のファイルとして扱うこともあるし、オブジェクトストレージ上のオブジェクトとして扱うこともある。そういった具象オブジェクトの如何によらず writer/reader のインターフェースに従うことでデータ処理とリソース管理を分離できる。これはプログラミングパラダイムにおける 関心の分離 と呼ばれていることを実現する。

バッチ処理において複数データを読み込みながら処理を行い、処理中にエラーが発生したら、そのエラーデータを特定のファイルに書き込みしたい。エラーが発生したときだけ特定ファイルを生成する。そういった要件は次のように実装できる。

func write(w io.WriteCloser) {
	defer w.Close()
	/*
		if true {
			return
		}
	*/

	records := [][]string{
		{"first_name", "last_name", "username"},
		{"Rob", "Pike", "rob"},
		{"Ken", "Thompson", "ken"},
		{"Robert", "Griesemer", "gri"},
	}

	cw := csv.NewWriter(w)
	for _, record := range records {
		if err := cw.Write(record); err != nil {
			log.Fatalln("error writing record to csv:", err)
		}
	}
	cw.Flush()
	if err := cw.Error(); err != nil {
		log.Fatal(err)
	}
}

func read(r io.ReadCloser) {
	rr := bufio.NewReader(r)
	line, _, err := rr.ReadLine()
	if err == io.EOF {
		return
	}
	/*
		writer, err := os.Create("./test.txt")
		if err != nil {
			log.Fatal(err)
		}
		defer writer.Close()
	*/
	writer := os.Stdout
	if _, err := writer.Write(append(line, []byte{'\n'}...)); err != nil {
		log.Fatal(err)
	}
	if _, err = io.Copy(writer, rr); err != nil {
		log.Fatal(err)
	}
}

func main() {
	r, w := io.Pipe()
	go write(w)
	read(r)
}

Pipe を使うことで読み込みの処理と書き込みの処理を分離できる。ここでいう読み込みはバッチ処理、書き込みはエラーデータの保存処理に相当する。普通に実装すると、バッチ処理中にエラーデータの保存処理も記述することになる。業務のコードだと本質ではないリソース管理が煩雑になってしまう。2つの処理に分割するため、どちらか一方は非同期で実行する必要がある。ここで goroutine のシンプルさが際立つ。さらに pipe を使えば、reader 側は writer が Close するまでブロックするから終了処理の同期も自然なコードで実現してくれる。

非同期/並行処理の難しいところを go のインターフェースによる抽象化、簡潔な goroutine 記法、pipe に隠蔽された自然な同期処理の3つで簡潔に表現している。複雑さに対してこれほど簡潔なコードはそうそうないと思う。このコードを他の言語で実装しようとしたらもっと複雑になるはず。このサンプルコードとともに若いメンバーにリソース管理のリファクタリングをコードレビューで指摘したものの、そのメンバーには理解できなくて実装できなかった。そして、その処理を私が作り直すことになったというのが、今日のお仕事だった。本当は難しい非同期/並行処理をいくら簡潔にしても、その経験がないと人間には理解できない。まさに本質的複雑さを表しているように思える。

プログラミングとバドミントン

よく働き、よく遊ぶみたいな一日だった。

リファクタリングの最後の集中力

昨日から集中力を維持してずっとリファクタリングしている。やや複雑な機能をリファクタリングしているため、今週いっぱいはかかる見通し。本当は私が一から書き直した方が早い。しかし、若いメンバーに指導する意図もあるため、既存のコードを読んで誤りを訂正したり、仕様を確認したりしながら手直ししていく。射撃しつつ前進 の成果は着実にみえてきて、しんどいけど、コードの品質はどんどん改善している。マネージャーと 1on1 でリファクタリングの状況も伝えながら開発のイテレーションをもう1つ増やした方がスケジュール的にはよさそうかなぁみたいな共有もしていた。

体育館でバドミントン

前回の所感 。17時でお仕事を終えて最初で最後?の しみん福祉スポーツセンター の体育館を借りてバドミントンをしてきた。18時から21時の3時間枠で料金は3,000円。磯上体育館が2時間で700円なのと比べると割高になる。しかも、磯上体育館の方が駅に近いため、他地域から来られる方も参加しやすい。今後はこのスポーツセンターの体育館を借りるのはやめようと考えている。体育館予約にはリスクがあって3ヶ月前に予約しないといけないため、予約しても本当に参加者が来るかどうかわからない。一番悲惨なのは予約したけれど、誰も来なかったというパターン。いまのところ、そういう状況は発生していない。

閑話休題。今日は新しい人も2人きて全部で5人参加した。初めて行ったスポーツセンターの体育館は広くて設備そのものはとてもよかった。

前回来られた方に教えてもらった KALIDIAバドミントンチャンネル の動画を ipad にダウンロードしてもっていった。体育館で参加者と一緒にみながら次の練習をやってみた。動画をみたら簡単そうにみえるけど、実際にやってみると、狙ったところに打てないことに気付く。こういう地道な練習をしないとスキルが上達しない。もっと練習のパターンを増やしていきたい。

初めて来られた若い方がテニスをずっとされていたらしく、バドミントンも上手だった。前回もうまい人が来てくれて練習方法を教えてくれた。うまい人のプレーをみていると、あんな風にできたら楽しそうとか、思い通りにラケットやシャトルの操作ができなくて悔しいとか、感情的にモチベーションがあがってきた。あと久しぶりに運動して汗をかいてよい気分転換になった。

業務としてのリファクタリングへの集中力

今週からメンバーが開発した、少し大きめの機能の品質改善のためにリファクタリングをしていく。本当は週末に進めておこうと思っていたものの、どうにもやる気がなくてまったく手をつけられていなかった。不思議なもので休日は朝起きられなかったりオフィスへ行ってもまったくコードを書けなかったのに、平日なら普通に朝8時までに起きてオフィスへ行って集中して作業できた。2日休んでいるので体力を回復していたというのはある。それでも休日と平日の業務としてのプログラミングへの集中力の違いは何なんだろう?とやぎさんに相談したら 反脆弱性[上] という本にその答えが書いてあったと教えてもらった。この本を読まないとその答えは得られない。しかし、いまの私は本を読む元気がない。誰か読んだ人がいたら私にわかりやすく教えてほしい。

私が書いたコードに対する不備や欠陥は責任感からリファクタリングをする一定のモチベーションになる。しかし、他人が書いたコードのリファクタリングはそのモチベーションがいくらかレベルダウンするのではないか?という仮説もある。他人のコードを読んで理解するのはコストがかかるし、本質的には他人のコードを読むことはできても理解することはできない。コードの出典に起因するストレスもあるのかもしれない。

秋休み2

昨日に引き続き、やる気が出なくてなにもせずお休みしていた。

秋休み1

お昼はだらだらしていて夜にオフィスへ行ったものの、全然やる気が出なくてとくに作業は進められなかった。

連休の初日

昨日も2時頃までコードを書いていて帰って寝たらお昼ぐらいまで寝ていた。ストレッチ終えてからコードを書こうと思っていたものの、モチベーションがあがらなくてだらだらしていた気がする。

ストレッチ

今週もデスクワークの時間が長かったせいか、軽く腰の張りを感じた。トレーナーさんからも運動不足からカラダの硬さがみえるといった指摘があった。先週はレビューの時間が多くてあまり時間を時間を取れなくて業務があまり進捗しなかった。そのストレスもあってなのか、また生活の余裕がなくなってきた。今日の開脚幅も開始前149cmで、ストレッチ後153cmだった。

責任を扱うコミュニケーションの在り方

水曜日から継続していたメンバーのコードレビューをお昼頃に完了して、それからは自分の時間に集中して開発に取り組めた。途中、晩ご飯休憩もしながら翌2時頃までコードを書いていて issue を2つ fix した。

隔週の雑談

顧問のはらさんと隔週の打ち合わせ。ここ1-2ヶ月はあまり議題がなくて近況について雑談した。いまの開発フェーズが終わらないと、私が開発以外にリソースを割いていないので他のことに取り組む余裕はないかもしれない。開発プロジェクトにおける、議題の1つで次のインタビュー記事の内容について雑談した。

責任をすぐに相手に投げてしまわないこと。責任をこちらで負えるように日頃から信頼貯金を貯めていくことが大事です。責任を負うこと自体はたいへんだけど、仕事は楽になるんです。

でも、人は責任を負いたくないと考えてしまうんですね。気持ちは分かりますよ、責任を負うのには胆力が必要ですから。そして責任を負いたくないから「いつまでですか?」と聞いてしまう。その途端、スケジュールを決めるのは相手の裁量になる。責任から降りて、自分から立場を下にして、受発注の関係にしてしまっています。

何が事業貢献なのか分からなくなっていた伊藤直也さんが再認識したユーザーエクスペリエンスへのコミット

私自身、このインタビュー記事を読むまで相手に仕様や納期を決めてもらうことを、相手に責任を負わせるという視点をもっていないかった。しかし、その通りでもあると新たな気付きを得た記事でもあった。うちらはプログラマーという、システムを作る専門家なのでビジネス課題や解決したい業務課題について、その課題意識をもっている人 (ビジネスオーナー) から教えてもらうのを至極当然のように考えてしまう。そして、それ自体はいまの業務の在り方としてそうならざるを得ないところもある。

システム開発はそれぞれの専門職が分業によって行う。とくに規模が大きくなればそうなる。この構造そのものは一般的といえる。しかし、一緒にプロジェクトをやっていく働き方や考え方によってはその責任を分散させられるというのがこの記事に書いてあることだとわかる。そして、私自身これまで意識せずにそういった行動をいくらか取ってきているところもあり、それは「気付き」のレベルによって起こるものだとずっと考えていた。気付くからより多くの、より本質的な、より優れた改善のための行動ができる。そして、私の考え方も間違ってはいないと思うが、私は他人よりリスクを取りがちな性格があるから責任をこちらで負うという意識をあまりもっていなかった。つまり、私は自分の価値観でこうしたいと思って勝手にやっていたことを、別の見方では相手から責任を受け取って進めているという解釈もできる。

ちょうど 兵庫県知事の百条委員会の答弁 もみていて感じたことだが、私はこういったコミュニケーションを本質的に好んでいない。自分の責任ではないという議論をしても、モノゴトは前に進まない。世間では百条委員会の後も知事の説明は十分ではなかったとみられている。その所以である。誰かが責任をもってモノゴトに取り組む必要がある。その責任の所在を明確にすることも大事ではあるが、自身の責任ではないという主張だけでは物足りなさを感じる。普段の業務においてもそういう姿勢やコミュニケーションを取る人が少なからずいる。はらさんの経験でもその点においては同意していた。よくある状況だと思える。自身に責任を負うことをためらわないコミュニケーションを取る人とそうではない人の2通りがあるのだと気付いた。そして、私は後者の人とコミュニケーションを続けていると疲弊したり苛々したりすることがある。それゆえに私自身も結果に対して潔くあろうと努めるし、潔い人たちとウマがあうのだろうともわかってきた。

課題管理の文脈においては、コミュニケーションのやり取りから責任の綱引きがどのような場所でどのぐらい起きるのか。人間であれば読み取れるが、ai はその意図を解釈できるか。そういった業務の責任という概念を見える化することに意味はあるかもしれない。責任の押し付け合い、または責任分散、本質的にどうあるべきだったかをなんらかの指標をもって数値化できればおもしろいのではないかと思えた。

go における簡単な式の評価

テスト自動化のツールを作っていて、テストデータでちょっとした式の評価をやりたくて調べたらまさに次の記事で解決した。

この記事では go/types パッケージに定数や式の評価を行う機能がその使い方が紹介されている。簡単に使える。ふとサードパーティのパッケージならどうなるんだろう?とインターネットを検索したものの、自分ではみつけられなかった。chatgpt に問い合わせたら次のようなコードを紹介してくれた。そして、たしかにほとんどは正しくて意図したように動いた。次のコードでは http フレームワークの echo の定数を参照している。types.Package を生成するためのモジュールの読み込み方法について標準ライブラリはそのためのユーティリティが用意されているが、サードパーティのパッケージは用意されていなかった。

import (
    "fmt"
    "go/token"
    "go/types"
    "golang.org/x/tools/go/packages"
)

func eval(expr string) (types.TypeAndValue, error) {
	cfg := &packages.Config{
		Mode: packages.NeedTypes | packages.NeedImports,
		Fset: token.NewFileSet(),
	}
	pkgs, err := packages.Load(cfg, "github.com/labstack/echo/v4")
	if err != nil {
		return types.TypeAndValue{}, err
	}
	if len(pkgs) == 0 || pkgs[0].Types == nil {
		return types.TypeAndValue{}, fmt.Errorf("failed to load echo package")
	}
	mainPkg := types.NewPackage("main", "main")
	mainPkg.Scope().Insert(types.NewPkgName(token.NoPos, mainPkg, "echo", pkgs[0].Types))
	return types.Eval(
		cfg.Fset,
		mainPkg,
		token.NoPos,
		expr,
	)
}

このコードで expr に次のように echo の定数を指定するとその値を参照できる。

echo.MIMEApplicationJSON

動的型付けのノリで関数も実行できたりするのかな?と試してみたら (当たり前だが) できなかった。chatgpt になぜ関数実行できないかを尋ねたら次であるとのこと。静的型付けのコードを実行するには、本来コンパイルしないといけないのだから式の評価よりもずっとやることがある。

packages.Load を使用してサードパーティパッケージをインポートできているにもかかわらず、expr からそのパッケージの関数を呼び出しても結果が取得できない原因は、go/types パッケージがサポートしているのは、型や定数のチェック、構文解析、式の評価であって、関数の実行そのものはサポートされていないためです。

go/types の Eval 関数はあくまでコンパイル時の型検査や式の評価を行うもので 関数の実行や実行時の評価 (ランタイムの処理) は行いません。これは、Eval が式を評価して型と値を返すだけであり、動的な関数の実行などはできないためです。

つなぎの雑多な一日

昨日の続きでメンバーのコードレビューをしながら、openldap スキーマの validator のバグ修正をしつつ、チーム勉強会で雑多な仕様や設計の議論をしていたらそれにも時間を取られて、1日としてはまあり開発を進捗させられなかった。コードを書くことに集中できる時間を2-3時間連続で確保できないとモチベーションにも影響が出てくる。それだけ取り組んでいる課題が複雑だったり煩雑だったりしているのもある。

openldap スキーマの validator と go のマルチエラー制御

今日もメンバーのコードレビューに半日以上の時間を割いたので自分の時間をあまり取れなくて開発が進捗しなかった。

隙間時間で openldap スキーマの情報 を使って validator を実装していた。バリデーションのようなものはまとめてエラーを返せるのが望ましい。go でマルチエラーを制御する仕組みが少し前に追加されていたことを思い出した。Go 1.20 Wrapping multiple errors のチュートリアル記事を読みながら openldap スキーマの validator を実装して複数のエラーを返すようにした。まだ抜け・漏れはあるかもしれないけど objectClass の validator は次のようになる。

type ObjectClassValidator struct {
	objectClassMap map[string]ObjectClassDescription
}

func (v *ObjectClassValidator) Validate(attr map[string][]string) error {
	var errs []error
	var objClassDescs []ObjectClassDescription
	hasStructuralKind := false
	allAttrMap := map[string]struct{}{}
	lowerAttrNameMap := make(map[string]struct{}, len(attr))
	for k, values := range attr {
		lowerName := strings.ToLower(ParseAttribute(k))
		lowerAttrNameMap[lowerName] = struct{}{}
		if lowerName == "objectclass" {
			objClassDescs = make([]ObjectClassDescription, 0, len(values))
			for _, objcName := range values {
				lowerObjcName := strings.ToLower(objcName)
				ocd, ok := v.objectClassMap[lowerObjcName]
				if !ok {
					msg := fmt.Sprintf("'%s' objectClass is not defined", objcName)
					errs = append(errs, NewErrorValidation(msg))
					continue
				}
				if ocd.Kind != nil && *ocd.Kind == Structural {
					hasStructuralKind = true
				}
				objClassDescs = append(objClassDescs, ocd)
				for mustAttr := range ocd.GetMust() {
					allAttrMap[strings.ToLower(mustAttr)] = struct{}{}
				}
				for mayAttr := range ocd.GetMay() {
					allAttrMap[strings.ToLower(mayAttr)] = struct{}{}
				}
			}
		}
	}

	// require at least 1 structural objectClass
	if !hasStructuralKind {
		errs = append(errs, NewErrorValidation("no structural objectClass"))
	}

	// must in objectClass requires must attributes
	for _, ocd := range objClassDescs {
		for mustAttr := range ocd.GetMust() {
			if _, ok := lowerAttrNameMap[strings.ToLower(mustAttr)]; !ok {
				msg := fmt.Sprintf("'%s' objectClass requires '%s'", ocd.GetName(), mustAttr)
				errs = append(errs, NewErrorValidation(msg))
				continue
			}
		}
	}

	// all attribute keys are allowed may/must in objectClass
	for attrName := range lowerAttrNameMap {
		if _, ok := allAttrMap[attrName]; !ok {
			msg := fmt.Sprintf("'%s' is not allowed by objectClass", attrName)
			errs = append(errs, NewErrorValidation(msg))
			continue
		}
	}
	return errors.Join(errs...)
}

func NewObjectClassValidator(
	objectClasses []ObjectClassDescription,
) *ObjectClassValidator {
	ocm := make(map[string]ObjectClassDescription, len(objectClasses))
	for _, v := range objectClasses {
		for _, n := range v.Name {
			ocm[strings.ToLower(n)] = v
		}
	}
	return &ObjectClassValidator{
		objectClassMap: ocm,
	}
}

次のように複数のエラーを返すテストケースを定義する。

{
	name:       "multiple errors",
	objClasses: objectClasses1,
	attr: map[string][]string{
		"objectClass":           []string{"top", "posixAccount", "myObjeClass"},
		"uid":                   nil,
		"mail":                  nil,
		"mail;lang-ja;phonetic": nil,
		"employeeType;lang-ja":  nil,
		"homeDirectory":         nil,
		"GIDNumber":             nil,
		"street":                nil,
	},
	expected: []error{
		openldap.NewErrorValidation("'employeetype' is not allowed by objectClass"),
		openldap.NewErrorValidation("'mail' is not allowed by objectClass"),
		openldap.NewErrorValidation("'myObjeClass' objectClass is not defined"),
		openldap.NewErrorValidation("'posixAccount' objectClass requires 'cn'"),
		openldap.NewErrorValidation("'posixAccount' objectClass requires 'uidNumber'"),
		openldap.NewErrorValidation("'street' is not allowed by objectClass"),
		openldap.NewErrorValidation("no structural objectClass"),
	},
},

テストコードで次のように返ってくる error を unwrap することで検証できる。

for _, tt := range tests {
	t.Run(tt.name, func(t *testing.T) {
		t.Parallel()
		validator := openldap.NewObjectClassValidator(tt.objClasses)
		err := validator.Validate(tt.attr)
		if actual, ok := err.(interface{ Unwrap() []error }); ok {
			t.Log("\n" + err.Error())
			if diff := cmp.Diff(tt.expected, actual.Unwrap(), opts...); diff != "" {
				t.Error(diff)
				return
			}
		}
	})
}

結果に対する潔さ

今日はマイルストーンの最終日で定例会議での確認事項が長引いたり、メンバーとのやり取りをしていたらあまり自分の時間を取れなかった。たまにはそういう日もあるか。

はてブですら兵庫県知事のパワハラ問題の記事がホットエントリーになる。自分の住んでいる自治体なのでちょくちょく読んでいる。先週に県議会の百条委員会の動画がアップされていた。すべてみたわけではないが、答弁のやり取りを少しみた。

知事は元官僚なので自身の責任を問われないよう、答弁はうまいように聞こえる。基本的に自身の過失を認めず、微妙な問題の判断は議会に委ね、自身の対応に問題はなかったという姿勢を貫いていたと思う。自身の保身を図るという側面ではこういった姿勢は適切だろうし、知事の優秀さも伺えるし、知事の視点からは理解できる。一方で政治家として結果責任をとるという視点では、自殺者が2人も出ていて、百条委員会が開かれる状況になっていることを自身の責任だと受け入れる必要はあると思う。

たまたま今日もはてブでホットエントリーになっていて議会で不信任案に全会一致という状況になっていて悲惨にみえる。

兵庫県議会の議員86人全員が斎藤知事に「辞職」を求めるという異例の事態となっています。

斎藤知事に兵庫県議全員86人が「辞職要求」へ 全会派に無所属4人も 知事は改めて辞職否定「来年度予算の議論含めてしっかりやっていきたい」

現時点で知事は辞職しないという姿勢をとっている。過去の事例を調べてみると、都道府県議会で不信任案が可決されたのは過去に4回しかなくて、さらに知事が辞職しなかった事例は1度もないという。その文脈を踏まえると、どこかで政治的な妥協案が取られて知事は辞職するのかもしれない。

都道府県議会で実際に可決されたのは岐阜(昭和51年)、長野(平成14年)、徳島(15年)、宮崎(18年)の4回。いずれも知事が辞職か失職を選び、議会が解散されたことはない。

知事の不信任案可決、過去には「脱ダム」田中氏ら4例のみ 議会側の最終手段も高いハードル

私は人を評価する視点の1つに「潔さ」をみている。かっこいい人は潔い。とくに失敗したときに潔いかどうかをみている。うまくいかなかったときにどのような行動を取れるか。百条委員会の答弁は知事の視点から自己弁護すること自体はわるくないと思う。しかし、実態はどうあれ、議会の議員全員が不信任案に賛成するという状況において辞職を決断できないのは潔くないという点のみで、私は知事を支持できない理由になっている。

コンテナー間のデータ通信はやはり http プロトコル

過去に コンテナー間のデータ通信に named pipe を使って実装 した。機能的には問題なく運用できていたが、環境構築時に named pipe の設定がわかりにくいという課題があった。また docker compose は volumes に設定したパスが存在しないときに歴史的経緯でディレクトリを作成してしまう。初期設定時に named pipe を期待するリソースがディレクトリになってしまっているところの、エラー制御のわかりにくさもあった。またこの運用はオンプレミスの環境でしか利用できず、将来的にクラウド対応 (k8s) で運用することを考慮すると、http 通信できる方がよいと考えを改め、データ通信の仕組みを再実装した。http サーバーと比べて named pipe の方が優れているのはセキュリティとして物理的にその OS 上からしかアクセスできないところ。

go でフレームワークを使わず、シンプルな http サーバーを実装してみた。セキュリティの制約さえなければ、このぐらいの手間を惜しんで named pipe を実装することもなかったかと、いまとなっての学びとなった。

func newMux(cfg config.MyConfig) *http.ServeMux {
	mux := http.NewServeMux()
	mux.HandleFunc("/version", handler.HandleVersion(cfg))
	return mux
}

func main() {
	ctx, stop := signal.NotifyContext(context.Background(),
		syscall.SIGINT,
		syscall.SIGTERM,
	)
	defer stop()

	cfg := config.MyConfig{}
	server := &http.Server{
		Addr:    ":7099",
		Handler: newMux(cfg),
	}
	go func() {
		if err := server.ListenAndServe(); err != http.ErrServerClosed {
			slog.Error("got an error providing http service", "err", err)
		}
		slog.Info("stopped http server normally")
	}()

	<-ctx.Done()
	slog.Info("caught the stop signal")
	ctxTimeout, cancel := context.WithTimeout(context.Background(), 10*time.Second)
	defer cancel()
	if err := server.Shutdown(ctxTimeout); err != nil {
		slog.Error("failed to shutdown normally", "err", err)
		return
	}
	slog.Info("done graceful shutdown")
}