mike-neckのブログ

Java or Groovy or Swift or Golang

Enumeration を使いやすくする(Iterable にする/Iterator にする)

TL で Enumeration が使いづらいというツイートが流れてたら、鮮やかに解決するツイートも流れてきた。


Enumeration<String> enumeration = ...;
Iterable<String> iterable = enumeration::asIterator;

f:id:mike_neck:20191128174829p:plain

asIterator というメソッドが Java9 から生えていたらしい。


残念なことに 拡張 for 文のソースの部分では推論がうまくできないため、コンパイルエラーになる

for (String s: enumeration::asIterator) { System.out.println(s); }

f:id:mike_neck:20191128174603p:plain

JJUG CCC 2019 Fall で発表した #jjug_ccc

表記の通り、 JJUG CCC 2019 で発表してきた。

f:id:mike_neck:20181019112748p:plain

jjug-cfp.cfapps.io

もともと発表資料を Key Note で作っていたものの、前日に予行演習した際に 90 分かかってしまい(セッションの時間は45分)、資料中に無駄な要素と有用な要素が分かちがたく結びついているなどで資料を削るのも難しいと判断、前日の 22:30 頃に 0 から資料を作り直すことにしました。極力短時間で書きたいため、レイアウトなどを考えなくてよいマークダウンで記述した結果、 GitHub のレポジトリーに発表資料ができる次第になりました。

github.com

発表中のツイートをまとめたのがこちらです

togetter.com

いつも参加された勉強会の発表をイラストでまとめてくださる 中山さん さんにまとめていただけました。光栄です!!!!


セッションについて

セッションでは時間が足らずにいくつか説明できなかったものがあるので、手短に雑に解説します

3-4. 概念に適切な例外を用いる

ざっくりいうと、

  • モデルとかレポジトリーインターフェースに実装依存の例外クラスを使わない(垂直方向に適切な例外を使う)
  • 業務内容的におかしな例外は投げない(ユーザーの認証モジュールが在庫モジュールの例外を投げてたらおかしい)(水平方向に適切な例外を使う)

3-6. 本当に必要なところだけに例外を使う

内容的にはバリデーションで引っかかったものについては例外を使わないですが、遠巻きに例外を使うなという過激なことを言っています

業務において期待した動作が不可能なのは

  • 入力が業務の入力値的に許容されない(バリデーション)
  • 業務的に操作が許可されない(パーミッション等DBへのアクセスが必要なものなど)

の 2 パターンだと思われますが、これらの違反状態は機能仕様として想定の範囲内だと思われます。したがって、これらは決して想定できない例外状態ではないので、例外で表現しないほうがよいだろうという主張をする予定でした。

発表の後に、技術的な例外と業務的な例外をうまく区別して扱う方法がないかという質問をうけて、 3-4. の回答をしましたが、こちらの業務には例外使わなくてもよいのではないかという回答でも良かったかと思っています。


最後に参考文献について

もともとは自分の経験を元に例外について喋っていこうと考えていました(CFPの時点では)。ところが、僕は語彙が圧倒的に足りないのか「例外とは何か?」という根本的な質問に対する適切な回答ができませんでした。 『Effective Java 第3版』 を読み返してみると、「契約による設計」というキーワードがそこかしこに見られ、また以前参加した java-ja「LOG.debug("nice catch!")」 での t_wada さんの資料と西尾さんの記録(はてな)を読み返すと、「契約」「事前条件」「事後条件」といったキーワードもあり、バートランド・メイヤーの 『オブジェクト指向入門 第2版 原則・コンセプト』 を読むことにしました。

オブジェクト指向入門』には例外の定義と例外処理の原則が書かれており、 JavaGolang などの実装から想定される例外に慣れていると考えもしないようなことを反省できます。その最たる例は「回復」というキーワードだと思っています。僕らの大好き『Effective Java』の項目70(第2版は項目58)に 「回復可能な状態にはチェックされる例外」という見出しが設定されており、これをもとにそこかしこで検査例外と非検査例外の使い分け条件として「回復」可能性を根拠としています。ところが、この回復についてちゃんと定義している人は『Effective Java』を含めてほとんどいないのです。『オブジェクト指向入門』では「失敗するケース」に「回復」について書いてあります。

ルーチンの実行中に例外が起き、ルーチンがその例外から回復しない場合に限り、そのルーチンコールは失敗となる。

オブジェクト指向入門 第2版 原則・コンセプト』p.530

これは逆に言うと、ルーチンから回復するとルーチンは成功になるということを意味します。ここから制御された例外処理の原則(『オブジェクト指向入門』p.534)をあわせて考えると、「回復」というのが「阻害されてしまった事後条件の達成を、再び獲得しようとする試み」と定義できる。また、「12.4.2 ハードウェアあるいはオペレーティングシステムの例外から回復する」(p.542)では浮動小数点数の 0 付近の値の除算命令の実行に対して、不可能な場合にはデフォルト値 0 を返すという回復の具体例があげられる。これはデフォルト値を返すことによって事後条件を達成するという「回復」を示す例である。

ここからは、芋づる式に例外処理ですべきことが明確かつ原理に即して(経験とか適当な推量ではなく)説明できるようになります。広義の事後条件の中にはクラスの不変条件が含まれており(『オブジェクト指向入門』p.472)、結果が成功であれ失敗であれルーチンの終了には不変条件の再構築が求められる(同p.549)(。『Effective Java』では、このことを項目76(旧64)「エラーアトミック性に務める」と言い直しています)。こうして、発表資料の 1-4.例外処理でやるべきこと3-5. 検査例外の使い分け の記述を書いていきました。


今回の発表資料は自分の無知からはじまって、『オブジェクト指向入門』を頼りに構成していきました。勢いと勘で書けばそれほど時間かからずに資料は作れたと思います。しかし今回はわからないことへの不安が強かったため、レンガを一つ一つ積み上げていくようなやり方で資料を書き上げました。そのため非常に時間がかかってしまい、所属する会社の皆様に協力をしていただくなどしました。大変感謝しております。

Apple 製の ネットワークアプリケーションフレームワーク ServiceTalk

2019/11/6 くらいに apple が servicetalk という Netty ベースのネットワークフレームワークを発表していたので、この数日間さわっていました。

さわった印象は、SpringBoot(Web) や micronaut 、 Helidon 、 Quarkus といった軽量 Web フレームワークに比べると明らかに機能が少ない(ネットワークフレームワークなので DI はない)ですが、 Netty をコントロールする煩わしさを解消(ByteBufBuffer というインターフェースで抽象化していて、さらにユーザーはそれを気にしなくて良い)して、より上位のレイヤーでアプリケーションを組めるようなフレームワークになっています。

github.com

apple.github.io


Hello World

それでは簡単なアプリケーションを書いてみます。

Gradle は次のような感じにします。なお、まだ Maven Central にはないようなので、 Bintray から jar を取得します。

plugins {
    id 'java'
    id 'application'
}

repositories {
    jcenter()
    maven {
        url "https://dl.bintray.com/servicetalk/servicetalk/"
    }
}

dependencies {
    implementation(
            "org.slf4j:slf4j-api:1.7.28",
            "io.servicetalk:servicetalk-http-netty:0.20.0",
            "io.servicetalk:servicetalk-annotations:0.20.0",
    )
    runtimeOnly "ch.qos.logback:logback-classic:1.2.3"
    testImplementation 'org.junit.jupiter:junit-jupiter-api:5.4.2'
    testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine:5.4.2'
}

application {
    mainClassName = "com.example.App"
}

test {
    useJUnitPlatform()
}

つづいてアプリケーションクラス

package com.example;

import io.servicetalk.concurrent.api.Single;
import io.servicetalk.http.api.*;
import io.servicetalk.http.netty.HttpServers;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.util.stream.Collectors;
import java.util.stream.StreamSupport;

import static io.servicetalk.http.api.HttpSerializationProviders.textSerializer;

public class App {
    private static final Logger logger = LoggerFactory.getLogger(App.class);

    public static void main(String[] args) throws Exception {
        HttpServers.forPort(8080)    // (1)
                .listenAndAwait((ctx, request, responseFactory) ->
                        hello(request, responseFactory)) // (2)
                .awaitShutdown();  // (3)
    }

    private static Single<HttpResponse> hello(
            HttpRequest request,
            HttpResponseFactory responseFactory) {
        Iterable<String> names = request.queryParameters("name"); // (4)
        String message = StreamSupport.stream(names.spliterator(), false)
                .collect(Collectors.joining(", ", "Hello, ", "."));
        logger.info("message: {}", message);
        return Single.succeeded(  // (5)
                responseFactory  // (6)
                        .ok()
                        .setHeader("Content-Type", "plain/text")
                        .payloadBody(message, textSerializer()));  // (7)
    }
}
  1. http を ポート 8080 で listen するように指定します
  2. HttpService を指定して、アプリケーションを起動します
  3. アプリケーションが終了するまで待ちます
  4. リクエストの情報(パス、ヘッダー、クエリ、ボディなど)を HttpRequest オブジェクトから取得します
  5. レスポンスは Single<HttpResponse> で返します。これは Reactor の Mono のような 0 個または 1個のストリームをあらわすオブジェクトです
  6. レスポンスのデータは HttpResponseFactory 経由で作成します。おそらく、これは ByteBufByteBufAllocator を直接操作しなくて良いようにするためだと思われます。
  7. HttpSerializer とレスポンスのオブジェクトをボディに指定します。 HttpSerializer はレスポンスのオブジェクトを Buffer に流し込むためのオブジェクトです。ここではテキスト(plain/text)専用のものを使っていますが、他にも json 用などがあります

というわけで、動かすとこんな感じになります

最近の Web アプリケーションフレームワークもこのような感じで書けるので、特に驚くほどではないかと思います…


Spring Integration

先述の通り、 ServiceTalk はネットワークフレームワークなので、 DI 機能はありません。したがって、足りない機能を Spring で補います。

Gradle のスクリプトは、 https://start.spring.io からとってきたものに ServiceTalk のものを追加した感じになります。

plugins  {
  id  'org.springframework.boot'  version  '2.2.1.RELEASE'
  id  'io.spring.dependency-management'  version  '1.0.8.RELEASE'
  id  'java'
}

group  =  'com.example'
version  =  '0.0.1-SNAPSHOT'
sourceCompatibility  =  JavaVersion.VERSION_12

ext {
  servicetalkVersion = '0.20.0'
}

repositories  {
  mavenCentral()
  maven {
    url "https://dl.bintray.com/servicetalk/servicetalk/"
  }
}

dependencies  {
  implementation  (
          'org.springframework.boot:spring-boot-starter',
          "io.servicetalk:servicetalk-http-netty:$servicetalkVersion",
          "io.servicetalk:servicetalk-annotations:$servicetalkVersion",
          "io.servicetalk:servicetalk-http-router-predicate:$servicetalkVersion",
          "io.servicetalk:servicetalk-data-jackson:$servicetalkVersion",
  )
  testImplementation('org.springframework.boot:spring-boot-starter-test')  {
    exclude  group:  'org.junit.vintage',  module:  'junit-vintage-engine'
  }
}

test  {
  useJUnitPlatform()
}

アプリケーションのメインクラスです。 ApplicationContextInitializer を用意していますが、普通にやる分には必要ないです。

@SpringBootApplication
public class App implements ApplicationContextInitializer<GenericApplicationContext> {

    private static final Logger logger = LoggerFactory.getLogger(App.class);

    public static void main(String[] args) {
        SpringApplication.run(App.class, args);
    }

    @Override
    public void initialize(GenericApplicationContext applicationContext) {
        logger.info("initializing application");
        applicationContext.registerBean(
                HttpSerializationProvider.class,
                () -> HttpSerializationProviders.jsonSerializer(new JacksonSerializationProvider()));
        applicationContext.registerBean(MyHandler.class);
        applicationContext.registerBean(ServiceTalkRunner.class);
    }
}

ServiceTalk の設定・起動は、 CommandLineRunner の実装クラスで行います。

public class ServiceTalkRunner implements CommandLineRunner {

    private static final Logger logger = LoggerFactory.getLogger(ServiceTalkRunner.class);

    private final MyHandler handler;

    public ServiceTalkRunner(MyHandler handler) {
        this.handler = handler;
    }

    @Override
    public void run(String... args) throws Exception {
        logger.info("starting service on http://localhost:8080");
        ServerContext serverContext = HttpServers.forPort(8080)
                .listenStreamingAndAwait(router());
        serverContext.awaitShutdown();
    }

    private StreamingHttpService router() {
        return new HttpPredicateRouterBuilder()
                .whenMethod(HttpRequestMethod.GET)
                .andPathMatches(Pattern.compile("/runner/\\p{N}+"))
                .thenRouteTo((ctx, req, factory) -> handler.handle(req, factory))
                .buildStreaming();
    }
}

ハンドラーは次のような感じ

class MyHandler {

  static final Logger logger = LoggerFactory.getLogger(MyHandler.class);

  final HttpSerializationProvider provider;

