コーディングについて思うこと垂れ流し
ミライトデザイン Advent Calendar 2021 の 11 日の記事です
カレンダーの右端は日曜日派の ほげさん ( zenn ) です
Qiita のアドカレの右端は土曜日だったんですね、気を付けましょうね
血圧低い感じで、盛り上がりもオチもなく文字数制限のない Twitter のような駄文を晒します
導入
ある Haskell はいいぞおじさんの誕生のはなし
2014 年くらいだったか、大したきっかけもないけど Haskell を触った[1]
根本的な基礎力が足りなかったしダラダラやってたので、ちゃんと H 本の内容を最後の章まで理解できたのは多分 2016 年くらいだった気がする
そのあと Haskell の考え方とコードって超クールだなって思ってこんなのを書いたりした
細かい内容は覚えてないけど、記事の内容はニュアンスとしては
こんな ↓ 実装をやめて
User findUserById(String id) { ... }
Item findItemByUserId(String id) { ... }
void pay(int rank, int price) { ... }
main {
User user = findUserById(id)
if user == null {
throw ...
}
Item item = findItemByUserId(id)
pay(user.rank, item == null ? 0 : item.price)
}
こんな ↓ 感じにしようぜ
User findUserById(String id) { null なら例外 }
Option<Item> findItemByUserId(String id) { ... }
Payment createPayment(User user, Option<Item> item) { 値段算出 }
pay(Payment payment) { ... }
main {
User user = findUserById(id)
Option<Item> item = findItemByUserId(id)
Payment payment = createPayment(user, item)
pay(payment)
って内容だった気がする
多分言いたかったことは、今思えば
-
null
だけじゃ許せる無い
と許せない無い
を識別できないので辛いぞ - 許せない
無い
は即中断として、起きないものとしていちいち意識したくないぞ - 許せる
無い
は型で表現したいぞ -
void
の処理にどかどか引数をバラ渡しするのをやめて、可能な限り関数にしようぜ - そうすると副作用が局所化されるのでテストとか保守が楽だぞ
- 副作用から関数に剥がしていく過程で、手続きに埋もれているドメインロジックが見つかるぞ
みたいなところだったんじゃあないかと思う
元記事が長すぎて読む気起きないんで、そんな感じだったことにする
この考えを当時は全く端的に言語化できなかったので、コードを書いた感覚だけで「Haskell はいいぞ」って周りに言うようになった
言語化って大事だ
「Haskell はいいぞ」しか言ってなかったので、ずっと言ってたのにひとりも仲間は増えなかった
ところで、つい先日こんな記事が公開されてた
電車で本当に序盤だけ読んで「あ、これ僕ができなかった言語化が全部されてる気がする...すげぇ...」ってなったので、今から残りを読んで、そして思ったことを垂れ流そうかと思う
6-7 年溜まった何かを雑多に吐き出しておく企画
表明について思うこと
コンパイルしてんのに実行時エラーが出るのが納得行かなかった
新人で PHP をやったあと移動して Java のコードをやってて、コンパイルができるのに呼ぶと落ちる処理というのが多々あった
状態不整合だの、必須パラメータがないだの、使い方が違うだのと
Contract contract = new Contract(id, plan, List.empty)
Payment payment = contract.getPayment()
// Error: plan が premium なのに subOptions がありません
SubOptionCount subOptionCount = new SubOptionCount(6)
Price price = calcPrice(user, subOptionCount)
// Error: subOptionCount が 5 を超えています
なんでじゃ? なんでこんなにエラーが出るんじゃ? コンパイルしとんのやぞ?
Contract contract = new Contract()
って 3 回も書かされてんのに!
何も安全じゃあないではないか!
コンパイルってのをしても実行エラーは普通に出るもので、ただ書く文字が増えるだけなのか?[2]
ってずっと思ってた
これで実行時例外が大嫌いになり、できないことをコンパイルできるコードが大嫌いになった
個人的にこういうコードを嘘コンパイルって言ってた
コンパイルの恩恵を大きくしたいと思ったら業務を整理するきっかけになった
当時、業務に興味はなく、DDD とかにも興味はなく、ただただコーディングがしたいだけだった
そのコードが嘘にまみれていたので、信じられるものがなかった
なかったので、自分で書いたコンパイルする部分だけを信じるようになった
Contract contract = new ContractBuilder(id)
.premium(subOptions) // premium で生成するなら必須
.build()
でもこれでも subOptions
が Empty List だとやっぱり落ちる
当時の僕は「コンパイルしたあとに実行時エラーが出るような実装は万死に値する」と思っていたので、仕様を調べるようになった
いろいろ調べていくと
「normal
と premium
で根本的に SubOptions
って別のサービスなのでは?」とか
「new SubOptionCount(int)
だけど、これ本質的には 1 or 2 ~ 5
というステータスでは?」
ということが見えてきた
この頃から「Value Object を作りましょう、数は int
ではなく XxxCount
とするのです」という脳死なやらされ Value Objector から一歩進んだな俺、って感覚になった
けど実行時エラーは許せないままだった
このころ「ありえない値が来たら Value Object のコンストラクタで落とそう」という意見を見た
ただ、実行時エラー出したら切腹しろマンだった僕は感覚的に受け入れられなかった
その辺の感覚が Haskell で Either
を見たときに昇華されて「あぁ、エラーを値にするという発想はなかった...」という考えにつながっていった気がする
当時は Value Object のコンストラクタの前で必ず検査して Either<L, ValueOjbect>
みたいな形として生成するみたいな形を模索したりしたけど、問題が 2 つあってうまくいかなかった
- 全員がコンストラクタを使う前に検査してくれるとは限らない ( コンパイルしか信じてない )
- 全部の Value Object に
Either
が付いてるの、マジ邪魔
結局、表明は受け入れた
当時は Either
に感動してたのでどうしても業務で使ってみたかったけど、冷静になったら「ありえない引数で生成できなかったのに処理を継続しても意味なくね?」って思うようになった
それで「実装者が誤った使い方のままコンパイルして実行時エラーにつながるのは認めない」が「ありえない状況だと気付いたら実行時エラーにするのは妥当」という感覚に落ち着いて、表明は受け入れた
ただ、脳死のコンストラクタ引数チェックはやらないよりは良いと思うけど、「実は根本的に使い方が業務に即していないのでは」みたいなことを考えるタイミングは逸しないようにしたい
この辺の言語化は、今後の課題かもしれない
エラーについて思うこと
システムのエラーとビジネスのエラーって違うよね
表明にも近いけど「データ不整合」とか、「システムが一部落ちてる」とか「仕様に照らして判定したら NG」とかって、エラーの毛色が違うと思う
現時点では僕は「システムのエラーとビジネスのエラーがあるぞ」くらいしか分類できてないけど、近いうちにこの記事を熟読したい
表明を受け入れるきっかけになったのは
「NullPointerException
が出た」
「ので超コードを辿ったら DB から 1 行も取れてなかった」
「なんでかっていうと yaml に書いた xxx-path が実在しない場所を指してた」
「ので前段のバッチで csv を 1 行も読んでなかった」
「から 0 records だった」
みたいなことが多すぎたから
こういうのは、辛い ( 辛い ( 辛い ) ) )
おかしいまま動き続けないでほしい
こういうことは例外で中断していい
おかしいことが起こったら、即時死んでほしい
おかしくなったところとエラーだと騒ぐ場所を可能な限り 1 行でも近づけたい
けどビジネスのエラーは値であるべきだと思う
だって仕様上起きるエラーだから型でわかるようになっていてほしいし、値として普通に次の処理で使いたい
そうすると Option<T>
だの Either<L, R>
だのが使いたくなる
Option は null をリプレースするものか
「null
チェック全箇所でやるのはしんどいので Option
にしよう」みたいなことを言うと「null
チェックが Option
チェックになるだけで何も変わらない」と言う話になることがある
本当にそうか
main {
User user = findUserById(id)
if user == null {
throw ...
}
Item item = findItemByUserId(id)
pay(user.rank, item == null ? 0 : item.price)
}
user
の null
と item
の null
は明らかに意味が違う
user
は主キー検索の結果なので null
であってはいけない
item
は任意購入らしい、なら null
でもおかしくない
これを知識と記憶だけで全てにおいて全員で正しく認識するのは、不可能だ
user
の null
は許さず即時死ぬこととし、許されたらもうおかしいという可能性は忘れたい
item
の null
はそういう結果だとして、ただ値として扱いたい
main {
User user = findUserById(id)
Option<Item> item = findItemByUserId(id)
Payment payment = createPayment(user, item)
pay(payment)
この時点でもう Option<Item>
はチェックする対象ではなくなっている
Option.empty
は正しいのだ
チェックなどしないのだ
料金も if
ではなく item.map(.price).else(0)
みたいに作ればいいのだ
だから empty
かどうかも認識しないのだ
これはただのそういう型なのだ、状態や値に興味はないなのだ
という感覚
既存の型を変更せず、新しい型もつくらず、拡張したい
本当に個人的な好みと宗教だけで言えば、これができない言語では個人開発か保守しないものの実装までしかしたくない
( 向き不向きのはなしで、好き嫌いではない )
ジェネリクス
Option<T>
みたいに <T>
できるやつ
僕は、これだけは本当に欲しい
できないと Option<Item>
が ?Item
みたいな Nullable になるんだけど、「null
っていうかエラーメッセージが欲しいですよね」みたいになると例外を使うか ↓ みたいなのを作るかしかなくなる
class ItemOrMessage
?Item item // nullable
?String message // nullable
こんなのを無数にある Value Object に対して XxxOrMessage
なんて作ってられない
バカみたいな工数すぎる
Either<String, Item>
ってしたい
ジェネリクスが使えると Item
を変更せず、ItemOrNothing
みたいなクラスも作らず、いろんなその時々の情報を型でアピールできる[3]
-
Option<Item>
: ないかもしれないぞ ( おかしいことではないぞ ) -
List<Item>
: いっぱいあるぞ -
NonEmptyList<Item>
: 絶対 1 つはあるぞ -
Either<String, Item>
: ないかもしれないぞ ( おかしくはないし理由もわかるぞ ) -
Lazy<Item>
: 必要になったら取得するぞ -
Future<Item>
: 非同期だぞ -
Stream<Item>
: ちょっとずつメモリに乗せるぞ
集団開発でこれほど楽で嬉しいことはないと思う
依存型
Haskell を知った後聞いた Idris[4] とかだと、もっとすごい
ただ Idris はほぼ全く書いたことはなくて、ただ知識として知っているだけ
英語の本しか参考情報ないっぽいし、頑張って読むか否か...
なんてペンディングしてたら、最近言い訳を失った
いつかやるんだ...
イメージ的には List<User, 3>
みたいに T
のところに 3
とか書けて 長さが 3 の User リスト
ってレベルでコンパイルできる
payments: List<Payment, 1..>
みたいに 必ず 1 以上の支払い情報
みたいなのが簡単に表現できるし、絶対に payments.get(0)
が失敗しない
...はず
これでドメイン層を実装したら楽しいだろうなぁと思うんだけど、ハードルが高くて手が出ない...
モナド
ってなんなんですかね、僕もよくわからないです
「コンパイル通ったらきーもちー!たーのしー!」くらいのスタンスでやればいいと思う
たとえば Haskell で
3 つの Maybe
( = Option
) を投げつけると、全部中身がある ( = Just
である ) 場合に、中身を全部足して Maybe
に包んで返すコード[5]
( ひとつでも Empty
だと最終結果も Empty
になる )
addAll :: Maybe Int -> Maybe Int -> Maybe Int -> Maybe Int
addAll mx my mz = do
x <- mx
y <- my
z <- mz
return (x + y + z)
useCase1_pattern1 :: Maybe Int
useCase1_pattern1 = do
let mx = Just 3
let my = Just 4
let mz = Just 5
addAll mx my mz
useCase1_pattern2 :: Maybe Int
useCase1_pattern2 = do
let mx = Just 3
let my = Nothing
let mz = Just 5
addAll mx my mz
main = do
print useCase1_pattern1 -- Just 12
print useCase1_pattern2 -- Nothing
addAll
の引数は全部 Maybe Int
なのに、まるで Int
のように足し算ができる
なのに if Just
だの getInt
だの return Nothing
だのをしていない
僕が 足し算したい
とだけ伝えると、ほかのことは Maybe
が全部やってくれる、こいつはすげぇ
これくらいの理解で良いのではないかな思う
驚くべきは Maybe
の部分を抽象化して List
にも Either
にも対応できること
addAll :: (Monad m) => m Int -> m Int -> m Int -> m Int
addAll mx my mz = do
x <- mx
y <- my
z <- mz
return (x + y + z)
useCase1 :: Maybe Int
useCase1 = do
let mx = Just 3
let my = Just 4
let mz = Just 5
addAll mx my mz
useCase2 :: [Int]
useCase2 = do
let mx = [1, 2]
let my = [0, 3]
let mz = [5, 7]
addAll mx my mz
useCase3 :: Either String Int
useCase3 = do
let mx = Right 42
let my = Left "error-1"
let mz = Left "error-2"
addAll mx my mz
main = do
print useCase1 -- Just 12
print useCase2 -- [6, 8, 9, 11, 7, 9, 10, 12]
print useCase3 -- Left "error-1"
Maybe
を渡せば 全部 Just なら云々
を
List
を渡せば n 重ループのように云々
を
Either
を渡せば 最初のエラーを伝播する
を
全部言語がやってくれる
僕は 1 回だけ足し算を実装すればいい
「データ構造のルール」と「僕がやりたい処理 ( 足し算 )」を分離することができている
しかも既存の型を変更せず、新しい型も作ってない
すげぇ
Id
という 素の状態と同じ
という何も特殊ルールがない構造もある
上手に Id
を使うと処理を 1 つだけ実装して、その同じ処理を
「登録時は Id User
で呼び、特殊ルールなくただ 1 つあるものとして捌く」
「バッチでは List User
で呼び、大量に裁く」
「解約時は Maybe User
で呼び、いれば捌く」
という再利用が簡単にできる
すげぇ
構造のルールと計算ロジックを分離するために
データ構造が interface
みたいになっていて ( e.g. List
)
具体的な型を T
でぶち込める ( e.g. <Int>
)
ような感じのストラテジパタン
みたいな理解でいいんじゃあないかな、というスタンスでいる
コンパイルきーもちー!たーのしー!
Rust の mut
ジェネリクスやモナドとは違うけど、Rust[6] の mut
も既存の型の扱い方を明記したり制限したりするものだ
けどもう文書が長すぎるしそもそも参照した記事が Rust なので僕の素人意見は割愛[7]
今は競技プログラミングの過去問トレーニングで Rust を使っているだけで、mut
はなんとか抑えたけどデータ構造とかメソッドとかをほとんど作らないのでライフタイムがよくわかってない、という状態
近いうちに Rust パワーも上げたいなと思う...
可変だとか不変だとか
Rust と言えばって感じで、最後に
何が不変なのか
Java 始めた頃よくわからんまま final
付けてたけど .add()
とかできるので、「バグか?」って思ってた
final List<Integer> ns = new ArrayList<>();
ns.add(1);
ns.add(2);
System.out.println(ns); // [1, 2]
ちなみに具体型によってはコンパイルはできても呼ぶと怒られたりするので辛い
final List<Integer> ns = Arrays.asList(1, 2, 3);
ns.add(4);
System.out.println(ns); // UnsupportedOperationException
ここら辺のもやもやは Scala[8] を書いてたら理解できた
出会えて運が良かったと思う
var / val は変数について
変数は値の場所を指している矢印で、その矢印を変えられるかどうかというはなし
なので ns
が immutable
を指していても一生 [1, 2, 3]
だという保証はない
var ns = scala.collection.immutable.Seq(1, 2, 3)
ns = scala.collection.immutable.Seq(7, 8, 9)
println(ns) // [7, 8, 9]
だから val
で再代入を禁止するんだなって思ったら腹落ちした
mutable / immutable は値について
値自体を変えられるかというはなし
なので ns
が val
でも一生 [1, 2, 3]
だという保証はない
val ns = scala.collection.mutable.ListBuffer(1, 2, 3)
ns.append(4)
println(ns) // [1, 2, 3, 4]
だから immutable
で破壊を禁止するんだなって思ったら腹落ちした
値が不変 / 振る舞いが不変
不変って聞くと 2018 年 ( だったかなぁ? ) の ScalaMatsuri で聞いた話を思い出す
処理の振る舞いが関数的[9] なら、全ての部分で無理にval
/ immutable
縛りをするよりも、性能や読みやすさを考えてごく狭い範囲で var
などを使うのは有効だ、という話だったはず
def upperBound(xs: Array[Long], x: Long): Int = {
var lSide: Int = -1
var rSide: Int = xs.length
while (rSide - lSide > 1) {
val mid = lSide + (rSide - lSide) / 2
if (xs(mid) > x) rSide = mid
else lSide = mid
}
rSide
}
コードは覚えてないけど、たとえばこんな場合とかかな?[10]
このロジックだと普通に探すより超速く探せるので、実装都合で var
を使っている
ただ、「良い var
」と「悪い var
」をチーム開発で上手に使い分けるのは、かなりの練度が必要
なんて思って「final
強制な?」みたいな空気を出してたこともあるけど、そういえば先日こんな記事を読んだ
反省しました
コード品質を守ろうとするあまり、コードを書いてる人に意識が向いてなかった
口答えをする余地がない
これからは改めたい
おしまい
本当にオチはない
いつか「なんか LT でも」と思ったときに、この辺から持ってきて使えたら面白いかもな
本当にそれだけの連投 Tweet でした
予定間違えて計画狂ったので超眠い...
明日は... また僕の次 bucchi か
Playwright の記事らしいです
初耳です、Selenium のようなものですか?
期待が高まる
-
実務じゃあないけど ↩︎
-
このころ、書くのがめんどくさい割にコンパイルの恩恵がわからなくて
わからん ゎからん = new わからん()
みたいなジョークが生まれたりした ↩︎ -
List<Item>
よりItemList
の方が処理を限定できるしitems.map(.name)
よりitems.getNames()
みたいなのが生やせて良いよね、みたいなのはまた別のはなし、これは良いものだと思っている ↩︎ -
当然実務経験なし、というか実務存在するのか? ↩︎
-
Applicative Functor でいいじゃんというはなしはぽいっ ↩︎
-
これも実務経験なし ↩︎
-
だれもここまで読んでないだろうけど ↩︎
-
これも実務経験はない、というか実務は PHP Java だけのにわかじゃ ↩︎
-
引数に応じてのみ結果が決まり、何かの破壊を伴わない、という意味 ↩︎
-
二分探索 ↩︎
Discussion