妥当なNullの使い道

「Null」ってあるじゃないですか。参照が何も指し示していないときに利用するアレ。

Nullが発明された当時はそれなりの事情があったのだと思いますが、昨今Nullは忌避すべきものという扱いが主流かなーと思います。

事実プログラミング言語においてはNullが存在しないものもあります。あるいはNullがあったとしても、Nullable/Non-nullを型で明確に区別していたり。いわゆるNull安全ですね。

こういったNull安全は間違いなく頼もしい存在です。ですが、そもそもNullの使い道が適切ではないケースには無力です。

私自身、最近では「Null安全はたしかに嬉しいし、あるに越したことはないけど、そもそもNullを使わないで済むならそうした方が良いよな……」と思っています。

とまあ、そんな悶々とした中で、Nullを用いるのが適切・不適切なケースが見えてきた気がするので、まとめてみます。

任意項目を意味するとき、Nullは適切

Nullを用いるのが適切なケースは、値が任意項目を意味するときです。

例えば年齢という値があったとします。年齢は未入力とすることも可能です。

このとき、年齢をNullable Integerとし、非Nullのときはそのまま年齢を、Nullのときは未入力とするなら、Nullを用いるのは適切だろう、ということですね。

任意項目以外を意味するとき、Nullは不適切

反対にNullを用いるのが不適切なケースはどんなときでしょう。私は任意項目であることを意味するとき以外すべて不適切だと思っています。単純明快、任意項目 or notで分けられるということですね。

そもそもNullという値はそれ自体に意味がありません。ただ指し示すものが何もないというだけです。そのNullにコンテキストに応じた意味を持たせると、それはたちまち暗黙知になります。

Nullを用いるのが適切・不適切を判断するポイントは単純ですが、どういうものが不適切で、どういう困りごとがあるのか、例にしてみます。

Nullにコンテキストに応じた意味を持たせる

例えば容量という概念があるとします。容量は1, 2, 3, ...のように整数を取りますが、無制限という値も取ることが可能です。容量同士は加算することが出来、結果は整数の加算と同様になります。無制限が絡む加算の結果は常に無制限となります。

このとき、容量をどう表現するのが適切でしょうか?

Nullを用いるなら、Nullable Integerとし、非Nullのときはそのまま容量を、Nullのときは無制限とする、といったことも可能でしょう。Kotlinで書くなら以下のような感じでしょうか。

val capacity: Int? = readCapacity()

if (capacity != null) {
    println("Limited capacity: $capacity")
} else {
    println("Unlimited capacity")
}

しかし、これはまさしく不適切なケースに該当します。Nullが無制限という意味を担ってしまっているからです。

事前に容量の定義を知っていれば、Null = 無制限と推察することも可能かもしれませんが、あくまで推察レベル。裏付けがない以上コードを読み込むなり、有識者に聞くなりして調べる必要は出て来ます。少なくとも私はそうしないと不安だな!

あと単純な話ですが、Nullは計算が不可能です。容量には加算が定義されていますが、1 + nullコンパイラが許してくれません。

ではどうするか。私は2つほど解決策が思い浮かびました。

  • 解決策1: 制限ありと無制限をはっきり区別する
  • 解決策2: Nullable Integerをクラスで包み、隠蔽する

そのうちの解決策1を実践してみます。解決策2でも大分マシになりますが、Nullable Integerである以上、どこかで漏れ出てしまうので。

というわけで解決策1の実践。まずは容量の定義に立ち返りましょう。

  • 容量は制限ありと無制限がある
  • 制限ありは整数をひとつ持つ
  • 無制限は何も持たない
  • 容量の加算は整数の加算に等しい
  • 容量の加算に無制限が絡むと、結果は常に無制限となる

そしてこれをそのままクラスにしましょう。

sealed interface Capacity {

    operator fun plus(other: Capacity): Capacity

    data class Limited(val value: Int) : Capacity {

        override fun plus(other: Capacity): Capacity =
            when (other) {
                is Limited -> Limited(value + other.value)
                Unlimited -> Unlimited
            }
    }

    object Unlimited : Capacity {

        override fun plus(other: Capacity): Capacity =
            Unlimited
    }
}

どうでしょう?容量の定義そのままを表現できていると思います。そしてNullは一切現れません。実際に使ってみましょう。

val aCapacity = Capacity.Limited(1)
val bCapacity = Capacity.Unlimited

println(aCapacity + bCapacity) // => Capacity$Unlimited@6182ffef

objectを用いたので出力結果がおもしろいことになっていますが、Nullは現れませんね。永続化するときも以下のように書けます。

fun save(capacity: Capacity) {
    when (capacity) {
        is Capacity.Limited -> saveAsLimited(capacity.value)
        Capacity.Unlimited -> saveAsUnlimited()
    }
}

とまあこんな感じで、Nullを使わなくても書けましたね。むしろこちらの方が定義通りでより伝わるかと思います。

不要な属性にNullを入れてごまかす

他にどんなケースがあるでしょうか。例えばメッセージという概念があるとします。メッセージにはテキストまたは画像を添えられます。テキストおよび画像の両方があったり、両方がなかったりするのは許されません。

