Doma 1.28.0 をリリース

Doma 1.28.0 をリリースしました。

ダウンロードはこちらからどうぞ。

Mavenをご利用の方はこちらを参照ください。

以前のバージョンから移行するには移行ガイドを参照ください。

今回は、小さめの改善のみを含んでいます。

リリースノート

Improvement

  • [DOMA-277] - [Core] 注釈処理によるソースコード生成で利用したDomaのバージョンと実行時のDomaのバージョンが同じであることを検証する処理を抑制可能にしました

DOMA-277について解説

Domaの注釈処理によって自動生成されるソースコードは、次のようなstatic初期化子を含んでいます。

    static {
        org.seasar.doma.internal.Artifact.validateVersion("1.28.0");
    }

文字列リテラルで示された"1.28.0"は、注釈処理(コード生成時)に使ったDomaのバージョンです。
このstatic初期化子は、アプリの実行時に評価され、コード生成時のDomaのバージョンが実行時のDomaのバージョンと同じであるかどうかを検証します(異なればエラーになります)。

なぜこのような処理を組み込んでいるかというと、注釈処理関連の設定ミス(EclipseでFactory Pathの更新忘れなど)に気づきやすくするためです。


今回の修正では、上記のstatic初期化子のコード片を生成しないことをオプションで選択可能にしました。主な用途は、Domaで自動生成されたコードをライブラリとして配布する場合です。これまでは、ライブラリにしてもDomaの特定バージョン(ライブラリの生成に使ったバージョン)でしか動作させられませんでしたが、今回追加された機能を使うことで、Doma本体に非互換な処理が入らない限りは、バージョンの縛りがなくなります。


バージョンチェックのstatic初期化子のコード片を生成を抑制するには、注釈オプションのversion.validationにfalseを設定します。

F#でインタープリタ : オブジェクト指向

クラスの定義とインスタンス化ができるようになりました。

    class Position {
      x = 0
      y = 0
      def move(nx, ny) {
        x = nx
        y = ny
      }
    }

    class Pos3D extends Position {
      z = 0
      def set(nx, ny, nz) {
        x = nx
        y = ny
        z = nz
      }
    }

    p = Pos3D.new
    p.move(3, 4)
    print(p.x + p.y)
    p.set(5, 6, 7)
    a = p.x + p.y + p.z
    print(p.x + p.y + p.z)

このスクリプトを実行すると7と18を出力します。


当初の目標どおり、今のところmutableな型を使わずにがんばっていますがだんだんと難しさも感じてきました。

値を共有しないことの不便さ

実装がいまいちなのか、慣れていないからか、永続データで値を共有しないことによる不便さがあります。
ある値が共有されている場合、その値を変更して共有元に追随させたいときに難しいです。
たとえば、次のようなコードでグローバルスコープでオブジェクトを作ってフィールドに値を代入した場合です。

p = Pos3D.new
p.x = 10

上のスクリプトに対応するインタープリタの実装部分は次のようになっています。
共有している部分を全部作り直さないといけないのが大変。

  let assign env lhs rhs cont =                   (* envがグローバルな環境 *)
    match lhs, rhs with
    | Var(id, _), v -> 
      let newEnv = Env.add id v env
      cont newEnv v
    | Member(objId, memberId, Obj(objEnv)), v ->  (* objIdがp、memberIdがx、objEnvがpの環境、vが10に対応 *)
      let newObjEnv = Env.add memberId v objEnv   (* pの環境を再作成(xを10に更新) *)
      let newObj = Obj(newObjEnv)                 (* pに新しい環境を設定 *)
      let newEnv = Env.add objId newObj env       (* グローバルな環境を再作成(新しいpを参照) *)
      cont newEnv v                               (* 新しいグローバルな環境で後続処理 *)
    | _ -> failwith (sprintf "%A is not variable." lhs)

相互参照の問題

永続データ型と判別共用体で相互参照できなくて悩みます。たとえば、this参照。

(* オブジェクトを表す判別共用体に環境を表す永続データを持たせる *)
let newObj = Obj(newObjEnv)
(* 環境にオブジェクトを追加してthisで自身を参照できるようにしたいが、無理。thisで取得できるのは古い環境をもったオブジェクト *)
let newObjEnv = Env.add "this" obj newObjEnv      

特にこれといった解決策がなくて、どうすべきか悩み中。

F#でインタープリタ : F#の関数や.NETのメソッドを呼び出す

F#の関数や.NETのメソッドを呼び出せるようにしました。
lengthで文字列の長さを返して、toIntで文字列を数値へ変換。printで出力。
次のコードを実行すると5が返ってきます。

    s1 = "abc"
    s2 = "2"
    print(length(s1) + toInt(s2))


evalも実装できました。
次のコードを実行すると3が返ります。

    x = 1
    y = 2
    expr = "x + y"
    eval(expr)

evalは呼び出し前後で環境を共有するようにしているので、次のコードも3を返します。

    x = 1
    eval("y = x + 2")
    y

F#でインタープリタ : クロージャの実装

やっとクロージャが実装できました。

    def counter (c) {
      fun () { c = c + 1 }
    }
    c1 = counter(0)
    c2 = counter(0)
    c1()
    c1()
    c2()
    c1()

上のコードを実行すると3が返ってきます。


関数呼び出しを評価するコードを抜粋。ごちゃごちゃしているけど現時点では満足です。クロージャを表現するのに、スコープチェーンのようなデータ構造を入れないと無理かなーと思いつつ、永続データ型のMapでなんとかなったのがうれしい。

    | PrimaryExpr(expr, arguments) ->
      match arguments with
      | Some(arguments) -> (* 引数があったら関数呼び出し *)
        eval env expr (fun exprEnv expr ->
        eval exprEnv arguments (fun argsEnv arguments ->
          match expr, arguments with
          | Var(id, Fun(parameters, block, funEnv)), Args(args) -> (* パラメータ、ブロック、静的スコープを取り出す *)
            eval argsEnv parameters (fun paramsEnv -> function
              | Params(``params``) ->
                let rec loop paramsEnv list = 
                  match list with
                  | [] -> (* 関数ブロックの処理*)
                    (* 関数ブロックのための環境を作成 *)
                    let blockEnv = (Map.toList funEnv) @ (Map.toList paramsEnv) |> Map.ofList
                    (* 関数ブロックを評価*)
                    eval blockEnv block (fun resultEnv result -> 
                      (* 自由変数だけを含んだ環境を作成 *)
                      let freeEnv = resultEnv |> Map.filter (fun key _ -> funEnv |> Map.containsKey key)
                      (* クロージャとして使えるように関数が保持する環境を更新 *)
                      let f = Fun(parameters, block, (Map.toList funEnv) @ (Map.toList freeEnv) |> Map.ofList)
                      (* 関数の束縛だけを変更した新しい環境を作成 *)
                      let newEnv = Map.add id f exprEnv
                      (* 関数の呼び出し終了。関数の束縛だけを変更した環境で後続処理。 *)
                      cont newEnv result)
                  | (param, arg) :: xs -> (* パラメータに引数を割り当て *)
                    assign paramsEnv param arg (fun paramsEnv _ -> loop paramsEnv xs)
                (* パラメータと引数の組のリストを再帰で処理 *)
                loop paramsEnv (List.zip ``params`` args)
              | _ -> failwith (sprintf "parameters %A is not found" parameters))
          | _ -> failwith (sprintf "function %A is not found" expr)))
      | _ -> (* 引数がなかったら式の評価 *)
        eval env expr cont

今回は素のMapでがんばりましたが、環境に相当する型を作ればもっとすっきり書けるはず。

Visual Studioでデバッグ

昨日書いた評価器は、いまいちバグってるなー。静的スコープじゃなくて動的スコープになっているし、環境がグローバルの環境しかない。
デバッグでずいぶん時間を無駄にしました(そしてまだ解決していない)。

しかし、そのおかげで、Visual Studioデバッグのノウハウが手に入りました。

funのbodyは別の行にしたほうがブレイクポイントを置きやすい

let add a b cont =
  cont (a + b)

add 1 2 (fun x -> printfn "%A" x)

と書くのではなく

