Slide 1

Slide 1 text

関数プログラミングの考え方 2023/07/19 ひむら ともひこ

Slide 2

Slide 2 text

注意事項 本内容には個人の見解が含まれます。独自の見解や説明の都合上一般的 な説明とは多少異なる定義や説明を行うことがあります。ウェブサイト や書籍を参考にする場合はその場の定義を確認するようにしてくださ い。また、同じ用語でも認識の違いによってSNSで炎上する事例が近年よ くみられます。内容を鵜呑みしないようにしたり、SNSでの発言には十分 に注意してください。

Slide 3

Slide 3 text

自己紹介 ひむら ともひこ 所属 コミュニケーションのためのSaaSを提供する会社 ● ソフトウェアエンジニア ● メタソフトウェアエンジニア

Slide 4

Slide 4 text

今日のあらすじ なぜ関数プログラミングの考え方を学ぶべきか 関数プログラミングとはなにか まとめ

Slide 5

Slide 5 text

なぜ関数プログラミングの考え方を 学ぶべきか

Slide 6

Slide 6 text

開発・設計に使える道具を増やせる

Slide 7

Slide 7 text

なぜ 開発・設計に使える道具を増やせるのか

Slide 8

Slide 8 text

オブジェクト指向と関数プログラミングは 共存できる 向き不向きに合わせて使い分けできる

Slide 9

Slide 9 text

なぜ関数プログラミングを使うべきか

Slide 10

Slide 10 text

No content

Slide 11

Slide 11 text

プログラムの動作を予測しやすい

Slide 12

Slide 12 text

なぜ関数プログラミングの考え方を学ぶべきか オブジェクト指向を習得済みのプログラマが関数プログラミングの考え 方を学ぶべき理由は、関数プログラミングはコードが簡潔で読みやす く、動作を予測しやすいプログラムを書くことができます。また、オブ ジェクト指向と共存できるため使い分けをすることができます。例え ば、動作が複雑な部分だけ関数プログラミングすることもできます。つ まり、開発・設計で使える道具を増やすことができます。

Slide 13

Slide 13 text

関数プログラミングとは何か

Slide 14

Slide 14 text

今日のあらすじ なぜ関数プログラミングの考え方を学ぶべきか 開発・設計の幅が広がる 動作を予測しやすい コードが簡潔 関数プログラミングとはなにか まとめ

Slide 15

Slide 15 text

プログラミングパラダイム 宣言型 プログラミング 命令型 プログラミング 関数 プログラミング 論理 プログラミング 手続き型 プログラミング オブジェクト指向 プログラミング 状態を保持できる (ステートフル) 状態が持てない (ステートレス) 関数を組み合わせる オブジェクトが連携する ※宣言型に状態を追加したものは厳密には命令型とは言い切れませんが便宜的に命令型としています

Slide 16

Slide 16 text

状態を持てないプログラミング

Slide 17

Slide 17 text

状態が持てない(ステートレス)とは ● 変数に束縛はできるが再代入することができない ○ ※過去の束縛を隠して再束縛できる言語はあります ● メンバ変数、インスタンス変数は持てるが変更できない ○ 変更する代わりに新しいオブジェクトを作る必要がある ○ ⇒ Immutable ● ⇒ 変数を共有しても並行並列に動作する処理に影響を与えない

Slide 18

Slide 18 text

ステートフルな関数プログラミングもある

Slide 19

Slide 19 text

関数プログラミング プログラミングパラダイム 宣言型 プログラミング 命令型 プログラミング 純粋関数 プログラミング ステートフルな関数 プログラミング オブジェクト指向 プログラミング 状態を保持できる (ステートフル) 状態が持てない (ステートレス) 純粋関数を組み合わせる オブジェクトが連携する ※宣言型に状態を追加したものは厳密には命令型とは言い切れませんが便宜的に命令型としています 非純粋関数を使う

Slide 20

Slide 20 text

