MethodHandlesで遊ぶ

初めに

今回出てくるのはこの辺

MethodHandleの朝はLookupオブジェクトを作ることから始まる。

Lookup lookup = MethodHandles.lookup();

フィールドを取得してみる。

  @Test
  public void test6() throws Throwable {
    Lookup lookup = MethodHandles.lookup();
    MethodHandle handle = lookup.findGetter(
      /* receiver type */ PublicTestObject.class,
      /* method name */ "stringField",
      /* return type */ String.class
    );
    assertThat(handle.invoke(new PublicTestObject("test"))).isEqualTo("test");
  }

  @AllArgsConstructor
  public static class PublicTestObject {
    public String stringField;
  }

はい。Lombokのアノテーションを使ってますが 大したコードじゃありません。 フィールドを参照するGetterとして、MethodHandle(関数みたいなもの)を取得してinvokeしています。

メソッドを実行してみる。

  @Test
  public void test1() throws Throwable {
    Lookup lookup = MethodHandles.lookup();
    MethodHandle handle = lookup.bind(
      /* method receiver */ new TestObject(),
      /* method name */ "get",
      /* method signature */ MethodType.methodType(/* return type */ String.class)
    );
    assertThat(handle.invoke()).isEqualTo("getting.");
  }

  // 上記とほぼ等価
  // 中の処理は少し違うが、いまいち違いが分かっていない。
  // 中でinvokeSpecialみたいな文言が飛んでいるが、findSpecialと少し動きが違う・・・?
  @Test
  public void test2() throws Throwable {
    Lookup lookup = MethodHandles.lookup();
    MethodHandle handle = lookup.findVirtual(
      TestObject.class,
      "get",
      MethodType.methodType(String.class)
    );
    assertThat(handle.invoke(new TestObject())).isEqualTo("getting.");
  }


  @Test
  public void test3() throws Throwable {
    Lookup lookup = MethodHandles.lookup();
    MethodHandle handle = lookup.findVirtual(
      HasMethodObject.class,
      "getField",
      MethodType.methodType(String.class, String.class)
    );
    // 引数の部分適用
    MethodHandle handle2 = MethodHandles.insertArguments(handle, 1, "xxxx");
    assertThat(handle2.invoke(new HasMethodObject("yyy"))).isEqualTo("yyyxxxx");

  }
  @Test
  public void test4(){
    Lookup lookup = MethodHandles.lookup();
    MethodHandle handle = lookup.findVirtual(
      HasMethodObject.class,
      "getField",
      MethodType.methodType(String.class, String.class)
    );
    // 引数の再配置
    MethodHandle permuted = MethodHandles.permuteArguments(
      /* 再配置するMethodHandle */ handle,
      /* 再配置後のシグネチャ */ MethodType.methodType(String.class, String.class, HasMethodObject.class),
      /* 再配置前後の順序 */ 1, 0);
    assertThat(permuted.invoke("zzzz", new HasMethodObject("yyy"))).isEqualTo("yyyzzzz");
  }

  @Test
  public void test5() throws Throwable {
    Lookup lookup = MethodHandles.lookup();

    MethodType methodType = MethodType.genericMethodType(1, true)
      .changeReturnType(String.class)
      .changeParameterType(0, String.class);
    assertThat(methodType.toMethodDescriptorString())
      .isEqualTo("(Ljava/lang/String;[Ljava/lang/Object;)Ljava/lang/String;");
    
    MethodHandle handle = lookup.findStatic(MessageFormat.class, "format", methodType);
    // 第一引数に部分適用
    MethodHandle formatter = handle.bindTo("format strings {0} {1}");

    // 可変長引数をいい感じに呼べるようにする
    assertThat(formatter.asVarargsCollector(Object[].class)
      .invoke("1", "2")).isEqualTo("format strings 1 2");

    // asVarargsCollectorを使わなかった場合:ダサい。
    assertThat(formatter.invoke(new Object[] {
      "1",
      "2"
    })).isEqualTo("format strings 1 2");
  }


  @Test
  public void test6() throws Throwable {
    Lookup lookup = MethodHandles.lookup();
    MethodType methodType = MethodType.genericMethodType(1, true)
      .changeReturnType(String.class)
      .changeParameterType(0, String.class);

    // 呼び出す関数
    MethodHandle formatter = lookup.findStatic(MessageFormat.class, "format", methodType)
      .bindTo("format strings {0} {1}");
    // 前処理関数
    MethodHandle trace = lookup.findVirtual(PrintStream.class, "printf", MethodType.methodType(PrintStream.class, new Class[] {
      String.class,
      Object[].class
    }))
      .bindTo(System.out)
      .asType(MethodType.methodType(void.class, String.class, Object[].class))
      .bindTo("arguments %s %s");

    // also prints "arguments xxx yyy"
    assertThat(MethodHandles.foldArguments(formatter, trace)
      .invoke(new Object[] {
        "xxx",
        "yyy"
    })).isEqualTo("format strings xxx yyy");
  }

  @AllArgsConstructor
  public static class TestObject {

    public String get() {
      return "getting.";
    }
  }

  @AllArgsConstructor
  public static class HasMethodObject {
    private final String string;

    public String getField() {
      return string;
    }

    public String getField(String xxxx) {
      return string + xxxx;
    }
  }
  • メソッドの呼び出し
  • メソッド引数の部分適用
  • 関数引数の再配置
  • 前処理関数による関数の合成?いまいち言い方が分かりません。

