by shigemk2

当面は技術的なことしか書かない

Scala関数型デザイン&プログラミング読書会@渋谷 #functional_shibuya

関数型プログラミングの基礎

  • 関数型プログラミング
  • 副作用の発生の阻止
  • 非正格性

副作用とは

  • 変数の変更
  • データ構造を直接変更する
  • オブジェクトのフィールドを設定する
  • 例外をスローする、またはエラーで停止する
  • コンソールに出力する またはユーザー入力を読み取る
  • ファイルを読み取る ファイルに書き込む
  • 画面上への描画

純粋関数にはモジュール性があり、モジュール性が向上することでテストや再利用が簡単になります。

副作用のあるやつ

戻り値以外に処理が発生している

class Cafe {
  def buyCoffee(cc: CreditCard): Coffee = {
    val cup = new Coffee()
    cc.charge(cup.price) // ココが副作用
    cup
  }
}

モジュール性とテスタビリティ向上の例(まだ副作用があるけど、テスタビリティは上がった)

class Cafe {
  def buyCoffee(cc: CreditCard, p: Payments): Coffee = {
    val cup = new Coffee()
    cc.charge(cc, cup.price)
    cup
  }
}

Chargeオブジェクトを返すようにして、実際のチャージ処理は別のクラスでやらせることで、副作用をなくし、トランザクションを1つにまとめている

class Cafe {
  def buyCoffee(cc: CreditCard): (Coffee, Charge) = {
    val cup = new Coffee()
    (cup, Charge(cc, cup.price))
  }
}

// プライマリコンストラクタを1つもち、引数リストのパラメータはclassのイミュータブルなパブリックフィールドになる
case class Charge(cc: CreditCard, amount: Double) {
  def combine(other: Charge): Charge =
    if (cc == other.cc)
      Charge(cc, amount + other.amount)
    else
      throw new Exception("Can't combine charges to different cards.")
}

さらに、n杯のコーヒーの購入を実装する

class Cafe {
  def buyCoffee(cc: CreditCard): (Coffee, Charge) = ...

  def buyCoffees(cc: CreditCard, n: Int): (List[Coffee], Charge) = {
    val purchases: List[(Coffee, Charge)] = List.fill(n)(buyCoffee(cc))
    ....
  }
}

関数型プログラミングを使えば、テスタビリティや再利用の・においても良い方法である単純な例を示した。

println(List(1,2,3,4,5).groupBy(n => n%2)) // Map(1 -> List(1, 3, 5), 0 -> List(2, 4))

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

  • 純粋関数とは副作用を持たない関数のこと
  • 純粋関数とは推論しやすいこと
  • 関数がプログラムの実行に対して観察可能な効果を与えることはない

  • 関数 プロシージャ

  • 参照透過性(どのようなプログラムにおいても、プログラムの意味を変えることなく、式をその結果に置き換えることが出来ること)

参照透過ではない例

class Cafe {
  def buyCoffee(cc: CreditCard, p: Payments): Coffee = {
    val cup = new Coffee()
    cc.charge(cc, cup.price)
    cup
  }
}

文字列はイミュータブル

scala> val x = "hello, world"
x: String = hello, world

scala> val r1 = x.reverse
r1: String = dlrow ,olleh

scala> val r2 = x.reverse
r2: String = dlrow ,olleh

副作用と参照透過

参照透過性 - Wikipedia

副作用 (プログラム) - Wikipedia

scala> val x = new StringBuilder("Hello")
x: StringBuilder = Hello

scala> val y = x.append(", World")
y: StringBuilder = Hello, World

scala> val r1 = y.toString
r1: String = Hello, World

scala> val r2 = y.toString
r2: String = Hello, World

scala> val r3 = x.append(", World").toString
r3: String = Hello, World, World

scala> val r4 = x.append(", World").toString
r4: String = Hello, World, World, World

StringBuilder.appendは純粋関数ではない…

副作用はプログラムの振る舞いについての論証を難しくする

必要なのは局所推論だけ

  • 計算の部分はブラックボックス化され、出力は計算され、返されるだけとなる
  • 合成も簡単。

