$30 off During Our Annual Pro Sale. View Details »

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

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

Tomohiko Himura

July 25, 2023
Tweet

More Decks by Tomohiko Himura

Other Decks in Programming

Transcript

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

  10. View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

  32. 関数を組み合わせる 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

    View Slide

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

    View Slide

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

    View Slide

  35. 別の組み合わせ方法 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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

  39. 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)

    View Slide

  40. 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)

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

  51. 具体例 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

    View Slide

  52. 具体例 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} // ボタンを押されるたびに数字が増える


    );
    }
    }

    View Slide

  53. 具体例 クロージャと再代入
    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

    View Slide

  54. 具体例 クロージャと再代入の代わりにクラス
    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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

  59. まとめ

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

  65. おまけ 高度な機能

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide