scalaの多相いろいろ

scalaでのいろいろな多相の実装方法とそのpimp my library pattern版をいろいろ書いて比較してみたいと思います。

オーバーロード

これも一応多相と言うらしい。

// オーバーロードによる多相
class John
class Taro

def greet(john: John) {
  println("My name is John!")
}

def greet(taro: Taro) {
  println("太郎です")
}

val john = new John()
greet(john)

val taro = new Taro()
greet(taro)

継承

一般的にはこれをポリモーフィズムと呼ぶことが多いと思います。

// 継承による多相
trait Greeter {
  def greet()
}

class John extends Greeter {
  def greet() {
    println("My name is John!")
  }
}

class Taro extends Greeter {
  def greet() {
    println("太郎です")
  }
}

val john = new John()
john.greet()

val taro = new Taro()
taro.greet()

pimp my library pattern

継承関係になくてもメソッドのように扱うことができる。

// pimp my library
class John
class Taro

trait Greeter {
  def greet()
}

implicit class JohnGreeter(john: John) extends Greeter {
  def greet() {
    println("My name is John!")
  }
}

implicit class TaroGreeter(taro: Taro) extends Greeter {
  def greet() {
    println("太郎です")
  }
}

val john = new John()
john.greet()

val taro = new Taro()
taro.greet()

型クラス

オーバーロードに近いが、関数自体の定義は1カ所のみ。

他のサンプルと挙動を合わせるためにdef greet[A: Greeter](a: A)aは利用していないが、say(a: A)として定義すればaによって挙動を変えることもできる。

// 型クラスによる多相
class John
class Taro

trait Greeter[A] {
  def say()
}

implicit val johnGreeter = new Greeter[John] {
  def say() {
    println("My name is John!")
  }
}

implicit val taroGreeter = new Greeter[Taro] {
  def say() {
    println("太郎です")
  }
}

def greet[A: Greeter](a: A) {
  implicitly[Greeter[A]].say()
}

val john = new John()
greet(john)

val taro = new Taro()
greet(taro)

型クラスのpimp my library patternバージョン。implicit classとcontext boundを使うと結構きれいに書ける。

// 型クラスによる多相(pimp my library pattern)
class John
class Taro

trait Greeter[A] {
  def say()
}

implicit class GreeterOps[A: Greeter](a: A) {
  def greet() {
    implicitly[Greeter[A]].say()
  }
}

implicit val johnGreeter = new Greeter[John] {
  def say() {
    println("My name is John")
  }
}

implicit val taroGreeter = new Greeter[Taro] {
  def say() {
    println("太郎です")
  }
}

val john = new John()
john.greet()

val taro = new Taro()
taro.greet()

型クラス(dependent method types)

型クラスをそのまま書くと戻り値の型は任意の型にできないが(間違ってたらすいません)、dependent method typesを利用することで戻り値の型を任意にすることができる。ただし、context boundで書くことができなくなる。

// 型クラスによる多相(dependent method types)
class John
class Taro

trait Greeter[A] {
  type Result
  def say(): Result
}

implicit val johnGreeter = new Greeter[John] {
  type Result = Unit
  def say(): Result = println("My name is John!")
}

implicit val taroGreeter = new Greeter[Taro] {
  type Result = Unit
  def say(): Result = println("太郎です")
}

def greet[A](a: A)(implicit ev: Greeter[A]): ev.Result = ev.say()

val john = new John()
greet(john)

val taro = new Taro()
greet(taro)

magnet pattern

以下の例ではメリットを感じにくいが、これもdependent method typesによって引数の型によって処理と戻り値の型をばらばらにできる。

また、type erasureによる衝突を回避できる。元記事はこちら。

ほぼほぼ型クラスのdependent method types版と同じで、implicit conversionを使うかimplicit parameterを使うか、という感じ。

// magnet patternによる多相
class John
class Taro

