XGHというものについて

日頃、スクラム開発は単なるバズワードであって手法ばかりに注目が行って有体に言ってコンサルの飯のタネとしか思ってない。 重要なのは不確定要素を可能な限り取り除いたうえで振り返りという形でイテレーションの終わりに自分たちが組織としてどのようにふるまえたかを客観的に評価する健康診断という強制的なイベントを実施して、代謝が機能するような体制づくりのことだと思う。

それゆえ、イテレーションを1か月とか長い期間とってしまうと不確定要素の相対量が増え、振り返りも難しくなるので1~2週間が望ましいと個人的には思っていて、たぶんそれが世のなかの一部エンジニアの皮をかぶった謎の存在も含めた現実を直視することが苦手なソフトウェア開発の苦労や困難の実情を知らない人間たちにとっては「開発スピードがあがるんだ!!!」みたいな的外れな幻想を抱かせてしまうのだろうと思っている。

で、そんなことを常に考えていてむしゃくしゃすることがないことはないんだけど medium.com の記事を読んで前半の Sprint に対する筆者の洞察は私と全く同じで、首肯のしすぎで頸椎が疲労骨折した。

ただし、後半の XGH なるものは極端すぎてほとんど賛同できなかった。というより、ものすごい優秀なエンジニアでないと成立しないという感想を持った。

どのようなものかは原文を参照してもらうこととして、以下が直感的な理由。

  • 優先順位とかなくて必要な時に必要な価値が届けられればよい
  • PO に当たるような役割はなく、要求はエンジニアが完璧に理解している前提
  • 問題が発生したら局所的な対応か、作り直しをする
  • テストは書かない
  • それゆえリファクタリングが存在しない
  • 他人の書いたコードに介入しない

一方、共感できる部分が特にない。

要はこの記事、差分を各担当者の責務としてメンテしろという話であってチーム運用の話ではないとしか思えないし、品質の話を一切してないんだけど皮肉として述べてるんだろうか。一筆書きで過不足なく動くときは動くものを作る(開発スピードについては言及なし)ということを目指しているよう思える。

チームメンバーにこの記事の感想を出してもらうことで価値観のすり合わせはできるかもしれないとは思ったけど、今のところ私には XGH は無理という結論が出ている。

Java21のパターンマッチは醜いと思う

JDK だか Java だか知らねーけど開いた口がふさがらなくて口腔内が砂漠化したのでヘイト記事を通して膿を出す。

Java (¬ JVM) がそれなりにひどいのはわざわざ表明する必要もないんだけど、JEP 441: Pattern Matching for switch で提案されているパターンマッチはひどく醜いと思う。

後付けなので仕方がないにしても permits で親子関係を明示するというのは拡張に開いていないので OCP 違反であるように思う。OCP の是非はおいておいて、とりあえず SOLID 原則の考え方は広く敷衍してるのでまあ恣意的なものであるけどそこまで無茶苦茶ではないだろうということにしておく。

Haskell のデータ型でもデータコンストラクタ並べるじゃんというのはあるんだけど、見かけは同じでも Javainterface は実用上実装が必要になる時点で全く別物だろう。

domain を決める直和的なデータ表現であることには確かに変わりないだろうけど JavainterfaceHaskelldata を同列に語るのはさすがに無理がある。

だからこそこの苦し紛れな文法が導入されたんだろうけど、Java を使わないに限るなという確信が強くなった。

Java、あなたはよくやったよ。もう言語としての進化はあきらめてぐっすりお眠り。

nix + cabal の環境で libz.so.1 が見つからなくてコンパイルエラーになるやつの解決法

事象

nix-shell に入った状態で zlib に依存したパッケージを含んだコンパイルをすると libz.so.1: cannot open shared object file: No such file or directory とかでてコンパイルが失敗することがある。本当に結構ある。

ghc 9.4.6, cabal 3.10.1.0 で発生

結論

mkShell の定義に LD_LIBRARY_PATH を指定するようにしてやればいい。参考

{ pkgs ? import <nixpkgs> { } }:
with pkgs;
mkShell rec {
  buildInputs = [
    zlib
    ghc
    cabal-install
  ];
  LD_LIBRARY_PATH = lib.makeLibraryPath buildInputs;
}

で、なんなの

ghc を 9.4, 9.6, 9.7 で試したけど特に解消しなかったし問題の所在が ghc なのか nix なのかはたまたそれ以外の何かなのか判然としなかった。

