watermint.org - Takayuki Okazaki's note

Javaパズラーの問題ができるまで

以前日経ソフトウエア2013年9月号〜2015年3月号にて至極のJavaクイズという連載を、持ち回りにて執筆していました。この連載は2ページの分量に、十数行程度のコードを示し、実行結果を4択程度の選択肢、掛け合いの形式で問題を補足し、正解を示し、問題から得られる教訓までを説明するというもので、もとはJoshua BlockらによるJavaOneの人気セッションJava Puzzlersを参考にしています。ほかにはJava Day Tokyoなどのイベントでも過去に同様のセッションを行なっていました。このJavaパズラーの問題ができるまでを数年ぶりに振り返ってみます。

問題ができるまで

良問といえるのは、問題を正解できなかったとしても、なるほどと言える教訓が得られるものです。そのような問題は間違えたとしてもスッキリと気持ちよく教訓として読み進めることができます。実際、本家Java Puzzlersにはそのような良問がたくさん掲載されています。

逆に良問と言いづらいのは重箱の隅をつついたような違いを問題として表現したものです。そのような問題は、教訓が得られるというよりは「ふーん、そうなの。。」ぐらいの感想しか得られないものです。

また、知識だけを問う問題もあまり良問とは言えないでしょう。たとえばjavax.timeのようなパッケージは問題を作りやすいのですが、インタフェースやクラス、メソッドもかなり多いので知識を問う問題になりがちです。出題する際には解説などで工夫が必要になります。

出題範囲について、あまり一般的でないライブラリの仕様などを出題するのはためらわれます。たとえばJava SE標準ライブラリで、日常よく利用するであろう基本ライブラリです。ラムダ式など比較的新しい言語仕様も良いでしょう。 パッケージで言えばjava.langjava.utilあたりで出題できると良問の期待が高まります。 java.iojava.niojava.sqlなども範囲として良いですが直接これらのパッケージを利用するよりはフレームワークなどに処理を任せることが多いでしょう。出題する場合には多めに注釈を入れるなどで調整を進めます。

個人的には問題を考えるには教訓から考えていきます。Java言語仕様はもちろんのこと、新しいJava SEバージョンで変更された振る舞いや、FindBugsCheckstyleなどのルールもよく参考にしていました。

nullの扱いやequalsの振る舞い、評価順などにまつわる問題は作りやすい印象があります。ただ、それらにまつわる問題はすでに出題済みであったり本家Java Puzzlersでだいたいカバーされているので新規性のあるテーマを探すのはなかなか骨の折れる作業です。

選択肢ができるまで

回答は4択程度のなかから選ぶことになっています。必ず4択でなければならない訳ではありませんが、2択だと大抵の場合回答があからさますぎるので、4択程度になるよう調整します。 たとえばequals==の違いについて出題しようとした場合、ぱっと思いつく選択肢のパターンは次のようなものです。

  1. equals==が同じ振る舞いになる
  2. equals==が異なる振る舞いになる
  3. NullPointerExceptionが発生する
  4. その他

選択肢を考える上で、4つ目が「その他」となるのはできれば避けたいものです。ボツとなりやすいのは「その他」で全く別の例外が発生するようなケースです。equals==の違いから教訓を説明したいのに、別の例外が発生するというようでは出題意図がよくわからなくなってしまいます。時間の都合で妥協することもありますが、なるべくこだわりたい部分です。

どうしても詰まってしまった場合は、あえて出題意図を変えて選択肢をすべて例外発生にしてみるというのも面白いでしょう。たとえば下記のような選択肢ではどうでしょうか。

  1. NullPointerExceptionが発生する
  2. StackOverflowErrorが発生する
  3. ConcurrentModificationExceptionが発生する
  4. IndexOutOfBoundsExceptionが発生する

IndexOutOfBoundsExceptionConcurrentModificationExceptionがあるのでjava.util.Listなどに関連する問題と教訓にすれば面白そうです。StackOverflowErrorと迷うように再帰処理をいれてみるなどいろいろ試せそうです。

