SchemeからJavaの機能を呼び出す

The Kawa language frameworkを使うと、SchemeからJavaの機能を呼び出すことができる。

(事例1) Kawaで提供されていない機能をJavaの標準ライブラリを使って実装する

Kawaはあまりライブラリが充実しているとは言えない。その代表的なものが正規表現によるマッチングだ。幸いJavaにはjava.util.regexというパッケージが標準で付属しているので、これを使って正規表現によるマッチングを行うことができる。

java.util.regex.Patternに載っている例を移植してみよう。

 Pattern p = Pattern.compile("a*b");
 Matcher m = p.matcher("aaaaab");
 boolean b = m.matches();
(define-namespace Pattern <java.util.regex.Pattern>)
(define-namespace Matcher <java.util.regex.Matcher>)

(let* ((p (Pattern:compile "a*b"))
       (m (Pattern:matcher p "aaaaab")))
  (Matcher:matches m))

このようにほとんど機械的な置き換えで済む。Matcher.group()等も問題なく使える。ただし、結果がString型で返ってくる場合、そのままではSchemeの文字列として扱えないので、

(make <string> (Matcher:group m 2))

のようにする必要があった。

(事例2) Java用の3rd PartyライブラリをSchemeから利用する

例として、形態素解析MeCabを使う場合。

各種スクリプト言語 (perl, ruby, python, Java) から, MeCab が提供する形態素解析の機能を利用可能です. 各バインディングSWIG というプログラムを用いて, 自動生成されています.

ということで、これをSchemeからJava経由で使えるようにする。

