ScalazのTagged typeとshapelessのNewtype

Scalaz Advent Calendarの二日目です。

Tagged type

ある型クラスのインスタンスが同じ型に対して複数定義されている場合、importによって使用するインスタンスを制御することができます。

object s1 extends App {
  type User = String

  object Hatena {
    import scalaz.Show
    implicit val HatenaUserShow: Show[User] = Show.shows("id:" + _)
  }

  object Twitter {
    import scalaz.Show
    implicit val TwitterUserShow: Show[User] = Show.shows("@" + _)
  }

  locally {
    import scalaz.syntax.show._
    import Hatena._

    "kxbmap".println  //> id:kxbmap
  }

  locally {
    import scalaz.syntax.show._
    import Twitter._

    "kxbmap".println  //> @kxbmap
  }
}

しかし、この方法では同一スコープ内で複数のインスタンスを利用することはできません。

そこでScalazではTagged typeというものが導入されています。 これはある型を別の型として(type User = Stringのようにただ別名をつけるだけではなく)コンパイラに見せるものです。

object s2 extends App {
  import scalaz._, Scalaz._

  type User = String

  sealed trait Hatena
  sealed trait Twitter

  def HatenaUser(id: User): User @@ Hatena = Tag(id)
  def TwitterUser(id: User): User @@ Twitter = Tag(id)

  implicit val HatenaUserShow: Show[User @@ Hatena] = Show.shows("id:" + _)
  implicit val TwitterUserShow: Show[User @@ Twitter] = Show.shows("@" + _)

  HatenaUser("kxbmap").println  //> id:kxbmap

  TwitterUser("kxbmap").println //> @kxbmap

  val s = "kxbmap"
  assert(s eq HatenaUser(s))
  assert(s eq TwitterUser(s))
}

User @@ HatenaUser @@ Twitterというのがそれです。UserにそれぞれHatenaTwitterというタグを付け別の型に見せることで同じ型クラスのインスタンスを別々に供給することができています。 また、型が変わっても実行時の値はそのままであることがassertで確認できます。*1

しかし、Tagged typeは元の型に対する操作をそのまま受け付けるので次のようなことができてしまいます。

(HatenaUser("kxbmap") + TwitterUser("kxbmap")).println  //> "kxbmapkxbmap"

今回の使用例ではあまり嬉しくない動作です。

Newtype

shapelessというライブラリには、この問題を解決するNewtypeというものがあります。 元々Tagged typeもshapeless作者のMiles Sabinさんが考案したものです。(のでshapelessにもTagged typeは定義されています)

object s3 extends App {
  import shapeless.TypeOperators._

  type User = String

  trait Printable extends Any {
    def shows: String
    def println: Unit = Console.println(shows)
  }

  implicit class HatenaOps(val id: User) extends AnyVal with Printable {
    def shows: String = "id:" + id
  }

  implicit class TwitterOps(val id: User) extends AnyVal with Printable {
    def shows: String = "@" + id
  }

  def HatenaUser(id: User): Newtype[User, HatenaOps] = newtype[User, HatenaOps](id)
  def TwitterUser(id: User): Newtype[User, TwitterOps] = newtype[User, TwitterOps](id)

  HatenaUser("kxbmap").println  //> id:kxbmap

  TwitterUser("kxbmap").println //> @kxbmap

  val s = "kxbmap"
  assert(s eq HatenaUser(s))
  assert(s eq TwitterUser(s))
}

型クラス関係なくなっちゃいましたが。

Newtype[A, Ops]で、Opsに定義されたメソッドを持ち、実体はAのままの別の型を表します。 newtypeA => Newtype[A, Ops]の変換をしてくれる関数です。

Tagged typeでは動作した以下のコードはNewtypeではコンパイルエラーになります。

HatenaUser("kxbmap") + TwitterUser("kxbmap")

他のStringのメソッドも呼び出せません。やったね。

Scalaz + shapeless

NewtypeOpsをただのマーカートレイトにして、例えばNewtype[User, Hatena]に対してScalazの型クラスのインスタンスを定義するという方法もありだと思います。 その場合はimportするものに気をつけないとTagged typeなどは衝突してしまうので注意が必要です。