このように最初equals==の違いを出題しようと考えていたものが、別の教訓を説明するような問題にどんどん変わっていくこともあります。問題のコードはequals==の違いを説明しようとしてそうなのに、実はjava.util.Listの振る舞いについての教訓だった。となれば読みがいがありそうです。

問題コードができるまで

問題コードができるまでにも紆余曲折があります。日経ソフトウエアのように紙面であれば40行程度あっても入ることがあるのですが、やはりあまり長いとテンポが悪くなるのでなるべく短く済ませたいものです。セミナーなどでプレゼンテーションとして出題する際にはスライドに選択肢まで入れた上で一画面に収まり、会場後ろの方にも見えるようフォントサイズはできるだけ大きくする必要があります。スライドテンプレートやレイアウトによりますが15行程度までがおおよその限度となります。

このためあまり冗長な初期化処理はできませんし、教訓のコアとなる出題部分以外に必要となるクラス・メソッド定義については最低限に抑える必要があります。 読みやすさを考慮しある程度改行も入れていく必要があり、インデントも不自然にならないよう工夫する必要があります。Java言語仕様などの制限を考えると実際に使えるのは20行程度と考える必要があります。

どれだけ削っても収まりきらない場合は残念ながらボツ案となります。よりテーマを絞り込むなど教訓を考えるところまで手戻りとなります。

掛け合い部分ができるまで

イベントなどでプレゼンテーションする場合は2人のプレゼンターがあらかじめ出題者と模擬回答者として役割をきめておきます。問題がJava SEバージョンに依存する場合や新仕様などをテーマにしている場合、模擬回答者がうまく出題者に質問するような格好で補足していきます。

追加で模擬回答者は「これはひっかけですね」などと、いくつかのトリックを説明しつつ、あまり教訓の核心に触れないように打ち合わせておきます。理想的にはこのように掛け合いで説明できるようなトリックが教訓以外でも含まれていたほうが良いのですが、あまりいろいろ高望みすると締め切りがどんどん厳しくなります。。

教訓ができるまで

問題と選択肢ができあがえれば作業のほとんどは完成です。あとは教訓をわかりやすく説明するのですが、そうは言っても紙面に収まるようわかりやすく手短に伝えようとするとなかなか難しいものです。

タイトルができるまで

タイトルはなるべく教訓や選択肢の特徴を表現しつつも、ヒントになりすぎないよう微妙な調整が必要です。教訓を知った上でニヤリとできるようなタイトルとなれば大成功といっていいと思います。

問題を作ってみる

数年ぶりに問題を作ってみました。

オーはオリンピックのオー

import java.util.List;
import java.util.stream.Stream;

class City {
  private List<String> futureOlympics =
        List.of("Tokyo", "Paris", "Los Angeles");
  private String city;
  City(String name) { this.city = name; }
  boolean isFuture() { 
    return futureOlympics.contains(city.trim()); 
  }
}
public class Olympics {
  static long countO(String city) {
    return city.chars().filter(c -> c == 'o').count();
  }
  public static void main(String... args) {
    System.out.println(
      Stream.of("London", "Rio", "Tokyo", "Paris", "Los Angeles")
        .map(City::new)
        .filter(City::isFuture)
        .map(c -> c + " Olympics")
        .map(Olympics::countO)
        .reduce(0L, (sum, count) -> sum + count));
  }
}

プログラムを実行すると何が出力されるでしょうか。

  1. 「0」
  2. 「2」
  3. 「3」
  4. 「6」

.

.

.

.

.

.

.

.

.

.

.

解答

答えは「1. 「0」」です。”London”や”Rio”といった文字列を順番にCityクラスのインスタンスにマッピングし、isFutureで将来のオリンピックだけに限定したうえで、残ったオリンピック開催都市の名前から o (小文字のオー)が何文字あるか合計を計算するプログラムです。

