副題 : Scala のマクロの強力さについて
おさらい 1 : Scala のマクロってどんなやつ
// Scala に慣れてる人は次の節まで読み飛ばしてください :)
マクロはコンパイル時に構文木を参照・展開できる機能で、例えば下記のような情報を得ることができます。
- マクロに渡された型パラメータ
- マクロの実行結果が代入される変数の名前
- マクロが呼ばれたメソッド名・クラス名, etc
これによって構文木を自動生成したり不正な呼び出しを検出したりといった機能が多くのライブラリ・フレームワークで実現されています。マクロにはいくつかの種類がありますが、このうち今回利用する blackbox macro と呼ばれるものについては、将来的にも (少なくとも 2.12 までは) ほぼ問題なく使えるといって差し支えありません。
参考
おさらい 2 : Android のロギングってどんなやつ
// Android に慣れてる人は次の節まで読み飛ばしてください :)
公式にちょうどいい リファレンス が用意されているので抜粋します。
private static final String TAG = "MyActivity";
まずログを出力するクラスで適当なタグを作ってから、
次にそのタグをログに含めるという手順です。
Log.v(TAG, "index=" + i);
Tip: A good convention is to declare a TAG constant in your class:
「クラス内に定数でタグを宣言するのは良い慣習です」とのことですが、正気か。もちろんそれ以外を含めることもできます。このタグは対象のログを絞り込むために使われるので、場合によっては他の情報を含めることもあるでしょう。
本題 : ログに含めるクラス名をコードに直書きしたくない
おそらく多くの人は前節のコードを見て最初はこう書き直したくなるとおもいます。
private static final String TAG = MyActivity.class.getName();
これでクラス名を変更しても自動で追従できる!幸せ!と思いきや、そう簡単には済みません。Android アプリでは Proguard というツールがあり、通常はリリースビルド時にこれによってバイナリの圧縮・難読化を施します。もちろん Proguard を利用しないという選択肢もありますが、実際にバイナリのサイズを数分の一にまで減らせるので、心情としてはできればかけておきたいといったところだとおもいます。
- Proguard 後のクラス名/メソッド名/変数名/etc は a や b などの人の目では判別不能な文字列に変わる
- なのでリフレクションによって実行時に名前を取り出すことはできない
- かといって文字列を直書きするとリファクタリングでこまる
- Proguard を使わなければ済む問題ではあるがバイナリのサイズが数倍に膨れてしまう…
- 特定のクラスやメンバについて proguard を適用しない設定も可能だがそれを毎回書くのか…?
うーん、この…
結論 : Log に渡す TAG の生成はマクロで自動化できる
Scala であれば前述した情報をコンパイル時に差し込めるので、そのためのマクロを用意すればこれらの問題は万事解決です。
import scala.language.experimental.macros
import scala.reflect.macros.blackbox
object Log {
def verbose(message: => String): Unit = macro LogImpl.verbose[String]
def debug (message: => String): Unit = macro LogImpl.debug[String]
def info (message: => String): Unit = macro LogImpl.info[String]
def warn (message: => String): Unit = macro LogImpl.warn[String]
def error (message: => String): Unit = macro LogImpl.error[String]
}
private object LogImpl {
import scala.language.existentials
def verbose[A: c.WeakTypeTag](c: blackbox.Context)(message: c.Expr[A]): c.Tree = {
new TreeFactory[c.type](c).create(message, "v")
}
def debug[A: c.WeakTypeTag](c: blackbox.Context)(message: c.Expr[A]): c.Tree = {
new TreeFactory[c.type](c).create(message, "d")
}
def info[A: c.WeakTypeTag](c: blackbox.Context)(message: c.Expr[A]): c.Tree = {
new TreeFactory[c.type](c).create(message, "i")
}
def warn[A: c.WeakTypeTag](c: blackbox.Context)(message: c.Expr[A]): c.Tree = {
new TreeFactory[c.type](c).create(message, "w")
}
def error[A: c.WeakTypeTag](c: blackbox.Context)(message: c.Expr[A]): c.Tree = {
new TreeFactory[c.type](c).create(message, "e")
}
}
private class TreeFactory[C <: blackbox.Context](val context: C){
import context.universe._
def traverse(x: context.Symbol): String = {
if (x.isMethod || x.isClass || x.isPackage) x.fullName
else traverse(x.owner)
}
def enclosing: String = traverse(context.internal.enclosingOwner)
def create[A: context.WeakTypeTag](message: context.Expr[A], method: String) = {
val logger = weakTypeOf[android.util.Log].companion
q"""$logger.${TermName(method)}($enclosing, $message)"""
}
}
例えば com.example.MyActivity
内の以下のようなコードが
def foo() = {
Log info "hoge"
}
コンパイル時にメソッド名まで含めて展開されます。
def foo() = {
android.util.Log.i("com.example.MyActivity.foo", "hoge")
}
これでもう毎回手作業でクラス名やメソッド名を入れる簡単なお仕事からは解放されました。やった!
補足
(あたりまえですが)この方法では Proguard をかけてもログに含まれるクラス名やメソッド名はユーザに漏れます。ログの出力内容すらも秘匿・難読化したいといったプロダクトでは利用不可なのでご注意を。
おわり
Android アプリを Scala で書くと多くの問題を解決できます!オススメです!