関数型プログラミングなんもわからん。を考えようと言うイベントを開きました。
先日Connpassにて、関数型プログラミングなんもわからん。を考えようと言うイベントを開かせていただきました。
関数型プログラミングがわからない! と言う方達の疑問に対して、普段関数型プログラミング言語を使っているわかる人たちが回答をして行くと言うスタイルのイベントでした。関数型プログラミング言語と一口に行っても、Elm, Scala, Haskell, Clojure, Elixir, F#と様々な言語があり(これは今回参加した人たちの使っている言語で、関数型プログラミング言語の一部にしか過ぎません)何が正解かなどはわからない中での意見の集約といった形のため参考程度にご覧ください。結果イベントとしては様々な視点からの意見が聞けて満足という声が多かったです。私自身知らないことがたくさん知れて勉強になり楽しかったです。
イベントの内容は、Figma上で開けるFigJamファイルとして、公開しますので是非ご覧ください。
疑問と回答の内容をカテゴリごとに整理してに大体のまとめをしていこうと思います。
関数型プログラミングって何?何が嬉しいの?
今までの扱っていたパラダイムとの違いのギャップに悩む方、どのような旨味を期待して学び続けるのか、プロダクト利用ではどのようにすべきなのか悩む方の質問です。
関数型言語を書いていて, 楽しい場面はあるでしょうか?
以下、回答
Clojureでは、あるデータ構造を操作する関数がたくさんある。そのような汎用的なデータ構造が多くあるのが楽しい。
「楽」だと感じる場面が楽しい。
コンパイルエラーが起きないけれど、うまく動かないみたいなケースはイヤ。こういうことが起きにくい。
基礎の概念を乗り越えたら、一気に学習コストが下がる感覚がある。
Haskellのパーサコンビネータ(関数の組み合わせで作って行くパーサ)を作ったときに、StateモナドとMaybeモナドを組み合わせるだけでほとんどパーサを表現できてしまう。
関数型らしい書き方ってどんな感じになるでしょうか…?オブジェクト指向っぽい書き方になってしまう…。結局のところ、関数型らしい書き方って何なんでしょうか。forがない?再帰関数を書くこと?map,reduce...??
読みやすい形で書いた動けばいい。(オブジェクト指向ぽくても、手続き型ぽくても良い)実際処理を書いたら、手続き型ぽく書いた方が読みやすくなり、落ち着くことも多い。
コップ本(Scala スケーラブルプログラミング)などは、手続き型の書き方からどのように関数型ぽい書き方にして行くか。なぜそのような書き方が嬉しいのか?ステップバイステップで教えてくれる書籍です。(分厚くて高いですが、興味があればどうぞ)
それでも関数型らしい書き方を目指すには、
- HaskellやElmのように書き方の制約の強い言語を使う
- Coreパッケージ(プリインストールされている、標準ライブラリ)のソースコードを読み、データ構造やメソッドの処理の書き方を見て学んでいく。いかがポイント。
- 変数をImmutableなものとして使う。
- 副作用を分離する
- 書き方は再帰が中心になる。
関数をどうまとめればいいですかね…?オブジェクト指向はオブジェクトの責務でまとめたり…。
関数型言語は「型」に注目して切り出す。Modelを受け取ってModelを返すような関数を作れば、ほとんどメソッドと同じ。オブジェクト指向言語と関数型言語の考え方は似ている部分も多い。
class Hoge(int count, String name) {
private setCount(int count): void
}
module Hoge exposing (Hoge, create)
type alias Hoge = { count: Int, name: String }
create: Int -> String -> Hoge
setCount: Int -> Hoge -> Hoge
...
オブジェクトの責務やドメインモデリングは関数型での考え方とも親和性ある。
- 中心的な役割を果たすオブジェクトやエンティティを型として切り出す
- エンティティ間の関係を関数としてまとめる
概念について考えることはパラダイムに依存しない
[参考資料: Domain Modeling Made Functional(https://www.amazon.com/dp/1680502549)
関数型におけるデザインパターンってありますか…? どうかけばいいかわからない…再帰もパターン?
関数型だと、そもそもデザインパターンが必要ない、というケースもある。
再帰だからといって、必ずしも読みやすいとは限らない。
recursion schemesというパターンがある。再帰している部分と、再帰していない部分に分離する。一番よくあるのが「fold」というパターン(foldlとかfoldrとか)。
参考文献
関数型プログラミング言語と数学の話
関数型プログラミングには数学の勉強が必須なの?
普通のアプリケーション開発者にとっては、圏論のモナドの概念を学ぶ必要は、ない。インターフェースとして、pureとかbindとかが使えるクラスの種類なんだな、ということだけわかれば十分。
ElmやClojureなどは、モナドという言葉は出てこない。
圏論の勉強方法はどうすれば良いの?
プログラマーの立場だと「圏論」という本がおすすめ。
圏論の歩き方なんかも、プログラミングと圏論の関わりについて書かれていたりするので、読んでみると面白いかもしれません
他には、ベーシック圏論など。
Haskell における Functor や Monad の扱いにおいて, 圏論の知識が有用な場面は何かあるでしょうか?
プログラムを書くうえでは、ほとんどないと思う。
ただ、Functorなどは則(Functor則)があって、これは数学的な法則をプログラミングで記述したもの。
このときに数学的な法則がわかっていれば、プログラミングを書く上でも、短くなるように書き換えられるなど、役に立つ場面もある。
関数型の詳しいテクニックの話
部分関数って何?(ElmガイドのMaybe型章より)上記のような数学の基礎をどうやって学べばいい?
たとえば、String→Floatに変換したいけれど、失敗する可能性がある場合。
- 実行時例外を起こす
- MayBeを使う
safeToFloat: String -> Maybe Float
unsafeToFloat: String -> Float
の2つのパターンがある。
部分関数とは、実質的には、MayBe型やEither型を返す関数と思っておけば良い。
- 部分関数とは、関数の返り値が「ない」場合があり得る関数。
- 返り値が「ない」ことをボトムいう記号で表すこともある。
数学的には、領域理論という分野に該当。(しかし、数学的知識は必須ではない)
カリー化は処理の共通化のための手段なのか?
カリー化された関数に対して、部分適用をした関数をあちこちで使い回すという意味であれば、処理の共通化のための手段と言える。基本はカリー化されている方が便利。
module Http
-- API Keyやドメインは、基本的に同じはず
get : String -> String -> String -> Cmd msg
get apiKey domain endpoint = handle apiKey domain "GET" endpoint
-- こんな感じで、methodTypeとendpoint以外の引数を埋めておけば
http: String -> String -> Cmd msg
http methodType endPoint = handle "GET" "xxxx" "hoge.com"
-- 残りは共通化されているhttpを使いまわせば楽。など。
get: String
get endpoint =
http "GET" endpoint
post: String -> String -> Cmd msg
post endpoint body =
httpWithBody "POST" endpoint body
関数型を勉強していくと 代数的データ型(ADT) という概念が出てくるが、ADTと言われるにはどのような条件が満たされればADTと言えるのか?
ADTの定義のようなものはない。タプルのunionのようなもの(かそれを再帰的に使ったもの)であれば代数的データ型といっていい。
掛け算・足し算(代数的な計算)により構成されているから、ADTと呼ばれているのかもしれない。
必ずしもunionがある必要はなく、パターンマッチがあれば(実質的にunionなので)代数的データ型といって良いのではないか。
関数型プログラミングって実用的なの?
関数型のパラダイムを学ぶことで得られる強みは何でしょうか?
チームでプロダクト開発を進める中で、バグを減らしたいという思いから関数型のパラダイムを浸透させたいと思っておりまして、他に何かあればご教示頂きたいです。
質問者様の理解は以下2つ。
- 副作用を可能な限り記述しない(分離する)
- 変更可能な変数を多く扱わない
以下はわかる枠回答。
コンパイラを利用する静的型付け言語(Haskell, Elm)では、式指向のため、分岐が網羅的でないとコンパイルが通らない。これによって、バグが発生しにくい・強みだとしています。例えば以下のようにif-elseあったときに、elseを省略することができないので、他言語でいう、null, undefined, nilなどが発生し得ない。(null安全の言語などでも今は同様の旨味が得られそうですね)
if bool then
v1
else
v2
例外的な扱いが型で扱えるので、失敗パターンの処理を強制力がバグを減らす効果があると思われる。例えば、以下のように文字列からFloatに変換するような関数の場合、すべての文字列がFloatに変換できるわけではないので、Maybe型に変換される方が好ましい。
safeToFloat: String -> Maybe Float
unsafeToFloat: String -> Float
制約が強い言語であればあるほど、書き方が統一されてプロダクト開発では有利に進むことが多い(書き方についてレビューなどで指摘・修正が発生することがない)
テストが後付けで絶対に書ける。リファクタリングでテストが書けない処理に対して、テストを書ける処理にして行くのようなステップが省略できる。
型があることで変更に強くなる。例えば、hogeという関数が必ず成功する処理だと、ドメインエキスパートに言われていたが、急に市場の変化で失敗するケースが出現した。などのような変化に対して型を明記していれば、型の変更があったとしてもコンパイラの指示に従えば良い。(関数型プログラミングというよりは、静的型付のメリット?)
hoge: Int -> String -- -> Int -> Maybe String
useHoge1 x y = hoge (x * y)
useUseHoge1 x = useHoge1 x (x ^ 2)
...
実務でよく使われる関数型言語は何かあるでしょうか?Haskell が実務で使われているイメージがあまり無いので疑問に思いました
日本ですごい多いわけではないが、求人は増え始めている。大々的に求人票に書いていなかったとしても、内部ツールや新規プロダクトなどで使われている例は珍しくはないのではないか?キチンと関数型プログラミング言語を学んで、その良さが活かせるプロダクトの時に自身で良さをアピールして採用するのがいちばんの近道。
関数型言語の求人サイト
https://functional.works-hub.com/?
Haskell
HerpScala 界隈マップ
Clojure 界隈マップ
OCaml
Elm
- ビズリーチ
- Fringe81
Elixir(Erlang)
- ニコニコ動画
- ABEJA
- ミクシィ
- アカツキ
- Discord
マニアックな話
コンパイラ実装のパラダイム(手続き型, 関数型など)ごとの違いは何かあるのでしょうか?
手続き型に比べて、構文がシンプルだと思うので、実装は楽なのではないかと思う。
Lisp系は、Abstract Syntax Treeをそのまま書いている形なので、ここから勉強するのが楽なのではないかと思う。
Haskellだとソースコードレベルの最適化をしてくれる。
たとえばfold/buildパターン(?)のようなものがある場合に、直接結果を生成するソースコードに書き換えてくれる。
React Hook は Algebraic Effect だ、と言われているらしいのだけど Algebraic Effect がよくわからない。
Algebraic Effectsとは何なのか?
副作用をモデリングする一つの方法としてモナドがある。他の方法としてAlgebraic Effectsという概念がある。
関数型界隈では、Algebraic Effectsはどう使われているのか?Algebraic Effectsを第一級オブジェクトとして組み込まれている言語もあるが、そうでないものもある。
組み込まれていない言語では、Extensible Effectsというものを使ったライブラリが作成されているものもある(?)。
まず、プログラムの中で副作用を起こすという宣言をする。それを実行する関数に渡して動作する。
たとえば、複数の副作用が引き起こされるとしてもハンドラーが対応していれば動く。
今までは、副作用がある場合にはモナドを使っていたが、複数の副作用を使う必要がある場合には、Monad Transformerというのを使っていた。
Monad Transformerは、複数のMonadを積み重ねるイメージ。
問題点として、一番下のMonadの副作用が頻繁に起きる場合に、Monadをはがす計算コストが大きくなってしまっていた。
Extensible Effectsベースであれば、この計算コストが小さくて済む。
まとめ
以上、雑ですがカテゴリごとに分けた疑問・回答集でした。このような会をまた開いて欲しいなどの要望があれば是非教えてください!
Discussion