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

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

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

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

Avatar for Tomoyuki TAKEZAKI

Tomoyuki TAKEZAKI

June 22, 2024
Tweet

More Decks by Tomoyuki TAKEZAKI

Other Decks in Programming

Transcript

  1. 前置き このLTでは、プログラミング経験がある人を対象に、 関数型プログラミング を紹介する。 関数型プログラミングの中心的概念には以下のものがある。 純粋関数 不変なデータ構造 副作用の分離 今回は 純粋関数

    に絞って具体的な解説を行い、全体像については最後に軽く説明する。 関数型プログラミングの考え方は言語に依存しないが、今回は説明のため Kotlin を用いる。 2
  2. 数学における関数とプログラミングにおける関数 (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 つまり、数学における関数は「引数を受け取り、 一つだけ 値を返す」 。 例えば と定義する。このとき、 の値は ただ一つ に決まる。 4
  3. 数学における関数とプログラミングにおける関数 (3/3) ところで、プログラミングパラダイムにおける進歩は、 プログラマができることを うまく制限する ことと関係している。 1. Structured programming 2.

    Object-Oriented programming プログラミングにおける関数に対するうまい制限とは、関数が「数学における関数」の性質 をもつようにすることである。 この うまく制限した関数 を「純粋関数」といい、これは関数型プログラミングにおける中心 的な概念の一つである。 6
  4. 純粋関数の性質: 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 } 7
  5. 純粋関数の性質: 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 の 二種類の値を表現できる。 8
  6. 純粋関数の性質: 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 */ } 12
  7. 純粋関数の性質: 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% が正しい。 13
  8. 純粋関数の性質: 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 } } 14
  9. 純粋関数の性質まとめ 純粋関数とは、 「プログラミングにおける関数」を制限し、 「数学における関数」の性質をも つようにしたものである。 純粋関数の性質 1. 純粋関数の戻り値は常に一つだけ 2. 純粋関数はその引数のみに基づいて戻り値を計算する

    3. 純粋関数は既存の値を変更しない 利点 単一責任: 一つの関数は、一つのことだけをする。 副作用がない: 副作用は、副作用を起こすことを目的とした非純粋関数に集約する。 再現性 (参照透過性): 関数を同じ引数で何度呼び出しても、常に同じ結果を返す。 15
  10. まとめ 関数型プログラミングの考え方では、以下のような手順に沿って、簡潔でバグの少ないプロ グラムを記述を目指す。 1. モデルを不変なデータ構造として表現する 2. ロジックを純粋関数として実装する <- 今回説明した部分 3.

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

    型で表現す る。 def fetchUser(userID: String): IO[User] = IO.delay { /* network access */ } この関数を呼び出すと、 IO 型の値が取得し、他のプログラムに渡すことができる。 そして、この時点では、まだネットワークへのアクセスは行われていない。 val user: IO[User] = fetchUser("takezaki") 19
  12. 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() // ネットワークアクセスおよび画面表示がここで行われる このようにして、値の取得と副作用の実行を分離することができる。 20