Open8

OOP vs FP

sankakusankaku

https://nowokay.hatenablog.com/entry/2023/02/24/213551
とその反論
https://twitter.com/sugimoto_kei/status/1630021926814117888
を読んで思ったこと。

  • これを読む前から「フレームワークとは思想を実装で固めたもの」と考えていた。
    • ポエミーな言い方だが、こうすればいい感じのプログラムができんじゃない?という考え(思想)と、それを比較的容易に実践するためのコード(実装)の2つがフレームワークであるという考え。
    • React なら。宣言的UI良い感じという思想と仮想DOMという実装。
    • lodash.js 的な、便利な奴色々入れておきましたみたいな奴は別(あれはフレームワークじゃないだろうけど)。
    • その考えに乗っ取れば、良いフレームワークは良い思想を持っていて、それをプログラマーに伝える(誘導/強制する)ことになる。
  • 今回これを読んで、それはフレームワークに限らず言語でも同じなんじゃないかと思った。
    • 以前は、言語はフレームワークよりも lodash.js 的なものに近いと感じていた。
    • ダイクストラさんが偉大なのは、おそらく「goto を殺したから」ではなく「goto は使わないほうがいい感じに書ける」という思想を伝えたから。もし 「goto を殺したけど世のプログラマ達は goto を求め、無理やり goto を使ってるっぽい感じに書いた」という風になっていたら、そこまで偉大ではなかったかもしれない。(そういう世界だった場合、偉大でない以前に恨まれているだろうけど。)
  • (昔は知らないが)OOP と FP には、できること・やることにそこまで差異はないのでは?
    • 最近はOOPでもオブジェクトが状態を持たない感じになっている気がするのだけど、もし全てのオブジェクトが状態を持たないなら、FP(非純粋)はモジュールをオブジェクトとすればOOPとほぼ同じだろう。
  • 言語のできること・やることに差異が無くても、思想が違えばプログラマがどういう風にコードを書くかが変わる。
    • OOP向けの言語においては、「プログラムはいい感じに分けるのが大事」という思想がかなり強いように思える(カプセル化とか)。ただ、「こういう風にプログラムを分割するといい感じ」という思想はあまりないように感じる。
    • 対してFP向けの言語、こっちは「プログラムはいい感じに分けるのが大事」という思想は全然見えないのだが、「こういう風にプログラムを分割するといい感じ」というのは結構強いんじゃないか?

FP 向け言語の「こういう風にプログラムを分割するといい感じ」: ネストの回避

  • ネストが深いと辛いのだけど(それは if 文でもメソッドでも純粋関数でも)、FPだとネストを浅くするように誘導していると感じる。これは以下の要素から誘導される。
    • パイプ演算子
    • 純粋関数と副作用の分離
    • 多彩な「よく知られた複合型」(この概念の適切な名前を知らないので、ここで名前付けと定義を行う)
  • パイプ演算子は言うまでもないだろう。フラットに書きたくなる。Python にもほしい。
  • 純粋関数と副作用の分離は本当に誘導しているか怪しい。FP ニュービーだからそう感じるだけかも。
    • 例えばある関数が「引数を変換してDBへ書き込み、引数と書き込み結果から出力を決めてそれを返す」という設計をしていたとき、FPだと私はこんな感じに書く。
    # Elixir
    def func_has_subeffect(input) do
      processed_input = process_input(input)
      write_result = processed_input |> convert_input_to_record() |> write_to_db()
      output = calc_output(processed_input, write_result)
      output
    end
    
    • 副作用を持つ関数 write_to_db を値を変換するだけの関数と混同したくないので、func_has_subeffect の直下か、そこから浅いネストから呼ぶことになる。
    • OOP だとこういう誘導はなくて、例えばfunc_has_subeffect() はオブジェクトAを作ってそれのメソッドを呼ぶだけなんだけど、実際は A が B を持っていて、 B が C を持っていて、C が write_to_db() を呼ぶみたいなのもできる。もちろんそれを回避できるけど、回避するような誘導はない。
  • 「よく知られた複合型」は多分既存の概念なんだけど、適切な名前を知らないのでここで名前付けと定義をする。
    • 「よく知られた複合型」は、プログラマがその存在をよく知っていて、ユーザ定義型(struct 的なやつ)を含むことができる複合型。Either, Tuple, Maybe などがこれに含まれるし、List, Map などのコレクションも含まれるし、{:ok, some_strcut} みたいな明示的な名前のついていない構造も含む。
    • これが豊富にあると、 UserDefStruct -> List<AnotherUserDefStruct> -> List<{:ok / :error, int}> (記法は適当) みたいな風にフラットに値を変換していきやすい感じがする。
    • これが無くても同じようなことはできるけど、そういうことをやるようには誘導されない。
      • 例えば Python で enumerate(iterable) 関数があって、これは Iterable[str] -> Iterable[Tuple[int, str]] みたいな変換をする。for文の時に便利。今日にこれが使えないとなったとき、プログラマは同じものを自作するか、zip(range(len(list)), list) みたいにして Iterable[Tuple[int, str]] を用意すると思う。なぜ用意するかというと、Iterable[Tuple[int, str]]という「よく知られた複合型」が使いやすいとわかっているから。もし enumerate() が最初からなかったら、また別のやり方でやるかもしれない。for i in range(len(list)):;v = list[i] みたいな風に。
        • いやこれは例が良くない。
    • 書いてて思ったが、メソッドだと「よく知られた複合型」は扱いにくいかもしれない。SomeStruct クラスが [{:ok, some_struct}] -> {:ok, other_struct} 的なメソッドを持つのは難しい。
    • これ「複合型」じゃなくて「複合データ型」が正しい呼び方か?