let add a b cont =
  cont (a + b)

add 1 2 (fun x -> 
  printfn "%A" x)  (* この行にブレイクポイントを置ける *)

と書くといいわけです。

「ローカル」ビューにはシャドウイング前の値も表示されるけど、

let x = 100        (* A *)

let add a b cont =
  cont (a + b)

add 1 2 (fun x ->  (* B *) 
  let x = 10       (* C *)
  printfn "%A" x)  (* この行にブレイクポイントを置く *)

最後の行にブレイクポイントを置いて止めたとき、ローカル」ビューにはシャドウイング前のx(行B)と後のx(行C)の値が表示されますが、エディタ上のシャドウイング前のx(行B)にカーソルを当てたときはシャドウイング後の値(行C)が表示されます。関数の外のx(行A)にカーソルを当てたときも最後のx(行C)の値が表示されます。これは、ちょっと直感的でないなぁ。

F#でインタープリタ

半年前くらいにJavaでおもちゃスクリプトを作ってみましたが、今度はF#で挑戦。

この週末にフィボナッチ計算ができるところまで作ってみました。
言語の構文は 2週間でできる! スクリプト言語の作り方 (Software Design plus) に出てくるStoneをベースにしています。

def fib (n) {
  if n < 2 {
    n
  } else {
    fib(n - 1) + fib(n - 2)
  }
}
fib(10)

だいたい、以下のような方針を採ってみましたが、今のところなんとかなっています。

  • FParsecでパース
  • 継続を使う
  • 永続データ型を使う(System.Collections.Generic.Dictionaryのような変更可能なデータ型を避ける)
  • 判別共用体を使う(Object型のような汎用的な型の利用を避ける)


とはいえ、FParsecは難しい。なかなか覚えられないです。スペースの扱いで結構はまりました。

継続は、まだ利点/欠点が把握できていないのですが、とりあえず、やたらと使っています。

現時点での一番の収穫は、Object型を使ったりダウンキャストしたりしないでパースした結果を評価できていることですね。汎用的な処理をするところでは、具体的な型が失われるのは仕方ないなと思っていましたが、判別共用体をうまく使えば避けられるんじゃないかと思いつつあります。

Doma 1.27.1 をリリース

Doma 1.27.1 をリリースしました。

ダウンロードはこちらからどうぞ。

Mavenをご利用の方はこちらを参照ください。

以前のバージョンから移行するには移行ガイドを参照ください。

今回は、小さめのバグ修正のみを含んでいます。

リリースノート

Bug

  • [DOMA-274] - [Core] 型引数で解決されるべき型がSQLの式コメント内で別の型として認識され評価に失敗する問題を解決しました。
  • [DOMA-275] - [Core] Domaが内部で使っているClass.forName(name)が式コメントの評価部分にまだ残っている問題を解決しました。
  • [DOMA-276] - [Core] SQLの式コメントの中でオーバーライドしているメソッドを呼び出すとDOMA4073のエラーになる問題を解決しました。

機能紹介: SQLの式コメント内での型引数の扱い

DOMA-274について簡単に解説します。

継承関係を持つこんなクラスが2つあるとします。

public abstract class AbstractDomain<V> {
    protected V value;

    protected AbstractDomain(V value) {
        this.value = value;
    }

    V getValue() {
        return value;
    }
}
@Domain(valueType = String.class)
public class PhoneNumber extends AbstractDomain<String> {
    public PhoneNumber(String value) {
        super(value);
    }
}

このとき、AbstractDomainの型パラメータVの実型引数はStringです。


これまで、SQLの式コメントにおいてジェネリックな型の値(AbstractDomainのvalue)にアクセスする場合、実型引数を認識していませんでした。つまり、次のような式コメントは、「Object型をパラメータにもつ@isNotEmptyメソッドが見つかりません」といったエラーになっていました。

select * from person where 
/*%if @isNotEmpty(phoneNumber.value) */ 
  phoneNumber = /* phoneNumber */'03456789'
/*%end */

今回の修正で実型引数を認識するようにしたので、String型をパラメータにもつ@isNotEmptyメソッドがちゃんと使えるようになりました。