色々触りました。 楽しそうなのは部分適用と引数の再配置ぐらいでしょうか?

SAM Type Proxy

  @SuppressWarnings("unused")
  private static boolean test(Object x) {
    return x instanceof String;
  }

  @Test
  public void test1() throws Throwable {
    Lookup lookup = MethodHandles.lookup();
    MethodHandle handle = lookup.findStatic(ProxiesTest.class, "test", MethodType.methodType(boolean.class, Object.class));

    @SuppressWarnings("unchecked")
    Predicate<Object> predicate = MethodHandleProxies.asInterfaceInstance(Predicate.class, handle);
    assertThat(predicate.test("test")).isEqualTo(true);
    assertThat(predicate.test(1)).isEqualTo(false);

    // デフォルトメソッドは呼び出せないみたい。
    assertThatThrownBy(predicate::negate).isInstanceOf(InternalError.class);
  }

単一メソッドを持つインターフェースのプロキシが簡単に作れる模様。 デフォルトメソッドは呼び出せない。 マジか。内部で元々ある、Proxy.newInstanceが使われてるみたいです。

終わり

簡単なコードを少し試してみましたが いまいち盛り上がりに欠ける感じになりました。 元々あるリフレクションと比べるとAPIJVMよりになっているように感じます。

また、ラムダと関連を伺えるようなオブジェクトがいくつか存在しています。

今回の記事はラムダ式がどうやって実行されるか、という話を調べた際の副産物です。

可変長引数の周りでだいぶ苦戦しました。

Enjoy java.

今回この記事で書いたコードはここに置いてます。 github.com

参考に見たリンク

java.lang.invoke で遊ぼう - Java読書会合宿2012 Java7でカリー化?(部分適用でした、、、) - tomoTakaの日記

無限にTODOアプリを作っていき

今回はReact+TypeScript+Spring Bootで書いてみる。

フロントエンド構成

  • TypeScript
  • React
  • Webpack
  • Jest (一応入れたけど、習熟度が足りず画面のIFがガンガンぶっ壊れるから現状テスト書いてない)
  • bulma (CSSフレームワーク, そういえばhack使ってみたかったけど忘れてた)
  • ts-lint (お前もうちょい機能強くなって。エディタ拡張とかエディタ拡張とか)

フロントその他ライブラリ

サーバサイド構成

  • Spring Boot
  • thymeleaf3
  • とりあえず感のあるh2
  • こちらもとりあえずDBに初期データ突っ込むだけのflyway
    • というか素振りみたいな感じでやってて何も固めずにガンガン作ってるのでマイグレーションもクソもない。

サーバーサイドその他

  • gradle (wrapper)
  • spotless (フォーマッタ)
  • spring-boot-devtools (すごい。楽。)

ヤケクソで進めている感じが否めないのですが、色々悩んでSpringのコード書き散らして、散々紆余曲折した後に
色々コードぶち壊してなんとなくDBに保存するまでできたわけなんですが。
ごらんのありさまだよ!!

f:id:reteria:20170328053838p:plain

もともと何をしようとしてたかというと
IndexedDB(もしくはlocalstorage)をベースに
オフラインで動くようなタスク管理アプリケーションを弄っていた。
(というかこの構成で毎回TODOアプリ作ってる気がする。進歩がない。)

で、なぜそうしたいかというと結局オフラインでも動くと面白い、それだけなんですが
このデータの同期をどうするかで悩んでしまった。

サーバーの状態とクライアントの状態が常に存在している。


なので少し、頭の整理のためにメモを書こうと思う。
ここから下は本当にメモ

例えば、サーバーにつながらない状態でタスクを追加したとする。
これはサーバーとつながったら同期されてほしい。
つまり、クライアントで表示はされているけど
クライアントしかデータを保持していない状態がある。

保存されていないデータがあれば何回もサーバにリトライかけて保存しにいくのかというと
現実そういうわけにもいかない。とりあえず5回ぐらいで考えてみるとする。

そういった場合には保存されていない状態で画面ではクルクル回ってるアイコンを出したい。
リトライ5回して失敗したら、エラー表示みたいなのを出したい。
エラー表示のアイコンを押したらエラーメッセージを出しておきたい。

サーバーとつながらない状態でタスクを削除したとする。
まぁ当然サーバーとつながらないので論理的に削除できない。
しかし、画面上消したい。
グレーアウトしておくとする。
これもクルクルとエラーアイコン欲しい。

更新は削除して書けって感じでとりあえず無視しておく。

