DSL

scalaでちょっとしたDSLを作ってみる( ̄o ̄)/

JodaTimeというJavaのライブラリがある。しかし、もちろんJavaなので、メソッド名がplusYearsとかになっているわけだが、それをラッピングしちゃって、


today + "3years" + "1month" - "2week" + "2days"

みたいな構文で呼べるようにするのが最終的な目標。ちなみに、実用とかを考えるより、scalaの勉強のためというのが大きいので、効率とかはあまり考えないことにした。
とりあえず出来る限りscalaっぽく、書いてみるのと、でも簡潔に、でも型安全に、でも読みやすくとかを重視。

で、できたのが以下のコード



object ScalaJoda{

/** メソッドの引数として有効なパターン */
private val ParamRegex = {"""(-?[0-9]+)(""" + Field.names.mkString("|") + ")(s?)"}.r

/** DateTimeからの暗黙変換 */
implicit def toScalaJoda(time:DateTime) = new ScalaJoda(time)

/** 抽出子 */
private object Value{
/**
* @param s もととなる足したい数値+field名の形式のString
* @return 形式があってたら、数値と、fieldに分解したものを返す
*/

def unapply(s:String):Option[(Int,Field )] = s match{
case ParamRegex(number,field,_) => Some( number.toInt , Field(field) )
case _ => None
}
}

/** fieldの種類を表す */
private object Field{

/** コンパニオンクラスのインスタンスをSetの中に保存(これ以外にインスタンスはない) */
private val fieldSet =
Set(
Field("year" ,_.plusYears(_) , _.minusYears(_) ),
Field("month" ,_.plusMonths(_) , _.minusMonths(_) ),
Field("day" ,_.plusDays(_) , _.minusDays(_) ),
Field("hour" ,_.plusHours(_) , _.minusHours(_) ),
Field("week" ,_.plusWeeks(_) , _.minusWeeks(_) ),
Field("minute" ,_.plusMinutes(_) , _.minusMinutes(_) ),
Field("second" ,_.plusSeconds(_) , _.minusSeconds(_) ),
Field("milli" ,_.plusMillis(_) , _.minusMillis(_) )
)

val names = fieldSet.map( _.name )

/** Stringのコンストラクタっぽく使えるが、newするのは無駄なので、fieldSetから探して返す */
private[ScalaJoda] def apply(s:String):Field = {
val Some(result) = fieldSet.find{ s == _.name }
result
}
}

/** まともに書くと名前長いので、別名つけた */
private type FuncT = Function2[DateTime,Int,DateTime]

final private case class Field private(
name:String, plusFunc:FuncT, minusFunc:FuncT
){

def getFunc(t:FuncType.Value):FuncT = t match {
case FuncType.plus => plusFunc
case FuncType.minus => minusFunc
}
}

/** getFuncの引数で使う */
final private object FuncType extends Enumeration{
val plus,minus = Value
}
}

/**
* @param self 内部に保持するDataTimeこれをラップして、違うかたちで関数を提供
*/

class ScalaJoda(private val self:DateTime){
import ScalaJoda._

/** +と-のメソッドのためのヘルパー関数 */
private def sub( funT:FuncType.Value , strSeq:String* ):DateTime = {

val paramfields =
for(s <- strSeq)yield{//それぞれの引数をチェックし、かつ、数字と適用する関数を抽出
s match{
case Value(num,f) => (num,f.getFunc(funT))
case _ => throw new IllegalArgumentException( s +" は入力形式として正しくありません" )
}
}

//順に関数適用
paramfields.foldLeft(self){
case ( receiver , ( param, method) ) => method( receiver , param )
}
}

/**
* 引数で渡された文字列を、それぞれ数値とフィールドに分解して適用する
* @param strSeq ScalaDoja.patternにマッチする文字列(の可変長引数)
* @return 新しいDateTimeオブジェクト
*/

def + (strSeq:String*):DateTime = sub( FuncType.plus , strSeq: _* )

/** +のほうと使い方同じ */
def - (strSeq:String*):DateTime = sub( FuncType.minus, strSeq: _* )
}

ところで、関数オブジェクトだと以下の14文字だけで、

_.plusYears(_)

classファイルが一個できるから、classファイルの数がすごいことになった(笑)

まあ一番のポイントというか基本的なところは、DateTimeを暗黙変換して、計算して、またDateTimeを返すようにすれば、他に影響なく、単にDateTimeのメソッドが増えた様な感じで、手軽に使えるという点かなぁ。
(DateTimeがfinalなので継承して拡張することができないが、あたかも継承して拡張したように使える。)


とりあえず、重複の定義を避けるために、+と-で同じヘルパー関数呼ぶようにしたり、plusYearsとかplusMonthsとか関数の形式が同じなので、一度関数オブジェクトとしてSetに保存してみた。(やりすぎか?)
Javaでやったら、絶対こんな形にはできないよねぇ。ていうかJavaと比べてもあんまり意味ないよね。

あと最初は単一の引数をとる関数作ったんだけど、可変長とるやつだけ定義すればよくね?ってことで、


val today = new JodaTime
today + ( "3years" ,"1month" ,"2week" , "-2days" )

みたいな感じでも呼べるようにしてみた。