関数型言語入門

Scalaで関数型言語に触れてみよう!

2014/01/08 エンジニア勉強会 - 西岡寛兼

Scala

Scalaとは

  • オブジェクト指向・関数ハイブリッド型
  • コンパイルするとJVM中間言語ができる(.classファイル)
    • つまりJarやWarを作れる
    • JVM上で動く(Write once, Run anywhere)
  • Javaとシームレスに連携
    • Mavenに蓄えられた資産をそのまま使える!

Better Java

ひとまず「関数型」の部分を無視して、とても楽に書けるJavaとしてScalaを使うことができます。

  • 型推論
  • パターンマッチ
  • traitを使った多重継承
  • case class, case object

Better Java

Better Javaでも、今すぐJavaを捨て去るだけの価値が
あります。

JavaコードをScalaに置き換えると、コード行数がだいたい1/3〜1/2になると言われています。

Better Java ...?

ただし・・・

Better JavaではScalaのポテンシャルを半分も引き出せてないです。もったいない!

自分はBetter Javaでよくても、例えばOSSライブラリを使うときには、Better Javaでは使えなかったり、ソースを読めなかったりします。これは避けられない・・・

ということで・・・

関数型プログラミング

Functional Programming

やりましょう

注) ここでは静的型付け関数型言語を扱います。単に「関数型」と表現していますが、静的型付け関数型言語のことを指していると思ってください。

関数脳をつくる

パラダイムシフト

オブジェクト指向から関数型に移行するのは、ただ新しい言語を学ぶのとは違います。

OOP未経験者がOOPを使いこなすには考え方そのものをオブジェクト指向にしなければならないように、FP未経験者がFPを使いこなすには関数脳にする必要があります。

FizzBuzz

※ただし、突飛な書き方はなしであくまで"普通に"書く前提で。

FizzBuzz

  • 1から100の数字を出力する
  • 3で割り切れる場合は数字の代わりに「Fizz」を表示する
  • 5で割り切れる場合は「Buzz」を表示する
  • 3でも5でも割り切れる場合は「FizzBuzz」を表示する

あなたの脳はどう考える?

1. 1から100までカウントアップしながらループ
    I. 変数の剰余演算結果で条件分岐
        i. 仕様に合わせて出力

→手続き型脳

じゃあ関数脳は
どう考えるか?

Int => String

これだけ!

Int値から表示用の文字列を得る関数があればいい。

作りたいプログラムの本質を関数で捉える。

書いてみる

val fizzBuzz: Int => String =




            

書いてみる

val fizzBuzz: Int => String = {
  case i if i % 15 == 0 => "FizzBuzz"
  case i if i % 5 == 0 => "Buzz"
  case i if i % 3 == 0 => "Fizz"
  case i => i.toString
}

え?ループと表示は?

と、思われるかもしれませんが、ぶっちゃけそんなの興味の対象じゃないんですよね。

とはいえそれじゃ仕様満たしてないと言われそうなので書いときますよ。

(1 to 100) map fizzBuzz foreach println

なぜこんなことになるのか

関数第一級オブジェクト(First Class Object)だからです。

第一級オブジェクトとは

  • リテラルとして表現できる
  • 変数に格納できる
  • それ自体が独自に存在できる(名前とは独立している)
  • 関数のパラメータとして渡すことができる
  • 関数の戻り値として返すことができる

関数が第一級オブジェクトだと何が起こるの?

これ説明したいんですけど、すごい抽象的な話になってしまいますので、頑張ってついてきてね☆

まず、手続き型だと

  • 手続き型におけるメソッドはあくまで、手続きのコンポーネント化。
  • 定義したメソッドは呼び出すことしかできない。
  • なので、常に「どうやって呼び出すか」を考えておかないと使えなくなっちゃいます。
  • つまり、手続き型は、トップダウンでないとメソッド設計しづらいんです。
1. 1から100までカウントアップしながらループ
    I. 変数の剰余演算結果で条件分岐
        i. 仕様に合わせて出力

トップダウン

それに対して関数型は

  • 関数をとして定義しておく。使い方は後から考える余地が十分にある。
  • なぜなら、関数を適用する(呼び出す)だけでなく、引数に渡したり戻り値にしたりできるから。
  • なので、関数型はボトムアップに関数を設計していくことができる。
  • 更に、関数を引数に渡したり戻り値にしたりできるとAPI設計の幅が広がるため、標準ライブラリ・OSSライブラリの機能が豊富になる傾向がある。

Int => String

これ、FizzBuzzのボトムですよね。

なんでここから考えることができるかというと、呼び出す側を事前に考えておかなくても、関数を値として定義しておけば、あとでいかようにも使える自信があるから。

(1 to 100) map fizzBuzz foreach println

ほら、実際に使えてるでしょ。標準ライブラリだけで。

関数脳まとめ

  • 手続き型脳は、(仮にボトムから書いてるつもりでも)メソッド設計はトップダウンでやっている
  • 関数脳は、関数設計をボトムアップでやれる