画面読み込み時ないしリフレッシュボタン押下にデータを更新したい。
ここが頭の中で整理ができていない。
まずここで存在する可能性があるのが次の状態のタスク。

  1. 画面で追加したけどサーバーに永続化されていないタスク
  2. 画面で削除したけどサーバーから削除されていないタスク
  3. 別のPCから追加したけどローカルストレージには反映されていないタスク
  4. 別のPCから削除したけどローカルストレージには反映されていないタスク

1はサーバーに追加したい。
2はサーバーから削除したい。
3はローカルストレージに反映して表示したい。
4はローカルストレージに反映して削除したい。

どうするのが正解なのかいまいち分からないので
とりあえず案を上げてみる。
上記4つの状態を観点として考える

  1. タスクの状態に画面で生成された <–> 永続化されたみたいな状態を持つ
    これはローカルストレージのみに保存する。DBには持たない。
    すると、ローカルストレージから読みだした際に、永続化されていないことは分かる。
    あとは永続化されていないものを登録しにいくだけである。
    でも削除された時はどうなのか?これも状態を持つとうまくいきそう。
    下みたいなイメージならどうか。
    画面で保持 –> 永続化 –> 画面で削除 –> 物理削除

  2. タスクを4種類にわけて画面で保持しておく。
    上で書いた状態のストレージを画面上で4種類用意しておいて、画面上は同じ場所に表示
    画面で保持 –> 永続化 –> 画面で削除 –> 物理削除
    楽にうまくいきそう。画面側で頑張ればいい。
    観点3に関しては読み込み時にサーバーから取ってきたものを永続化されてるとして
    データを持っておけばいい。観点1, 2は何も考えなくてもよさそう。
    観点4の状態が難しい気がしたけど、永続化されている情報を持ってきた際に
    永続化されている情報と突合すればいい。

またもう少し図とか書いて練ってみる。

リポジトリここ
oauthで遊ぼうかと思ったけど余裕がなかった。
というか使う必要性が現状なかったのでディレクトリ名変えなきゃ・・・

TypeScriptでモデルの型定義を良い感じに管理したい。

効率良くTypeScriptでドメインモデルの型定義を管理したい。

例えばここに、以下のようなツイッターのような投稿を模したモデルがあるとする。

type Post = {
  id: number,
  content: string
};

ふむ。特筆すべきものはない。 ではこれをモジュールとして型だけexportすることにする。 一文増えた。

type Post = {
  id: number,
  content: string
};

export = Post

ではこの型定義を利用した利用シーンを考える。 addPostという投稿を追加する関数を例で上げてみる。

import * as Post from "./model/post"

const addPost=(post:Post)=>{/* some implementation */}

...

ふむ。良さそうに見える。

ではaddPostを利用するシーンに移ってみる。 ここではテキストエリアに書かれた内容をonClick時に投稿するシーンを想定してみる。 一緒に連番でIdを生成するような関数を想定する。

import {addPost} from "./addPost"

let idCounter=0
const generateId = () => ++idCounter

const onClick=(content:stirng) => {
  const id=generateId()
  addPost({id, content})
}

はて、contentはstringになってしまい、無味乾燥な型になっている。 このstringはなんのstringだっけ?どんな意味を本来持っているんだっけ?ということになりうる。

ここでPostの型定義をしたモジュールに対して色を加えてみようと思う。 次のようなコードだ。

type Post = {
  id: number,
  content: string
};

declare namespace Post{
  export type Id = Post["id"] 
  export type Content = Post["content"] 
}

export = Post

ちょっと面倒な感じがする。 では、先ほどの無味乾燥なソースに手を加えてみようと思う。

import {addPost} from "./addPost"
import * as Post from "./model/post"

let idCounter=0
const generateId:(() => Post.Id) = () => ++idCounter

const onClick=(content: Post.Content) => {
  const id=generateId()
  addPost({id, content})
}

IdやContentの型がコードに表れており、カラフルなコードになったように思う。

さて、ここでリファクタリングのことを考える。 VSCodeのF2で出来るただのRenameのことである。

ここでPostのidの名前変えたくなった。screenIdにしたい。 F2でコードを変えるとどうなるか。

次のようになった。(まとめて書く)

type Post = {
  screenId: number,
  content: string
};

declare namespace Post{
  export type Id = Post["id"] // ここにエラーが発生する。
  export type Content = Post["content"] 
}

export = Post

// ....

import {addPost} from "./addPost"
import * as Post from "./model/post"

let idCounter=0
const generateId:(() => Post.Id) = () => ++idCounter

const onClick=(content: Post.Content) => {
  const id=generateId() // ここにエラーが発生する。
  addPost({screenId, content})
}

コメントで示したところはエラーが発生しており 手で編集する必要がある箇所が2か所発生する。

ここでは修正方法についてはそこまで難しいわけではないので解説しないが Id用の型があるおかげで、ある程度、楽に型を柔軟にメンテしやすくなるように思う。 他にいい方法があれば誰か教えていただければと思う。

Happy TypeScripting!