build.sbt

今回使用したbuid.sbtです。

scalaVersion := "2.10.0-RC3"

libraryDependencies ++= Seq(
  "org.scalaz" % "scalaz-core" % "7.0.0-M5",
  "com.chuusai" % "shapeless" % "1.2.3"
) map (_ cross CrossVersion.full)

*1:eqJavaでいう==で参照の比較です

Squirrelの関数を持ち上げる

前回の続きです。
Squirrelの関数をcurry化してどうすんのって話ですが、関数を持ち上げるのに使ってみます。 というよりそれをやりたくて必要になったのでcurryを書いたのでした。

関数を持ち上げるとは?普通の関数を文脈の中で作用する関数に変換することです。
例えば、前に書いたOptionは、成功(Some)かもしれないし失敗(None)かもしれない、という文脈を表します。
その中で作用する関数とはどういうものでしょうか。実際書いてみます。

function add(a, b) {
  return a + b;
}
local r5 = add(1, 2);  // r5 = 3

addM <- liftM(add);

local r6 = addM(Some(1), Some(2));  // r6 = Some(3)

local r7 = addM(Some(1), None);  // r7 = None

addは普通の関数ですが、liftM関数によって文脈を持った値に適用できるaddMに持ち上げられています。 そこにOptionの値を渡すと、両方成功の場合は成功の結果が返り、失敗が含まれると結果も失敗となります。

型を見てみると、addの型は(Scala風に書くと) (A, B) => CaddM(M[A], M[B]) => M[C] ですね。 よってここで欲しい関数であるliftMの型は ((A, B) => C) => (M[A], M[B]) => M[C] となります。 このような関数を定義できればいい訳です。

また文脈といっても色々ある訳ですが、ここではmapflatMapが適切に実装されているもののこととします。 適切な実装とかの詳しいことは略です。

さて、mapflatMapがあればこう書けますね。2引数関数用なので名前はliftM2とします。

// ((A, B) => C) => (M[A], M[B]) => M[C]
function liftM2(f) {
  local cf = curry2(f);
  return function (ma, mb) : (cf) {
    return ma.map(cf)
      .flatMap(mb.map.bindenv(mb));
  }
}

ここで前回作ったcurry2が出てきます。文脈の中の値に1個ずつ関数を適用していきたいので、そのためには関数がカリー化されている必要がある訳です。
最初からliftM2にカリー化された関数(A => B => C)を渡すことにしてもいいんですが、普通にSquirrelを書く上では普通の関数の方が書きやすいのでカリー化は内部でやることにしました。

さて、liftM2はカリー化した関数cfmap/flatMapを使って順にma,mbに適用する関数を返します。この部分です。

function (ma, mb) : (cf) {
  return ma.map(cf)
    .flatMap(mb.map.bindenv(mb));
}

最初のaddの例を使ってこの関数がちゃんと期待した型になっているか見てみます。 まずaddcurry2でカリー化されているのでcfは次のようになっているイメージです。

// 型はA => B => C
local cf = function (a) {
  function (b) : (a) {
    return a + b;
  }
}

そしてliftM2が返す関数の中では、まずmaの中身にmapcfを適用します。
先の例ではmaSome(1)を渡しているので、そう置き換えていくと

return ma.map(cf)
  .flatMap(mb.map.bindenv(mb));
// ↓
return Some(1).map(function (a) { /* 略 */ })
  .flatMap(mb.map.bindenv(mb));
// ↓
return Some(function (b) { return 1 + b; })
  .flatMap(mb.map.bindenv(mb));

と、こうなります。mapの結果、Someの中身がcfを部分適用した関数になっているのが分かると思います。 型で言えば Option[A]A => B => C でmapしたので、Option[B => C] になっています。
そしてflatMapで中身の関数にmb.mapを適用します。ここでmbSome(2)です。

// ↓
return mb.map(function (b) { return 1 + b; });
// ↓
return Some(2).map(function (b) { return 1 + b; });
// ↓
return Some(1 + 2);

はい。Some(1)Some(2)を渡してSome(3)が得られました。 確かにこの関数は (M[A], M[B]) => M[C] という型になっているようです。MOptionA,B,C は全部 integer ですね。例が悪かったですね。