isFuturecountOにはあまり不審なところはありませんが、いかにも怪しいのは次の行です。

                .map(c -> c + " Olympics")

Cityクラスのインスタンスに文字列を連結しています。オートキャストによりCity#toStringが呼び出され、その後に” Olympics”が連結されます。CityクラスはtoStringをオーバーライドしていないためObject#toStringが呼び出されCity@2ed94a8bのようなクラス名とハッシュ値を使った文字列表現になります。

このCity@2ed94a8bにはo (小文字のオー)は含まれませんから合計値は0となるというわけです。

教訓

toStringの実装は忘れずに。

.

.

.

問題のレビュー

さてではこのパズラー問題をレビューしてみましょう。先ほど怪しいとご紹介したこの行。

                .map(c -> c + " Olympics")

処理の文脈上、isFutureなどまでの処理は自然ですが、oをカウントする処理の前に” Olympics”と追加するというのはかなり不自然です。できればこの不自然さをうまく消したいところです。また、いくつかStream APIを利用しているのにあまり特徴を活かせていないにのも改善したいところです。

選択肢についても同様でうまく迷いを誘導できていないように見えます。「2.「2」」はTokyoだけがカウントされる、あるいはTokyoとLos Angelsで1ずつカウントされればそうなりますがちょっと考えにくい選択肢です。 「3. 「3」」はTokyoとLos Angelsでどちらもoがカウントされたと考えるなら選択される可能性があります。 「4. 「6」」はすべての都市名についてoがカウントされたと考えるなら選択される可能性があります(isFutureがうまく働かなかった)。ただちょっとそれも強引でしょう。

まとめ

このように、教訓を作る → 選択肢を作る → コードを作る → レビューというサイクルを何度も繰り返すことでより良い問題が作成できます。 ただ、本当にちゃんとした教訓となるような良問を作るのはなかなか難しいものです。(特に締め切りがある場合には。。)

Jekyllサイトにタグとミニマイズを

このサイトをJekyllに移行したことでメンテナンスはかなり簡単になりました。Github Pagesのホスティングを使っているのでJekyllで構築したサイトも自動生成してくれます。

ただ、Github Pagesで利用できるJekyllプラグインには制限があり、利用したかったタグごとのページ生成プラグインや、ミニマイズのプラグインは利用できませんでした。仕方なしとは思っていたのですが、タグごとに記事がまとまっているURLがあったほうが記事を紹介する際にも便利ですからGithub Pages側でHTML生成してもらうのではなく、あらかじめ手元でHTMLを生成することにしました。

Docker環境

HTML生成のためにプラグインなどを導入済みのDocker環境を作っておきます。

FROM jekyll/jekyll:stable

RUN apk add --no-cache --virtual build-dependencies build-base
RUN apk add --no-cache libxml2-dev libxslt-dev
RUN apk add --no-cache ruby-dev curl-dev zlib-dev yaml-dev
RUN gem install nokogiri
RUN gem install minima
RUN gem install jekyll-import
RUN gem install jekyll-minifier
RUN gem install jekyll-tagging
RUN gem install jekyll-paginate

ENTRYPOINT ["jekyll", "build", "--config", "/srv/jekyll/source/_config.yml,/srv/jekyll/source/_config_prd.yml", "--destination", "/srv/jekyll/release", "--source", "/srv/jekyll/source"]

なお、ディレクトリ構成は次のようなイメージです。必要に応じて読み替えてください。

  • サイトルート
    • source … Jekyllのソース一式
    • release … Github PagesにアップロードするHTML一式
    • staging … 表示確認用HTML生成先

表示確認用にjekyllをserveで起動するには最後の行を下記のように変えたイメージを準備しておきます。

ENTRYPOINT ["jekyll", "serve", "--config", "/srv/jekyll/source/_config.yml,/srv/jekyll/source/_config_dev.yml", "--watch", "--destination", "/srv/jekyll/staging", "--source", "/srv/jekyll/source"]

