IT練習ノート

IT関連で調べたこと(実際は嵌ったこと)を書いています。

JavaParserを使ってみる

Javaのコードレビューをしていて、指摘事項の水平展開をする時に、エディタ等のgrepで検索して、類似の指摘箇所を探したりします。 ただ、それだと、複数行にまたがり相関をもった内容の抽出に手間がかかります。

例えば、前のプロジェクトでは、stream処理の中に、さらにstream処理を使っているケースでは、可読性が著しく下いコードがたくさんありました。当初は、一律禁止するつもりはなかったのですが、一律禁止することにしました。禁止するので、このルールが守られているかチェックが必要になり、grepソースコードの当たりをつけて、その前後を目視確認する作業に手間がかかってしまいました。

なんらかのツールをカスタマイズすればよかったのですが、カスタマイズのルールを学ぶのが面倒だったので、結局、grep+目視をしていました。

結局、込み入ったチェックやプロジェクトのルールに従っていることをチェックをするには、Javaソースコードをパースするしかありません(というか、各種ツールもパースしてますが)。

そこで、次回に活かせるようにJavaソースコードをパースの練習をしてみました。

パーサーはJavaParserが良さそうだったので、使ってみました。基本的な使い方は、

  • ソースコードを読み込み、
  • Visitorを適用するだけです。
  • 各種ノードに対するドメソッドがたくさん用意されているので、それをオーバーライド実装するだけです。

Visitorパターンなので、同じノードでも階層が異なった時の対処が面倒くさいのですが。

最初はどのようなノード構造になっているかわからないので、

  • いったんパースした後のノードを全部出力しておく
  • ツリーを出力しておく

と良いと思います。

下のコード例では、

  • ノード全体の出力(デバッック用)
  • ツリーをJSONとして表示(デバッック用)
  • メソッドの行数(練習)
  • forEachのなかにforEachがある部分(前のプロジェクトでチェックしたかった箇所)

を出力しています。

いろいろできると思います。

  • 長いラムダ式を抽出
  • 嘘をついている変数名の抽出(例、Mapなのに変数名がListとなっている)
  • コメントだけを抜き出して、文章をチェックするとか(オフショア案件の時)

JavaParser Sample

JOOXで要素をwrapする。

過去記事で、jooxを取り上げたました。これを案件で補助ツールとして使っています。一律にxmlを補正したい時や、xmlの記述がプロジェクトのルールに従っているかをチェックするためにつかっています。

JavaでjQueryライクにXMLを操作する - IT練習ノート

ただ、jQueryにあるwrap相当のものがありません。もちろん、完全に無いわけではありません。

Match tags = $(document).find("body");
tags.wrap("aaaa"); // OK

というような、タグをwrapすることはできます。しかし、属性があるタグをwrapするインターフェースがありません。次のコードは実行時エラーになります。

tags.wrap("<aaaa/>");  // NG
tags.wrap("<aaaa id='abc'/>");  // NG

Matchインターフェースを実装したImplクラスを拡張もしたかったのですが、実装が正しく隠蔽化されているので、拡張する余地がないようい思えました。

仕方がないので、簡易メソッドを作成しました。

コードを示します。

嵌った点としては、異なる要素は直接追加できないことでした。

xml - Java: how to wrap all elements with <sometag> in org.w3c.dom? - Stack Overflow

Javaのstreamで嵌る

IntStreamStream<T>インターフェースに親子関係がない

 java.lang.AutoCloseable
     java.util.stream.BaseStream<T,S>
         java.util.stream.DoubleStream
         java.util.stream.IntStream
         java.util.stream.LongStream
         java.util.stream.Stream<T>

そのために(?)、IntSteramに3つの引数をもつreduceがない。これに気がつかず、相当時間を無駄にした。

順次および並列の集約操作をサポートするプリミティブint値要素のシーケンスです。これは、Streamに対してintプリミティブ特殊化を行ったものです。

とドキュメントにはあるが、特殊化といっても型だけ特殊化されているのではない。

多分、このようなクラス階層になっているのは、IntStreamIntegerではなくintだからなんだろうけど。

Streamインターフェースの2つの引数を持つreduceと3つの引数を持つreduceで2番目のパラメータ部分の型が異なる。

2引数

reduce(T identity, BinaryOperator accumulator)

3引数

reduce(U identity, BiFunction; < U,? super T,U > accumulator, BinaryOperator < U > combiner)

2引数の場合、BinaryOperatorの型がTなので、accumlatorの型はStreamの型と同じ必要がある。

また、実際のコーディングでは、3引数のcombinerのコーディングが定型的になる(と思うので)冗長になる。

intelliJ型推論が微妙にずれている

3引数reduceでパラメータの3番目が間違っているのに、2番目のエラーがでる。javacでは3番目のパラメータのコンパイルエラーになる。

f:id:naotoogawa:20181202152735p:plain
第3パラメータの問題なのに第2パラメータのエラーと表示される

f:id:naotoogawa:20181202152730p:plain
コンパイルした時のエラー

単位元?

T reduce(T identity, BinaryOperator accumulator)

指定された単位元の値と結合的な累積関数を使ってこのストリームの要素に対してリダクションを実行し、リデュースされた値を返します。これは、次の操作に相当します。

足し算の単位元は0だけど、0でなくても動作する。

        int r = Stream.of(1,2,3).reduce(10, (x,y) -> x + y);
        System.out.println(r); // 16

