読者です 読者をやめる 読者になる 読者になる

新人SEの学習記録

14年度入社SEの学習記録用に始めたブログです。気づけば社会人3年目に突入。

学習記録:Scala関数型デザイン、今後の予定

[学習記録] Scala関数型デザイン

第1章:関数型プログラミングとは

関数型プログラミングでは、純粋関数だけを使ってプログラムを構築することが前提となる。
純粋関数=副作用のない関数のこと。副作用とは、単に結果を返す以外のことで、例えば

  • 変数を変更
  • データ構造を直接変更
  • オブジェクトのフィールドを設定
  • 例外をスロー、エラーで停止
  • コンソール・ファイルへ/からの入出力

などなどを行う関数には、副作用があるということになる。

関数型プログラミングは上記のアクションを実行しない、あるいは実行するタイミングや方法がかなり制限されたプログラミングであり、要はプログラムの書き方を制約するものである。
この制約によって、(ループのような単純処理だけでなく、エラー処理やI/Oといったものを含む)全てのプログラムを副作用なしで実現し、モジュール性のある=テスト、再利用、一般化が容易なプログラムを作り上げることができる・・・ということらしい。

関数型プログラムの利点:単純な例
  • 副作用を持つプログラム

クレジットカード型の変数ccを引数に、Coffee型の変数を返す関数buyCoffeeを考える。
Coffeeオブジェクトを返すための関数であるが、クレジットカード会社に問い合わせて決済を承認する手続き=副作用が入り込んでいる。
このような副作用があるとテストがしにくくなる(=テストの度に実際の決済が行われてしまう)。

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

上記はCafeクラスをScalaで書いたもの。
buyCoffeeの横のカッコ内が引数、その横のコロンの後が戻り値を表している。
行末のセミコロンが要らないのと、ブロックの最後の行(ここではcup)が自動で返されるのでreturnが不要なことに注意。

  • 副作用の排除

というわけで、副作用を排除するため、buyCoffeeがチャージを値として返すようにする。

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

buyCoffeeではクレジット会社への問い合わせや決済といった処理は行わず、別の場所で(ここで戻り値として得たChargeを使用して)行われる。
ここで、Chargeとは以下のデータ型である。

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")
}

Chargeにはクレジットカードと料金の情報が含まれており、同じクレジットカードに対するチャージをまとめる関数combineを持っている。
このようにすることで、複数のコーヒーを購入する場合、以下のbuyCoffees関数を使用できる。
(最初の副作用がある状態では1回1回カード会社への問い合わせが必要だった)

def buyCoffees(cc: CreditCard, n: Int): (List[Coffee], Charge) = {
  val purchases: List[(Coffee, Charge)] = List.fill(n)(buyCoffee(cc))
  val (coffees, charges) = purchases.unzip
  (coffees, charges.reduce((c1, c2) => c1.combine(c2)))
}

Scalaに慣れてないのでわかりにくいけど・・・
purchasesはCoffeeとChargeのペアから成るリストで、List.fillによって生成されている。
List.fill(n)(x)はxのコピーをn個含んだListを生成するというもの。buyCoffee(cc)をn回やった戻り値のペアのリストがparchasesということ。
unzipはペアのリストをリストのペアに分割するもの。ペア構造を分解して(coffees, charges)というリストを生成している。
最後はcombineを使ってチャージのリスト全体を一つのチャージに変換している。reduceは高階関数の一種(詳細は次章)。

関数とはいったい何か
  • 純粋関数

入力型がA、出力型がBの関数fは、A型のすべての値aを、B型の一つの値bに関連付ける。
bはaの値によってのみ決定され、これをScalaではA => Bという単一の型として記述する。
たとえば、Int => String型である関数intToStringは、すべての整数を対応する文字列に変換し、それ以外は何も行わない。
このような副作用の無い関数を純粋関数と称するが、本書では関数=副作用がないという意味で使用する。

  • 参照透過性

プログラムの意味を変えることなく、式をその結果に置き換えることができることを参照透過性と呼ぶ。
これはどういうことかと言うと、式=1つの結果として評価できるプログラムの任意の部分とすると、
「2 + 3」という式は純粋関数+を2と3の値に適用する式であり、この式の評価は常に5の値になる。
ここで、プログラムに「2 + 3」という式が含まれている場合、その部分を「5」の値に置き換えてもプログラムの意味は変わらない。
これが参照透過性であり、裏返せば、参照透過な引数によって呼び出しが参照透過になる関数を純粋関数と呼ぶ。