設定ファイル_config.ymlは共通のものと、表示確認用のもので分離しています。これはMinifierは実行にそれなりに時間がかかるプラグインを外したり、デバッグ用にURLを切り替えるなどしています。 ちなみにMinifierを有効化した際、本サイトのデータを手元のMacBook Pro (Late 2016、Core i7)で実行してMinifierがない場合と比べ1分程度の追加時間が必要となります。

タグの有効化

jekyll-taggingプラグインを利用します。プラグインの設定とjekyll-taggingへの設定は下記のようなイメージです。ハマりどころとして、jekyll-taggingは jekyll/taggingとスラッシュで区切ります。他のプラグインはだいたいjekyll-paginateのようにハイフン区切りなのでそれに倣ってしまいがちです。。

plugins:
  - jekyll-paginate
  - jekyll-sitemap
  - jekyll/tagging

tag_page_layout:          tagpage
tag_page_dir:             tags
tag_permalink_style:      pretty

あとはjekyll-taggingの説明にあるように_plugins/ext.rbへプラグイン指定追加、_layouts/tag_page.htmlを作成して完成です。

Dropbox API: チーム全体のフォルダ・ファイル一覧を取得する

Dropbox BusinessではAPIを通じてチーム内のユーザーについてファイル・フォルダ一覧を取得したり、更新することができます。これは、マルウェア対策システムがファイルを検査・隔離するために利用したり、ワークフローや生産性向上ツールが自動的にファイルを仕分けするといった用途を想定していると考えられます。なお便利な一方、非常に強力な権限となるため取り扱い上の注意は管理者パスワードなどと同様厳重に管理する必要があることは言うまでもありません。

このようにチーム全体のファイルを扱う場合にはDropbox APIの概要のBusiness Endpointsでご紹介したTeam member file access権限を用います。この権限を使うアプリケーションを作成する手順などは該当記事をご参照ください。

チームメンバーのファイルやフォルダを操作するには大きく分けて二種類の方法があります。

  1. チームメンバーの代理として実行
  2. 管理者として実行

チームメンバーの代理として実行する方法では、指定したユーザーが所有しているファイルやフォルダ、参加している共有フォルダ・チームフォルダなどへアクセスすることができます。 管理者として実行する場合にはチーム内すべてのファイル・フォルダへアクセスすることができます。

使い分けとして、たとえばワークフローアプリケーションなどで承認されたドキュメントを所定フォルダに移動するといった用途であれば、承認者の権限で承認者が所有フォルダに移動するといった場合には代理での実行が適切でしょう。マルウェアのスキャンや監査といったチーム全体を対象としたい場合には管理者としての実行が適切でしょう。

チームメンバーの代理として実行

チームメンバーの代理として実行する場合にはまずteams/members/listでアカウント一覧を取得します。User Endpointに対してAPIコールする際にHTTPヘッダDropbox-API-Select-User: チームメンバーIDを追加すると、その該当アカウントとして処理が行われます。

たとえばfiles/list_folderを代理として実行する場合には次のように呼び出します。

curl -X POST https://api.dropboxapi.com/2/files/list_folder \
  --header 'Authorization: Bearer 認証トークン' \
  --header 'Content-Type: application/json' \
  --header 'Dropbox-Api-Select-User: チームメンバーID' \
  --data '{"path":""}'

チーム管理者として実行

チーム管理者として実行する場合にはDropbox-API-Select-Userヘッダの代わりにDropbox-API-Select-Adminヘッダを用います。また、チームメンバーIDにはチーム管理者のメンバーIDを指定します。 Dropbox-API-Select-Adminヘッダを用いた場合、チーム内のネームスペースすべてにアクセス可能となりますがAPIによって二つのモードがあり可視範囲が変わります。