trait GreetingMagnet {
  type Result
  def apply(): Result
}

object GreetingMagnet {
  implicit class JohnGreetingMagnet(john: John) extends GreetingMagnet {
    type Result = Unit
    def apply(): Result = println("My name is John!")
  }

  implicit class TaroGreetingMagnet(taro: Taro) extends GreetingMagnet {
    type Result = Unit
    def apply(): Result = println("私は太郎です")
  }
}

def greet(magnet: GreetingMagnet): magnet.Result = magnet()

val john = new John()
greet(john)

val taro = new Taro()
greet(taro)

magnet patternとpimp my library patternの組み合わせ。だいぶごつごつしてしまった。

// magnet patternによる多相
class John
class Taro

trait GreetingMagnet {
  type Result
  def apply(): Result
}

object GreetingMagnet {
  def apply(john: John): GreetingMagnet = new GreetingMagnet {
    type Result = Unit
    def apply(): Result = println("My name is John!")
  }

  def apply(taro: Taro): GreetingMagnet = new GreetingMagnet {
    type Result = Unit
    def apply(): Result = println("私は太郎です")
  }
}

class GreetingMagnetOps(val magnet: GreetingMagnet) {
  def greet(): magnet.Result = magnet()
}

implicit def john2GreetingMagnetOps(john: John): GreetingMagnetOps =
  new GreetingMagnetOps(GreetingMagnet(john))

implicit def taro2GreetingMagnetOps(taro: Taro): GreetingMagnetOps =
  new GreetingMagnetOps(GreetingMagnet(taro))

val john = new John()
john.greet()

val taro = new Taro()
taro.greet()

比較

パターン 多相 継承関係 戻り値の型変更 定義箇所
オーバーロード アドホック 不要 N
継承 部分型 必要 不可 1
pimp my library pattern アドホック 不要 一部可 N
型クラス アドホック 不要 一部可 1
型クラス(dependent method types) アドホック 不要 1
magnet pattern アドホック 不要 1

まとめ

従来のオブジェクト指向デザインパターンとは違ったことができるので、何でもできる一方で設計力が試されますね。

ざっくりとした知識で書いているので、間違っていたらご指摘いただければ!

playframeworkにpull requestを出す

メモ。あとから追記していきます。

基本

以下のドキュメントをよく読む。TCLAには事前に署名しておくこと。

以下、適当な要約。(英語力皆無かつちゃんと読んでないので、PR出すときには責任をもって各々原文を読んでください)

issue

  • ドキュメントのみの修正の場合はissueを上げずPRを出すこと
  • バグかどうか確信がもてないときはMLで聞くこと
  • 新機能のリクエストについては開発者MLに議題を上げること
  • バグを発見した場合は環境やコードを可能な限り詳細にまとめてissueを上げること

ワークフロー

  1. Typesafe CLAに署名する
  2. 次のガイドラインに則っているか保証する
    • DRY原則ボーイスカウトルールを遵守する
    • 包括的なテストを行う
    • コードのドキュメント性
    • ロックとかスレッドローカルとかいろいろ気をつける
    • javaとscalaのためのフレームワークなので両方に配慮する
    • 新規ファイルにはコピーライトを含める
  3. PRを出す。このときすでにissueが上がっているものの場合は、下記の手順でissueをPRに含める

コミットとコミットコメント

  1. 複数コミットは1コミットにまとめる。詳細はGit flowを参照
  2. コミットコメントの1行目は簡潔に。ただし、チケット番号だけとか、'minor fix'みたいな適当なやつはだめ。issueがある場合は行末に#issue番号を含めること。
  3. 小さな修正でない場合は、2行目を空行とし、3行目以降に詳細なコメントをリスト形式で書く
  4. 特定の誰かへのレビューや他のブランチへのcherry-pickが必要な場合は追記する

ビルド

これを参照しておく。

cd framework
./build

するとsbtが立ち上がるので以下のコマンドを入力。