このとき、どのようにメッセージを表現するでしょうか。以下のようなのがすぐ思い浮かぶかもしれません。

class Message(
    val text: String?,
    val image: ByteArray?
)

たしかにテキストまたは画像を持つ、ということを表現できます。しかし、両方を持つ、両方を持たないも表現できてしまいます。それなら以下のようにすれば良いでしょうか。

class Message(
    val text: String?,
    val image: ByteArray?
) {

    init {
        val hasText = text != null
        val hasImage = image != null
        val hasOnlyOne = hasText xor hasImage
        if (!hasOnlyOne) {
            throw IllegalArgumentException("The message must have only one of a text or an image")
        }
    }
}

これでめでたくテキストまたは画像のどちらか一方のみを持つことが許されるようになりました!

でもちょっと待ってほしい。結局のところ、ルールに則っているかという検査を、値が妥当であるかという方法で守っているだけです。それはつまり、「実行しないと分からない」ってことです。実行しないと分からないということは、コンパイルエラーではなくランタイムエラーなわけだし、問題のあるなしはコンパイラではなくテストで担保する必要があります。

さきほどのように値の妥当性で保証するのもひとつの手だとは思います。でも私はちょっとイヤかな〜。だってルールに則っているかの検査している箇所を読み解かないと、どういうルールか分からないし……。

そもそもインスタンス生成時にルールに則っているか検査していても、そのインスタンスを用いる箇所でNullかそうでないかを見てあげる必要があり、もにょもにょしてしまう……。

fun save(message: Message) {
    if (message.text != null && message.image == null) {
        saveAsText(message.text)
    } else if (message.text == null && message.image != null) {
        saveAsImage(message.image)
    } else {
        // ここはどうすれば良い?
    }
}

こうなってしまう根本的な原因はなんでしょう。

その答えを出す前に、メッセージの定義に立ち返りましょう。

  • メッセージはテキストまたは画像を持つ
  • テキストおよび画像を両方持つことは出来ない
  • テキストおよび画像を両方持たないことは出来ない

大切なのは1つ目です。このルールを素直に実装すれば以下のようになるんじゃないでしょうか。

class Message(
    val content: MessageContent
)

sealed interface MessageContent {

    class Text(val value: String) : MessageContent

    class Image(val value: ByteArray) : MessageContent
}

MessageはMessageContentを持ち、MessageContentはTextまたはImageという定義になっています。これはメッセージの定義そのものです。両方持つ・持たないはクラスの定義上、表現不可能になっています。

もにょもにょしていたコードも以下のようにすっきり記述できます。

fun save(message: Message) {
    when (val content = message.content) {
        is MessageContent.Text -> saveAsText(content)
        is MessageContent.Image -> saveAsImage(content)
    }
}

さきほど答えを保留した根本的な原因についてです。私はこの問題を「属性そのものを持つべきではないのに、Nullを入れてごまかしているから」と捉えています。まあね、「ごまかす」ってちょっと人聞き悪いように聞こえますが……。まあ、だからこそごまかさないで向き合おうぜ、って心持ちでいたいわけですよ(?)。

Null安全の向こう側

今回の話はあくまで「Nullはどういう場面で用いた方が良いのか」です。

ですので、Null安全とは直接関係のない話です。「Null安全の向こう側」という言い回しは、もっともらしく聞こえます。だけどそもそも「向こう側」ですら無いよねって。どちらかと言うと横並び?

とは言え、気軽にNullable/Non-nullを指定でき、その差を明確に区別するNull安全だからこそ、Nullの使い方が不適切なときにはより強い違和感になるのかもしれません。

Null安全は冴えたやり方だと思っています。Nullable/Non-nullを区別しない言語には戻れないなあ、とさえ思っています。

でもそこからさらに一歩を踏み込んで、「そもそもNullableにする必要なくない?」と考えることが出来ると、よりハッピーになれるんじゃないかなあ、とも思います。

解決策のコードは奇しくも(全然奇しくない)代数的データ型(ADTs)なわけですが、やれ直積だの直和だの持ち出さなくても、「Nullやめてみよっか」の一言をきっかけにしても良いんじゃないかななんて。ほら、RDBでもNOT NULL制約を徹底すれば、うまく事が運びやすいってのあるじゃないですか。あんな感じ。たぶん。

というわけで、まずはNullをやめてみるところから始めてみるのも良いんじゃないでしょうか。

一緒にビズリーチ・キャンパスの成長を支えてくれる仲間を募集しています!

このブログでも書きましたが、2021年の年始から転職活動をしていました。おかげさまで無事に転職できて、4月から株式会社ビズリーチで働いています。今は新卒事業部で、ビズリーチ・キャンパスというサービスの開発に携わっています。

で、近況はこれぐらいにして。この記事で伝えたいことを単刀直入にズバッと伝えると、「一緒にビズリーチ・キャンパスの成長を支えてくれる仲間を募集しています!」ということです。まあ記事タイトルで言ってるけど!