(require 'list-lib)

(define-namespace Node <jp.ac.aist_nara.cl.mecab.Node>)
(define-namespace Tagger <jp.ac.aist_nara.cl.mecab.Tagger>)

(java.lang.System:loadLibrary "MeCab")

(define mecab-tagger #f)

(define (mecab-init-tagger)
  (let ((args ((primitive-array-new <String>) 3))
        (setter (primitive-array-set <String>)))
    (setter args 0 "java")
    (setter args 1 "-r")
    (setter args 2 "mecabrc")
    (set! mecab-tagger (Tagger:new args))))

(define (mecab-parse str)
  (or mecab-tagger (mecab-init-tagger))
  (let ((node (Tagger:parseToNode mecab-tagger (as <String> str))))
    (let loop ((node (Node:next node)) (r ()))
      (if (Node:hasNode (Node:next node))
          (loop (Node:next node)
                (cons node r))
          (reverse! r)))))
(mecab-parse "太郎は二郎にこの本を渡した.")
==> (太郎   名詞,固有名詞,人名,名,*,*,太郎,タロウ,タロー は 助詞,係助詞,*,*,*,*,は, ハ,ワ
 二郎   名詞,固有名詞,一般,*,*,*,二郎,ニロウ,ニロー に  助詞,格助詞,一般,*,*,*, に,ニ,ニ
 この   連体詞,*,*,*,*,*,この,コノ,コノ 本      名詞,一般,*,*,*,*,本,ホン,ホン を      助詞,格助詞,一般,*,*,*,を,ヲ,ヲ
 渡し   動詞,自立,*,*,五段・サ行,連用形,渡す,ワタシ,ワタシ た   助動詞,*,*,*,特 殊・タ,基本形,た,タ,タ
 .      未知語,*,*,*,*,*,*,*,*)

mecabrcを指定しているのは、Javaから呼び出す場合UTF-8版の辞書を使う必要があるため(SWIGの問題?)。mecabrcの内容は実質的に次の1行だけ。

dicdir = /usr/lib/mecab/dic/ipadic-utf8

さらに、このままだと扱いづらいので、以下のような関数を定義してSchemeの構造体として扱えるようにしている。

(define (node->word node)

  (define (normalize str)
    (if (string=? str "*")
        #f
        (string->symbol str)))

  (let ((surface (make <string> (Node:getSurface node)))
        (feature (*:split (as <String> (Node:getFeature node)) ",")))

    (define (getter n)
      (make <string> ((primitive-array-get <String>) feature n)))

    (let ((pos (remove not
                       (map (lambda (n)
                              (normalize (getter n)))
                            '(0 1 2 3))))
          (form (normalize (getter 4)))
          (type (normalize (getter 5)))
          (base (normalize (getter 6)))
          (ruby (getter 7))
          (reading (getter 8)))
      (make-word surface pos form type base ruby reading))))

(define-record-type word
  (make-word surface pos form type base ruby reading)
  word?
  (surface word-get-surface word-set-surface)
  (pos word-get-pos word-set-pos)
  (form word-get-form word-set-form)
  (type word-get-type word-set-type)
  (base word-get-base word-set-base)
  (ruby word-get-ruby word-set-ruby)
  (reading word-get-reading word-set-reading))

(事例3) SchemeからJavaの内部情報を弄る

リフレクション機能を使って遊んでみる。これを使ってコード補完とかも(頑張れば)できるかも。

(define-namespace Class <java.lang.Class>)
(define-namespace Field <java.lang.reflect.Field>)
(define-namespace Method <java.lang.reflect.Method>)

(define-namespace Pattern <java.util.regex.Pattern>)
(define-namespace Matcher <java.util.regex.Matcher>)

(define-namespace ClassType <gnu.bytecode.ClassType>)

(define (primitive-array->list element-type array)
  (let ((len ((primitive-array-length element-type) array)))
    (let loop ((i 0) (r '()))
      (if (< i len)
          (loop (+ i 1) (cons ((primitive-array-get element-type) array i) r))
          (reverse r)))))

(define (declared-methods class :: <java.lang.Class>)
  (primitive-array->list <java.lang.reflect.Method>
                         (Class:getDeclaredMethods class)))

(define (declared-fields class :: <java.lang.Class>)
  (primitive-array->list <java.lang.reflect.Field>
                         (Class:getDeclaredFields class)))

(define (element-type->class element-type :: <gnu.bytecode.ClassType>)
  (*:getReflectClass element-type))

(define (suggest-class-identifier class regex)

  (define (writer obj)
    (display " ")
    (display obj)
    (display "\n"))

  (define (display-fields fields)
    (display "Fields: ")
    (display (length fields))
    (display " field(s) found.\n")
    (for-each writer fields))

  (define (display-methods methods)
    (display "Methods: ")
    (display (length methods))
    (display " method(s) found.\n")
    (for-each writer methods))

  (let* ((p (Pattern:compile regex (Pattern:.CASE_INSENSITIVE)))
         (matcher (lambda (str :: <String>)
                    (Matcher:find (Pattern:matcher p str))))
         (fields (filter (lambda (field)
                           (matcher (Field:getName field)))
                         (declared-fields class)))
         (methods (filter (lambda (method)
                            (matcher (Method:getName method)))
                          (declared-methods class))))
    (display-fields fields)
    (display-methods methods)))
(suggest-class-identifier (element-type->class <String>) "^to")
==>
Fields: 0 field(s) found.
Methods: 6 method(s) found.
 public java.lang.String java.lang.String.toString()
 public char[] java.lang.String.toCharArray()
 public java.lang.String java.lang.String.toLowerCase(java.util.Locale)
 public java.lang.String java.lang.String.toLowerCase()
 public java.lang.String java.lang.String.toUpperCase(java.util.Locale)
 public java.lang.String java.lang.String.toUpperCase()

(suggest-class-identifier (element-type->class <String>) "value")
==>
Fields: 1 field(s) found.
 private final char[] java.lang.String.value
Methods: 11 method(s) found.
 public static java.lang.String java.lang.String.copyValueOf(char[],int,int)
 public static java.lang.String java.lang.String.copyValueOf(char[])
 public static java.lang.String java.lang.String.valueOf(java.lang.Object)
 public static java.lang.String java.lang.String.valueOf(char[])
 public static java.lang.String java.lang.String.valueOf(char[],int,int)
 public static java.lang.String java.lang.String.valueOf(boolean)
 public static java.lang.String java.lang.String.valueOf(char)
 public static java.lang.String java.lang.String.valueOf(int)
 public static java.lang.String java.lang.String.valueOf(long)
 public static java.lang.String java.lang.String.valueOf(float)
 public static java.lang.String java.lang.String.valueOf(double)

プログラムを書いていると、どうしても実行して実験してみないと分からないことも多い。そんな時、Kawaを一種のJavaインタプリタとして利用することができる。

通常のJavaプログラミングでは、ソースコード修正→コンパイル→実行という手順を踏まないと行けないのに対して、Scheme からは入力した式をOn the flyにコンパイルして実行することができるから、対話的にちょっとした実験をするのに便利。究極のRADツール?

まとめ

KawaはSchemeインタプリタ/コンパイラを含むJavaで実装された多言語フレームワークである。Kawaで実行されるSchemeプログラムからは任意のJavaクラスを利用できる。具体的には次の3つの利用方法が考えられる:

  • Kawaで提供されていない機能をJavaの標準ライブラリを使って実装する
  • Java用の3rd PartyライブラリをSchemeプログラムから利用する
  • Javaインタプリタの一種として

最近はPerlRubyのようなLightweight Languageが流行っているようなので、Kawa + Javaをその一つとして位置づけることもできるかもしれない。

参考URL

KawaでJavaのメソッドやフィールドに直接アクセスする方法など。

クラス構造のドキュメント。

Schemeソースファイルをclassファイルにコンパイルする方法。(この場合Javaから直接呼べる)

Schemeのリファレンスとして分かりやすい。