ともあれ、引数2つのliftM2は出来たので次は引数3つを書きましょう。

// ((A, B, C) => D) => (M[A], M[B], M[C]) => M[D]
function liftM3(f) {
  local cf = curry3(f);
  return function (ma, mb, mc) : (cf) {
    return ma.map(cf)
      .flatMap(mb.map.bindenv(mb))
      .flatMap(mc.map.bindenv(mc));
  }
}

引数を増やしてcurry3を使ってflatMapを一段追加して…… liftM4も引数を増やしてcurry4を使ってflatMapを一段追加して……
これは抽象化できそうです。

まず文脈内の関数を文脈内の値に適用するap関数を用意します。

// (M[A => B], M[A]) => M[B]
local ap = function (mf, m) {
  return mf.flatMap(m.map.bindenv(m));
}

カリー化関数は引数の数をキーにしてテーブルにまとめておきます。

local curryN = {
  [1] = function (f) { return f; }
};
local rt = getroottable();
for (local i = 2; "curry" + i in rt; ++i) {
  curryN[i] <- rt["curry" + i];
}

引数1つの関数というのは既にカリー化されていると言えるので、curry[1]には恒等関数を入れておきます。

すると、liftMを以下のように定義できます。

function liftM(f) : (ap, curryN) {
  return function (...) : (f, ap, curryN) {
    local r = vargv[0].map(curryN[vargc](f));
    for (local i = 1; i < vargc; ++i) {
      r = ap(r, vargv[i]);
    }
    return r;
  }
}

liftMは関数fを引数にとり、そのまま可変長引数を取る関数を返します。 その関数は呼び出し時の引数の数に合わせてfをカリー化、map/apを使って文脈内の値に関数を適用していき最終的な値を返します。

使ってみます。

function product(a, ...) {
  local r = a;
  for (local i = 0; i < vargc; ++i) {
    r *= vargv[i];
  }
  return r;
}

productM <- liftM(product);

local r8 = productM(Some(2), Some(5), Some(9));  // r8 = Some(90)

local r9 = productM(Some(4), None);  // r9 = None

local r10 = productM(Some(1));  // r10 = Some(1)

productM(Some(1), Some(2), Some(3), Some(4), Some(5));  // Error! curry5がない

元が可変長引数を取る関数のproductMは、curryNが定義されている範囲内で同様に可変長引数を取る関数として扱えます。

またOption以外でもmap/flatMapが定義されていればよいので、例えばarrayをラップするListクラスを定義すればそのまま次のような計算に使えます。

local r11 = productM(List([2, 3]), List([4, 5, 6]));  // r11 = List([8, 10, 12, 12, 15, 18])

local r12 = productM(List([2, 3]), List([]), List([4, 5, 6]));  // r12 = List([])

非常に便利、なのですが…… 例によって使う側で型を合わせなければ実行時エラーになるのはどうしようもないです。

Squirrelでカリー化

これの資料自体は結構古かったですね。部分適用の特殊な場合をカリー化としている例は今まで見たことなかったのでつい反射的に。
カリー化そのものについては以下のリンクを参照してください。丸投げです。

ではSquirrel 2.xで関数をカリー化する関数を書いてみます。

function curry2(f) {
  return function (a) : (f) {
  return function (b) : (f, a) {
    return f(a, b);
  }}
}

function curry3(f) {
  return function (a) : (f) {
  return function (b) : (f, a) {
  return function (c) : (f, a, b) {
    return f(a, b, c);
  }}}
}

function curry4(f) {
  return function (a) : (f) {
  return function (b) : (f, a) {
  return function (c) : (f, a, b) {
  return function (d) : (f, a, b, c) {
    return f(a, b, c, d);
  }}}}
}

はい。*1 関数fの仮引数の数を判別する手段がないので必要な分だけベタ書きでございます。可変長引数もあるししょうがない(?)です。
Squirrel3やJavaScriptと違って、外側のスコープのローカル変数の値を使うには関数定義時に明示的に渡しておく必要があります。 見た目愉快ですね。

curry2には引数を2つ取る関数を渡すことができます。

