HaskellのLanguage.Java.*を試してみた

Haskell Advent Calendar のためのエントリです。

Haskellのライブラリでlanguage-javaというものを見つけたのでご紹介です。


仕事のプログラムでJavaのコード生成を行なっているものがあり、現状ではテンプレートを穴埋めするような処理をPerlで行なっています。
しかしいろいろと要求が複雑になってくると、穴埋め処理のままではJavaソースコード内の意味の整合を維持するのが次第に面倒になり、ソースコードの構造を意識した処理に置き換えたくなってきました。


Haskellにはそのようなコード生成向けライブラリが無いだろうかと探してみたのがきっかけでした。


http://hackage.haskell.org/package/language-java/


現状だと4つのモジュールを含んでいます。
それぞれ、javaのレキサ (Language.Java.Lexer)、パーサ (Language.Java.Parser)、プリティプリント(Language.Java.Pretty)、構文定義(Language.Java.Syntax)となっているようです。わざわざ説明しなくても良いぐらいに名前のままですね。

Parser

今回はコード生成を行ないたいので、Syntax で定義した構文木を Pretty でフォーマットしてみたいと思います。
しかし、Syntaxの定義を覗いてみるとわかるのですが、構文木を手書きだけでがんばるのはかなり面倒そうです。
なので Parser の機能も使って、構文木を出力させてみることにしました。

Foo.java

package foo.bar;
public class Foo {
    private void foo(String a) {
        System.out.println("Hello " + a);
    }
}

Parse.hs

module Parse where

import Text.ParserCombinators.Parsec (ParseError)
import Language.Java.Syntax (CompilationUnit)
import Language.Java.Parser (parser, compilationUnit)

parseCompilationUnit :: String -> Either ParseError CompilationUnit
parseCompilationUnit =  parser compilationUnit

parse :: FilePath -> IO (Either ParseError CompilationUnit)
parse fn = fmap parseCompilationUnit (readFile fn)
% ghci -hide-package parsec-3.1.0 Parse.hs
GHCi, version 6.12.1: http://www.haskell.org/ghc/  :? for help
Loading .. 略
Ok, modules loaded: Parse.
Prelude Parse> parse "Foo.java"
Loading .. 略
Right
(CompilationUnit (Just (PackageDecl (Name [Ident "foo",Ident "bar"])))
 [] [ClassTypeDecl (ClassDecl [Public] (Ident "Foo") [] Nothing []
 (ClassBody [MemberDecl (MethodDecl [Private] [] Nothing (Ident "foo")
 [FormalParam [] (RefType (ClassRefType (ClassType [(Ident
 "String",[])]))) False (VarId (Ident "a"))] [] (MethodBody (Just
 (Block [BlockStmt (ExpStmt (MethodInv (MethodCall (Name [Ident
 "System",Ident "out",Ident "println"]) [BinOp (Lit (String "Hello "))
 Add (ExpName (Name [Ident "a"]))])))]))))]))])
Prelude Parse> 

それらしく動作しているようです。
しかし、ちょっと注意が必要だったのは language-java は parsec の version 2 に依存しているらしく、parsec の version 3 と parsec の version 2 をインストールしている環境では競合が起きてしまいます。
ghc に -hide-package parsec-3.1.0 というように parsec の version 3 を hide するようにしてやると、競合を解決することができるようです。

Pretty

次は Pretty Printer を試してみました。

FooP.hs

module FooP where

import Text.PrettyPrint.HughesPJ (render)
import Language.Java.Pretty (pretty, Pretty)
import Language.Java.Syntax

showCompilationUnit :: Pretty a => a -> IO ()
showCompilationUnit =  putStrLn . render . pretty

ast0 :: CompilationUnit
ast0 =
 CompilationUnit (Just (PackageDecl (Name [Ident "foo",Ident "bar"])))
 [] [ClassTypeDecl (ClassDecl [Public] (Ident "Foo") [] Nothing []
 (ClassBody [MemberDecl (MethodDecl [Private] [] Nothing (Ident "foo")
 [FormalParam [] (RefType (ClassRefType (ClassType [(Ident
 "String",[])]))) False (VarId (Ident "a"))] [] (MethodBody (Just
 (Block [BlockStmt (ExpStmt (MethodInv (MethodCall (Name [Ident
 "System",Ident "out",Ident "println"]) [BinOp (Lit (String "Hello "))
 Add (ExpName (Name [Ident "a"]))])))]))))]))]

test0 :: IO()
test0 =  showCompilationUnit ast0
.. 略
Prelude FooP> test0
Loading .. 略
package foo.bar;
public class Foo
{
  private void foo (String a)
  {
    System.out.println("Hello " + a);
  }
}
Prelude FooP>

無事にもとに戻ってきたようです。

バグとか

上の例では問題はなさそうですが、いろいろ試してみた結果、問題も見つかりました。

Bad0.java

package foo.bar;
public class Bad0 {
    private synchronized void foo() {}
}
.. 略
Prelude Parse> parse "Bad0.java"
Loading .. 略
Left (line 3, column 13):
unexpected KW_Synchronized
expecting refType or resultType
Prelude Parse> 

synchronized のところでエラーになってしまいました。

--- bug/haskell-language-java-0.1.0/Language/Java/Parser.hs
+++ haskell-language-java-0.1.0/Language/Java/Parser.hs
@@ -294,6 +294,7 @@
     <|> tok KW_Native      >> return Native    
     <|> tok KW_Transient   >> return Transient 
     <|> tok KW_Volatile    >> return Volatile  
+    <|> tok KW_Synchronized >> return ModSynchronized
 
 ----------------------------------------------------------------------------
 -- Variable declarations
--- bug/haskell-language-java-0.1.0/Language/Java/Syntax.hs
+++ haskell-language-java-0.1.0/Language/Java/Syntax.hs
@@ -149,6 +149,7 @@
     | Transient
     | Volatile
     | Native
+    | ModSynchronized
   deriving (Eq, Show)
 
 -----------------------------------------------------------------------

調べてみたところ、modifier に synchronized を書き忘れている単純なミスのようでした。
加えてみるとあっさり直りました。


もうひとつ、parsecのバックトラックの罠にハマっていそうな問題もありました。

Bad1.java

/**/
package foo.bar;
public class Bad1 {
    private void foo() {}
}
/**/

このように上下をコメントではさんだコードにすると、

.. 略
Prelude Parse> parse "Bad1.java"
Loading .. 略
Right (CompilationUnit Nothing [] [])
Prelude Parse> 

なんと、parseの結果が空になってしまいました。こちら問題はまだ原因を調査中です。なにか分かったらまた書くかもしれません。

まとめ

  • language-javaを使うとHaskellからJava構文木を操作できそう。
  • でもまだバグがある。まだ新しいライブラリのようなので、今後に期待。