Skip to content

Instantly share code, notes, and snippets.

@gakuzzzz
Last active February 12, 2024 02:05
Show Gist options
  • Star 30 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save gakuzzzz/6cbf407928fbc637c64a371bcc20f050 to your computer and use it in GitHub Desktop.
Save gakuzzzz/6cbf407928fbc637c64a371bcc20f050 to your computer and use it in GitHub Desktop.
Design Patterns in Scala (ScalaMatsuri 2018 Unconfoerence)

Design Patterns in Scala

Scala でよく見るパターン

Type-Safe Builder

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

型クラス(Concept Pattern)

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)

Loan Pattern

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"

à la carte import

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 {
  ...
}

Aux Pattern

https://www.slideshare.net/TaisukeOe/auxdotty

https://dev.oro.com/posts/2017/07/programming/aux-pattern/

GoFその他の有名パターンがScalaではどう扱われるか

Singleton

object 使いましょう。

Visitor

sealed trait または sealed abstract class と パターンマッチ使いましょう。

Factory Method

Companion object の apply がよく使われます。

Null Object Pattern

Null Object Pattern と Option はモチベーションが違います。

Option は利用者側で値が存在しないことを強制的に意識させるためのもの。

Null Object Pattern は利用者側に値が存在しないことを意識させないようにするためのもの。

まだ要検証なパターン

The magnet pattern

http://yuroyoro.hatenablog.com/entry/2013/01/23/192244

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment