mike-neckのブログ

Java or Groovy or Swift or Golang

それでも僕はGroovyが好きですけど、何か?

こんにちわ、みけです。

なんか、金曜日のデブサミでgroovyのアレでJavaに対するdisがあって云々かんぬんというツイートを見たのですが…

JavaをdisしなくてもGroovyはいい言語なので、僕がGroovyを好きな理由2015年版を上げておきます。


ちょっとした下らないプログラムを書ける

僕はGroovyを使って下らない統計的な検証とかやってたりします。

宝くじで一等が同じ店から出る確率 - mike-neckのブログ

【メモ】ヒープ領域を16mにして小さいながらも50,000,000件あるデータをGroovyであれこれしようとした結果wwwwwwwwwww - mike-neckのブログ

なんかJavaの人たちでじゃんけんのプログラムが流行ってるので、便乗して書いてみた Groovy編 - mike-neckのブログ

カイジの地下チンチロで班長大槻が勝つ確率をシミュレートするGroovyスクリプト - mike-neckのブログ

本当に下らない内容なのですが、ちょっと気になったことで、IDEをいちいち取り出してプログラムを書くのもなんだかなーという時にLLで書くものですが、僕はpythonとかrubyとか、あまり良く知らないので、Javaの延長で書けるGroovyを使います。

GroovyはJVMの起動が遅いですって???

GroovyServで高速起動

bash力もないので、gitのhookもgroovyで書くのですが、groovyで起動していると、コミットするのが億劫になるほど遅いです。

そこで登場するのがgroovyservです。

これが、お前、本当にgroovyか?と思うほど高速です。groovyが云々という人はgroovyservをやってから石を投げなさい。


Annotation Processorの代わり

ボイラープレートなコードを生成する場合、Lombokだとか、Annotation Processorを使ったりすると思います。

ただ、LombokはIntelliJ IDEAのプラグインにバグ(直ってるかも)があったりして使いたくないし、Annotation Processorはそれ自体書くのが面倒だったりします。

そこで登場するのがgroovyによるコードの自動生成です。

最近、僕は歳をとってきたせいもあって、技術を高めてよいサービスを提供するためにコードを書くことよりも、自分の好きなことにコードを書きたいという思いが強くなってきていて、今は見るのが大好きな陸上競技のデータを収集するサイトを作ろうとしています。

そこで、問題になるのが陸上競技という種目の種目数が多いということです。

ほとんど同じようなデータ構造なのに(跳躍系/混成競技は除く)検索性をよくするためにクラスを別のものにしたいという理由から種目ごとにクラスを作る必要があって、それをIDEで頑張ろうとすると死ねます。

それをテキストで簡単に必要な情報だけを記述しておいて、クラスを自動で生成するために、groovyを書いたりします。

例えば、次のはmarkdown形式で書いた種目の一覧からクラスとenumを生成するコードです。

/**
 * Created by mike on 15/02/19.
 */
import java.nio.file.*

def path = Paths.get('src/main/groovy/taf/repo')
println path.toAbsolutePath()
assert Files.exists(path)

def license = $//*
 * Copyright 2015Shinya Mochida
 * <p>
 * Licensed under the Apache License,Version2.0(the"License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 * <p>
 * http://www.apache.org/licenses/LICENSE-2.0
 * <p>
 * Unless required by applicable law or agreed to in writing,software
 * Distributed under the License is distributed on an"AS IS"BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND,either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
/$

def pkgName = "${path.toString().replace('/', '.').replace('src.main.groovy.', '')}"

println pkgName

interface TypeSupport {
    String asClassName()
    String impDef()
}

enum Type implements TypeSupport {
    Sprint, MiddleDistance, LongDistance,
    Hurdle, Relay, HighJump, LongJump,
    Throwing, Combined
    @Override String asClassName() {"Event.${this}"}
    @Override String impDef(){"import taf.model.Event;"}
    static TypeSupport fromString(String s) {
        try {
            return valueOf(s)
        } catch (IllegalArgumentException ignore) {
            return [asClassName: {s},impDef: {''}] as TypeSupport
        }
    }
}

import static Type.*

assert Sprint.asClassName() == 'Event.Sprint'

class Event {
    static String LICENSE = $//*
 * Copyright 2015Shinya Mochida
 * <p>
 * Licensed under the Apache License,Version2.0(the"License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 * <p>
 * http://www.apache.org/licenses/LICENSE-2.0
 * <p>
 * Unless required by applicable law or agreed to in writing,software
 * Distributed under the License is distributed on an"AS IS"BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND,either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
/$
    static String DQ = '"'
    String pkg
    TypeSupport type
    String name
    String title
    boolean combinedEvent
    @Override String toString() {
        "Event(pkg: $pkg, type: $type, name: $name, title: $title, combinedEvent: $combinedEvent)"
    }
    String pathName() {"src/main/groovy/${pkg.replace('.', '/')}/${name}.java"}
    String pkgDef() {"package $pkg;"}
    String impDef(boolean eventsJava) {
        eventsJava ? "import ${pkg}.${name};" : type.impDef()
    }
    String classDesc() {"$name extends ${type.asClassName()}"}
    String classDef() {"public final class ${classDesc()}"}
    String fileBody() {
        $/${LICENSE}
${pkgDef()}

${impDef(false)}

${classDef()} {
}
/$
    }
    String asEnumEntry(int index) {
        if (!combinedEvent) {
            return "    $name(${name}.class, $index, $DQ$title$DQ)"
        } else  {
            return "    ${type.asClassName()}Of${name}(${type.asClassName()}.class, $index, $DQ$title$DQ)"
        }
    }
}

def txt = Paths.get('memo/event.txt').toFile().text

txt.metaClass.define {
    collect = {Closure c ->
        def result = []
        delegate.eachLine {
            result << c(it)
        }
        result
    }
}

class Holder {
    String value
    void swap(String v) {
        if (v != '-') value = v
    }
    String get() {value}
}

def pk = new Holder(value: '')
def t = new Holder(value: '')

class Builder {
    String pkg
    String title
    String name
    String type
    boolean combinedEvent
}

def events = txt.collect {String line ->
    def e = line.split('\\|')
    pk.swap(e[0])
    t.swap(e[1])
    def combinedEvent = e[2].contains('/')
    def type = combinedEvent ? {String s -> s + e[2].split('/')[1]} : {String s -> t.get()}
    def name = combinedEvent ? e[2].split('/')[0] : e[2]
    List<Builder> l = []
    if(e[3] == 't') l << new Builder(pkg: "${pkgName}.${pk.get()}", title: e[5], name: "M${name}", type: "${type('M')}", combinedEvent: combinedEvent)
    if(e[4] == 't') l << new Builder(pkg: "${pkgName}.${pk.get()}", title: e[5], name: "W${name}", type: "${type('W')}", combinedEvent: combinedEvent)
    l
}.flatten().collect {Builder b ->
    new Event(pkg: b.pkg, type: fromString(b.type), name: b.name, title: b.title, combinedEvent: b.combinedEvent)
}

events.metaClass.define {
    collectWithIndex = {Closure c ->
        def l = []
        delegate.eachWithIndex { e, i -> l << c(e, i) }
        l
    }
}

events.findAll{!it.combinedEvent}.each {event ->
    try {
        def p = Paths.get(event.pathName())
        println(p.toAbsolutePath())
        Files.write(p, event.fileBody().bytes)
    }catch (Exception e) {
        pritln "error on writing file for [$event]. exception = $e"
    }
}

def body = $/$license
package $pkgName;

import java.util.stream.Stream;
import java.util.NoSuchElementException;

import taf.model.Event;
${events.findAll{!it.combinedEvent}.collect {
    it.impDef(true)
}.join('\n')}

import org.jetbrains.annotations.Contract;

public enum Events {

${events.collectWithIndex{Event e, int i ->
    e.asEnumEntry(i)
}.join(',\n')};

    private final Class<? extends Event> event;

    private final int id;

    private final String title;

    Events(Class<? extends Event> event, int id, String title) {
        this.event = event;
        this.id = id;
        this.title = title;
    }

    public int getId() {
        return id;
    }

    public String getTitle() {
        return title;
    }

    @Contract(" -> !null")
    public EventName toObject() {
        return new EventName(id, title);
    }

    public static EventName fromIdToObject(int id) {
        return Stream.of(values())
                .filter(e -> e.id == id)
                .map(Events::toObject)
                .findFirst().get();
    }

    public static Events fromId(int id) throws NoSuchElementException {
        return Stream.of(values())
                .filter(e -> e.id == id)
                .findAny().get();
    }
}
/$

def eventsJava = Paths.get('src/main/groovy/taf/model/events/Events.java').toFile()

eventsJava.write(body)

このスクリプトのお陰で、ちょっとした勘違いや考慮漏れによってクラスを一々修正する手間が省けたりして、非常に助かっております。え?こういうのもbashとかrubyとかpythonで書けるって?はい、すみません。

metaClassの存在

ここまでのスクリプトを読まれてきた方は気づかれるかもしれませんが、僕はmetaClassでクラスの拡張をよくやります。このような標準ではちょっと足りない機能を追加して書きやすくできるのもGroovyのいいところだと思っています。


なんかpivotalがgroovyの開発から距離をおいてしまって、今後どうなるか不安があるgroovyですが、今後もこのJava使いにとって非常に使いやすいgroovyが普及・発展していってほしいと思います。

おあり