一つ目はWhole Teamと呼ばれるモードです。主に参照系APIが該当します。該当APIはAPIドキュメントにてAUTHENTICATIONにDropbox-API-Select-Admin (Whole Team)と記載されています。Whole Teamモードではメンバーのプライベートファイルを含むすべてのファイルにアクセスすることができます。

もう一つはTeam Adminと呼ばれるモードです。主に更新系APIが該当します。該当APIはAPIドキュメントにてAUTHENTICATIONにDropbox-API-Select-Admin (Team Admin)と記載されています。Team Adminモードではメンバーのプライベートファイルにはアクセスできません。

チーム管理者として実行する際の流れは大まかに次のようになると思います。

  1. 実行するチーム管理者のメンバーIDを取得
  2. チームフォルダ一覧やネームスペース一覧を取得
  3. 各ネームスペースについて処理

まずチームメンバーの代理として実行する場合と同様にteams/members/listでアカウント一覧を取得しチーム管理者のメンバーIDを取得します。アプリケーション用に専用のアカウントが用意できるようであればアプリケーションにあらかじめメンバーIDを設定しておくといった運用も可能でしょう。

次にチームフォルダ一覧やネームスペース一覧を取得します。チームフォルダ一覧はteam/team_folder/list、ネームスペース一覧はteam/namespaces/listで取得できます。ワークフロー処理などチームフォルダ内で処理が完結する場合はteam/team_folder/list、チーム全体のファイルを監査するといった処理の場合はネームスペース一覧から処理を始めるとよいでしょう。

処理したいチームフォルダやネームスペースのネームスペースIDを取得します。なお、チームフォルダIDはそのままネームスペースIDとして利用できます。たとえば、team/team_folder/listのレスポンスが次のような場合、ネームスペースIDは123456789となります。

{
    "team_folders": [
        {
            "team_folder_id": "123456789",
            "name": "Marketing",
            "status": {
                ".tag": "active"
            },
            "is_team_shared_dropbox": false
        }
    ],
    "cursor": "ZtkX9_EHj3x7PMkVuFIhwKYXEpwpLwyxp9vMKomUhllil9q7eWiAu",
    "has_more": false
}

ネームスペースを使ってファイル一覧を取得する

パスとネームスペースの扱いについては以前の記事Dropbox API: ネームスペースとパスの表現にてご紹介いたしました。こちらでご紹介したとおり、ns:ネームスペースID/パスといった形式でパス指定すれば通常のファイル一覧取得と同じようにファイル一覧を取得することができます。

たとえばチームフォルダ(ネームスペースID = 123456789)のトップフォルダからファイル一覧を取得するには次のように実行します。

curl -X POST https://api.dropboxapi.com/2/files/list_folder \
  --header 'Authorization: Bearer 認証トークン' \
  --header 'Content-Type: application/json' \
  --header 'Dropbox-Api-Select-Admin: チーム管理者のメンバーID' \
  --data '{"path":"ns:123456789"}'

mruby and go: value mapping library

I’m recently working on my hobby project that requires DSL on Go language. From my little research of DSL on Go; go-mruby looks attractive to implement DSL. mruby is the lightweight version of Ruby. That is embeddable into an app. With go language, an app can be compilable into a single binary.

go-mruby

go-mruby have set of functions that enable essential integration with mruby code and Go. For example, go-mruby can define mruby class or method from Go. But current functions are it’s too primitive for dealing with real-world applications.

Like mapping values from mruby to Go, or vice versa. It’s a bit tiresome for getting/putting each struct field. For example below, just for two fields of struct require 9 lines of code.

mrb := mruby.NewMrb()
defer mrb.Close()

type Planet struct {
    Id   int
    Name string
}

rv, err := mrb.LoadString(`
class Planet
  attr_accessor :id
  attr_accessor :name
end

p = Planet.new
p.id   = 3
p.name = "Earth"
p
`)
if err != nil {
    panic(err.Error())
}