サンプル

Snake caseCamel caseの返還をstreamやラムダで書くといい感じになるかなと思ったのですが、そうでもないですね。

    private static String toCamel(String str) {
        return Arrays.stream(str.split("_"))
            .map(x -> x.substring(0,1).toUpperCase() + x.substring(1).toLowerCase())
            .collect(Collectors.joining());
    }

    private static String toSnake(String in) {
        String out = in.codePoints()
            .mapToObj(c -> (char)c) // IntStream to Stream<Character>
            .reduce("" ,
                (acc, v) -> acc + (Character.isLowerCase(v) ? v : "_" + Character.toLowerCase(v)) ,
                (s1, s2) -> s1 + s2
            );
        return out;
    }

もっと簡潔にかけるのなら教えて下さい。

感想

Javastreamは、慣れてないだけかもしれないが、難しい。というか面倒。

reduceは可読性が上がらなさそう。結局2変数の関数を作らないといけないこともあるので。

stream処理を考えた時には、reduceを使わない範囲で使うと良いのではないか。

reduce使うくらいならforでループして処理するのが良さそう。

JavaでjQueryライクにXMLを操作する

github.com

jOOXというライブラリを使えば良さそうです。

嵌ったところは

DOCTYPEがあると読み込んでくれないので、その場合、自分でxmlファイルを読み込む必要がある。

childrenメソッドでセレクタでタグを2個指定するのは動かない模様。(findなら複数指定できる模様)

出力は、xml宣言とかはつけてくれない。なので自分で出力する必要がある。

joox example

Unfoldの使いどころ

haskellのコーディングでfoldrは頻繁に使う印象がありますが。その双対(で、あってるのかな?)のunfoldは、あまり使わない印象があります。というか、使いどころのイメージがつかめませんでした。

この前、n進数間の変換に使えることに気づきました。 が、n進数間の変換にunfoldを使うのは、教科書の定番のようです。教科書をしっかり読むべきでした。

Programming in Haskellの7.9の練習問題6など。

[https://www.amazon.co.jp/%E3%83%97%E3%83%AD%E3%82%B0%E3%83%A9%E3%83%9F%E3%83%B3%E3%82%B0Haskell-Graham-Hutton/dp/4274067815:title]

実際にやりたかったことは、数値をバイナリにすることだったのですが、

generate :: (Num a, Integral b, B.Bits b) => b -> Maybe (a, b)
generate val =
  if val == 0
    then Nothing
    else Just (fromIntegral $ val B..&. 0xFF, val `B.shift` (-8))

を定義しておくと、

 > :t Data.List.unfoldr generate 0x0efed
Data.List.unfoldr generate 0x0efed :: Num a => [a]
 > Data.List.unfoldr generate 0x0efed
[237,239]
 >
 > :t Data.ByteString.unfoldr generate 0x0efed
Data.ByteString.unfoldr generate 0x0efed :: B8.ByteString
 > Data.ByteString.unfoldr generate 0x0efed
"\237\239"

ができます。

unfolrを使い分けることによって、結果を変えることができます。

Data.BinaryのputとputByteString

ByteStringBinaryクラスのインスタンスになっているので、putputByteStringは同じものかと思ったのですが、差がありました。

Data.Binary

 > runPut $ put $ BSL.pack [0xFF,0xFE,0xFD]
"\NUL\NUL\NUL\NUL\NUL\NUL\NUL\ETX\255\254\253"
 > runPut $ putLazyByteString $ BSL.pack [0xFF,0xFE,0xFD]
"\255\254\253"
 ```

実装を見ると```ByteString```の```Binary```のインスタンスは、バイト列だけでなく、その長さの情報もデコードしています。

Haskellのerrorで嵌る

次のような作りかけのコードを書いていました。errorの部分はあとで修正していくつもりでした。コーディングが進んで、errorの部分をコーディングする段階に来たのですが、show tの内容が出力されませんでした。

paramMetaData2TypeString_ (VAR   tipe len)      =
  case tipe of
    (VARLEN_BYTELEN_TYPE   t) ->
      case t of
        INTNTYPE -> case (lengthTYPE_VARLEN len) of
                        0x01 -> "tinyint"
                        0x02 -> "smallint"
                        0x04 -> "int"
                        0x08 -> "bigint"
        _          -> error  "not yet implemented. type=" ++ (show t)  -- TODO

結局errorの後に$が抜けているだけなのです。

ただ、$がなくてもタイプチェックになってしまうのが落とし穴でした。

 >
 > :t error
error :: [Char] -> a
 > error "foo"
*** Exception: foo
CallStack (from HasCallStack):
  error, called at <interactive>:25:1 in interactive:Ghci2
 > :t error "foo"
error "foo" :: a

> -- $をつけない

 > error "foo" ++ show "bar"
"*** Exception: foo
CallStack (from HasCallStack):
  error, called at <interactive>:27:1 in interactive:Ghci2
 > :t error "foo" ++ show "bar"
error "foo" ++ show "bar" :: [Char]

> -- $をつける

 > error $ "foo" ++ show "bar"
*** Exception: foo"bar"
CallStack (from HasCallStack):
  error, called at <interactive>:32:1 in interactive:Ghci2
 > :t error $ "foo" ++ show "bar"
error $ "foo" ++ show "bar" :: a
 >