この時点でピンと来る方はなかなかいないとは思いますが、もしいたら私まで一言いただけるとめっちゃ嬉しい!ピンとまでは来ていないけど、気になるという方もぜひぜひ。カジュアル面談からやっておりますので〜。私のTwitterアカウント@takkkunにメンション・DMいただければと思います!

カジュアル面談でやりとりしてピンと来た、あるいは現時点でピンと来すぎた方は以下からご応募ください〜。もちろん採用担当の方や私に応募の意向を伝えていただいても構いません!

hrmos.co

あとはこの募集の目的や、私の入社してみての感想です。感想に関しては入社してまだ5ヶ月のものですが、興味ある方はご覧ください〜。それで「気になってきた!」ということもあるかもしれないしね!

続きを読む

今の会社に入って良かったこと・気付いたこと

今の会社に入社してから1年とちょっとが経った。で、もう辞めるつもりで、ただいま絶賛転職活動中。まあ請け負ったものがあるので、転職時期は年度明けとかになりそうだけど。

いろんな会社を転々としてきたけど、その中でこの会社で感じたことがあるので書き留めておく。

身1つで飛び込む体験

基本私はリファラルで転職していて、今の会社も例に漏れずなんだけど、経緯がちょっと特殊。普通なら社内の人から「うち来なよ」みたいな誘いだと思うけど、今回は社外の人から「この会社いいよ」なんて薦めてもらった。

この場合何が起きるかと言うと、リファラルなのに入社したら周り誰も知らないという状況になる。まあ転職サイト経由などだと当然のことだと思うんだけど、私は先述したようにリファラルばかりだったので初めてだった。

私の性格上、見知った人が居ると、その人の目を気にすると言うか、陰に隠れてしまい、あまり自ら表に立とうとしない。精々サポート程度と言うか。

反対にそういう人が居ないと奮闘する。これ最近感じる性質。打ち合わせでこちらが2人で行くとメインで喋らないけど、1人で行くと喋る(喋らざるを得ない)。3人以上で雑談していると控え目に喋るけど、2人、つまりマンツーマンなら普通に喋る。なんかそんな感じ。

つまり、今の会社では奮闘していた状態だった。知識・技術を振るうのは当然(当然と言うか業務遂行の上で出し惜しみなんてしないけど)だが、自身の立場と言うか、存在感も出していこう、という気になった。その気概と言うか、その気概故の行動は同僚に察されていたけど、ある意味多少なりとも出来ていた証拠なのかもしれない。別にその行動してて悪いわけではないと思うし、たぶん。

この体験で抱いた正直な感想としては、「意外と出来るじゃん」だった。もうすぐ34歳でこんな転職繰り返していて良いのか、とか思うんだけど、(求められる・求められないはあるにせよ)「どこでも自身次第でやっていけるのかもしれない」とも思った。

チームで働く体験

今までもチームで働く体験は当然しているんだけど、それなりの人数で進めたのは初めてかもしれない。

その中で人によって知識・技術にはムラがあり(当然なんだけどさ!)、今までの私は人に学ぶことが大半だったと思うんだけど、私が伝えられることもあるんだなと思った。

目的を達成するために協力するのは大切なんだけど、協力というのは請け負い切れない溢れたタスクを拾うという直接的な負担軽減だけに留まらない。チーム全体の知識・技術を底上げしていくのだってとても大切だと思う。

人によるとは思うけど、開発者として自身を磨くというのは死んでも付き纏う課題だし、付き纏うというか楽しみそのものなんじゃないかなあ。少なくとも私はそう。その姿勢はチームの雰囲気に現れると思うし、それが欠け、楽しんでいなければチームとして魅力がないと思う。魅力がないと私は人を誘えないし、いざ採用面談となっても、とてもじゃないが採用する気になれない。採用を担当した者としてダメな姿勢かもしれないけど、ごめん、自分が楽しめないのにそれは無理だわ。正直者ですまんな!

話逸れた。チームと言うのは、単にタスクを並行して進めるまとまりではない。副次的かもしれないが、無視できないほどに取り組む姿勢が大事だと感じた。

戦略の重要性

プロジェクトを推進するために戦略は欠かせない。そしてそれに依存し、実際に取る戦術などが決まる。まあそこは戦術のおかしさからフィードバックを受けるとは思うので、思考自体は行き来する(設計とコードみたいな)んだと思うけど。

何が言いたいかと言うと、戦略はよく検証する必要があるし、随時調整も必要だろう。間違っていた場合はすぐさまそれを認め、軌道修正と思い切ることも必要だと思う。

ごめん、戦略とか知ったように言ったけど、よく分からんかも。でも計画は必要だと思う。それも地に足の付いた。そしてそれを振り返る習慣も。

そこも重要なんだと気付けたのは大きかったなあ……。当たり前だとは思うんだけどさ。


良かったこと・気付いたこととは関係がないんだけど、自身のモチベーションがどこにあるか、という点についても整理できた気がする。