discourse.nixos.org を見てみると

To summarize the email thread, it sounds like GCC is used directly by cabal configure, and GCC is patched in nixpkgs to looks at environment variables like NIX_LDFLAGS. nix-shell (or is it stdenv.mkDerivation?) automatically populates NIX_LDFLAGS based on buildInputs. This chain of events causes cabal build to work.

とのことらしい。たしかに自分の $NIX_LDFLAGS には zlib のパスが通ってる。ということは nix の問題でないのはそうなんだろう。

で、

The solution you’re proposing is to just modify LD_LIBRARY_PATH so that GHC can find zlib. Given the above info, it makes sense that this would work.

だというわけだ。

dynamic link が必要なパッケージだと zlib に限らず起きる気がするけど今のところ遭遇はしてない。

なんとなく解決法の指針にはなるからよしとする。

Functor 深い

HaskellFunctor について調べてたらこんなのを見つけた。 stackoverflow.com

ここで挙げられているエクササイズを見てみると Functor の深さにめまいがしてちょっとだけウンコを漏らした。

2問目

Functor の composition をデータ型で定義しろという問題で、実装はこうなる。

{-# LANGUAGE TypeOperators #-}

newtype (:.) f g x = Comp {comp :: f (g x)}

instance (Functor f, Functor g) => Functor (f :. g) where
  fmap f (Comp x) = Comp $ fmap (fmap f) x

instance (Applicative f, Applicative g) => Applicative (f :. g) where
  pure = Comp . pure . pure
  Comp f <*> Comp x = Comp $ fmap (<*>) f <*> x

instance (Foldable f, Foldable g) => Foldable (f :. g) where
  foldMap f (Comp x) = foldMap (foldMap f) x

instance (Traversable f, Traversable g) => Traversable (f :. g) where
  traverse f (Comp x) = Comp <$> traverse (traverse f) x

TraversableFoldable な値はこれまでに扱ったことはあるが自前のデータ型に実装するのは初めてだったのでよくわからんという感じになりながらも定義した。

特に f (g x) を掘っていくという感覚になれるのがちょっとてこずったが、一度感覚をつかめばまあこうなるかという感じでうまく言語化するには多分圏論の知識が必要なんだと思う。

それはいいとして、Comp を定義することによって以下のような使い方ができる。

data Triple a = Tr a a a

instance Functor Triple where
  fmap f (Tr a b c) = Tr (f a) (f b) (f c)

instance Applicative Triple where
  pure a = Tr a a a
  Tr f g h <*> Tr a b c = Tr (f a) (g b) (h c)

instance Foldable Triple where
  foldMap f (Tr a b c) = mconcat [f a, f b, f c]

instance Traversable Triple where
  traverse f (Tr a b c) = Tr <$> f a <*> f b <*> f c

type Zone = Triple :. Triple

type Board = Zone :. Zone

Triple は3要素のタプルとみなせるので Zone3x3 の配列で Board 9 x 9 の配列と解釈することができる。これを実現するのが Comp であり、その奥には Functor の存在がある。

ところで、Zonetraverse すると何が起きるのか把握しておきたい。

ghci> z = Comp (Tr (Tr 1 2 3) (Tr 4 5 6) (Tr 7 8 9)) :: Zone Int
ghci> traverse (Just . (+1)) z
Just (Comp {comp = Tr (Tr 2 3 4) (Tr 5 6 7) (Tr 8 9 10)})

なるほど!

というわけで、Functor の composition と Traversable の組み合わせは非常に強力だということが分かった。

5問目

設問としては

(5) Consider the constant functors

であって、実装含めたのがこれ

newtype K a x = K {unK :: a}

instance (Monoid a) => Functor (K a) where
  fmap _ (K a) = K a

instance (Monoid a) => Applicative (K a) where
  pure _ = K mempty
  K a <*> K b = K $ a <> b

crush :: (Traversable f, Monoid b) => (a -> b) -> f a -> b
crush f t = unK $ traverse (K . f) t

newtype K a x は設問にある通り constant な Functor であるがゆえに fmap の実装は K a をそのまま返していて、Applicative の実装をするにあたっては Monoid であるという型制約を考慮した実装をしていて特段難しいことは無い。がゆえに K がどういう意図のデータ型なのかあまりわからなかった。(Monoid のラッパーであるようにしか見えない)

crush における K の使い方を見てみると Functor スゲーとなる。この関数が何をしているかと言えばシグネチャのまんまではあるけど Traversable な値を Monoid に変換してその値を unwrap するということになるので、順次 traverse した結果を取り出すということになるのでこのように使える。

ghci> import Data.Monoid (Sum(Sum))
ghci> crush (Sum . (+10)) [1, 2, 3] :: Sum Int
Sum {getSum = 36}

これの何が利点なのかと言えば、とあるデータ型について閉じた演算が定義されているので値を直接こねくり回すことなくとても簡潔に関数を定義できることにある。(はず。Haskell で実現しているのはそういうもんのはず。)

同エクササイズで allany を定義しろというのだけどもこれも以下の通りデータ型を実装できる。

import Prelude hiding (all, any)

newtype Any = Any {unAny :: Bool}

instance Semigroup Any where
  (Any x) <> (Any y) = Any $ x || y

instance Monoid Any where
  mempty = Any False

newtype All = All {unAll :: Bool}

instance Semigroup All where
  (All x) <> (All y) = All $ x && y

instance Monoid All where
  mempty = All False

any :: (Traversable t) => (a -> Bool) -> t a -> Bool
any p = unAny . crush (Any . p)

all :: (Traversable t) => (a -> Bool) -> t a -> Bool
all p = unAll . crush (All . p)

こう見ると Traversable そのものもものすごく強力なように思える。

The Grug Brained Developer というものについて

marcochiappetta.medium.com を読んでて grug brained developer というものを知って原典の grugbrain.dev を読んでみると僕自身が割と grug brained developer に共感してしまった。

grug brained developer がなんなのかを掻い摘んで要約すると以下のような考えを持ってる種族のこと。

  • 複雑さを忌み嫌う
    • 複雑さが導入されようものならまずは No を突き付ける
    • Yes というのは明確な妥協ができる場合
  • リファクタリングは小さく
  • big brain developer*1を黙らせるにはプロトタイプを作らせておく
  • ユニットテスト/E2Eテスト/統合テストはバランスをみて適宜適用する
  • テストにおけるモックを嫌う
  • アジャイル開発は銀の弾丸ではない
  • 意図のわかるコードであること
  • 安易な破壊的変更をしないこと(Chesterton's Fence)
  • 使えるツールを使い倒す
  • 型理論(システム)のメリットをコンパイルに見出す
  • (有意義な)ログが好き
  • big brain developer の言う "早すぎる最適化" ではなく、計測結果を根拠とした現実世界におけるパフォーマンス上の課題に対して対処すべき
  • Visitor Pattern はカス

big brain developer を対比の概念として生み出して grug brained developer の存在をうまく補完しているように思う。

big brain developer って無駄に複雑なことをして他人が理解できないという表面的な部分で、経験や思慮が浅い人からは有能に見える可能性があるし、対外的なアピールがうまくてかっこいいエンジニア像みたいな感じにみられることもあるんだろうなという感想を持ったし、自分も気を付けなくてはいけないとも思う。

ところで Zen of Python っていいこと言ってんなーと思うけど python3.10 で導入された mtach 文は血迷ってんじゃないのかという憤りを感じている。あんな中途半端な機能誰が喜んでるんだろ。

*1:出典元で言及している big brain developer とは、どうやら現状を無視したアーキテクチャおよび技術選定をする性質を持っている熟練した経験者みたいな意味合い

非NixOS の Linux ディストリで nixpkgs を使ってるけど今のところはこれでいいかとなってる

WSL2 上で Ubuntu を使っていたけど Arch Linux に乗り換えて数か月が経った。WSL2 なので正直システムがどんなけ汚れようが構わないんだけどなんとなくユーザーランドで汚染の範囲をとどめたかったので Ubuntu の時代からかれこれ1年くらい nixpkgs を使い続けている。

nixpkgs を使った環境再現は本当に楽でエントリポイントのシェルスクリプトを1枚用意しておいてそいつを実行すれば特に問題なく勝手に環境が再現できるため今のところストレスが全くなく、nix そのものに対しての印象はいいツールだなという感じ。

ただ、NixOS を素直に使うべきかなと自問するタイミングがたまに訪れる。事実 Arch Linux を使う積極的な理由は特に思い浮かばない。 とはいえ NixOS が nixpkgs としてユーザーリポジトリを切り出している時点で正常な用途ではあるはずだ。なので、個人的にはとりあえずこれでいいやとなっている。

nix のサブレに入り浸って情報収集はできてるので nix が普通に使われてると錯覚しているけど、多分一般的にはまだそんなに認知されてないのはなぜだろうか。

よく言われてそうなのは nix のアーキテクチャや nix expression の理解と定着が単純に学習コストをそれなりに必要とするし(Nix pills だけではどう考えても足りない)、環境の独立性を実現する手段として direnv や Docker でもいいじゃんという考えもある。それに gc しない限り nix-store がバカみたいにストレージを食い続けることになるのでむやみに使用するパッケージを追加するといろいろとオーバーヘッドが増えるみたいなところに原因があるんだと思う。

でもこんなもの一度でも使い慣れてしまえば Docker に比べれば簡潔で簡単とも思えるし、ビルドが必要という場合であれば nix がそこを担保してくれるので DevOps みたいな領域もカバーできるはず。(nix で真面目に構成管理なんてやったことない)

個人的な cabal を使った Haskell 製の自作シングルバイナリアプリでは cabal2nix を使うのが当たりまえになってるし、もはや nix を使わない選択肢がそもそもなくなってるのでかなりバイアスがあるとは思う。でも、個人差で要領の良し悪しはあれどツールの使い方を身に着けるのはそれを使い続けるしかないし、近道なんてないでしょって感じなので日々学習するしかないんじゃねえのと割り切るしかないだろと思ってる。

とりあえず nix を使って開発環境を整える中で感じる一番のメリットは Docker みたいになんかややこしいプロセスがなくてまさに本物の環境で作業できるってところかなとは思う。

まあよく言われてるようにそもそも Docker と nix では役割が違うので比べること自体がナンセンスだと思うので Docker 使えるなら別に Docker でも十分だとは感じるけど。どっちが優れてるとかいう定性的な話は無意味で、個人の経験と用途に依存する話でしかないので不毛だなこれは。

ところで devbox が登場したことで nix 自体が普及するだろうか。多分しない。devbox はすんごい nix のラッパーツールであり nix を理解する必要性がほぼなくなるからだ。そうすると、間接的に nix が人口に膾炙する可能性はあるとしても nix そのものが受け入れられているというより、devbox が受け入れられるみたいなことになる気がする。

現段階で今のところは引き続き nix コミュニティにはひれ伏す気持ちではあるので、僕ができることと言えばどんな形であれ nix が受け入れられて nix コミュニティの寿命が延び続けることを願うばかりだ。

リーダブルなテストコードの書き方って記事についての雑感

テストの書き方を人と話していたとき

logmi.jp

の記事を権威付けのために出された。

読んでみてなんかウッとなったところがないでもないので、今後この手の記事を同じようなシチュエーションで出された時の自分の立場を明確にすべくこの機会に言語化しておく。 これは自分の考えとギャップがあるというだけで出典元が間違ってることを言ってるとか主張したいものではない。

ちなみに本記事でのテストはユニットテストを対象としている。

リーダブルとは

出典元にはリーダブルであることの十分条件として脳内メモリを消費しないことを挙げている。 ただ、脳内メモリを消費する場面って色々パターンがあると思っていて、例えばテスト対象の関数の責務やテストデータの妥当性などが例として挙げられる。 そもそもテストデータが作りづらいものやテストケースが長大になるような原因はそもそもテストコードではなく設計に問題があったりもするのでテストコードをリーダブルにしようってのは限界があると考える。そのため個人的にはリーダブルであることと脳内メモリの消費はあまり関係ない。

あとリーダブルは往々にして主観のぶつけ合いなのでもうちょっと定性的な定義をしておいた方がいいと思う。 自分としては以下の条件を満たすと一貫性のある"リーダブル"なテストコードだと感じる。

  • テストデータは宣言的、説明的な名称でつくる
  • テストダブルを使っていいのは原則として副作用をテストする場合のみ
  • real duplication 以外での再利用は行わない
  • 文脈の集まりがテストスイートである
  • expectation で行う検証は1つの事柄のみ
  • 原則として性質をテストする

ほかにもあるかもしれないけどぱっと思いつく重要なものはこんな感じ。

過度なDRYという表現について

「過度な」ものはすべて肯定できない状態のものだと思うので印象操作の感を受けたのであまり読んでいて気持ちよくなかった。 なんでも適度がいいに決まっているだろう。恣意的な表現は避けてほしかった。

ようわからんなら approve しちゃだめについて

完全に同意見。 テストコードはなめられがちなのであまり真剣にレビューされなかったりするのは本当によくない。 テストデータこそレビューする上で重要なはずなのに。

読みやすいコードのポイントについて

logmi.jp

にあるが、ドキュメントのように読めるというのはある程度考慮したほうがいいとは思うが無理があると思う。 出典元の著者は RSpec に明るいようだからこの意見が出るのは自然だなと思うし、うまく context をきることで自然言語としてテストケースを読むことはできるだろう。

しかしそれはツールに依存するもので例えば QuickCheck のようなフレームワークであれば上から下によむようなものではない。そのためポジショントークに見える。

つぎにスカラ値のハードコードというのがあるが、これは性質のテストが困難になるので同意できない。程度や管理頻度の問題もあるかもしれないが、基本的に適切な名前を付けたテストデータを使って「ある文脈」のテストであることを明示すべきだ。

賢くてロジカルなテストコードより誰でも読める愚直なテストコードをとの標語を掲げているが、複数の計算方法の結果が同じという検証方法もあるわけだし、そもそもテストなんてものはニンゲンのために書くのではなくシステムのために書くのであるのだからロジカルなのは当たり前じゃなかろうかとも思う。

誰でも読めることに振った結果品質の低いゴミを生み出していたら世話がないと思う。テストはそもそもそんなに安易なものじゃない。

実例について

さすがに例はそれなりに粗悪なものを出しているのでいいとして、これの修正案について思うところがある。 一番気になるのはハードコードした場合 age の算出に脳内メモリを使わなければならなくなっている。

自分が書くとしたらこのようにする

let(:birth_date) {fuzzer(:date)}  # ランダムに日付を生み出すファクトリを仮定
let(:user) {create(:user, birth_date: birth_date)}
let(:current_age) {fuzzer(:positive_int)}  # ランダムに正の整数を生み出すファクトリを仮定
def setup_current_date(day) # 指定の日数を足す
  passed_year = current_age.year
  travel_to(birth_date + passed_year + day.day)
end
def expect_age_is(user, delta)
  expect(user.age).to eq(current_age + delta)
end
def expect_age_no_change(user)
  expect_age_is(user, 0)
end
def expect_aged(user)
  expect_age_is(user, 1)
end
context "実行時点が誕生日"
  context "以前の場合" do
    before :example do
       setup_current_date(1)  # 定義域としては ∀x ∈Z( 0 < x < 当該年の日数) 
    end
    it "誕生日を過ぎた年齢を返すこと" do
      expect_aged(user)
    end 
  end
  context "当日の場合" do 
    before :example do
       setup_current_date(0)  # エッジケースなので1件のみ
    end
    it "誕生日を過ぎた年齢を返すこと"  # 仕様わからんけど
      expect_aged(user)
    end
  end
  context "以降の場合" do
    before :example do
       setup_current_date(-1)  # 定義域としては ∀x ∈N( -当該年の日数 < x < 0) 
    end
    it "現在の年齢を返すこと"  # real duplication なので shared_example 使える
      expect_age_no_change(user)
    end
  end
end

これが万人にとって脳内メモリを使わないリーダブルなコードだろうがというつもりは毛頭ない。が、ポリシーはある。 性質をランダムテストしているのでマジックナンバーが出てこない。また、expectation を説明的にしつつ事前条件の表明をすることで、文字列で記述しているテストケースの文脈がテストできていることになる。

この程度でも"過度なDRY"になるんだろうか。気になるところだ。

テストコードは仕様書と思いたい気持ちについて

わかるんだけど、ユニットテストレベルでは無理。E2Eテストでやれ。

E2Eテストの考え方について

これは基本的に統合テストになるため出典元の言うとおりテストの値はベタ書きでいいと思う。 テストの値というよりパターンと再現性のほうがよっぽど大事なのでそちらに腐心したい。

結局のところ

とはいえ、こんなものは人の好き好きだ。書き手にポリシーがあって、意図のわかるコードであればいいと思う。 大事なのはなぜそのテストデータで、なぜそのテストケースなのか(あるいはそうではないのか)が、それこそ誰が見ても理解できることではないだろうか。