関数プログラミングには2種類ある ● 純粋関数プログラミング ○ 動作を予測しやすい ● ステートフルな関数プログラミング ○ 簡潔な記述ができる

Slide 21

Slide 21 text

概要 なぜ関数プログラミングの考え方を学ぶべきか 開発・設計の幅が広がる 関数プログラミングとはなにか 純粋関数プログラミング 動作を予測しやすい ステートフルな関数プログラミング 簡潔な記述を提供 まとめ

Slide 22

Slide 22 text

純粋関数プログラミングとはなにか

Slide 23

Slide 23 text

純粋関数プログラミングとは ● 純粋関数を組み合わせてプログラミングする ● 純粋関数とは ○ 同じ入力に対して常に同じ出力を返す関数 ○ 入力が出力以外のものに影響を与えない ■ 「画面に出力する」など、 実行に影響がない場合無視しても良いとする場合がある ○ 入力が出力以外に影響がある場合、「副作用がある」という ● 純粋関数は「副作用がない」

Slide 24

Slide 24 text

具体例 純粋関数 ● 足し算をする関数 add ○ add(1, 1) # => 2 ○ add(1,2) # => 3 ● 足し算は引数が同じであれば常に同じ結果を返す

Slide 25

Slide 25 text

具体例 副作用のある関数 ● 時間を返す関数 ○ getDateTime() # => 実行するたびに結果が変わる ● 画面に出力する関数 ○ println(“Hello, World”) # => 常に出力を返さない ○ 画面にはHello, Worldと印字される ○ 何度も実行するとHello, Worldが増えていく ■ 画面を表示するための状態が変化している ○ 常に出力を返さない関数は副作用があることが期待される

Slide 26

Slide 26 text

純粋関数プログラミングって 画面に出力すらできないの…?

Slide 27

Slide 27 text

そもそも純粋関数だけでは アプリケーションがつくれない

Slide 28

Slide 28 text

純粋関数だけではアプリケーションはつくれない ● プログラム自体は作れるが画面すら出力できない ○ ユーザからフィードバックを得ることができない ● 非純粋関数はなるべく局所化し分離する ○ ステートレスとステートフルは組み合わせることができる ○ オブジェクト指向でも純粋関数を使うことができる ● 純粋関数プログラミングは ○ 再代入なしでプログラムしやすいように進化した

Slide 29

Slide 29 text

雑談 純粋関数型言語の副作用 ● 純粋関数型言語は純粋関数だけでプログラムが書ける ○ Haskell, PureScript ● Haskell自身は副作用のある関数は取り扱わない ○ 再代入する値を生成する関数を作るだけである (????) ● 具体例 文字列を画面に出力する putStrLn ○ 文字列を受け取って関数を返す関数 (????) ■ String => IO () ○ 現在の状態を受け取って次の状態を返す ■ IO () = State => (State, ()) ■ 画面の状態を受け取って、新しい画面の状態をつくる ■ 実際に新しい状態へ反映する部分はプログラムできない

Slide 30

Slide 30 text

雑談 プログラミングの純粋関数と数学の関数 ● 数学の関数 ≒ 純粋関数 ○ 理論上は同じものとしてよい ● プログラム上の純粋関数は実際のコンピュータで動く ○ 動かすコンピュータによって結果が変わる要素がある ○ メモリ不足 ⇒ 結果を返せず停止してしまう ○ CPUパワー不足 ⇒ 結果を返すのにかかる時間が違う ● 事実上無視できる前提で純粋関数プログラミングは行われている

Slide 31

Slide 31 text

関数を組み合わせる ● 関数と関数を組み合わせることでプログラムする ● 一番簡単な組み合わせ方法 ○ 関数の出力を次の関数の入力に使う +3する関数 ×2 する関数 1 1+3 4 4 × 2 8 2 2+3 5 5 × 2 10

Slide 32

Slide 32 text