具体的に言うと、ベンチャーはもういいかな。良いものを作りたいという気持ちは変わらないんだけど、検証を積み重ねて0から1へ物事を進めるというのは、その事業への熱意、いやそれすら生温いほどの執念も必要だと思う。あまり「これで!」という分野がないと自覚しているんだけど、それを改めて強く感じた。

反対に「これで!」と思うのは、やはりプログラミングという手段なんだよなあ。だから事業も大切なんだけど、自分にとってはその事業に向かう人たちと良いもの作りたいし、その人たちが学習し、成長し、楽しいと思える環境に協力したいなあ、という感じ。ベンチャーがもういいと言うのは、この状況ではないことが多そう、というのにも起因する。やっぱスピードが求められる段階だと思うしね。是非ではなく、単にステージの違い。

というわけなんで、事業はあるけど開発を牽引するアプリケーションエンジニアがいないんだよなあ、とお思いのみなさま。お声掛けお待ちしております。

結局宣伝になった!!!ちなみに職歴などはいつでも出せるので、そこから見せてというのも全然ウェルカムです(というか気になって当たり前だと思うし)。

エルビス演算子はプログラミング言語によって定義が違うので、あまり言いたくない

エルビス演算子ってありますよね。"?:"で、これを90度傾けるとElvis Presleyに見えるからそう呼ばれているやつ。

Wikipediaのエルビス演算子の記事には:

左式exprLの評価結果が真と判定される場合にはその結果が、それ以外の場合には右式exprRの評価結果

と述べられている。

それとは別にNull合体演算子というのがある。これはエルビス演算子と似た挙動をするが、第1項が「真と判定される場合」ではなく、「Nullでない場合」になる。

それらを踏まえて、主要なプログラミング言語ごとに「偽と判定される(Falsy)場合に第2項を返す」と「Nullの場合に第2項を返す(Null合体演算子)」ための演算子をまとめてみた。

プログラミング言語 Falsyの場合、第2項を返す Nullの場合、第2項を返す(Null合体演算子
Swift なし ??
Kotlin なし ?:
Ruby or, || なし
Perl or, || //
Python or なし
PHP ?: ??
JavaScript or, || ??

SwiftとKotlinはFalsyという概念がない(Booleanのfalseのみ条件判断に用いることが出来る)ため、「Falsyの場合、第2項を返す」ということが出来ないのは納得がいく。しかし、KotlinはNull合体演算子エルビス演算子であり、本来のエルビス演算子の挙動と異なる。ここ混乱ポイントな気がする。

他の言語はFalsyという概念があり、大体orで代用するのだが、PHPはorが返す値が0/1になってしまうので、エルビス演算子が導入されている。反対に、Null合体演算子の方が用意されていないことが多い。ただし、Null条件演算子Rubyの"&."とか)などがあり、そちらを使ってくださいね、というのが多い印象。

ここからは憶測だが、エルビス演算子("?:")というのは、PHPのために生まれた演算子な気がする。実際調べるとPHPの文脈で用いられていることが多い。故にエルビス演算子 = ?: = Falsyの場合、第2項を返すという定義になった。

しかし、KotlinがNull合体演算子として"?:"を採用し、これが表層としてPHPエルビス演算子に合致してしまったため、PHPとKotlinでエルビス演算子の定義が違う、ということになっている。個人的にはエルビス演算子という名前と由来がキャッチーなので、記憶に残りやすいのも一因?

この辺はなんか見た目によって命名するのではなく、機能によって命名しろ、みたいな話とかに通じる気がする。CSSでredと命名すると、フォントカラーを青にしたとき齟齬が生じるから、titleと機能によって名前付けた方が良いよね、みたいな。

何が悪いと言うつもりもないが、思ったこととしては:

かなあ。

XcodeGenの導入でつまずいたところ

今私の中でちょっとアツいのがXcodeGenで、まあアツいんでアツいうちに導入するか〜って昨日ぐらいに重い腰を上げました。

結構スムーズに書けたんですが、いくつかつまずいたので、その点をピックアップ。ちなみに書くときに参考にしたのは XcodeGen/ProjectSpec.md at master · yonaskolb/XcodeGen · GitHub です。ここ見れば大体書いてある。

Xcode上でTestingのHost Applicationが設定されていない(ように見える)

テストターゲットの指定の仕方は:

targets:
  SampleApp:
    scheme:
      testTargets:
        - SampleAppTests
  SampleAppTests:
    # ...

でOKで、実際これでテストの設定がされたSampleAppのスキームが出力されます。ただし、SampleAppのPRODUCT_NAMEを.xcconfigファイルなり:

targets:
  SampleApp:
    settings:
      PRODUCT_NAME: Sample

のように設定してしまうと、Xcode上でSampleAppTestsのHost Applicationを見ても設定されていないように見えます。

f:id:takkkun:20190313163418p:plain

原因なんですが、TEST_HOSTに設定される値は変更前のPRODUCT_NAME(= SampleApp)、ただ実際のPRODUCT_NAMEは変更されている(= Sample)からです。要は食い違っているということですね。

解消するには:

targets:
  SampleApp:
    productName: Sample
    settings:
      PRODUCT_NAME: Sample