  MyHandler(HttpSerializationProvider provider) {
    this.provider = provider;
  }

  Single<HttpResponse> handle(HttpRequest request, HttpResponseFactory factory) {
    logger.info("request: {}", request.path());
    String num = request.path().split("/")[2];
    return Single.succeeded(
        factory.ok()
            .payloadBody(
                Map.of("value", Integer.parseInt(num), "message", "hello"),
                provider.serializerFor(new TypeHolder<Map<String, Object>>(){})));
  }
}

これを動かしてみると、次のツイートのようになります。


これ以外にも、 http2 / gRPC / JAX-RS などがあり、まだまだ遊び足りないくらいですが、とりあえず紹介だけしてみました。

なお、僕が遊んでみたコードは次のレポジトリーにあります。

github.com

第96回箱根駅伝予選会のデータを勝手に分析してみた

先週末くらいに箱根駅伝の予選会があったようで、いろいろな実況ツイートが先週末流れていました。

その中で気になったのは以下のようなツイートたちです

前者は統計とか中央値とか人間騙されやすいよねという興味と麗澤大学で模擬試験とか受けたなーという記憶からの興味、後者は山梨学院大学が全盛期の時代を知っているおっさんにとって感慨深いという興味から気になりました。


さて、ここでは前者の様子を可視化してみようと思います。なお、この記事ではデータとグラフの傾きしか見てないので言っていることが非常に偉そうに見えます。すみません。

具体的には以下のデータをプロットします。

  • 各チーム上位10人のデータをタイムの良い順にソートする
  • 予選通過ラインの 10:57 を 10 で割った値(1:05:42) を基準タイムとして、各ランナーのタイムと基準タイムの差分を積み上げていく
  • 横軸は各チーム内での順位
  • 縦軸は差分タイムの積み上げ

これを予選通過ライン上の数校(以下の5校) + 1位 + 山梨学院大学 でプロットしてみました。

f:id:mike_neck:20191105103049p:plain

グラフで右上に上がっていく場合は、 n 番目のランナーは予選通過ラインの個人タイム(1:05:42)よりもよいタイムで走っていることを表します。逆に右下に下がる場合は予選通過ラインの個人タイムよりも悪いタイムで走っていることを表します。また、チームの10人通過順位とはグラフ上の x = 9 の点から x = 10 の点の傾きの値を表します(傾きが大きい値のほうがよいタイム、多くの場合は傾きはマイナスになっているので、水平に近いほうが10人通過順位が速いことになります)。

このグラフから…

  • 東京国際大学はさすが1位だけあって、チーム内8位くらいまでのランナーが 1:05:42 よりも速いタイムで走っていて、かつ、10人の中で大きく崩れている選手もいません。
  • 中央大学は4番手までの選手はいいのですが、それ以降の選手が伸びてくれていません。その結果7番手の選手で早稲田大学に追いつかれて、8番手で逆転されています。
  • 上武大学は1〜3番手までの選手はよいですが、4番、5番手の選手と力の差がありそうです。また、4番、5番手以降の選手がさらに力不足のようで、右肩下がりなグラフになっています。
  • 山梨学院大学は2番手の選手ですでに予選通過ライン上のタイムになってしまっているので、全体的に厳しい状況になっています。
  • 早稲田大学麗澤大学は非常によく似た傾向を示していて、6〜7番手の選手までが予選通過ラインのタイムとほぼ同じか若干速いタイムで走っています。選手層が厚いチームと言えます。では麗澤大学早稲田大学との違いですが、早稲田大学麗澤大学の差は1番手の差をずっとキープしている状態ですので、エースの有無が最終的な結果にあらわれたと言えそうです。もちろん駅伝なので、選手層をさらに厚くすることでも解決できますし、参加できなかったエースの回復によっても解決可能だと思います。

以上、趣味に全振りしたエントリーでしたが、プロット画像やデータの取得は Go 言語でやってみました。コードはこちら。

github.com

箱根駅伝というか関東学連箱根駅伝予選会の結果データをテキストファイルとかjsonなどという気の利いた形式ではなく、 PDF という残念な形式で配布していますので、以下の PDF のライブラリーを使いました。

github.com

これのフォーク元の rsc/pdf というのを最初使ったのですが、怪しい挙動だった(リソースの Close もれがあるっぽい)ので、フォークされたこちらを使ってみました。

グラフ画像の作成はこちらを使いました。

github.com


おわり