compile
test
publish-local

ドキュメントの修正

documentation/manual以下がドキュメントとなる。

書き方は以下を参照する。

修正したら以下コマンドで確認を行う。

cd playframework/documentation
./build run # ドキュメントサーバの起動
./build test # テスト
./build validate-docs # バリデーション

このとき

unresolved dependency for com.typesafe.play#sbt-plugin;2.3-SNAPSHOT

というエラーが出る場合は、上記のplayframeworkのビルド手順を実施すること。 (参考: https://groups.google.com/forum/#!topic/play-framework/zfKRKntj4vA)

sbtプラグインを作る

開発メモ。

20141002追記

sbtのドキュメントのリンクを最新のものに変更しました。 また0.13.5以降のAuto Pluginの説明には対応してないので、気が向いたら追記します。

作り方

以下の公式ドキュメントがわかりやすい。

必要なのは以下の2点のみ。

  • build.sbtにplugin用設定を書く
  • Pluginを継承したObjectを定義する
plugin用のビルド定義ファイルを用意する

おそらく最低限必要なのは以下の内容。

// build.sbt

sbtPlugin := true

name := "sample-plugin"

organization := "com.example.sample"
Pluginを継承したObjectを定義する

定義方法は2種類あり、Taskを利用する方法と、Commandを利用する方法。

全部読んでないけど、単純な処理はTaskを使って、Taskではできない処理(Stateを利用して、sbtの設定を参照/変更するなど)はCommandを利用する、という使い分けでよさそう。

以下は"Hello SBT World!"と表示するsampleCommandというCommandを定義する例。

// SamplePlugin.scala

package com.example.sample

import sbt._
import Keys._

object SamplePlugin extends Plugin {
  override lazy val settings = Seq(
    commands ++= Seq(
      sample
    )
  )

  lazy val sample = Command.command("sampleCommand") { state =>
    println("Hello SBT World!")
    state
  }
}

同じことをするsampleTaskというTaskの例。こっちではsampleSettingというSettingを定義しているとおり、sbtコンソールやビルド定義ファイルから参照や変更ができる。

// SamplePlugin.scala

package com.example.sample

import sbt._

object SamplePlugin extends Plugin {
  val sampleTask = taskKey[Unit]("Sample task")
  val sampleSetting = settingKey[String]("Sample setting")

  override val settings = Seq(
    sampleSetting := "Hello SBT World!",
    sampleTask := println(sampleSetting.value)
  )
}

作成したプラグインを利用する

プラグイン開発中の検証なども同じ要領。

  1. publishする
  2. プロジェクトに追加する
  3. 読み込む
1. publishする

プラグインのプロジェクトでsbtコンソールを開き、以下を実行。 publishLocalはローカルのivyリポジトリに作成したプラグインを登録しているため、参照は開発環境内でのみ解決可能となる。

> compile
> publishLocal
2. プロジェクトに追加する

プラグインを利用したいプロジェクトにプラグインを追加する。 例として作成したプラグインの場合だとバージョンは自動的に"0.1-SNAPSHOT"となる。

// project/plugins.sbt

addSbtPlugin("com.example.sample" % "sample-plugin" % "0.1-SNAPSHOT")

グローバルプラグインとして登録する場合も同じ要領で記述場所が違うだけ。

// ~/.sbt/0.13/plugins/build.sbt

addSbtPlugin("com.example.sample" % "sample-plugin" % "0.1-SNAPSHOT")
3. 読み込む

プラグインを利用したいプロジェクトのsbtコンソールで以下を実行。

> reload plugins

Command名を変更する場合はreload pluginsだけでは解決できず、sbt自体を再実行する必要があるようでちょっとハマった。

giter8テンプレート

すでにあるような気がするけど、wikiに載ってなさそうだったので作ってみた。

g8 akiomik/sbt-pluginを実行すれば上記のsampleCommandの実装例を吐き出してくれます。