と、productNameキーで指定してあげれば大丈夫です。なお、productNameキーで指定しても、実際にPRODUCT_NAMEが変わるわけではない(あくまでXcodeGenのコード中で扱う値が変わるだけ)ので、別途設定する必要はあります。

か、TEST_HOSTを自前で設定してあげれば大丈夫ですね(最初こっちでやってた)。

R.swiftなどで生成されるコードがコンパイルの対象にならない

project.ymlで設定したsourcesは、xcodegenを行ったタイミングで取り込まれているみたいで、その後に追加されたファイルはコンパイルの対象になりません。ですので、R.swiftなどのように、コンパイル直前にコードを生成するものと組み合わせると、その生成物はコンパイル対象に入らず、大量のコンパイルエラーに見舞われます。

じゃあもう1回xcodegenすればって、まあそうなんですけど、めんどくさい(というかだいたい新たにgit cloneしたタイミングでこの問題が起きるので、そこでコケさせたくない)ので、自動でコンパイル対象になっていてほしいです。

ので、project.yamlで:

targets:
  SampleApp:
    sources:
      - path: Source
      - path: Source/R.generated.swift
        optional: true
    preBuildScripts:
      - name: R.swift
        script: # ...

と、してあげます。sourcesに指定されたフォルダなどはxcodegen時にあるかどうかチェックされるんですが、optional: trueとしておくと、なくてもスルーしてくれるようになります。これで実際のビルド時はR.generated.swiftが生成されるので、問題なくビルドできます。

ただpreBuildScriptsで生成されたものをいちいちsourcesに書かないといけないので、やや面倒……。もう少し良い方法ある?

OpenCVのDescriptorMatcherの話

特徴量の検出(detect)と記述(compute)については割愛。これはその後の特徴量を比較するときに使うDescriptorMatcherについての話。

まずDescriptorMatcherの使い方

コードはScalaです。

import org.opencv.core.MatOfDMatch
import org.opencv.features2d.DescriptorMatcher

import scala.collection.mutable.ListBuffer
import scala.collection.JavaConverters._

val matcher = DescriptorMatcher.create(アルゴリズム)

val matches = ListBuffer[MatOfDMatch]()
matcher.結果の取得方法(queryDescriptors, trainDescriptors, matches.asJava, ~)

matches // この中にマッチングの結果

基本はこう。結果の取得方法によってシグネチャが違うが、大筋としては:

  • DescriptorMatcherのアルゴリズムを選択し
  • descriptorsを2つ渡してマッチングを行う

となる。

アルゴリズム

「どのようにマッチングを行うか」を指定する。大きく分けると:

  • 総当たり法
  • FLANN

の2つに分けられる。

総当たり法

問い合わせる特徴量の集合(queryDescriptors)のひとつひとつが、訓練特徴量の集合(trainDescriptors)のどれにマッチングするかなんて、それぞれひとつひとつ地道に比較してみないと分かんないよね?

というわけで、それを実際に行うのが総当たり法。

さらに分けると:

  • BruteForce
  • BruteForce-SL2(ユークリッド2乗距離)
  • BruteForce-L1(マンハッタン距離)
  • BruteForce-Hamming/BruteForce-HammingLUT(ハミング距離)
  • BruteForce-Hamming(2)(ハミング距離……だと思う)

の5つに分けられます。定数ではBruteForce-Hamming(2)がなく、その代わりBruteForce-Hammingと同等のBruteForce-HammingLUTがあったりはしますが、その辺の事情はよく分かりません。

ユークリッド距離マンハッタン距離ハミング距離は適当にググってください。

FLANN

総当たりするとめっちゃ時間かかるから、マッチングする範囲を限定しない?

というわけで、そうするのがFLANN(高速近似近傍探索法)。

総当たり法と比べて以下の特徴がある。

  • 速度は総当たり法のだいたい4〜6倍(自環境でパッと見た感じ)
  • 範囲を限定しているので、本来マッチングしてほしい箇所同士がマッチしないこともある

使えるアルゴリズム

すべてのマッチングアルゴリズムがすべての特徴量に対して適用できるわけではない。使えるものと使えないものがあり、以下のようになる。

  • 総当たり法(BruteForce, BruteForce-SL2, BruteForce-L1): 何にでも使える
  • 総当たり法(BruteForce-Hamming, BruteForce-Hamming(2)): 特徴量の表現がバイナリコードのもの(ORB, AKAZEとか)
  • FLANN: 特徴量の表現が実数ベクトルのもの(SIFT, SURFとか)

使えないものの場合は実行時エラーが出るので分かるはず。

どのアルゴリズム使えば良いんだ!ってなると思うんですが、結局特徴量の表現によって使えるアルゴリズムが決まっているので、迷うこともあんまりない。迷ったらBruteForceで良いのかなと思う。最近の特徴量検出アルゴリズムによるものだとFLANN使えないこと多いしな!

結果の取得方法

アルゴリズムを決めたらいざマッチングで良いんですが、それの取得方法もいくつかあります。