function add(a, b) {
  return a + b;
}

curried_add <- curry2(add);

local r0 = curried_add(2)(3);  // r0 = 5

// 部分適用
add1 <- curried_add(1);

local r1 = add1(5);  // r1 = 6

もしくは、その数の引数を取ることのできる可変長引数を取る関数をカリー化(?)することもできます。

// 1つ以上の引数を取る関数を
function product(a, ...) {
  local r = a;
  for (local i = 0; i < vargc; ++i) {
    r *= vargv[i];
  }
  return r;
}

// 引数を3つ取る関数としてカリー化
curried_product <- curry3(product);

local r2 = curried_product(2)(3)(4);  // r2 = 24

この時、curried_productの引数の数は固定化されています。 元の関数から変わってしまうのでカリー化と言えるのかは分かりません。

curried_product(2)(3)(4)(5);  // => Error! 24(integer)に対するcall

また、カリー化された関数の引数の順番を入れ替えるflip関数を以下のように定義できます。

function flip(f) {
  return function (a) : (f) {
  return function (b) : (f, a) {
    return f(b)(a);
  }}
}
function join(a, b, c) {
  return a + "," + b + "," + c;
}

curried_join <- curry3(join);

local r3 = curried_join(1)(2)(3);  // r3 = "1,2,3"

flipped_join <- flip(curried_join);

local r4 = flipped_join(1)(2)(3);  // r4 = "2,1,3"

Squirrelでカリー化とかして何が嬉しいのか、というのはまた次に。

SquirrelでOption

Squirrelの標準関数は失敗したらnull返すことも多くて*1、チーム内で-1返すのとどっちがいいのよみたいな話もしたけど、どっちも微妙ってのが正直なところです。
そこでOptionですよ。


実際には仕事で使ってるの2.xなのでラムダ式とかなくて結局辛いし*2、動的型付けなのでOptionの嬉しさがかなり減ってしまいます。

*1:string#find とか

*2:function (...) { return ...; } 地獄

IntelliJ IDEA Scalaプラグインのimplicit関連機能

IntelliJのScalaプラグインでよく使うimplicit関係の機能について。

code completion

f:id:kxbmap:20111215025757p:image
暗黙型変換によって使えるようになるメソッドも補完されます。また、それらには名前に下線が引かれます。
あるデータに対して何かやりたいとき、とりあえずimport scalaz._, Scalaz._して補完候補からそれらしい名前と型のメソッド探す、ということを自分は結構やります。Scalazなんてそんな感じで使えばいいと思うのです。

implicit conversion method

f:id:kxbmap:20111215025758p:image
Ctrl+Shift+Q でカーソル位置のオブジェクトを引数に取って呼び出すことのできるimplicitメソッドの一覧を表示します。メソッドを選択してEnterで定義元にジャンプします。
また、実際にその場所で暗黙型変換が行われている場合、使用されているメソッドが太字になります。ここではpureが定義されているのはmkIdentityによって変換されるIdentityだということが分かります。

actual implicit parameters

f:id:kxbmap:20111215025759p:image
Ctrl+Shift+P ではメソッドが取る暗黙的な引数について、実際に何が渡されているのかが表示されます。ここでもEnterでジャンプします。

def pure[F[_]](implicit p: Pure[F]): F[A] = p pure value

pureの定義はこうなっています。1.pure[List]という呼び出しでは、この implicit p には TraversablePure(の戻り値)が渡されているということです。
残念ながらTraversablePureに渡されるimplicit parameterを更に表示、ということは今のところできないようです。

util-evalでゲームの設定を書く

Scala Advent Calendar jp 2011 - [PARTAKE]の8日目です。


Scalaを利用している企業というとTwitterが有名ですね。そのTwitterは開発したScalaライブラリを色々と公開しています。
util-evalはその内のひとつで、実行時にScalaコードをコンパイル&評価するライブラリです。主に設定ファイルをScalaで記述するために利用されます。


で、ゲームの設定もScalaで書いたら(自分が)幸せになれるので使ってみました。

設定の定義

ゲームの設定として、IDが割り振られた武器のMapがあるとします。
武器には名前と攻撃力が設定できます。

