👣

コーディングについて思うこと垂れ流し

13 min read

ミライトデザイン Advent Calendar 2021 の 11 日の記事です

カレンダーの右端は日曜日派の ほげさん ( zenn ) です

Qiita のアドカレの右端は土曜日だったんですね、気を付けましょうね

血圧低い感じで、盛り上がりもオチもなく文字数制限のない Twitter のような駄文を晒します

導入

ある Haskell はいいぞおじさんの誕生のはなし

2014 年くらいだったか、大したきっかけもないけど Haskell を触った[1]

image

根本的な基礎力が足りなかったしダラダラやってたので、ちゃんと H 本の内容を最後の章まで理解できたのは多分 2016 年くらいだった気がする

そのあと Haskell の考え方とコードって超クールだなって思ってこんなのを書いたりした

https://qiita.com/suzuki-hoge/items/82229b903655ca4b5c9b

細かい内容は覚えてないけど、記事の内容はニュアンスとしては

こんな ↓ 実装をやめて

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 はいいぞ」しか言ってなかったので、ずっと言ってたのにひとりも仲間は増えなかった

ところで、つい先日こんな記事が公開されてた

https://blog.j5ik2o.me/entry/2021/12/08/000000

電車で本当に序盤だけ読んで「あ、これ僕ができなかった言語化が全部されてる気がする...すげぇ...」ってなったので、今から残りを読んで、そして思ったことを垂れ流そうかと思う

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 だとやっぱり落ちる

当時の僕は「コンパイルしたあとに実行時エラーが出るような実装は万死に値する」と思っていたので、仕様を調べるようになった

いろいろ調べていくと
normalpremium で根本的に 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」とかって、エラーの毛色が違うと思う

現時点では僕は「システムのエラーとビジネスのエラーがあるぞ」くらいしか分類できてないけど、近いうちにこの記事を熟読したい

https://zenn.dev/j5ik2o/articles/6c4dbab802c9701fd878

表明を受け入れるきっかけになったのは

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)
}

usernullitemnull は明らかに意味が違う

user は主キー検索の結果なので null であってはいけない
item は任意購入らしい、なら null でもおかしくない

これを知識と記憶だけで全てにおいて全員で正しく認識するのは、不可能だ

usernull は許さず即時死ぬこととし、許されたらもうおかしいという可能性は忘れたい
itemnull はそういう結果だとして、ただ値として扱いたい

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 かどうかも認識しないのだ

これはただのそういう型なのだ、状態や値に興味はないなのだ

という感覚

image

既存の型を変更せず、新しい型もつくらず、拡張したい

本当に個人的な好みと宗教だけで言えば、これができない言語では個人開発か保守しないものの実装までしかしたくない
( 向き不向きのはなしで、好き嫌いではない )

ジェネリクス

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 はほぼ全く書いたことはなくて、ただ知識として知っているだけ

英語の本しか参考情報ないっぽいし、頑張って読むか否か...
なんてペンディングしてたら、最近言い訳を失った

https://zenn.dev/blackenedgold/books/introduction-to-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] を書いてたら理解できた

出会えて運が良かったと思う

image

var / val は変数について

変数は値の場所を指している矢印で、その矢印を変えられるかどうかというはなし

image

なので nsimmutable を指していても一生 [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 は値について

値自体を変えられるかというはなし

image

なので nsval でも一生 [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 強制な?」みたいな空気を出してたこともあるけど、そういえば先日こんな記事を読んだ

https://irof.hateblo.jp/entry/2019/09/27/233547

反省しました
コード品質を守ろうとするあまり、コードを書いてる人に意識が向いてなかった
口答えをする余地がない

これからは改めたい

おしまい

本当にオチはない

いつか「なんか LT でも」と思ったときに、この辺から持ってきて使えたら面白いかもな

本当にそれだけの連投 Tweet でした

予定間違えて計画狂ったので超眠い...


明日は... また僕の次 bucchi

Playwright の記事らしいです
初耳です、Selenium のようなものですか?

期待が高まる

脚注
  1. 実務じゃあないけど ↩︎

  2. このころ、書くのがめんどくさい割にコンパイルの恩恵がわからなくて わからん ゎからん = new わからん() みたいなジョークが生まれたりした ↩︎

  3. List<Item> より ItemList の方が処理を限定できるし items.map(.name) より items.getNames() みたいなのが生やせて良いよね、みたいなのはまた別のはなし、これは良いものだと思っている ↩︎

  4. 当然実務経験なし、というか実務存在するのか? ↩︎

  5. Applicative Functor でいいじゃんというはなしはぽいっ ↩︎

  6. これも実務経験なし ↩︎

  7. だれもここまで読んでないだろうけど ↩︎

  8. これも実務経験はない、というか実務は PHP Java だけのにわかじゃ ↩︎

  9. 引数に応じてのみ結果が決まり、何かの破壊を伴わない、という意味 ↩︎

  10. 二分探索 ↩︎

Discussion

ログインするとコメントできます