Publicité
Publicité

Contenu connexe

Présentations pour vous(20)

Publicité

Dernier(20)

Publicité

Scala の関数型プログラミングを支える技術

  1. Scala の関数型プログラミング を支える技術 2017/09/09 Scala関西Summit 2017 1 / 59
  2. Whoami Naoki Aoyama Twitter: @AoiroAoino GitHub: @aoiroaoino Monocle コミッター (株)セプテーニ・オリジナル 2 / 59
  3. 3 / 59
  4. 前提 Scala 2.12.3 時点での話です。 また、発表中の「一般的」という表現は静的型付き言語を前提とします。 話さないこと Scala の基本的な文法 教育 歴史や時代背景 Haskell 数学的素養が求められる話 4 / 59
  5. Agenda 関数型プログラミングとは? 関数 参照透過と副作用 純粋関数を用いたプログラミング 多相性 多相性の種類 型クラスとは 実は身近な存在 なぜ、型クラスが重要なのか あれもこれも型クラス 「map が使える」型クラス 「for 式が使える」型クラス まとめ 5 / 59
  6. 関数型プログラミングとは? 6 / 59
  7. 関数型プログラミングとは? 「関数」を用いて行うコーディングスタイル。 値に関数を適用していく事で計算を進めていく。 7 / 59
  8. 関数 8 / 59
  9. 関数 Scala の関数 Scala の関数はファーストクラス。 変数に代入したり、他の関数の引数に渡したり出来る。 val inc = (i: Int) => i + 1 val cl: String => Int => String = (msg: String) => (rep: Int) => msg * rep Scala では、変数/定数定義で型パラメータをとるような値は定義出来ない。メソッ ド定義で代用するので、便宜上def を用いた定義も関数と呼ぶこととする。 def show[A]: A => String = (a: A) => a.toString 9 / 59
  10. 関数 Scala の関数 関数を引数にとったり、返り値として返したりする関数を高階関数と呼ぶ。 val calc: (Int => Int => Int) => Int => Int => Int = (f: Int => Int => Int) => (i: Int) => (j: Int) => f(i)(j) val add: Int => Int => Int = (i: Int) => (j: Int) => i + j val multi: Int => Int => Int = (i: Int) => (j: Int) => i * j scala> calc(add)(1)(2) res15: Int = 3 scala> calc(multi)(1)(2) res16: Int = 2 10 / 59
  11. 関数 参照透過 ここで登場する「関数」は数学的な関数ほぼそのまま。 参照透過とは、関数適用の結果が明示的に与えた引数の値にのみ依存する性質の 事。例えば、関数が同じ引数で二回呼ばれたら同じ値を返す。このような性質を 参照透過性と言い、参照透過な関数を純粋関数と呼ぶ。 val add = (i: Int) => (j: Int) => i + j scala> add(1)(2) res0: Int = 3 scala> add(1)(2) res1: Int = 3 11 / 59
  12. 関数 純粋関数のメリット 純粋関数を用いることで例えば以下のようなメリットを享受出来る。 メンテナンスしやすい 再利用性が高くなる テストしやすい バグが発生しにくい 最適化を行いやすい 並列実行させやすい etc... Scala では関数が純粋であることを強制しない。メリットを得る為に可能な限り純 粋関数であることが望ましい。 12 / 59
  13. 関数 純粋関数のメリット 純粋関数intToChar とcharToString を合成して、新たにintToString という関数 を得られる。純粋関数であるため型で表現される事以外の作用を及ぼさないので、 型を見るだけで実装を推測しやすく、メンテナンス性も得られる。 val intToChar: Int => Char = (i: Int) => i.toChar val charToString: Char => String = (c: Char) => c.toString val intToString = intToChar andThen charToString // or val intToString = charToString compose intToChar scala> intToString(97) res21: String = a 13 / 59
  14. 関数 副作用 参照透過でない、外部に及ぼす/外部の影響を受ける作用(処理)。グローバルな 値が破壊的変更されたり、環境やタイミングによって結果が変わってしまう。 var counter = 100 def incCounter(): Unit = counter = counter + 1 def printCounter(): Unit = println(counter) 14 / 59
  15. 関数 副作用 val now = () => System.currentTimeMillis() scala> now() res1: Long = 1504897705832 scala> now() res2: Long = 1504897708058 val isExistsFile: String => Boolean = (fileName: String) => new java.io.File(fileName).exists scala> isExistsFile("/tmp/file.txt") res24: Boolean = false // $ touch /tmp/file.txt scala> isExistsFile("/tmp/file.txt") res25: Boolean = true 15 / 59
  16. 関数 イミュータブルなデータ構造 破壊的代入や状態の操作など、副作用のある操作を行えない。この純粋関数の考え 方をデータ型に対しても適用するとイミュータブルなデータ構造となる。操作の実 行後は新しい操作後のデータが返され、操作前のデータはそのまま残る。 scala> val l = List(1, 2, 3) l: List[Int] = List(1, 2, 3) scala> l.map(_ * 10) res0: List[Int] = List(10, 20, 30) scala> l res1: List[Int] = List(1, 2, 3) 16 / 59
  17. 多相性 17 / 59
  18. 多相性 関数型言語特有の概念ではない。 しかし、関数型プログラミングを行う上でも重要な要素。 多相性にはいくつか種類がある。 サブタイプ多相 パラメトリック多相 アドホック多相 18 / 59
  19. 多相性 パラメトリック多相 複数の型が型パラメータを使って統一的に表現できる多相。 sealed abstract class List[+A] extends ... { ... } val strList = List("foo", "bar", "baz") val intList = List(1, 2, 3) sealed abstract class Option[+A] extends ... { ... } final case class Some[+A](value: A) extends Option[A] { ... } case object None extends Option[Nothing] { ... } val strOpt = Option("foo") val intOpt = Option(100) 19 / 59
  20. 多相性 パラメトリック多相 Scala では高カインド型も扱うことが出来る。 型コンストラクタをとる型コンストラクタ。 trait Cache[F[_]] { def get[A](key: String): F[A] } class TryCache[Try] { def get[A](key: String): Try[A] = ??? } class OptionCache[Option] { def get[A](key: String): Option[A] = ??? } 20 / 59
  21. 多相性 アドホック多相 ある関数に複数の型の値が与えられるが、それらの型の間に特に関連性がないよう な多相の事を指す。 主にオーバーロードによって実現される。特に演算子の場合は言語の組み込み機能 として提供され、プログラマーが拡張出来ないことがしばしば。 def combine(i: Int, j: Int): Int = i + j def combine(i: String, j: String): String = i + j 21 / 59
  22. 型クラス 22 / 59
  23. 型クラス アドホック多相を実現するためのアプローチ(デザインパターン)。 Scala のimplicit parameter は型クラスを実現する為の機能。 23 / 59
  24. 型クラス implicit parameter 暗黙の値とも。implicit が付いた引数を暗黙のうちに供給される。 def hello(msg: String)(implicit suffix: String): String = msg + suffix scala> hello("John") <console>:13: error: could not find implicit value for parameter suffix: String hello("John") ^ scala> implicit val s: String = "!!!" s: String = !!! scala> hello("John") res1: String = John!!! 24 / 59
  25. 型クラス implicit parameter よく使われる標準機能。 Context Bound 暗黙の値を定義する際の糖衣構文。 def foo[A: Ordering](a: A): Int = ??? 下記のように展開される。 def foo[A](a: A)(implicit x: Ordering[A]): Int = ??? implicitly 暗黙の引数を召喚する為の便利メソッド。scala.Predefより。 @inline def implicitly[T](implicit e: T) = e 25 / 59
  26. 型クラス Ordering(Scala の標準ライブラリより) // List#sorted の場合、Repr は List[A] def sorted[B >: A](implicit ord: Ordering[B]): Repr = { ... } // List#max の場合、A は List の要素の型 def max[B >: A](implicit cmp: Ordering[B]): A = { ... } trait Ordering[T] extends Comparator[T] with PartialOrdering[T] with Serializable { ... def compare(x: T, y: T): Int ... } 26 / 59
  27. 型クラス Ordering(Scala の標準ライブラリより) 自分で定義したデータ sealed abstract class Num(val i: Int) case object Zero extends Num(0) case object One extends Num(1) case object Two extends Num(2) implicit val numOrdering: Ordering[Num] = new Ordering[Num] { def compare(x: Num, y: Num): Int = if (x.i == y.i) 0 else if (x.i < y.i) -1 else 1 } scala> List[Num](One, Two, Zero).max res4: Num = Two scala> List[Num](One, Two, Zero).sorted res6: List[Num] = List(Zero, One, Two) 27 / 59
  28. 型クラス TypeBinder(scalikejdbc 公式ドキュメントより) import scalikejdbc._ import java.sql.ResultSet case class MemberId(id: Long) implicit val memberIdTypeBinder: TypeBinder[MemberId] = new TypeBinder[MemberId] { def apply(rs: ResultSet, label: String): MemberId = MemberId(rs.getLong(label)) def apply(rs: ResultSet, index: Int): MemberId = MemberId(rs.getLong(index)) } val ids: Seq[MemberId] = sql"select id from member".map(_.get[MemberId]("id")).list.apply() case class WrappedResultSet(underlying: ResultSet, cursor: ResultSetCursor, index: //... def get[A: TypeBinder](columnLabel: String): A = { ensureCursor() wrapIfError(implicitly[TypeBinder[A]].apply(underlying, columnLabel)) } } 28 / 59
  29. 型クラス 「文字列に変換する」型クラス trait Show[A] { def show(a: A): String } object Show { implicit val stringShow: Show[String] = new Show[String] { def show(a: String): String = a } implicit val intShow: Show[Int] = new Show[Int] { def show(a: Int): String = a.toString } } 29 / 59
  30. 型クラス 「文字列に変換する」型クラス def show[A](a: A)(implicit sa: Show[A]): String = sa.show(a) scala> show(100) res2: String = 100 scala> show("foo") res3: String = foo scala> show('a') <console>:14: error: could not find implicit value for parameter sa: Show[Char] show('a') ^ 30 / 59
  31. 型クラス 「文字列に変換する」型クラス scala> case class Cat(name: String) defined class Cat scala> implicit val catShow: Show[Cat] = new Show[Cat] { | def show(a: Cat): String = s"${a.name} say, meow!" | } catShow: Show[Cat] = $anon$1@71179b6f scala> show(Cat("moko")) res5: String = moko say, meow! 31 / 59
  32. 型クラス 「なんか結合できる」型クラス trait Additive[A] { def combine(x: A, y: A): A } object Additive { implicit val stringAdditive: Additive[String] = new Additive[String] { def combine(x: String, y: String): String = x + y } } 32 / 59
  33. 型クラス 「なんか結合できる」型クラス def combine[A](a: A, b: A)(implicit aa: Additive[A]): A = aa.combine(a, b) scala> combine("foo", "bar") res10: String = foobar case class Natural(i: Int) implicit val naturalAdditive: Additive[Natural] = new Additive[Natural] { def combine(x: Natural, y: Natural): Natural = Natural(x.i + y.i) } scala> combine(Natural(1), Natural(2)) res0: Natural = Natural(3) 33 / 59
  34. 型クラス Enrich my library implicit conversion を組み合わせることで、あたかも既存のデータ型に対してメ ソッドが定義されているかのように見せかけることが出来る。 implicit class ShowOp[A](a: A) { def show(implicit sa: Show[A]): String = sa.show(a) } scala> 100.show res7: String = 100 scala> Cat("moko").show res8: String = moko say, meow! 34 / 59
  35. 型クラス Enrich my library implicit class AdditiveOp[A](a: A) { def combine(b: A)(implicit aa: Additive[A]): A = aa.combine(a, b) } scala> Natural(1) combine Natural(2) res1: Natural = Natural(3) 35 / 59
  36. 型クラス なぜ、型クラスが重要なのか 以下のようなMyList について考えてみる。 sealed abstract class MyList[+A] case class MyCons[A](val head: A, val tail: MyList[A]) extends MyList[A] case object MyNil extends MyList[Nothing] scala> val l: MyList[Int] = MyCons(1, MyCons(2, MyCons(3, MyNil))) l: MyList[Int] = MyCons(1,MyCons(2,MyCons(3,MyNil))) 36 / 59
  37. 型クラス なぜ、型クラスが重要なのか 主に、オブジェクト指向ではMyList などのデータ構造に対してメソッドを追加す るようなアプローチをとることが多い。 sealed abstract class MyList[+A] { // メソッドの追加 def map(f: A => B): MyList[B] = ??? } 37 / 59
  38. 型クラス なぜ、型クラスが重要なのか 関数型プログラミングでは異なるアプローチをとる。既存のデータ型を型クラスを 用いて操作を追加する。 implicit val mappable: Mappable[MyList] = new Mappable[MyList] { def map[A, B](fa: MyList[A])(f: A => B): MyList[B] = ??? } 38 / 59
  39. 型クラス なぜ、型クラスが重要なのか このような関数型プログラミングのアプローチによって 標準ライブラリや外部ライブラリで定義されたデータ構造の拡張できる 自作データ型の操作を型クラスのインスタンスを定義することで容易に、 そして既存のデータ型と共通の名前、シグネチャの操作を得ることができる データ構造とその操作を分離できる などのメリットを享受できる。 39 / 59
  40. あれもこれも型クラス 40 / 59
  41. あれもこれも型クラス より抽象的な型クラスを考えてみよう。 41 / 59
  42. あれもこれも型クラス 「map が使える」型クラス Option#map scala> Option("Hello").map(s => s + "!!!") res0: Option[String] = Some(Hello!!!) List#map scala> List(1, 2, 3).map(i => i * 10) res2: List[Int] = List(10, 20, 30) 42 / 59
  43. あれもこれも型クラス 「map が使える」型クラス trait Mappable[F[_]] { def map[A, B](fa: F[A])(f: A => B): F[B] } object Mappable { implicit val optionMappable: Mappable[Option] = new Mappable[Option] { def map[A, B](a: Option[A])(f: A => B): Option[B] = a.map(f) } implicit val listMappable: Mappable[List] = new Mappable[List] { def map[A, B](a: List[A])(f: A => B): List[B] = a.map(f) } } 43 / 59
  44. あれもこれも型クラス 「map が使える」型クラス 使い方 scala> implicitly[Mappable[Option]].map(Option(100))(i => i + 1) res3: Option[Int] = Some(101) scala> implicitly[Mappable[List]].map(List(1, 2, 3))(i => i + 1) res4: List[Int] = List(2, 3, 4) 44 / 59
  45. あれもこれも型クラス for 式再説 Scala のfor 式は糖衣構文。 val fooOpt: Option[Int] = ... val barOpt: Option[Int] = ... for { foo <- fooOpt bar <- barOpt } yield foo + bar 以下のように展開される。 fooOpt.flatMap(foo => barOpt.map(bar => foo + bar ) ) 45 / 59
  46. あれもこれも型クラス for 式再説 for 式の展開には他にもルールがある。compiler の型付け前に展開されるので、 必ず全ての展開対象メソッドを実装している必要はない。 abstract class C[A] { def map[B](f: A => B): C[B] def flatMap[B](f: A => C[B]): C[B] def withFilter([: A => Boolean): C[A] def foreach(b: A => Unit): Unit } 46 / 59
  47. あれもこれも型クラス 「for 式が使える」型クラス 便宜上map, flatMap のみを考える。 trait CanWriteForSyntax[F[_]] { def map[A, B](fa: F[A])(f: A => B): F[B] def flatMap[A, B](fa: F[A])(f: A => F[B]): F[B] } 例えば、Option のインスタンスは以下の通り。 implicit val optionCanWriteForSyntax: CanWriteForSyntax[Option] = new CanWriteForSyntax[Option] { def map[A, B](fa: Option[A])(f: A => B): Option[B] = fa.map(f) def flatMap[A, B](fa: Option[A])(f: A => Option[B]): Option[B] = fa.flatMap(f) } 47 / 59
  48. あれもこれも型クラス 「for 式が使える」型クラス 標準機能で求められるmap, flatMap とはシグネチャが異なるので、実際に使う場 合にはimplicit conversion と組み合わせる必要がある。 implicit class CanWriteForSyntaxOp[F[_], A](fa: F[A])(implicit x: CanWriteForSyntax def map[B](f: A => B): F[B] = x.map(fa)(f) def flatMap[B](f: A => F[B]): F[B] = x.flatMap(fa)(f) } 48 / 59
  49. あれもこれも型クラス 「for 式が使える」型クラス def foo[F[_]: CanWriteForSyntax, A, B](fa: F[A]): F[String] = for { a <- fa } yield a.toString scala> foo(Option(100)) res6: Option[String] = Some(100) 49 / 59
  50. あれもこれも型クラス 「for 式が使える」型クラス もちろん、自作のデータ型に対しても定義出来る。 case class Response[A](value: A) implicit val responseCanWriteForSyntax: CanWriteForSyntax[Response] = new CanWriteForSyntax[Response] { def map[A, B](fa: Response[A])(f: A => B): Response[B] = fa.copy(value = f(fa.value)) def flatMap[A, B](fa: Response[A])(f: A => Response[B]): Response[B] = fa match { case Response(v) => f(v) } } scala> foo(Response(100)) res13: Response[String] = Response(100) 50 / 59
  51. あれもこれも型クラス 「for 式が使える」型クラス Cache に対する操作を考えてみる。 abstract class SyncCache { def get[A](key: String): Try[A] def set[A](key: String, value: A): Try[Unit] def update[A](key: String, f: A => A): Try[Unit] } abstract class AsyncCache { def get[A](key: String): Future[A] def set[A](key: String, value: A): Future[Unit] def update[A](key: String, f: A => A): Future[Unit] } 51 / 59
  52. あれもこれも型クラス update という処理はget とset という二つの関数をmap, flatMap による合成で実 装できる。 class SyncCache { def get[A](key: String): Try[A] def set[A](key: String, value: A): Try[Unit] def update[A](key: String, f: A => A): Try[Unit] = for { v <- get[A](key) _ <- set(key, f(v)) } yield () } class AsyncCache { def get[A](key: String): Future[A] def set[A](key: String, value: A): Future[Unit] def update[A](key: String, f: A => A): Future[Unit] = for { v <- get[A](key) _ <- set(key, f(v)) } yield () } 52 / 59
  53. あれもこれも型クラス 返り値の型をF[_] という型パラメータをとる型パラメータに包むことで共通化でき る。ただし、update ではF[_] がmap, flatMap が定義されていることを知らない (保証出来ない)。 abstract class Cache[F[_]] { def get[A](key: String): F[A] def set[A](key: String, value: A): F[Unit] // compile 出来なくなる def update[A](key: String, f: A => A): F[Unit] = for { v <- get[A](key) _ <- set(key, f(v)) } yield () } class SyncCache extends Cache[Try] { ... } class AsyncCache extends Cache[Future] { ... } 53 / 59
  54. あれもこれも型クラス F[_] にCanWriteForSyntax 型クラスのインスタンスであるという制約をかけれ ば、map, flatMap の実装を持つことが保証されるのでcompile 出来る。※実際 にはCanWriteForSyntaxOp の定義も必要。 abstract class Cache[F[_]: CanWriteForSyntax] { ... } implicit val tryCanWriteForSyntax: CanWriteForSyntax[Try] = new CanWriteForSyntax[Try] { def map[A, B](fa: Try[A])(f: A => B): Try[B] = fa.map(f) def flatMap[A, B](fa: Try[A])(f: A => Try[B]): Try[B] = fa.flatMap(f) } implicit def futureCanWriteForSyntax(implicit ec: ExecutionContext): CanWriteForSyntax new CanWriteForSyntax[Future] { def map[A, B](fa: Future[A])(f: A => B): Future[B] = fa.map(f) def flatMap[A, B](fa: Future[A](f: A => Future[B]): Future[B] = fa.flatMap(f) } 54 / 59
  55. あれもこれも型クラス ただし 実用的にはTry やFuture のままでも十分であることが多い。 型クラスを介した実装の解決が行われる為、オーバーヘッドがある。 55 / 59
  56. まとめ 56 / 59
  57. 紹介したもの Scala の言語機能 Function implicit parameter implicitly Context Bound implicit conversion 型パラメータ 高カインド型 for 式 etc... 57 / 59
  58. 紹介したもの デザインパターン(のようなもの) 型クラス Enrich my library 58 / 59
  59. 最後に 関数プログラミングとは何か? 純粋関数を定義するモチベーションとメリット 改めて意識するとより抽象的に書けてボイラープレートが減らせる Scala でFP を意識する際に覚えておきたい機能の紹介 関数型プログラミング楽しい 59 / 59
Publicité