Upgrade to Pro — share decks privately, control downloads, hide ads and more …

関数プログラミングの考え方

 関数プログラミングの考え方

Tomohiko Himura

July 25, 2023
Tweet

More Decks by Tomohiko Himura

Other Decks in Programming

Transcript

  1. プログラミングパラダイム 宣言型 プログラミング 命令型 プログラミング 関数 プログラミング 論理 プログラミング 手続き型

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

    オブジェクト指向 プログラミング 状態を保持できる (ステートフル) 状態が持てない (ステートレス) 純粋関数を組み合わせる オブジェクトが連携する ※宣言型に状態を追加したものは厳密には命令型とは言い切れませんが便宜的に命令型としています 非純粋関数を使う
  3. 純粋関数プログラミングとは • 純粋関数を組み合わせてプログラミングする • 純粋関数とは ◦ 同じ入力に対して常に同じ出力を返す関数 ◦ 入力が出力以外のものに影響を与えない ▪

    「画面に出力する」など、 実行に影響がない場合無視しても良いとする場合がある ◦ 入力が出力以外に影響がある場合、「副作用がある」という • 純粋関数は「副作用がない」
  4. 具体例 純粋関数 • 足し算をする関数 add ◦ add(1, 1) # =>

    2 ◦ add(1,2) # => 3 • 足し算は引数が同じであれば常に同じ結果を返す
  5. 具体例 副作用のある関数 • 時間を返す関数 ◦ getDateTime() # => 実行するたびに結果が変わる •

    画面に出力する関数 ◦ println(“Hello, World”) # => 常に出力を返さない ◦ 画面にはHello, Worldと印字される ◦ 何度も実行するとHello, Worldが増えていく ▪ 画面を表示するための状態が変化している ◦ 常に出力を返さない関数は副作用があることが期待される
  6. 雑談 純粋関数型言語の副作用 • 純粋関数型言語は純粋関数だけでプログラムが書ける ◦ Haskell, PureScript • Haskell自身は副作用のある関数は取り扱わない ◦

    再代入する値を生成する関数を作るだけである (????) • 具体例 文字列を画面に出力する putStrLn ◦ 文字列を受け取って関数を返す関数 (????) ▪ String => IO () ◦ 現在の状態を受け取って次の状態を返す ▪ IO () = State => (State, ()) ▪ 画面の状態を受け取って、新しい画面の状態をつくる ▪ 実際に新しい状態へ反映する部分はプログラムできない
  7. 雑談 プログラミングの純粋関数と数学の関数 • 数学の関数 ≒ 純粋関数 ◦ 理論上は同じものとしてよい • プログラム上の純粋関数は実際のコンピュータで動く

    ◦ 動かすコンピュータによって結果が変わる要素がある ◦ メモリ不足 ⇒ 結果を返せず停止してしまう ◦ CPUパワー不足 ⇒ 結果を返すのにかかる時間が違う • 事実上無視できる前提で純粋関数プログラミングは行われている
  8. 関数を組み合わせる 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
  9. 関数を組み合わせる Haskellの例 add3 = (+3) mul2 = (*2) add3mul2 =

    mul2 . add3 # . は関数と関数を組み合わせる演算子 add3mul2 1 -- => 8 add3mul2 2 -- => 10 (mul2 . add3) 1 -- => 8 # 関数を先に組み合わせて関数を使っている mul2 (add3 1) -- => 8 # add3を先に使って、結果をmul2に使っている
  10. 別の組み合わせ方法 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
  11. 別の組み合わせ方法 Haskell版 add3fxMul2 f = (*2) . f . (+3)

    add3fxMul2 (+3) 1 -- => 14 add3fxMul2 (*2) 1 -- => 16
  12. なぜ純粋関数プログラミングが動作を予測しやすいか • 純粋関数と純粋関数を組み合わせても純粋関数である ◦ 同じ入力であれば出力がかわらない ◦ 組み合わせた関数に渡される入力も変わらない ◦ ⇒ 最終的な出力も変わらない

    • 動作に影響を与えるものはすべて入力で受け取る ◦ 入力が分かれば計算結果を求められる ◦ 期待した入力がされてないならそれ以前に問題がある ◦ 期待した入力がされているならそれ以前に問題はない ◦ 同時に複数の関数を計算してもお互いに影響を与えない ▪ 並行並列化しやすい
  13. 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)
  14. 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)
  15. 具体例 React Hooks function Counter() { const [counter, setCounter] =

    useState(0); return ( <div> <button onClick={() => setCounter(x => x+1)} > counter is {counter} // ボタンを押されるたびに数字が増える </button> </div> ); } https://codesandbox.io/p/sandbox/bold-cache-pxp83l?file=%2Fsrc%2FApp.tsx%3A18%2C1
  16. 具体例 React hooksの代わりにclass class Counter extends React.Component { constructor(props) {

    super(props); this.state = { count: 0 }; } render() { const { counter } = this.state; return ( <div> <button onClick={() => { const { counter } = this.state; this.setState({ counter: counter+1 }); }} > counter is {counter} // ボタンを押されるたびに数字が増える </button> </div> ); } }
  17. 具体例 クロージャと再代入 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
  18. 具体例 クロージャと再代入の代わりにクラス 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
  19. 具体例 再びクロージャと再代入 function createCounter() { let state= 0; return {

    increment: () => { state = state + 1; return state; } } } const counter = createCounter(); counter.increment() // => 1 counter.increment() // => 2
  20. クラスとクロージャ • メソッドを一つしか持たないクラスであれば ◦ クロージャで書き換えることができる ◦ 複数メソッドも実現可能 • さらにコンストラクタも不要な場合 ◦

    ただの関数とみなせる • 言語によっては記述量が減るため見通しがよくなる ◦ Javaでは匿名クラスなどが以前は使われていた ◦ iOSもイベントハンドラにはdelegateパターンが使われていた ▪ 現在ではクロージャが使われる
  21. 関数プログラミングには2種類ある • 純粋関数プログラミング ◦ 本来の関数プログラミング ◦ 入力に対して出力が一定な純粋関数を使う ◦ 動作が予測しやすい •

    ステートフル関数プログラミング ◦ 再代入が使え、オブジェクト指向と同等の能力がある ◦ クラスよりも記述が短くなる
  22. 純粋関数プログラミングの考え方 • 小さな純粋関数を組み合わせてプログラムを作成する ◦ 出力を別の関数の入力に使うことで組み合わせることができる ◦ 純粋関数を組み合わせて作った関数もまた純粋関数である • 実際には非純粋関数を組み合わせる必要がある ◦

    非純粋関数を組み合わせると純粋関数プログラミングのメリットが薄まる ▪ 入力に対して出力が不定になり、動作してみないとわからなくなる ▪ 非純粋関数の利用を局所化することでメリットを享受しやすくする
  23. なぜ関数プログラミングの考え方を取り入れるか 適材適所で使うことでよりよいアプリケーションをつくることができる • 関数プログラミングのメリット ◦ 動作を予測しやすい ◦ 並行・並列環境でも安全に動作する ◦ ステートフルな関数プログラミングは記述を短くする

    • 関数プログラミングのデメリット ◦ ステートレスな関数は引数が長くなる ◦ 非関数型プログラミング言語だと最適化できず速度に影響を与えることがある ◦ 関数プログラミングの高度な手法は数学的概念がベースで学習コストが高いと言わ れている
  24. 数学の概念から取り入れらているもの(雑な説明) • Monoid ◦ 2項演算子をもち、単位元があり、結合法則が成り立つ ◦ 2項演算子はどこからでも計算できる • Functor ◦

    ある関数を別の型の関数として使えるようにできる • Monad ◦ ネストした構造をフラットな構造に戻す自然な手段が存在する