小休止

ここまでが抽象的な話

ここから具体的なコードと共に、
Scala白魔法を紹介していきます。

ちなみに「黒魔術」と表現する人もいますが、
より安全で保守性の高いコードにする技術なので、
紛れも無く白魔法です。

以前日報でこんな主張をしました。

僕が考えるオブジェクト指向の最もダメなところは、「クラス内に隠蔽されているから」という免罪符のもとデータと処理が密結合になるところなんですね。

なので、ここでは静的型付け関数型言語でデータと処理を疎結合に保つ書き方をお見せしようと思います。

データ

代数的データ型

それぞれの代数的データ型の値には、1個以上のコンストラクタがあり、各コンストラクタには0個以上の引数がある。

Wikipediaより

なんのこっちゃ。

代数的データ型

case class Color(red: Int, green: Int, blue: Int)

// Javaのこれとほぼ同じ
@lombok.Value
public class Color {
  int red;
  int green;
  int blue;
}

代数的データ型

sealed trait RGB
case object Red extends RGB
case object Green extends RGB
case object Blue extends RGB

// Javaのこれとほぼ同じ
public enum RGB {
  Red, Green, Blue;
}

代数的データ型

sealed trait RGB
case class Red(hoge: Int) extends RGB
case class Green(piyo: String) extends RGB
case object Blue extends RGB

// そろそろJavaでは書きたくない

代数的データ型

sealed trait JsonValue
case class JObject(values: Map[String, JsonValue]) extends JsonValue
case class JArray(values: List[JsonValue]) extends JsonValue
sealed trait PrimitiveJsonValue extends JsonValue
case class JInt(value: Int) extends PrimitiveJsonValue
case class JString(value: String) extends PrimiriveJsonValue

// Java・・・( ´Д`)=3

代数的データ型

  • 処理は持たず、データ構造のみ
  • Immutable
  • 型の親子関係を作れる(多層も可)
  • 親子関係は性質を共有することを表すのではなく、直和を表す

処理

型クラス

アドホック多相 を実現するもの
アドホック多相とは異なる型の間で共通したインターフェースでの異なる振る舞いを定義済みの型に対して拡張するような多相

Scalaで型クラス入門より

型クラス

"型クラス" = "型に関する性質" = "処理のテンプレート"

型クラス

例:「足し算ができる」という性質

trait Semigroup[A] {
  def append(a1: A, a2: A): A
}

型クラス

実際の処理は「型クラスのインスタンス」に実装する。

例:Int型のSemigroup型クラスのインスタンス

implicit object IntSemigroup extends Semigroup[Int] {
  def append(a1: Int, a2: Int): Int = a1 + a2
}

これを定義することにより、「Int型は足し算することができますよ」と宣言したことになる。

型クラス

もちろん自作の代数的データ型に対して型クラスのインスタンスを定義することもできます。

case class Vector2(x: Double, y: Double)

implicit object Vector2Semigroup extends Semigroup[Vector2] {
  def append(a1: Vector2, a2: Vector2): Vector2 =
     Vector2(a1.x + a2.x, a1.y + a2.y)
}

性質を定義したのはわかった。

データと疎結合なまま定義できているのもわかった。

で、それどうやって使うの?

白魔法

型クラス

scala> 3 |+| 5
res0: Int = 8

scala> Vector2(1, 2) |+| Vector2(4, 5)
res1: Vector2 = Vector2(5.0,7.0)

型クラス

登場人物をまとめるとこんな感じ

型クラス

さらに、型クラスは継承関係を作ることもできます。

trait Monoid[A] extends Semigroup[A] {
  def zero: A
}

「足し算ができる」という性質(Semigroup)に加え、零元を持つという性質も持つ型クラス。

型クラス

先ほどのIntSemigroup、Vector2Semigroupを以下に置き換えます。

implicit object IntMonoid extends Monoid[Int] {
  def append(a1: Int, a2: Int): Int = a1 + a2
  def zero: Int = 0
}

implicit object Vector2Monoid extends Monoid[Vector2] {
  def append(a1: Vector2, a2: Vector2): Vector2 =
     Vector2(a1.x + a2.x, a1.y + a2.y)
  def zero: Vector2 = Vector2(0, 0)
}

型クラス

もちろんMonoidとSemigroupは継承関係にあるので、Semigroupの性質を持っていれば使えた機能は、Monoidが定義されている場合でも使えます。

scala> 3 |+| 5
res0: Int = 8

scala> Vector2(1, 2) |+| Vector2(4, 5)
res1: Vector2 = Vector2(5.0,7.0)

ここからもう少し型クラスの本領見せていくよ。

型クラス

Monoidは「足し算ができる・零元がある」という性質ですが、Monoidが定義されている型に提供される機能はその2つにとどまる必要はありません。

つまり、足し算ができて、零元があるならば可能な処理は、全てのMonoidに機能として提供することができます。

型クラス

例えば、「足し算ができて、零元ある」ならば、Listに格納された要素の合計を計算することができます。