case class Weapon(name: String, attack: Int)

trait GameConfig {
  def weapons: Map[Int, Weapon]
}

武器設定ファイル

1つの設定ファイルに1つの武器を書くとします。

1 -> Weapon(
  name = "ナイフ",
  attack = 10
)

(key, value) のタプルです。
読み込み時にディレクトリを指定するとその下のファイルを全部拾ってきてMapを構築します。
また、1ファイルにMap全体を書くこともできて、その場合は読み込み時にファイルを指定することにします。


設定フォルダはこんな構成。

config
├weapons
│ ├axe.scala
│ ├bow.scala
│ └knife.scala
└config.scala

weapons以下のファイルには先程のように武器が1つずつ定義されています。
config.scalaは後述。

Eval!

そんな感じで設定を読み込んで評価するtraitです。
util-evalがscala-compilerに依存していて楽だったのでnsc.ioを使っていますが、ホントは別のものを使った方がいいでしょう。

import com.twitter.util.Eval
import scala.tools.nsc.io.{File, Path}

trait ConfigReader {
  private val eval = new Eval()

  def read[A](file: File): A = {
    val in = file.inputStream()
    try eval[A](in)
    finally in.close()
  }

  def readMap[A, B](path: Path): Map[A, B] =
    if (path.isFile)
      read[Map[A, B]](path.toFile)
    else
      readDirectoryMap(path)

  private def readDirectoryMap[A, B](dir: Path): Map[A, B] =
    dir.walkFilter(_.isFile)
      .map(_.toFile)
      .map(read[(A, B)])
      .foldLeft(Map.empty[A, B])(_ + _)
}

readメソッド内の eval[A](in) が実際にファイルを読んで評価している部分になります。
内部ではファイルの内容を Function0[Any] を継承したクラスのapplyで包んでコンパイル、そのクラスのインスタンスを生成してapplyを呼び、結果を型キャストしています。*1
要するに、設定ファイル内ではメソッド内で書けるものが全て書けて*2、ファイル末尾に書いた値が帰ってくるということです。


readMap[A,B](Path)は引数にファイルが渡されたらそのままreadを、ディレクトリならPath#walkFilterを利用してディレクトリ内のファイルを再帰的に評価し、Mapにまとめます。

読み込み

ゲームからはconfig/config.scalaを読んでGameConfigを取得します。
config.scalaの中身はこんな感じ。

import scala.tools.nsc.io.Path

(path: Path) => new GameConfig with ConfigReader {
  val weapons = readMap[Int, Weapon](path / "weapons")
}

これを以下のようにして読み込み。

import scala.tools.nsc.io.Path

object Main extends App with ConfigReader {
  val path = Path("config")

  val config = read[Path => GameConfig](path / "config.scala" toFile)(path)

  println(config.weapons)  // Map(1 -> Weapon(ナイフ,10), 2 -> Weapon(弓,20), 3 -> Weapon(斧,30))
}

設定ファイルに関数を記述できるのがポイントですね。
武器以外の設定が必要になったらGameConfigに追加していくわけです。

ローカライズ

さて、作ったゲームを沢山の国の人に遊んでもらうにはローカライズが必要です。
ConfigReaderに以下のようなメソッドを追加します。

  def localize[A, B](base: Map[A, B], path: Path): Map[A, B] = {
    val loc = readMap[A, B => B](path)
    for ((id, config) <- base)
    yield id -> (loc.get(id).map(_(config)) getOrElse config)
  }

型Bを変換する関数を値に持つマップを読み込み、IDが一致する設定に対して適用します。


config/localize.scalaとして以下の内容のファイルを作成します。

import scala.tools.nsc.io.Path

(base: GameConfig, path: Path) => new GameConfig with ConfigReader {
  val weapons = localize(base.weapons, path / "weapons.scala")
}

weapons.scalaはこんな内容です。

Map[Int, Weapon => Weapon](
  1 -> (_.copy(name = "knife", attack = 15)),
  2 -> (_.copy(name = "bow")),
  3 -> (_.copy(name = "axe"))
)

case classのcopyメソッドで名前だけ変更することができます。
ナイフは弱かったので攻撃力も上げてみました。