関数を組み合わせる pythonの例 def add3(x): return x+3 def mul2(x): return x*2 def add3Mul2(x): # 実質関数の組み合わせをしている return mul2(add3(x)) add3Mul2(1) # => 8 add3Mul2(2) # => 10

Slide 33

Slide 33 text

関数を組み合わせる Haskellの例 add3 = (+3) mul2 = (*2) add3mul2 = mul2 . add3 # . は関数と関数を組み合わせる演算子 add3mul2 1 -- => 8 add3mul2 2 -- => 10 (mul2 . add3) 1 -- => 8 # 関数を先に組み合わせて関数を使っている mul2 (add3 1) -- => 8 # add3を先に使って、結果をmul2に使っている

Slide 34

Slide 34 text

別の組み合わせ方法 ● 途中の処理をあとから追加する ○ 同じ色の矢印は同じ関数とする +3する関数 ×2 する関数 +3 する関数 4 + 3 1 1 + 3 4 7 7 × 2 14 ×2 する関数 4 × 2 1 1 + 3 4 8 8 × 2 16

Slide 35

Slide 35 text

別の組み合わせ方法 Python版 def add3fxMul2(f): return lambda x: f(x+3) * 2 add3fxMul2(lambda x: x+3)(1) # => 14 add3fxMul2(lambda x: x*2)(1) # => 16

Slide 36

Slide 36 text

別の組み合わせ方法 Haskell版 add3fxMul2 f = (*2) . f . (+3) add3fxMul2 (+3) 1 -- => 14 add3fxMul2 (*2) 1 -- => 16

Slide 37

Slide 37 text

なぜ純粋関数プログラミングが動作を予測しやすいか ● 純粋関数と純粋関数を組み合わせても純粋関数である ○ 同じ入力であれば出力がかわらない ○ 組み合わせた関数に渡される入力も変わらない ○ ⇒ 最終的な出力も変わらない ● 動作に影響を与えるものはすべて入力で受け取る ○ 入力が分かれば計算結果を求められる ○ 期待した入力がされてないならそれ以前に問題がある ○ 期待した入力がされているならそれ以前に問題はない ○ 同時に複数の関数を計算してもお互いに影響を与えない ■ 並行並列化しやすい

Slide 38

Slide 38 text

純粋関数プログラミングは順番に左右されない ● 直線を引く関数を使って、三角形を描くプログラムを考える (1,2) (1,0) (0,0)

Slide 39

Slide 39 text

state = [] state = drawLine(state,0,0,1,0) state = drawLine(state,1,0,1,2) state = drawLine(state,1,2,0,0) exec(state) # 画面に反映する非純粋関数とする # 3本ある直線をどの順番で描いても三角形が表示できる 純粋関数は順番に左右されない (1,2) (1,0) (0,0)

Slide 40

Slide 40 text

drawLine(1,0) # 原点からx方向に1移動 drawLine(0,2) # (1,0)からy方向に2移動 drawLine(-1, -2) # (1,2)から x方向に-1,y方向に-2移動 # 引数を減らせるが順番を変えると違う形になる drawLine(0,2) # 原点からy方向に2移動 drawLine(1,0) # (0,2)からx方向に1移動 drawLine(-1,-2) # (1,2)からx方向に-1,y方向に-2移動 現在の位置を覚えておいて移動する距離を渡して描く場合 (1,2) (1,0) (0,0) (1,2) (0,2) (0,0)

Slide 41

Slide 41 text

雑談 純粋関数メタプログラミング ● 純粋関数の特性を活かしたコンパイル前処理 ● 純粋関数は入力が分かれば出力がわかる ○ もし入力値がコンパイル前からわかっているものがあれば… ○ コンパイル時に計算することができる

Slide 42

Slide 42 text

雑談 純粋関数パッケージマネージャ ● NixOSのパッケージマネージャ ● 純粋関数の特性を活かしたパッケージマネージャ ● インストールしたいパッケージやパッケージリポジトリ、システム設定 などを入力にすることでシステムを算出する ● パッケージや入力を追加してシステムが壊れたとしても ○ 追加前と同じ入力を与えることでシステムを元の状態に戻せる

