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

関数型プログラミングへの第一歩: 純粋関数を知る

関数型プログラミングへの第一歩: 純粋関数を知る

福岡市エンジニアカフェでの登壇資料です。

2024/06/24追記
純粋関数の性質: 3 について、具体例をよりわかりやすいものに差し替えました。

Tomoyuki TAKEZAKI

June 22, 2024
Tweet

More Decks by Tomoyuki TAKEZAKI

Other Decks in Programming

Transcript

  1. 自己紹介 名前 t-takezaki 所属 株式会社 U-NEXT 動画配信・電子書籍・ライブ配信などを行っている 業務 Android アプリ開発者

    (5年目 ) 勤務地 本社は東京にあるが、 2020年 3月から WFH コロナが落ち着いて以降も出社は任意なので、福岡に移住 2
  2. 前置き この LTでは、プログラミング経験がある人を対象に、 関数型プログラミング を紹介する。 関数型プログラミングの中心的概念には以下のものがある。 純粋関数 不変なデータ構造 副作用の分離 今回は

    純粋関数 に絞って具体的な解説を行い、全体像については最後に軽く説明する。 関数型プログラミングの考え方は言語に依存しないが、今回は説明のため Kotlin を用いる。 3
  3. 数学における関数とプログラミングにおける関数 (1/3) 数学における関数 Any procedure or a rule that assigns

    to each member of one set X one, and only one, element of another set Y is called a function. <拙訳 > ある集合 の要素 に別の集合 の要素 を 一つだけ 割り当てる手順または 規則を、関数という。 Encyclopedia Of Mathematics by James Stuart Tanton つまり、数学における関数は「引数を受け取り、 一つだけ 値を返す」 。 例えば と定義する。このとき、 の値は ただ一つ に決まる。 5
  4. 数学における関数とプログラミングにおける関数 (3/3) ところで、プログラミングパラダイムにおける進歩は、 プログラマができることを うまく制限する ことと関係している。 1. Structured programming 2.

    Object-Oriented programming プログラミングにおける関数に対するうまい制限とは、関数が「数学における関数」の性質 をもつようにすることである。 この うまく制限した関数 を「純粋関数」といい、これは関数型プログラミングにおける中心 的な概念の一つである。 7
  5. 純粋関数の性質 : 1. 戻り値は常に一つだけ (1/2) 純粋関数は、あらゆる入力に対してシグネチャ通りの結果を返す。 例えば increment 関数は Int

    型の値を受け取り、必ず Int 型の値を一つ返す。 // pure function fun increment(x: Int): Int { return x + 1 } 非純粋関数は、入力値次第ではシグネチャ通りの結果を返さない可能性がある。 getFirstCharacter や div は、どのような入力に対して「嘘をつく」だろうか? // impure function fun getFirstCharacter(s: String): Char { return s.get(0) } // impure function fun div(a: Int, b: Int): Int { return a / b } 8
  6. 純粋関数の性質 : 1. 戻り値は常に一つだけ (2/2) getFirstCharacter は空文字、 div はゼロ除算でシグネチャ通りの結果を返さない。 fun

    main() { println(getFirstCharacter("")) // throws StringIndexOutOfBoundsException println(div(1, 0)) // throws ArithmeticException } // impure functions fun getFirstCharacter(s: String): Char { return s.get(0) } fun div(a: Int, b: Int): Int { return a / b } これらを純粋関数にするには、 失敗する可能性のある値 を取り扱うための型を利用する。 例えば Kotlin では、言語標準の Result 型で Result.success と Result.failure の 二種類の値を表現できる。 9
  7. 純粋関数の性質 : 2. 引数のみに基づいて戻り値を計算する (1/2) 純粋関数は、引数のみに基づいて計算を行うため、同じ引数に対する呼び出し結果は常に一 定である。 fun main() {

    println(isAdmin("takezaki")) // true println(isAdmin("suzuki")) // false } // pure function fun isAdmin(userId: String): Boolean { return if (userId == "takezaki") true else false } 10
  8. 純粋関数の性質 : 3. 既存の値を変更しない (1/4) 既存の値を変更する関数は、純粋関数ではない。 例えば、ショッピングカートの中身から割引率を算出するロジックを考える。 要件 カートにアイテムを追加できる。 カートからアイテムを削除できる。

    カート内のアイテムから、割引率を算出する。 カート内に "Book" を含むアイテムが存在すれば、割引率は 20% である。 カート内に "Book" を含むアイテムが存在しなければ、割引率は 0% である。 12
  9. 純粋関数の性質 : 3. 既存の値を変更しない (2/4) 例えば、次のような実装が考えられる。 (ネタバレ:この実装にはバグがある。 ) class ShoppingCart

    { private var shouldDiscount: Boolean = false private val items = mutableListOf<String>() fun addItem(item: String) { items.add(item) if (item.contains("Book")) { shouldDiscount = true } } fun removeItem(item: String) { items.remove(item) if (item.contains("Book")) { shouldDiscount = false } } fun getDiscountPercentage(): Int { return if (shouldDiscount) 20 else 0 } /* snip */ } 13
  10. 純粋関数の性質 : 3. 既存の値を変更しない (3/4) 実は、先に示した実装は、 Book が複数個カートに入ることを考慮できていない。 fun main()

    { val cart = ShoppingCart() cart.addItem("Book 1") cart.addItem("Book 2") cart.removeItem("Book 1") println(cart.getDiscountPercentage()) // 0 } カート内のアイテムは "Book 2" のみなので、割引率は 20% が正しい。 14
  11. 純粋関数の性質 : 3. 既存の値を変更しない (4/4) 既存の値の変更は避け、純粋関数で計算するように変更するとミスが起こりにくい。 fun main() { val

    cartItems = mutableListOf<String>() cartItems.add("Book 1") cartItems.add("Book 2") cartItems.remove("Book 1") println(ShoppingCart.getDiscountPercentage(cartItems)) // 20 } object ShoppingCart { fun getDiscountPercentage(items: List<String>): Int { return if (items.any { it.contains("Book") }) 20 else 0 } } 15
  12. 純粋関数の性質まとめ 純粋関数とは、 「プログラミングにおける関数」を制限し、 「数学における関数」の性質をも つようにしたものである。 純粋関数の性質 1. 純粋関数の戻り値は常に一つだけ 2. 純粋関数はその引数のみに基づいて戻り値を計算する

    3. 純粋関数は既存の値を変更しない 利点 単一責任 : 一つの関数は、一つのことだけをする。 副作用がない : 副作用は、副作用を起こすことを目的とした非純粋関数に集約する。 再現性 (参照透過性 ): 関数を同じ引数で何度呼び出しても、常に同じ結果を返す。 16
  13. 状態や副作用はどこへ? (1/2) 純粋関数は、その性質から次のような特徴をもつ。 外部の 状態 (共有された可変なデータ構造 ) を参照したり変更したりしない 副作用 を起こさない

    純粋関数がプログラムに占める割合を高めることで、簡潔でバグの少ないプログラムに近づ けることができる。 しかし、プログラムから状態や副作用をなくすことはできない。状態や副作用はどのように 扱うべきだろうか? 17
  14. まとめ 関数型プログラミングの考え方では、以下のような手順に沿って、簡潔でバグの少ないプロ グラムを記述を目指す。 1. モデルを不変なデータ構造として表現する 2. ロジックを純粋関数として実装する <- 今回説明した部分 3.

    副作用の実行をロジックから分離する さらに詳しく学びたい方には、以下の文献をお勧めする。 なっとく!関数型プログラミング https://www.shoeisha.co.jp/book/detail/9784798179803 今回は、この本の 2 章までの内容をかいつまんで紹介した。 19
  15. Backup: 副作用の分離についての補足 (1/2) 例えば Scala の cats ライブラリ では、副作用を伴うプログラムの記述を IO

    型で表現す る。 def fetchUser(userID: String): IO[User] = IO.delay { /* network access */ } この関数を呼び出すと、 IO 型の値が取得し、他のプログラムに渡すことができる。 そして、この時点では、まだネットワークへのアクセスは行われていない。 val user: IO[User] = fetchUser("takezaki") 20
  16. Backup: 副作用の分離についての補足 (2/2) 他にも副作用を伴う処理がある場合には IO 型に wrap することができる。 // この時点では、まだネットワークへのアクセスは行われていない。

    val videos: IO[List[Video]] = IO.delay { /* network access */ } val books: IO[List[Book]] = IO.delay { /* network access */ } そして、複数の IO 型の値を "潰して " 一つにまとめることができる。 // この時点では、まだネットワークへのアクセスは行われていない。 val program: IO[Unit] = ??? // user, videos, books を取得して画面に表示するプログラム 最終的に、メインプロセスでまとめて副作用を実行する。 // main process program.unsafeRunSync() // ネットワークアクセスおよび画面表示がここで行われる このようにして、値の取得と副作用の実行を分離することができる。 21