読み込みはこうなります。

object Main extends App with ConfigReader {
  /* 省略 */
  val localized = read[(GameConfig, Path) => GameConfig](path / "localize.scala" toFile)(config, path / "localize" / "en")

  println(localized.weapons)  // Map(1 -> Weapon(knife,15), 2 -> Weapon(bow,20), 3 -> Weapon(axe,30))
}

ローカライズできました!

嬉しいこと

設定ファイルがScalaで何が嬉しいか。
Scalaそのものを書けることで自由度が高いというのはもちろんですが、IDEの支援をフルに受けることができるのが大きいでしょう。
ちょっと例を挙げてみます。

コード補完

f:id:kxbmap:20111208231809p:image
シンタックスハイライトも効きますね。

リファクタリング

f:id:kxbmap:20111208231810p:image
設定ファイル内でリファクタリングできます。設定ファイルから定義にジャンプすることもできます。

エラーハイライト

f:id:kxbmap:20111208231811p:image
もちろん型が違ったら表示してくれます。

検索

f:id:kxbmap:20111208231812p:image
参照の検索も。


素晴らしいですねー。

おまけ

Scalaz*3を使ってreadMapを以下のように書き換えます。

  def readMap[A, B: Semigroup](path: Path): Map[A, B] =
    /* 省略 */

  private def readDirectoryMap[A, B: Semigroup](dir: Path): Map[A, B] = {
    implicit val r = Reducer[(A, B), Map[A, B]](Map(_))
    dir.walkFilter(_.isFile).toStream
      .map(_.toFile)
      .map(readFile[(A, B)])
      .foldReduce
  }

そしてWeaponのコンパニオンオブジェクトを定義。

object Weapon {
  implicit val WeaponSemigroup: Semigroup[Weapon] =
    semigroup((w1, w2) => throw new IllegalArgumentException("duplicate key: " + w1 + ", " + w2))
}

元のコードではweaponsのkeyが被っていた場合後から読んだもので上書きされていましたが、これによって被ってたらエラーを吐くようになります。他にも先に読んだものを優先したり、どうにかして合成する、という定義もできますね。


さらにはこんな感じ

object Weapon extends ConfigReader {
  implicit lazy val WeaponSemigroup: Semigroup[Weapon] =
    read("typeclasses" / "WeaponSemigroup.scala" toFile)
}

色々楽しいことができそうな予感がします。

*1:このとき生成されるクラスのコードは実行時引数に-Deval.debugを指定すると標準出力に出力されます

*2:#includeというのもありますが割愛