Slide 43

Slide 43 text

雑談 スケールしやすいWebアプリはステートレス ● Webアプリケーションは急激なアクセスがあったとき ● 大量のアクセスに耐える必要がある ○ アプリケーションサーバーを増やせるようにする ○ 状態をデータベースにおきステートレスにする ○ ステートレスなら複数のサーバで同時に実行できる ■ 並列処理 ■ もちろんデータベースがボトルネックになる

Slide 44

Slide 44 text

まとめ 純粋関数プログラミングとは何か ● 宣言型プログラミングの一種でステートレス ● 宣言型プログラミングでは変数を再代入できない ● 関数プログラミングでは、純粋関数を用いてステートレスを実現 ● 純粋関数は動作を予測しやすい ● 純粋関数と純粋関数を組み合わせても純粋関数 ● 関係ない純粋関数同士は同時に計算できる

Slide 45

Slide 45 text

概要 なぜ関数プログラミングの考え方を学ぶべきか 開発・設計の幅が広がる 関数プログラミングとはなにか 純粋関数プログラミング 動作を予測しやすい ステートフルな関数プログラミング 簡潔な記述を提供 まとめ

Slide 46

Slide 46 text

ステートフルな関数プログラミングとはなにか

Slide 47

Slide 47 text

関数プログラミングで発展した機能 オブジェクト指向 値としての関数 関数合成 パターンマッチ カリー化 高階関数 再帰 近年のオブジェクト指向言語 取り入れる Monad 型クラス

Slide 48

Slide 48 text

再代入可能な環境での関数プログラミング ● 純粋関数プログラミングで生まれた機能がオブジェクト指向で使える ○ 再代入と純粋関数プログラミングを組み合わせることができる ● 近年、関数プログラミングといったとき、こちらを指す場合がある

Slide 49

Slide 49 text

ステートフルな関数プログラミング ● 関数が状態を持てると特化したクラスのように使える ○ 具体例 ■ React Hooks ■ クロージャと再代入

Slide 50

Slide 50 text

具体例 React Hooks ● React登場時は状態を持つコンポーネントはクラスコンポーネント使 う必要があった ● React Hooksによって関数コンポーネントで状態を扱えるようになっ た

Slide 51

Slide 51 text

具体例 React Hooks function Counter() { const [counter, setCounter] = useState(0); return (
setCounter(x => x+1)} > counter is {counter} // ボタンを押されるたびに数字が増える
); } https://codesandbox.io/p/sandbox/bold-cache-pxp83l?file=%2Fsrc%2FApp.tsx%3A18%2C1

Slide 52

Slide 52 text

具体例 React hooksの代わりにclass class Counter extends React.Component { constructor(props) { super(props); this.state = { count: 0 }; } render() { const { counter } = this.state; return (
{ const { counter } = this.state; this.setState({ counter: counter+1 }); }} > counter is {counter} // ボタンを押されるたびに数字が増える
); } }

Slide 53

Slide 53 text