knnMatch, radiusMatch

基本的に結果は「問い合わせる特徴量の個数 * 訓練特徴量の個数」の2次元配列になります。実際はマッチングしなかった場合結果として返されないので、それよりは少なくなります。

そして各マッチング結果には特徴量同士の距離が込められています。この距離が近ければ近いほど(数値的には小さいほど)その特徴量同士は似ていると言えます。

で、各問い合わせる特徴量ごとに距離が近い上位k個を取得するのが、knnMatchです(k-nearest neighbor)。上位k個とかではなく、距離が指定した閾値以下かどうかでフィルターをかけるのがradiusMatchです。この2つは問い合わせる各特徴量ごとに複数個の結果を持ち、その各特徴量ごとにMatOfDMatchを返すので、結果はList[MatOfDMatch]になります。複雑ですね。

import org.opencv.core.MatOfDMatch
import org.opencv.features2d.DescriptorMatcher

import scala.collection.mutable.ListBuffer
import scala.collection.JavaConverters._

val matcher = DescriptorMatcher.create(DescriptorMatcher.BRUTEFORCE)

val matches = new ListBuffer[MatOfDMatch]()
matcher.knnMatch(queryDescriptors, trainDescriptors, matches.asJava, 2)

これは各特徴量ごとに距離が近い上位2個を取得します。2個に満たない場合もあるので、それよりも少なくはなる可能性はあります。

radiusMatchについては割愛。knnMatchのように個数を指定する箇所に距離の閾値を指定するだけです。

match

じゃあ何も修飾していないmatchはどうなるかと言うと、各特徴量において最も距離の近い1点を結果とする、となります。結果としては「問い合わせる特徴量の個数 * 1」となるので、ただの配列になります。よってMatOfDMatchで表されます。こう見ると、matchの方が特殊な形で、knnMatch, radiusMatchの方がより一般形のようですね。

import org.opencv.core.MatOfDMatch
import org.opencv.features2d.DescriptorMatcher

val matcher = DescriptorMatcher.create(DescriptorMatcher.BRUTEFORCE)

val matches = new MatOfDMatch()
matcher.`match`(queryDescriptors, trainDescriptors, matches)

matches変数をprintlnなりすると、次元数が「問い合わせる特徴量の個数 * 1」になっているのが分かると思います。

マッチングの結果を描画する方法

マッチングの結果によってはその結果の描画方法も異なります。なぜなら結果がknnMatch, radiusMatchはList[MatOfDMatch]になるけど、matchはMatOfDMatchになり、型が異なるからです。

型を揃えても良いんですが、それぞれの型に描画する方法があるので、使い分けてあげれば大丈夫です。

// matchesの型がMatOfDMatchの場合
val result = new Mat()
Features2d.drawMatches(image1, keyPoints1, image2, keyPoints2, matches, result)

// matchesの型がList[MatOfDMatch]の場合
val result = new Mat()
Features2d.drawMatches2(image1, keyPoints1, image2, keyPoints2, matches, result)

drawMatchesdrawMatches2の違いです。どうでもいいですけど、この命名の感じ、古臭さを感じますね。

実際は結果を描画せず、matchesの中身を見て類似しているか判定して終わることが多いと思いますが、一応。

OpenCVをJavaから使うぞ〜〜〜完全インストールマニュアル

まあJavaというかScalaから使いたかったんですが、同じことなんでとりあえずJavaから使うぞ〜〜〜という話です。

macOSの話だけど)Homebrewで入れていいの?Mavenリポジトリにあるの使っていいの?とまあいろいろ躓いたんで、まとめときます。

OpenCV(とJavaバインディング)をインストール

Homebrewから入れます。どのみちソースをビルドするので、ソースをダウンロードして自前でビルドしてもそんな変わらないと思います、たぶん。そっちの方がオプションなど細かくいじれると思うので、最終的にはそっちの方がおすすめな気はしてる。

Java用のバインディングもオプションで一緒にインストールします。するんですが、Antでビルドするようなので、Antをあらかじめインストールしておかないと、Java用のバインディングを作ってくれないっぽい。のでまず:

$ brew install ant

し、そのあと:

$ brew edit opencv

します。OpenCV用のFormulaを少し変えるんですが、-DBUILD_opencv_javaが書いてある箇所を探し、OFFならばONに変更しておきます。これをONにすると、Java用のバインディングも一緒にインストールされます。あとは:

$ brew install --build-form-source opencv

でインストールできます。正しくインストールできていれば、/usr/local/Cellar/opencv/VERSION/share/OpenCV/javaというディレクトリが存在し、中にJava用のバインディング(共有ライブラリと.jarファイル)が入っていればインストール大成功です。

build.sbtを書く

インストール自体はもう終わってるんですが、とりあえず使えるところまで書きます。というわけでbuild.sbtを書きましょう。中身はこんな感じ。

javaOptions in run += "-Djava.library.path=lib"

fork in run := true

forktrueにしているのは、こうしておかないと共有ライブラリを読み込めないから。