参照透過性、純粋性、置換モデル
  • buyCoffee関数からみる参照透過性

参照透過性について、最初の(副作用がある状態の)buyCoffee関数を例に考えていく。

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

これを見ると、cc.charge(cup.price)の戻り値は関数内では使用されておらず、buyCoffee関数の戻り値=評価結果は単にnew Coffee()しただけのcupになる。
先の参照透過性の定義に照らし合わせれば、buyCoffeeが純粋関数であるためには、プログラム中のbuyCoffee(x)をすべてnew Coffee()に置き換えても同じ振る舞いをしなければならない。
が、これが当てはまらないことは明白で、buyCoffeeではクレジットカード会社に問い合わせなどをしているのに対し、new Coffee()は何もせず、この2つの式は大きく異なっている。

  • 置換モデル

参照透過性では、「関数が実行するすべてのことがその戻り値によって表される」という不変条件が適用される。
この制約によって、置換モデルと呼ばれる論証が可能となる。
ここで、Scalaインタープリタを使い、置換モデルを使った推論が可能な例を見てみる。

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

上記の例では、xを反転させた文字列をr1とr2に入れており、当然r1とr2は同じである。
ここで、すべてのxを、xが参照している式=評価結果に置換してみると、

scala> val r1 = "Hello, World".reverse
r1: String = dlroW ,olleH

scala> val r2 = "Hello, World".reverse
r2: String = dlroW ,olleH

となり、r1とr2の値が以前と同じ=この変換は結果に影響を与えていない=xは参照透過であることがわかる。

一方で、参照透過でない関数として、java.lang.StringBuilderクラスのappend関数について見てみる。

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

上記の例でも、r1とr2は同じ値である。
ここで、すべてのyをyが参照している式に置換してみると、

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

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

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

となり、r1とr2が同じではなくなってしまう。
これは、r1がx.appendを呼び出した際に、xが参照しているオブジェクトを変更してしまったためである。

  • 局所推論

先の例でわかったとおり、純粋関数を使用していない場合には、関数の動作を理解するために関数の前後で発生するかもしれない状態の変化をすべて頭の中で追跡する必要がある
一方で、純粋関数を使用した置換モデルであれば、関数の理解には局所推論=引数をその本体に代入するだけで済む。
関数型プログラムのモジュール性が高い傾向にあるのは、純粋関数によって計算そのものの処理が「結果を処理する方法」や「入力を取得する方法」から切り離されるためである。

まとめ

この章では、関数型プログラミングがどのようなものであるか、使用する理由について説明した。
参照透過性と置換モデルを取り上げ、関数型プログラミングによってプログラムの推論が容易になること、モジュール性が向上することを示した。
この後の章では、関数型プログラミングでループを記述する方法、データ構造を実装する方法、エラーと例外を処理する方法などを取り上げる。

[今後の予定] 5月の予定

4月はだいぶサボってしまったので、5月はしっかり勉強したい。
業務ではJenkinsとかSeleniumとかAWSあたりを使いそうなのでその辺を勉強したいけど、めぼしい本/教材が見つからない&&どうせ業務で嫌でも学習するので、前々から学習したいと思っていた関数型プログラミングを中心にやる予定。

それ以外だとこの辺の本を買いました。

CentOS 7実践ガイド (impress top gear)

CentOS 7実践ガイド (impress top gear)

WEB+DB PRESS Vol.85

WEB+DB PRESS Vol.85

  • 作者: 菅原元気,磯辺和彦,山口与力,澤登亨彦,濱田章吾,宮田淳平,松本亮介,海野弘成,佐藤歩,泉水翔吾,佐藤太一,hide_o_55,青木良樹,武本将英,道井俊介,伊藤直也,橋本翔,渡邊恵太,舘野祐一,中島聡,はまちや2,竹原,牧大輔,工藤春奈,WEB+DB PRESS編集部
  • 出版社/メーカー: 技術評論社
  • 発売日: 2015/02/24
  • メディア: 大型本
  • この商品を含むブログを見る

とりあえず一通り目は通しておきたいなぁ〜