def sum[A: Monoid](l: List[A]): A =
  l.foldLeft(implicitly[Monoid[A]].zero)(_ |+| _)

scala> sum(List(1, 2, 3))
res0: Int = 6

scala> sum(List(Vector2(1, 2), Vector2(3, 4)))
res1: Vector2 = Vector2(4.0,6.0)

型クラス

このsumメソッドはMonoidを要素に持つListであれば要素の型は何であっても適用することができますが、もしMonoidでない要素を持つListがsumメソッドに渡されると、コンパイルエラーになります。つまり「誤ってMonoidでない要素を持つListをsumに渡してしまった」というバグは発生しません。

case object NotMonoid

sum(List(NotMonoid, NotMonoid))
error: could not find implicit value for evidence parameter of type Monoid[NotMonoid.type]

データと処理まとめ

  • データは、その形を代数的データ型として表現します
  • "性質"を、型クラスとして表現します
  • データ型ごとに、性質の振る舞いを、型クラスのインスタンスとして表現します
  • アドホック多相と型安全を同時に実現します

さて、お待たせしました

モナド

モナドってなに?

ただの型クラスの1つです。

「足し算ができる性質」みたいにわかりやすく簡単な日本語で表せないので、難しそうに見えますけどね。

最初は「何か値を入れるコンテナ」だと思うのがいいかもしれません

慣れてくると「モナドは文脈だ」と思うのがしっくりくるようになってきます

モナドが持つ性質

1つだけ、さっきのMonoidと違うところがあるので先に。

  • Monoid[A] ← 型Aに対する性質
  • Monad[M[_]] ← 型Mに対する性質だが、Mは型パラメータを1つ持つ

例えばList[_]などがM[_]になれる

モナドが持つ性質

Monad[M[A]]は以下の2つの関数を持つ

point: A => M[A]
bind: (A => M[A]) => M[A] => M[A]

この2つの関数は以下の条件(Monad則)を満たす

bind(f)(point(a)) == f(a)
bind(point)(m) == m
bind(g)(bind(f)(m)) == bind({ x => bind(g)(f(x)) })(m)

覚えなくてもいいですw

モナドが持つ性質

モナド則の意味を完全に理解する必要は全くありませんが、あえて本質だけをかいつまんで表現すると

「Mに包んだままAに対して実行する処理」のMonoid
0 + a = a
a + 0 = a
(a + b) + c = a + (b + c)

モナドに提供される機能

Semigroupの|+|やMonoidのsumのように、Monadに提供される機能もあります

M[A].flatMap: (A => M[B]) => M[B]
M[A].map: (A => B) => M[B]

mapとflatMapの活躍はこのあと紹介します

つまりモナドとは

  • ただの型クラス
  • 「コンテナに包まれた中の値に対する処理」を合成するための性質
  • モナドに提供される機能が「プログラミング」と相性がとても良い

モナドを使ってみよう

Maybeモナド(Optionモナド)

値が1つだけある状態 or 値が無い状態 を表すコンテナ

sealed trait Option[A]
case class Some[A](a: A) extends Option[A]
case class None[A]() extends Option[A]

Maybeモナド(Optionモナド)

例えばScala標準のMap.getはOptionを返します

scala> val m = Map(1 -> "a", 2 -> "b")
m: scala.collection.immutable.Map[Int,String] = Map(1 -> a, 2 -> b)

scala> m.get(1)
res0: Option[String] = Some(a)

scala> m.get(0)
res1: Option[String] = None

Maybeモナド(Optionモナド)

Mapから取り出した値に対して何らかの処理をしたい

でも本当に使いたい値はOptionに包まれてる

この中身を、Optionに包まれたまま処理したい

Maybeモナド(Optionモナド)

そんなときはモナドに提供される機能を使います

M[A].map: (A => B) => M[B]

例えば取得した文字列を大文字にする

scala> m.get(1).map(_.toUpperCase)
res0: Option[String] = Some(A)

scala> m.get(0).map(_.toUpperCase)
res1: Option[String] = None

Maybeモナド(Optionモナド)

もし合成する処理の途中で値がなくなる可能性がある場合は

val m1 = Map(1 -> 'a', 2 -> 'b')
val m2 = Map('a' -> "aaa", 'c' -> "ccc")

scala> m1.get(1).flatMap(m2.get _)
res0: Option[String] = Some(aaa)

scala> m1.get(0).flatMap(m2.get _)
res1: Option[String] = None

scala> m1.get(2).flatMap(m2.get _)
res2: Option[String] = None

ちょっと整理

「Option」の機能

値が1つある or 値がない のいずれかを表す代数的データ型

「モナド」の機能

mapやflatMapを使って、
「コンテナに包まれた値に対する処理」を合成する型クラス

「Optionモナド」の機能

値がある限り合成された処理を継続し、
値が無くなった時点で後続の処理は無視する
という実装をされた型クラスのインスタンス

THE END

ごめんなさい。資料ここで力尽きました・・・

時間と元気があれば他のモナドも紹介していきます。