あとはjavaOptionsjava.library.pathにJava用のバインディングまでのパスを通せば良いだけ。$ ln -s /usr/local/Cellar/opencv/VERSION/share/OpenCV/java libしておき、作成したlibシンボリックリンクを指定します。IntelliJ IDEAを使用している方はその中の.jarファイルをプロジェクトの依存に追加しておけば、補完も効いておすすめです。

コードも書く

OpenCVのコードはちょくちょく変わっているので、ググった記事の通りに書いたら動かないとかざらです。imreadメソッドで画像を読み込めますが、そのメソッドがHighguiからImgcodecsに移動してたり。まあその辺はインストールしたOpenCVのバージョンに適したドキュメント読めば分かると思います。

.jarファイルだけsbtのlibraryDependenciesでインストールする

こっからは捕捉なんですが、上記の方法でインストールすると、IntelliJ IDEAが補完してくれないんですね。sbtならlibraryDependenciesに指定したものは自動的に補完の対象(というかクラスパスに入る)になるのに、わざわざ指定しないといけないと。

ので、この部分だけはlibraryDependenciesに指定したいという方もいるかもしれません。というわけでその方法。

まずMavenリポジトリにあるopencvのバージョンを調べます。そのバージョンと同一のOpenCVでないと動作しないので注意が必要で、大抵はOpenCVの方がバージョンが進んでいるので間違いなく気に掛ける必要があるでしょう。

2018年12月5日現在ではorg.openpnpのopencvが3.4.2-1で、HomebrewのOpenCVは3.4.3です。なので何も考えずにインストールするとあ゛〜〜〜ってなること必至です。というわけで、HomebrewのOpenCVも3.4.2をインストールする必要があります。

まず/usr/local/Homebrew/Library/Taps/homebrew/homebrew-core/Formulaに移動し、$ git log opencv.rbします。で、3.4.2のコミットを探し、$ git checkout COMMIT -- opencv.rbで、3.4.2のFormulaを得たら準備は完了。あとは「OpenCV(とJavaバインディング)をインストール」と同じ手順で大丈夫です。なお最近のHomebrewはinstallのときなどに勝手にupdateをしてしまうので、brewコマンドの前にはHOMEBREW_NO_AUTO_UPDATE=1を付けておくと良いでしょう。

あとはbuild.sbtに:

libraryDependencies += "org.openpnp" % "opencv" % "3.4.2-1"

を追加すれば大丈夫です。

参考

ObjectOutputStreamでシリアライズしたオブジェクトを永続化したら、serialVersionUIDが変わっちゃって破滅するやつ

題名のことやったやつおる?

ここにおる。こういう例外が投げられる。

InvalidClassException: local class incompatible: stream classdesc serialVersionUID = 1083041391519420705, local class serialVersionUID = -8102506628468650634

シリアライズした文字列(stream classdesc)中に含まれるserialVersionUIDと、今JVMの中にあるクラス(local class)のserialVersionUIDが違うわボケということの模様。

時と場合によるけど、永続化するならもっとマシなシリアライズ方式使えば良かったと思う。か、serialVersionUIDを手動で設定しておくか。私が遭遇したケースでは、永続化してた値はScalaで言う値クラスみたいなやつだったので、その値そのものを入れとけば良かったのに……、と30秒天井仰いだ。

ただまあ永続化していたものはセッションでログイン中のユーザーIDとCSRFトークンで、件数も少なかった。Redisに入れてたので、正直「FLUSHALLしてええか?」ってなった。なったけど、グッと堪えてシリアライズした文字列中に含まれるserialVersionUIDを書き換えてやった(これもどうかと思うが……)。

調べると、オブジェクト直列化ストリームプロトコルの仕様に則りシリアライズされる模様。保存されていたバイト列と比較すると当然だが一致し、クラス名の後にserialVersionUIDが書かれていることが分かった。今回のケースでは以下のようなバイト列だったので:

  • 0xaced: STREAM_MAGIC
  • 0x0005: STREAM_VERSION
  • 0x73: TC_OBJECT
  • 0x72: TC_CLASSDESC
  • 0x0046: 10進数で70だが、次のクラス名の長さ
  • クラス名(70バイト)
  • serialVersionUID(8バイト) ← ここを書き換えた

でなんとかなった。なんとかなったのか?

JSR-310のDateTimeFormatterに気を付けろ

JSR-310に限った話ではないかもしれませんが。

JSR-310で日時なりを文字列にするときは、fomratメソッドにDateTimeFormatterクラスのインスタンスを渡してやる。んー分かりやすい。ちなみにtoStringメソッドでもOK。この場合はISO 8601の形式でフォーマットされます。

で、このtoStringメソッドで使われているフォーマッター(おそらくDateTimeFormatter.ISO_*として用意されている出来合いのフォーマッター)なんですが、気が利いています。どう気が利いているかと言うとミリ秒がゼロの場合はミリ秒を出力しない。き、気が利いてる〜。

けど、他の言語やライブラリがフォーマッターに厳密に従ってパースする場合はミリ秒があったりミリ秒がなかったりで非常に厳しいので、どちらかに統一した方がいいねという話です。というかそれだけの話でした。一応フォーマッターの定義だけ掲載しときます。