FP 向け言語の「こういう風にプログラムを分割するといい感じ」: 状態の管理

  • FP だと状態に制限がつく(DBの中だけに置くとか)ので、状態の管理に気をつかう。OOP にはそういう誘導はない。
    • いい感じの書き方が思いつかない。書くのめんどくさくなった。
sankakusankaku
  • これを書くときは状態管理の方が重要だと思っていたのだけど、フラットな構造の方が重要に思えてきた。
  • 多分フラットにするべきはマインドモデルで、実際の(関数呼び出しなどの)ネストの深さそれ自体ではない。
    • 状態を持つと過去の動作が現在の処理に関係してくるので、マインドモデルのネストの深さ = 過去の処理の深さ + 現在の処理の深さ になりそう。
    • ただし、「状態とか引数みたいなもんでしょ」ぐらいの気軽な扱いなら、マインドモデルのネストの深さ = 現在の処理の深さ になりそう。
      • GenServer はものによってはこれになる?
      • React コンポネントにとっての Redux 状態はこれっぽい気がする。
  • Flat is better than nested.
  • よく考えたら a |> f() |> g()g(f(a)) なので関数呼び出し上ではネストがあるけれど、マインドモデル上はフラット。
    • いや、 g(f(a)) は関数呼び出しがネストされているわけではない。ネストはしているけど。
    • g = fn a -> 1 + f(a) + 2 end みたいな形で関数呼び出しがネストされているとき、 マインドモデルのネストが(g 前半 -> f -> g 後半)みたいになってなりする?
      • そうすると、すべての関数がそんな感じだった時、マインドモデルのネスト = 2 * 関数呼び出しのネスト かな?一気に深くなる。
sankakusankaku

jsの obj?.f() の記法はメソッドの this に maybe 型を使えないから存在することになった?

sankakusankaku

  • FP向け言語の思想に則って設計を良くしようとする場合、前もってOOP向け言語の思想に触れておいたほうがいいのでは?
  • FP向け言語の思想に「フラットに組むといい感じ」という思想があっても、その前にプログラマが「プログラムはいい感じに分けるのが大事」という思想を持っていなければ意味がない。
    • 分割されていないプログラムをフラットに組むのは無理があるから。
  • ここでOOPに「フラットに組むといい感じ」という思想がないので、分割した後にネストを深くする方向に行くのを止められない。
    • ネストを深くするのを止めるためには、別のところから思想を持ってくる必要がある(別にFPでなくてもいい)。
  • 図に入れ忘れたけどフラットにしすぎてもつらい。FP向け言語はそれを止められない。
    • OOP向け言語の思想が「プログラムはいい感じに分けるのが大事」でなく、「抽象化して分けるのが大事」であったなら、OOP向け言語 + FP向け言語 の思想で適度にフラットに持っていける?
  • 「過剰なOOP」は違うな。「過剰な親子関係を持つOOP」?
sankakusankaku
  • メソッドは「良く知られた複合型」を this に入れられないっていうのは結構重要な気がしてきた。
  • これは「良く知られた複合型」を使うのを苦痛にし、避けさせるのでは?
    • 記述するのが苦痛なので、プログラマがどんな思想を持っていても関係ない。
sankakusankaku

状態を持つオブジェクトが散逸し、それらが相互に影響を与える場合、処理のマインドモデルはツリーじゃなくてネットワークだったりする?

sankakusankaku

上層の知識を得るために下層の知識が必要、というのがマインドモデル上のネスト?
フラットなら知識を得るための深掘りを中断しやすい?

sankakusankaku

OOPはプログラムの構造を動的に決めるスタイルという気がしてきた。DIはプログラム実行時の各interfaceの繋がりを決め、Abstract Factory はコード実行時にinterfaceの繋がりを決める。