*3:Scalazについては[http://partake.in/events/4b3afdc8-e4ec-4010-b8ec-31b89210dda0:title]を楽しみにしているといいんじゃなイカ

IntelliJ IDEAのプラグイン開発

ScalaでIntelliJのプラグインを作ろうと思ったのでメモついでに。

参考

http://confluence.jetbrains.net/display/IDEADEV/PluginDevelopment
http://www.jetbrains.com/idea/training/demos/google_search.html
下のLive Demoと同じものを作るところまでやってみます。

環境設定

IntelliJのJDKScalaライブラリの設定、Scalaプラグインのインストールは済んでいるとします。

IntelliJ IDEA Community Editionをインストール

次の設定に使うもので、開発はUltimate Editionでするとしてもこちらも入れる必要がありそうです。*1
今回はちょっと試してみたかったのでIntelliJ IDEA 11(Nika)のEAP(記事時点でbuild110.187)をここから取ってきてインストール。
ソースも一緒にダウンロードするか、リポジトリから取ってきます。

git clone git://github.com/JetBrains/intellij-community.git
git checkout idea/110.187
IntelliJ IDEA Plugin SDKの設定

開発に使うIntelliJを起動します。自分はNika Ultimateを別にインストールして使いましたが、先にインストールしたものでもいいはずです。
File > Project Structure > Platform Settings > SDKs から「+」ボタンで「IntelliJ IDEA Plugin SDK」を追加。
f:id:kxbmap:20111031024057p:image
SDKのホームを求められるのでCommunity Editionのパスを指定します。次にJDKも選択。
追加されたSDKのSourcepathにダウンロードしたソースのディレクトリを追加します。
f:id:kxbmap:20111031024058p:image:w480

新規プロジェクト作成

File > New Project... からCreate project from scrachを選びます。
Plugin Moduleを選択、プロジェクト名などを入力します。
f:id:kxbmap:20111031030508p:image:w480
Nextを押していき、最後に先程設定したSDKを選択、Finishを押してプロジェクトを作成します。
f:id:kxbmap:20111031031342p:image:w480

プロジェクト設定

Scalaで開発できるように設定します。いらなければ飛ばしてOK。
File > Project Structure > Project Settings > Modules からScala Facet*2 *3とDependenciesにscala-libraryを追加*4します。
f:id:kxbmap:20111031032444p:image:w240:left
f:id:kxbmap:20111031032445p:image:w240

これで準備完了です。

プラグイン作成

最初に貼ったデモ内で作成しているプラグインを作ります。エディタのカーソル位置にある単語をブラウザ起動してGoogle検索するものです。

META-INF/plugin.xml

プロジェクト作成時にplugin.xmlというファイルが生成されています。プラグインの情報などはこのファイルに書いていくようです。
f:id:kxbmap:20111031034206p:image:w480
各要素を設定していきます。プラグインの名前、説明、バージョン、ベンダーと。
idea-versionはsince-buildやuntil-build属性にプラグインの動作するIntelliJのビルド番号を指定します。互換性についてはよく調べてないですが*5、とりあえずsince-buildにSDKに利用しているIntelliJのビルド番号を入れておきます。

新規アクションを作成

src以下にパッケージを作成し、NewからActionを選びます。
f:id:kxbmap:20111031040525p:image
新規アクションの設定ウィンドウが開くので諸々入力します。
Toolsメニューの最後に「Search in Google」という表示で追加されるように設定。ショートカットもここで設定します。
f:id:kxbmap:20111031040526p:image:w480
パッケージ内にAnActionというクラスを継承したJavaクラスが作成され、plugin.xmlのactionsにactionが追加されています。
f:id:kxbmap:20111031041531p:image:w240:left
f:id:kxbmap:20111031041532p:image:w240

Scala!

生成されたJavaクラスを開いた状態で Cmd+Shift+G を押します。
f:id:kxbmap:20111031042155p:image:w480
Scalaクラスに変換されました。元のJavaクラスも残ってるので消しちゃいましょう。
デモのコードと同じものをScalaで書きます。

実行

実行設定

Run > Edit Configurations を開き、「+」ボタンから Plugin を選んで追加します。
が、今のバージョンのNikaがちょっとバグってる模様。追加された実行設定のVM Optionsに内部の設定ファイルのxmlがそのまま出ちゃってるっぽい。
f:id:kxbmap:20111031050244p:image:w480
ちょっと別にIntelliJインストールして確認。以下のような感じで設定しておけばよさそうです。

-Xms128m
-Xmx512m
-XX:MaxPermSize=250m
-XX:ReservedCodeCacheSize=64m
-ea
Run!

緑三角の実行ボタンかShift+F10を押すと、起動中のものとは別にIntelliJが起動します。
f:id:kxbmap:20111031051450p:image:w480
プラグインマネージャーを開くと作成したプラグインがインストールされていることが確認できます。
f:id:kxbmap:20111031051805p:image:w480
Toolsメニューを開くと下の方に追加したアクションが表示されてます。まだエディタを開いていないので非アクティブです。
f:id:kxbmap:20111031052112p:image
適当なプロジェクトを開いて設定したショートカットを押すとカーソル位置の単語がググられました!
f:id:kxbmap:20111031054510p:image:w480

*1:Community Editionじゃないとcore codeのデバッグが出来ないみたいなことが書いてある

*2:Scala Facetの設定はいつの間にか充実してるなー。内部FSCやらCompiler Pluginの設定やら

*3:ところで facet ってどういう意味です?

*4:最初DependenciesのAddボタンがどこに行ったのか分からなかったですが、リストの下にまとめられてます

*5:IntelliJ IDEA 10向けのプラグインはNikaではちゃんと動かないようです。ScalaプラグインのNightly buildも10向けと11向けで別になってます