Scala関数型プログラミングの準備

まずはやってみよう。

  • object新しいシングルトン型を作成する
  • defキーワードを使ってオブジェクトまたはクラス内で定義された関数またはフィールドをメソッドと呼ぶ
  • Scalaにreturnはない。最後の式が戻り値となる。
  • コンソール出力は副作用、ゆえにmain関数はプロシージャ、非純粋関数と呼ばれる
object MyModule {
  def abs(n: Int): Int =
    if (n < 0) -n
    else n

  private def formatAbs(x: Int) = {
    val msg = "The absolute value of %d is %d"
    msg.format(x, abs(x))
  }

  def main(args: Array[String]): Unit =
    println(formatAbs(-42))

mainの入力はArray[String] 戻り値はUnit で、戻り値がUnitなのはそのメソッドに副作用があることのヒントになりうる。

qiita.com

プログラムの実行

REPLの使い方は覚えておけばよいでしょう

➜  ~  scalac MyModule.scala 
➜  ~  ll MyModule*
-rw-rw-r--. 1 shigemk2 shigemk2 1.4K  3月 24 20:41 MyModule$.class
-rw-rw-r--. 1 shigemk2 shigemk2  712  3月 24 20:41 MyModule.class
-rw-rw-r--. 1 shigemk2 shigemk2  257  3月 24 20:41 MyModule.scala
➜  ~  scala MyModule
The absolute value of -42 is 42
➜  ~  

モジュール、オブジェクト、名前空間

object MyModule {
  def abs(n: Int): Int =
    if (n < 0) -n
    else n

  private def formatAbs(x: Int) = {
    val msg = "The absolute value of %d is %d"
    msg.format(x, abs(x))
  }

  def main(args: Array[String]): Unit =
    println(formatAbs(-42))
scala> :load MyModule.scala
Loading MyModule.scala...
defined object MyModule

scala> MyModule.abs(-42)
res0: Int = 42

MyModule.abs(-42)みたいな書き方をしなければならない。これはMyModuleが名前空間であることを意味する。 (文脈が明らかな場合は、関数とメソッドを同じ意味で使う)

糖衣構文

scala> 2 + 1
res1: Int = 3

scala> 2.+(1)
res2: Int = 3

こういう書き方も可能。

scala> MyModule.abs(-42)
res0: Int = 42
scala> MyModule abs -42
res3: Int = 42

スコープに入れたい

scala> import MyModule.abs
import MyModule.abs

scala> abs(-42)
res4: Int = 42

高階関数: 関数に関数を返す

関数は値である

という考え方を踏まえて、関数を変数として代入し、関数を別の関数の引数として使いたい。

のっけから末尾再帰を使っているのでそこは注意されたし。

gist.github.com

再帰ヘルパー関数はgoまたはloopが命名としては常道。 なお、Scalaでwhileを使うのは邪道とされている。

こういうテクニックを末尾呼び出しの除去と呼ぶ。

fibonacciの末尾再帰

  def fib(n: Int): Int = {
    @annotation.tailrec
    def loop(n: Int, prev: Int, cur: Int): Int =
      if (n == 0) prev
      else loop(n - 1, cur, prev + cur)
    loop(n, 0, 1)
  }

gist.github.com

なお、高階関数のパラメータは、f g hみたいなのが慣例となっている。

多相関数 型の抽象化

単相関数 vs 多相関数

  def findFirst(ss: Array[String], key: String): Int = {
    @annotation.tailrec
    def loop(n: Int): Int =
      if (n >= ss.length) -1
      else if (ss(n) == key) n
      else loop(n + 1)
    loop(0)
  }
  def findFirst[A](as: Array[A], p: A => Boolean): Int = {
    @annotation.tailrec
    def loop(n: Int): Int =
      if (n >= as.length) -1
      else if (p(as(n))) n
      else loop(n + 1)

    loop(0)
  }

簡約! λカ娘 - 参照透明な海を守る会

http://www.paraiso-lang.org/ikmsm/images/c80-cover.jpg

次回 P30 無名関数を使った高階関数の呼び出し から