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には事前に署名しておくこと。
- http://www.playframework.com/documentation/2.2.x/Guidelines
- https://github.com/playframework/playframework/blob/master/CONTRIBUTING.md
以下、適当な要約。(英語力皆無かつちゃんと読んでないので、PR出すときには責任をもって各々原文を読んでください)
issue
- ドキュメントのみの修正の場合はissueを上げずPRを出すこと
- バグかどうか確信がもてないときはMLで聞くこと
- 新機能のリクエストについては開発者MLに議題を上げること
- バグを発見した場合は環境やコードを可能な限り詳細にまとめてissueを上げること
ワークフロー
- Typesafe CLAに署名する
- 次のガイドラインに則っているか保証する
- PRを出す。このときすでにissueが上がっているものの場合は、下記の手順でissueをPRに含める
コミットとコミットコメント
- 複数コミットは1コミットにまとめる。詳細はGit flowを参照
- コミットコメントの1行目は簡潔に。ただし、チケット番号だけとか、'minor fix'みたいな適当なやつはだめ。issueがある場合は行末に
#issue番号
を含めること。 - 小さな修正でない場合は、2行目を空行とし、3行目以降に詳細なコメントをリスト形式で書く
- 特定の誰かへのレビューや他のブランチへの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) ) }
作成したプラグインを利用する
プラグイン開発中の検証なども同じ要領。
- publishする
- プロジェクトに追加する
- 読み込む
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
の実装例を吐き出してくれます。