p := Planet{}
if rId, err := rv.Call("id"); err != nil {
    panic(err.Error())
} else {
    p.Id = rId.Fixnum()
}
if rName, err := rv.Call("name"); err != nil {
    panic(err.Error())
} else {
    p.Name = rName.String()
}
fmt.Printf("Planet: %v\n", p)

For more productivity, I wrote mapping functions. Functions are open sourced on GitHub in MIT license. grb.

grb

Unmarshal Ruby values into Go struct data

Func Unmarshal fetch values for every field in Ruby object that defined by mruby:"METHOD_NAME" tag.

type Planet struct {
    Id         int            `mruby:"id"`
    Name       string         `mruby:"name"`
    Radius     float64        `mruby:"radius"`
    HasMoon    bool           `mruby:"has_moon"`
    Moon       []string       `mruby:"moon"`
    MoonRadius map[string]int `mruby:"moon_radius"`
}

Define each value accessor in Ruby class.

class Planet
  attr_accessor :id
  attr_accessor :name
  attr_accessor :radius
  attr_accessor :has_moon
  attr_accessor :moon
  attr_accessor :moon_radius
end

Retrieve ruby value *ruby.MrbValue, then unmarshal it into the Go struct.

rv, _ := mrb.LoadString(`
p = Planet.new
p.id          = 3
p.name        = "Earth"
p.radius      = 6371.0
p.has_moon    = true
p.moon        = ["Moon"]
p.moon_radius = {"Moon" => 1737}
p
`)

p := Planet{}
Unmarshal(rv, &p)

fmt.Printf("Planet: %v\n", p)
// Planet: {3 Earth 6371 true [Moon] map[Moon:1737]}

Decode/encode between Ruby values and Go values

Similar to func Unmarshal. Func DecodeMrbValue and EncodeMrbValue enable mapping values between mruby and Go. DecodeMrbValue/EncodeMrbValue have limitation for a type of value. This supports JSON equivalent types like below.

  • nil
  • bool
  • int
  • float
  • string
  • Array / slice
  • Hash / map (Hash/map key must be a string)
mrb := mruby.NewMrb()
defer mrb.Close()

// Mapping to mruby value
rv, err := EncodeMrbValue(mrb, map[string]interface{}{"Earth": 6371, "Moon": 1737})

// Mapping from mruby value
gv, err := DecodeMrbValue(rv)

GolangでBOM付きCSVファイルを読み込む

先行事例・解決策は山ほどあるかと思いますが、はまったのでメモがてら解決方法を。BOM付きUTF16などでエンコードされたテキストを扱うに当たって、ライブラリなどが対応してくれているといいのですが、GolangのCSVReaderは対応していないようです。このため、次のような関数を準備してエンコーディングを変換してやります。

func NewBomAwareCsvReader(r io.Reader) *csv.Reader {
  var (
    bomUtf8    = []byte{0xef, 0xbb, 0xbf}
    bomUtf16BE = []byte{0xfe, 0xff}
    bomUtf16LE = []byte{0xff, 0xfe}
  )
  br := bufio.NewReader(r)
  mark, err := br.Peek(3)
  if err != nil {
    panic(err)
  }

  // decoder
  d := unicode.UTF8.NewDecoder()

  if bytes.HasPrefix(mark, bomUtf8) {
    br.Discard(len(bomUtf8))
  } else if bytes.HasPrefix(mark, bomUtf16BE) {
    d = unicode.UTF16(unicode.BigEndian, 
      unicode.UseBOM).NewDecoder()
  } else if bytes.HasPrefix(mark, bomUtf16LE) {
    d = unicode.UTF16(unicode.LittleEndian,
     unicode.UseBOM).NewDecoder()
  }

  return csv.NewReader(transform.NewReader(br, d))
}

あとはファイルを読み込めば変換できます。ひとまず必要が無かったので対応しませんでしたが、utf32パッケージを使って同様処理を追加してやればUTF32も対応できます。