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の結果が空になってしまいました。こちら問題はまだ原因を調査中です。なにか分かったらまた書くかもしれません。