Javaで多引数のコンストラクタを避けるために使われる Builder パターンは、Scala では名前付き引数呼び出しが使えるので必要ありません。
Builderパターンに制約を加えて条件に合わない場合、コンパイルエラーにしたい。
引用元: http://tototoshi.hatenablog.com/entry/20120602/1338624041
case class Recipe[HasOlive](ingredients: Map[String, Int])
sealed trait HasOlive
trait Oliveあり extends HasOlive
trait Oliveなし extends HasOlive
type NotMocoRecipe = Recipe[Oliveなし]
type MocosRecipe = Recipe[Oliveあり]
object MocosRamenBuilder {
def apply(): MocosRamenBuilder[Oliveなし] = new MocosRamenBuilder()
}
class MocosRamenBuilder[T <: HasOlive] private(
recipe: Recipe[HasOlive] = Recipe(Map.empty[String, Int].withDefaultValue(0))
) {
type ThisRecipe = Recipe[T]
type This = MocosRamenBuilder[T]
def men(num: Int): This =
new MocosRamenBuilder(
recipe.copy(ingredients = recipe.ingredients + ("men" -> (recipe.ingredients("men") + num)))
)
def soup(num: Int): This =
new MocosRamenBuilder(
recipe.copy(ingredients = recipe.ingredients + ("soup" -> (recipe.ingredients("soup") + num)))
)
def olive(num: Int): MocosRamenBuilder[Oliveあり] =
new MocosRamenBuilder(
recipe.copy(ingredients = recipe.ingredients + ("olive oil" -> (recipe.ingredients("olive oil") + num)))
)
def cook()(implicit HAS_OLIVE_OIL: ThisRecipe =:= MocosRecipe): Unit = {
println("オリーブオイルラーメン!!!: olive oil: %d".format(recipe.ingredients("olive oil")))
}
}
コンパイル結果
MocosRamenBuilder().men(1).soup(1).cook()
t-takahashi@/Users/t-takahashi/tmp% scala KataAnzenMoco.scala
/Users/t-takahashi/tmp/KataAnzenMoco.scala:43: error: Cannot prove that this.Recipe[this.Oliveなし] =:= this.Recipe[this.Oliveあり].
MocosRamenBuilder().men(1).soup(1).cook()
^
one error found
MocosRamenBuilder().men(1).soup(1).olive(100).cook()
t-takahashi@/Users/t-takahashi/tmp% scala KataAnzenMoco.scala
オリーブオイルラーメン!!!: olive oil: 100
Ad-hoc polymorphism を実現した。
trait Comparator[A]
def sort[A](values: Seq[A])(implicit comp: Comparator[A]): Seq[A] = ...
case class MyCoolClass(...)
object MyCoolClass {
implicit val comparator: Comparator[MyCoolClass] = ...
}
val list: Seq[MyCoolClass] = ...
val sorted = sort(list)
http://www.ne.jp/asahi/hishidama/home/tech/scala/sample/using.html
def using[A, B](resource: A)(func: A => B)(implicit closer: Closer[A]) =
try {
func(resource)
} finally {
closer(resource)
}
trait Closer[-A] {
def apply(resource: A)
}
object Closer {
def apply[A](f: A => Unit) = new Closer[A] {
def apply(resource: A) = f(resource)
}
}
implicit val IoSourceCloser = Closer[io.Source] (_.close)
implicit val disposer = Closer[{def dispose()}](_.dispose)
implicit val closer = Closer[{def close()}] (_.close)
import java.io._
using(new FileInputStream("source.txt")) { in =>
using(new InputStreamReader(in, "UTF-8")) { reader =>
using(new BufferedReader(reader)) { buf =>
using(new FileOutputStream("dest.txt")) { out =>
using(new OutputStreamWriter(out, "UTF-8")) { writer =>
using(out.getChannel.lock()) { lock =>
var line = buf.readLine()
while (line != null) {
writer.write(line)
line = buf.readLine()
}
}
}
}
}
}
}
継続モナドによるネストの除去
https://qiita.com/jwhaco/items/224113324fd454b8ca77
package continuation
trait Callback[A] {
def apply[R](f: A => R): R
}
class Continue[+A] private(callback: Callback[A]) {
def map[B](f: A => B): Continue[B] =
Continue apply new Callback[B] {
override def apply[R](g: B => R): R = {
callback(f andThen g)
}
}
def flatMap[B](f: A => Continue[B]): Continue[B] =
Continue apply new Callback[B] {
override def apply[R](g: B => R): R = {
callback(a => f(a) apply g)
}
}
def apply[R](f: A => R): R = callback(f)
def run(): A = apply(identity)
}
object Continue {
def apply[A](f: Callback[A]): Continue[A] = {
new Continue(f)
}
def from[A](a: A): Continue[A] = {
Continue apply new Callback[A] {
override def apply[R](f: A => R): R = f(a)
}
}
}
object Close {
def apply[A: Closer](res: => A): Continue[A] =
Continue apply new Callback[A] {
override def apply[R](f: A => R): R = {
val target = res
try f(target)
finally implicitly[Closer[A]] close target
}
}
}
Scalaz の Cont
とは型表現がちょっと異なります。
val foo = for {
in <- Close(new FileInputStream(getClass.getResource("/source.txt").getPath))
reader <- Close(new InputStreamReader(in, "UTF-8"))
buff <- Close(new BufferedReader(reader))
out <- Close(new FileOutputStream("dest.txt"))
writer <- Close(new OutputStreamWriter(out, "UTF-8"))
} yield {
...
"hello"
}
println(foo.run()) // "hello"
implicit value を import するにあたり、不必要な implicit value を scope に増やしてコンパイル時間の増大を招く事を避けるために、利用者側で import 単位をコントロールできるようにする方法。
Scalaz とかで使われてます。
package my.cool.library
trait FooImplicits {
implicit val foobar = ...
implicit val foobaz = ...
}
trait BazImplicits {
implicit val bazhoge = ...
implicit val bazpiyo = ...
}
object all extends FooImplicits with BazImplicits
object foo extends FooImplicits
object bar extends BazImplicits
import my.cool.library.all._
or
import my.cool.library.foo._
利用側で trait を mixin するのはバイナリ互換性等で問題がでるのでアンチパターン
// NG
class MyApplicationService extends FooImplicits {
...
}
https://www.slideshare.net/TaisukeOe/auxdotty
https://dev.oro.com/posts/2017/07/programming/aux-pattern/
object
使いましょう。
sealed trait
または sealed abstract class
と パターンマッチ使いましょう。
Companion object の apply
がよく使われます。
Null Object Pattern と Option
はモチベーションが違います。
Option
は利用者側で値が存在しないことを強制的に意識させるためのもの。
Null Object Pattern は利用者側に値が存在しないことを意識させないようにするためのもの。