具体例 クロージャと再代入 function createCounter() { let state= 0; return () => { // クロージャ state = state+ 1; return state; } } const counter = createCounter(); counter() // => 1 counter() // => 2 結果が変わるので純粋関数ではない const counter2 = createCounter(); counter2() // => 1 独立したcounterを作成できる counter() // => 3

Slide 54

Slide 54 text

具体例 クロージャと再代入の代わりにクラス class Counter: state = 0 def increment(self): self.state = self.state + 1 return self.state counter = Counter() counter.increment() # => 1 counter.increment() # => 2 counter2 = Counter() counter2.increment() # => 1 counter.increment() # => 3

Slide 55

Slide 55 text

関数を返す関数は特化したクラス クラスはコンストラクタ関数を呼び出すとオブジェクトを返す オブジェクトにはメソッド(関数)があり呼び出すことができる つまり、クラスは関数をもったオブジェクトを返す関数とみなせる

Slide 56

Slide 56 text

具体例 再びクロージャと再代入 function createCounter() { let state= 0; return { increment: () => { state = state + 1; return state; } } } const counter = createCounter(); counter.increment() // => 1 counter.increment() // => 2

Slide 57

Slide 57 text

クラスとクロージャ ● メソッドを一つしか持たないクラスであれば ○ クロージャで書き換えることができる ○ 複数メソッドも実現可能 ● さらにコンストラクタも不要な場合 ○ ただの関数とみなせる ● 言語によっては記述量が減るため見通しがよくなる ○ Javaでは匿名クラスなどが以前は使われていた ○ iOSもイベントハンドラにはdelegateパターンが使われていた ■ 現在ではクロージャが使われる

Slide 58

Slide 58 text

ステートフルな関数プログラミングとはなにか 再代入できる環境での関数プログラミングはクラスと同等の能力を短く 記述できる。関数プログラミングでは再代入しない前提で様々な機能が 発展したが、現代ではオブジェクト指向言語にも取り込まれつつある。 そのため、関数プログラミングといったときに純粋関数プログラミング を指してない事が増えてきた。

Slide 59

Slide 59 text

まとめ

Slide 60

Slide 60 text

関数プログラミングの分類 純粋関数プログラミング ステートフルな関数プログラミング 再代入 できない できる 純粋関数 利用できる 利用できる 特徴 動作を予測しやすい 並行並列化が容易 クラスより短くに記述可能

Slide 61

Slide 61 text

関数プログラミングには2種類ある ● 純粋関数プログラミング ○ 本来の関数プログラミング ○ 入力に対して出力が一定な純粋関数を使う ○ 動作が予測しやすい ● ステートフル関数プログラミング ○ 再代入が使え、オブジェクト指向と同等の能力がある ○ クラスよりも記述が短くなる

Slide 62

Slide 62 text

純粋関数プログラミングの考え方 ● 小さな純粋関数を組み合わせてプログラムを作成する ○ 出力を別の関数の入力に使うことで組み合わせることができる ○ 純粋関数を組み合わせて作った関数もまた純粋関数である ● 実際には非純粋関数を組み合わせる必要がある ○ 非純粋関数を組み合わせると純粋関数プログラミングのメリットが薄まる ■ 入力に対して出力が不定になり、動作してみないとわからなくなる ■ 非純粋関数の利用を局所化することでメリットを享受しやすくする

Slide 63

Slide 63 text

なぜ関数プログラミングの考え方を取り入れるか 適材適所で使うことでよりよいアプリケーションをつくることができる ● 関数プログラミングのメリット ○ 動作を予測しやすい ○ 並行・並列環境でも安全に動作する ○ ステートフルな関数プログラミングは記述を短くする ● 関数プログラミングのデメリット ○ ステートレスな関数は引数が長くなる ○ 非関数型プログラミング言語だと最適化できず速度に影響を与えることがある ○ 関数プログラミングの高度な手法は数学的概念がベースで学習コストが高いと言わ れている

Slide 64

Slide 64 text

関数プログラミングの考え方を知ることで 設計や実装方法の幅を広げることができる

Slide 65

Slide 65 text

おまけ 高度な機能

Slide 66

Slide 66 text

理論上、純粋関数は数学の関数とみなせる

Slide 67

Slide 67 text

つまり、数学で証明されている 理論を活用することができる

Slide 68

Slide 68 text

数学の概念から取り入れらているもの(雑な説明) ● Monoid ○ 2項演算子をもち、単位元があり、結合法則が成り立つ ○ 2項演算子はどこからでも計算できる ● Functor ○ ある関数を別の型の関数として使えるようにできる ● Monad ○ ネストした構造をフラットな構造に戻す自然な手段が存在する