import java.time.ZonedDateTime;
import java.time.format.DateTimeFormatter;

DateTimeFormatter alwaysWithoutMillis = DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ssZZZZZ");

ZonedDateTime.of(2018, 4, 13, 18, 48, 21, 1000000, ZoneId.of("Asia/Tokyo")).format(alwaysWithoutMillis); // => 2018-04-13T18:48:21+09:00

DateTimeFormatter alwaysWithMillis = DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ss.SSSZZZZZ");

ZonedDateTime.of(2018, 4, 13, 18, 48, 21, 0, ZoneId.of("Asia/Tokyo")).format(alwaysWithMillis); // => 2018-04-13T18:48:21.000+09:00

Javaめっちゃ久し振りに書いた!

ELBを介したSSEを行う場合の設定や気を付けるところ

SSE(Content-Type: text/event-stream)便利ですよね。フレームワークがサポートしていることも多いですし、その通りやれば結構サクッと動作します。

しかし、ローカルで試した場合にサクッと動作しても、ELBを介した場合に結構ハマったりしたので、その場合の解消法というか、ここ設定しとくと良いかもを書きます。

クライアント(アプリなど) ⇔ ELB ⇔ nginx(EC2インスタンス) ⇔ サーバアプリケーション

という構成が前提です。

結論

結論から言うと、下記の設定をしておくと私が試した限りはスムーズでした。

  • ELBのリスナーはHTTP/HTTPSではなく、TCP/SSLを使用
  • nginxのproxy_bufferingはoff

ELBのリスナーはHTTP/HTTPSではなく、TCP/SSLを使用

これがほとんどなのですが、ELBのリスナーはHTTP/HTTPSを使用すると、ELBが接続を保持したままになり、クライアントがHTTPコネクションを切断しても、サーバアプリケーション側に切断が伝わらない場合があるっぽいです。Redis pub/subなどに接続し、そっからデータを受け取るようなことをしている場合、Redisとの接続を切る必要があるかと思いますが、そういう場合致命的です。

この場合、ELBのリスナーにTCP/SSLを使用してあげたら、スパッと解消されました。ただし、HTTP/HTTPSは専用の処理を挟んでいたり(X-Forwarded-Forリクエストヘッダの付与など)、あとこいつのせいでSSEとの相性が悪いのだと思いますが、バックエンドとのコネクションを維持するキープアライブが働いています。それらが必要な場合はまた一考の余地ありとなります。

nginxのproxy_bufferingはoff

単純な話なのですが、nginxのproxy_bufferingが有効なままだと、サーバアプリケーションでイベントを書き込んでもnginxがバッファリングしてしまい、しばらくの間クライアントが受け取れません。SSEの場合、接続を維持するためにハートビート("data:"など意味を為さないイベント)を流すと思うので、結果それによって押し出されはしますが、SSEの性質上バッファリングしない方が正しいと思うので、切ってしまった方が良いかと思います。

location /path/to/sse {
  # ...

  proxy_buffering off;

  # ...
}

とでも書いておきましょう。併せてproxy_read_timeoutも確認しておいた方が良いかもしれません。ハートビートの間隔より短いと接続が切れてしまいます。nginxのデフォルトは60秒(ついでに言うとELBのアイドルタイムアウトも60秒)なので、あまり考える必要はないかもしれませんが……。

紆余曲折の話

私がいろいろ試した話をだらだら書くだけなので、あまり実のある話はありません。

事の発端はAkka StreamsのalsoToを使うとSourceが停止しないで書いたような、既読処理が停止しない問題を調べていたとき。alsoToの使用をやめてこれで問題解消やろ!と思ったが、そんなことはなくあれー?てなった。

ELBを介さず、直接EC2インスタンスにアクセスするとすんなり動作することから、どうやらELBがコネクション張ったままにしてるっぽいことまで分かった。これ無効にしたいんだけど〜と正直思ったが、どうやら無理っぽい。

じゃあSSEのハートビートを無効にし、ELBのアイドルタイムアウト任せに切断、クライアントで再接続と割り切るかと思ったが、それでもサーバアプリケーションの処理が停止しない。ならばサーバアプリケーションから終了を送ってしまえと、60秒で完了を送るようにしたが、これだとサーバアプリケーションの処理は停止しても、クライアントでエラーが発生し(Google Chromeだとnet::ERR_INCOMPLETE_CHUNKED_ENCODING)、再接続もしばらくの間失敗するというイケてない感じになった。

ちなみにSSEのハートビートを無効にしたときもいろいろあった。nginxのproxy_bufferingがonになっていてクライアントがイベントを受け取れなかったり、Akka Streamsが下流の停止を検出できず、動作が停止しなかったり。まあ結局ハートビートは有効にしたのでこの辺は関係ないのだけど。

で、チームにひたすら進捗投げてたんだけど、そこでHTTP/SSLリスナーはどう?とアドバイスをもらい、一発で解決したのであった。おしまい。