心のローパスフィルタ

地殻を毛細血管のごとく駆け巡る光ファイバが空間の垣根を取り去った結果、四半世紀前なら触れることすらなかった情報に晒されるようになった。 これは幸福でも不幸でもある。

観測する限り、多くの人は自分の置かれている環境の平均値で構成されている。朱に交われば赤くなるというやつだ。 これは環境のお陰で自分が周りのレベルまで引き上げられる例も、自分自身がいわば浸透圧の合った塩分濃度に自然と落ち着く例もあるだろう。 とにかく、環境から著しく外れた存在に触れるには周囲の人間を何ホップもしなければならなかった。

ところがインターネットが魔界の界峡トンネルのように突然1ホップで次元の違う人々に出会うことを可能にしてしまった。その結果、ブラウザ越しの玄関先にはS級妖怪がゴロゴロ歩いている。テクノロジーの進化はいつも喜ばしい。問題はこれにどう向き合うかだ。

ある種の人間にとって―まあこれは僕のことなのだが―半径30m以内にいる*ように見える*人たちは、自分が到達すべき平均目標に見えてしまうことがある。こうなると不幸だ。本来は界峡トンネルを越えないと出会うことすらできなかった文字通り次元の違う存在が当然にできることを、僕はどうして努力してもなお達成できないのだろうと思い悩むことになる。立っている土台も、越えてきた屍の数も、これから対峙する課題のスケール感も違うにも関わらずだ。

結局の所、昔からお母さんに言われる「他所はよそ、家はうち」に行き着く。どこかで心のローパスフィルタで余りにも高い理想をカットしないと自分の足元もおぼつかない。インターネットは簡単に他人の人生を摂取することができ、それはアルコールのようなエンターテインメント性と常習性をもたらすが、あくまで他人の物語だ。慎ましくも愛らしい自分の人生を、諦めるのではなく、前向きに受け入れなければならない。自分の他に自分の人生の面倒を見てくれる人はいないのだから。そんなことを考えていた。

肉体と精神の不可分性

一週間ほど出張で非常に寒い地域にいて、不運にもインフルエンザに罹患してしまい寝込んでいた。ようやく帰国できたが、身体的にも精神的にも参ってしまった。

 

元々体力には自信があって、この10年ぐらいインフルはおろか風邪で休むことすら稀だったし、昨年9月で酒もやめたので「二日酔いで翌日を棒に振る」みたいな現象もまったくなくなって、健康とは完全にコントローラブルなものになっていたはずだった。

 

ところがいざ39℃の熱で数日間夢うつつの生活を送ると、健康というのがどれほど得難いものであったかという当たり前の事実を思い知らされた。とりわけ、肉体的健康とは完全に独立して存在していると思っていた精神的健康というものが、肉体的な衰弱によりあっさりと共倒れになったのは大きなショックだった。知力は体力に依存しているのだ。考えもしなかった。

 

久しぶりに出社して懸垂機にぶら下がってみると、果たして体が持ち上がらない。あれだけ毎日取り組んでいたことが、砂上の楼閣ように一夜にして失われてしまった。また、毎朝3時に起きてコツコツつづけていたアルゴリズムクイズと数学も、いまは何をやっていたのか思い出せないほどだ。特に極度に頭を使う作業が先々週ぐらいから絶不調で、一体どうなってしまったのか自分でも呆然としていたのだが、思えば病気の兆候だったのだろう。体力的にはようやく回復したが、まだ頭の中は蜘蛛の巣がかかったように茫漠としている。いまも言葉がうまく出てこない。

 

これまで、自分に対する投資はもっともROIが高いという信念をもって絶えず勉強し続けてきた。結果的に、努力はすべてを叶えないが、自分がなりたいソフトウェア技術者ぐらいはかならず努力でたどり着けるという確信を持つに至った。ところが、このような前提は肉体的な健康というものに大きく依存していることに図らずも気付かされた。これから30代後半を過ごし、40代に突入するにあたって、この基盤はますます脅かされる可能性が高い。これまでのように、ただ盲目的に自分のやれることをやってさえいればどこかにたどり着けるという確信がすこし揺らいでしまった。自分がいまの状況にあるのは、単に幸運だったのだ。慎まねばならない。

 

また近い将来、怨念にも似た執念の火が自分の中で燃え始め、再び元の自分に戻れる日がくると信じているが、それまではほんの少しだけ無茶な早起きをやめてしばらく自分の健康と向き合いたいと思う。命あっての物種。人生は長く短い。このバランスが最も難しい。それでは、近いうちにまた。

レッテル貼りを嫌っていた自分がレッテル貼りモンスターになっていた話

ここのところずっと考えていたことが自分の中でまとまってきたので言語化する。

僕の非常によくない傾向として、自虐的・自罰的な物言いをするという点がある。たとえば、

  • CS学位がないので何をやってもダメ
  • 算数がわからないので競プロやる資格なし
  • 英語サッパリできない、TOEIC900点は現地校の幼稚園なみ

というようなやつだ。これは正直に言って、自分の中では経験に基づく事実のようなもので、いずれも「だから何とかしなくては!」という部分が自分に対してかかっているのだが、言い方が稚拙すぎて、同一属性の全人類に対して銃口を向けていることに気付いていなかった。

「僕は文系なので…」みたいな自虐もそうだ。そもそも自分がこういうレッテル貼りに対して人一倍憤慨していたはずなのに。

この画像が一時期Twitterで流行っていて「文系はこんなのも解けないのか」みたいなdisりをみるたびに、微分導関数ぐらい導けるわいふざけんなよという思いがあった。"文系"を無学のエイリアスとして使うんじゃないよと。これは、ともすれば"文系"を自分に対する言い訳として使っていた軟弱な態度と完全に矛盾する。それに、理転して情報科学の大学院に進学した今では、この言葉は自虐としてすら機能していない。自分が最も嫌い、軽蔑していたレッテル貼りを自ら進んで行うモンスターになっていたということだ。いまは自分を恥じている。

US駐在したときもそうだった。日本では謎のコンサルが「シリコンバレーのエンジニアは年収2000万!」みたいな物価も何も考えてねえだろみたいなことを言うのを見て疲弊し、たまに現地就職の日本人にまで「駐在組はクビにもならないし家賃補助もあっていいですよね」と謎の憎しみを向けられたりしたものだ。どれも実態を伴わないレッテル貼りによるものだ。全部自分が嫌っていたやつだ。現代のイソップ童話の登場人物に自分がなってしまっていた。

僕個人としては次の点に気を付けたい。

  • 自分の属するカテゴリについて言及しない。常に主語は自分とする。
  • 自虐を控える。
  • 何かを貶めない。批判は建設的対案とともにする。

自分の娘に見られて恥ずかしくないことだけインターネットに書こうと思います。それではポケモン捕まえてきます。

とにかく次の10年を生き残りたい

ついに2020年になってしまった。

この10年はとにかくネイティブアプリ開発にすべてを投資した。
京都GTUGのイベント情報によれば、第1回Androidハッカソンは2009年9月5日に開催されている。このハッカソンのためにドコモ HT-03Aを契約しに行ったのがAndroidとの出会いだった。当時APIレベル3、コードネームCupcake、バージョン1.5である。加速度がホントにJavaのコードから取れてびっくりしたのを覚えている。

実に丸10年の月日が過ぎた。スマートフォンの普及率は20-30代で90%を超え、今後も低くなるということはないだろう。当然、それらスマートフォンで動作するネイティブアプリ開発の需要もしばらくは続くに相違ない。ただし、開発の前線から見える情景は少しずつ変わってきているようにも感じられる。
3-4年前だとネイティブファースト(特にiOS)が当然であり、ウェブが存在しないサービスも少なくなかったが、ウェブへの回帰も見られるようになってきた。また、FlutterによるiOS/Androidアプリ同時開発も流行しているように思う。この辺りは個人の感覚だし、東京とそれ以外、あるいは米国、欧州でもかなり違っているのだろうと想像するが、本ポエムの主眼ではない。

次の10年は何に投資すればよいのだろうか。本当に知りたい。僕は深刻にこの辺りの嗅覚が鈍い。
iPhoneがリリースされた2007年当時、僕はすでにプログラマだった。当時所属していた会社はSkypeのようなP2P通信アプリを開発しており、LinuxザウルスやSymbianOSなんかにも対応していた記憶がある。その渦中にあって、iPhoneがその後天下を取ることを想像できなかった。BlackBerryNokia端末との違いが分からなかった。当事者とは案外そんなものだ。何回目かのxR元年と、いつ来るかわからないシンギュラリティ。また同じように、10年後振り返って「ああ、なんであのときアレに投資していなかったのか…」と嘆息するのだろう。

次の10年は僕の40代を捧げる10年となる。実際に35歳を過ぎてみて、新しいことに興味を持って勉強をし続ければまだまだ全然戦えるという感覚はある。理論や研究を補うために大学院にも進学した。競プロも始めた。酒もやめた。ただまあ、抗いきれない事実として体力は落ち続けているし頭の回転も少しずつ鈍っている。圧倒的な瞬発力の戦いから、総合格闘技で最後は寝技で絞めるみたいな感じになってきている。次の投資先は慎重に決めねばならない。

一旦、2020年Q1は再びウェブフロントエンドへの投資を少し増やしてみようと思っている。2013年まではフロントエンド開発(なんと、jQueryとbackbone.js)もしていたのだが、7年も経ってしまって自分の知識はバクテリアに分解されて石油となっている。その間ウェブ技術は目覚ましい発展を遂げ、現在のJSは当時とは完全に別物だと感じる。TypeScriptで型の恩恵も受けられる。ありがたいことだ。この状況を作ってくれた人々に感謝したい。

という訳で何卒諸々ご指導ください。本年も宜しくお願いいたします。

アルゴリズムと数学的思考力

厳しい。年始早々厳しさを感じている。自分のプログラミング力にだ。伸び悩んでいる。

端的に言って、数学力のなさが自分のプログラミング能力に制限をかけている。例えばこの問題。

560. Subarray Sum Equals K
入力として与えられる配列 nums のうち、合計が k となる部分配列の個数を数え上げよ。どうも有名な問題らしいが…

まず大前提として、部分配列なので i, j の2重ループで始点・終点を定めて sum(nums[i, j]) = k になるものを数え上げれば必ず答えが得られる。最悪計算量は O(N^3) ただし i < nums.length < 20000 という制約があるので N^3 では遅すぎるから何か考えてくださいというのがスタート地点。

ここで、結果の変わらない累積和を何度も求めているので nums[i, j] = k を求めたい場合、 nums[0, j] - nums[0, i - 1] としてこれまでの計算結果を利用できることに気付けると計算量を大幅に減らせる。ただし、nums には負数が含まれているので同じ累積和が何度も出現する可能性があるので Map で出現頻度をカウントする

public int subarraySum(int[] nums, int k) {
    int j = 0;
    Map<Integer, Integer> seen = new HashMap<>();
    seen.put(0, 1);
    int counter = 0;
    for (int num : nums) {
        j += num;
        int i = j - k;
        if (seen.containsKey(i)) {
            counter += seen.get(i);
        }
        seen.put(j, seen.getOrDefault(j, 0) + 1);
    }
    return counter;
}

これで時間計算量 O(N) 空間計算量 O(N) で通った。 seen.put(0, 1) の部分だけトリッキーで、このアルゴリズムがそもそも前の計算結果を利用するものなので、初めて sum(nums[0, j]) == k になるケースをカバーする。

これってもはや算数の問題で、区間 nums[i, j] = knums[0, j] - nums[0, i - 1] で求まるっていうのは答え見て初めてマツコデラックスが感心する時の声で「ハ〜〜〜〜〜〜!」って唸ることしかできなかった。もう1題。

373. Find K Pairs with Smallest Sums
ソート済み配列 nums1, nums2 と整数 k が与えられる。このとき nums1, nums2 から1つずつ取り出してつくるペア (u,v) を、和が小さい順に k 個取り出せ。

Input: nums1 = [1,7,11], nums2 = [2,4,6], k = 3
Output: [[1,2],[1,4],[1,6]] 

これも O(N^2) で全ペアが列挙でき、それを整列するコストが O(N log N) なので最悪計算量 O(N^2) で必ず解ける問題。これには特に nums.length, k の制約が示されていないが、AtCoderなどのコンテストでは間違いなく O(N^2) では制限時間に収まらない(=不正解となる)データが与えられるだろう。したがってもっと高速なアルゴリズムを考えなさいというのが趣旨となる。

ここで nums1, nums2 はソート済みなので、nums1[0], nums2[0] は常に最小のペアであることが保証される。このような最小のインデックスをそれぞれ i, j とした場合、nums1[i], nums2[j] が解答のひとつならば nums1[i + 1], nums2[j]nums1[i], nums2[j + 1] が次の答えの候補になる。言われてみればそうである(孫権顔)

後は候補が複数あった場合に低コストで最小値を得られるデータ構造を考える。そう、二分ヒープ(最小)である。min heap の追加・削除の最悪コストは O(log N) なので、今回要素数 k の候補から常に最小を取り出しつつ k 個集めるまで繰り返すので O(K log K) が模範解答のひとつとなる。本件は競プロ界で著名な 宇宙ツイッタラーX さんに解説いただいて初めて理解できた。スレッドを参照されたい。

実装例も、氏のそれより綺麗には到底書けないので 氏のgist を見ていただきたい。

で、ここからが主題なのだが、これらの問題はプログラミング能力というか算数の問題なのだ。最初の問題はデータ構造と言っても HashMap ぐらいしか使ってないし、2題目も min heap こそ確かにコンピュータ・サイエンス的なデータ構造ではあるものの、これは最小(最大)値を O(log N) で出し入れできる(最小(最大)値の参照だけなら O(1))データ構造とだけ覚えていれば使える類のものだし、そこがこの問題を解く最大のキーというわけではない。現に僕は min heap を使うと模範解答になるというヒントを貰った後も「 nums1[i], nums2[j] が答えならば nums1[i + 1], nums2[j]nums1[i], nums2[j + 1] がその候補になる」という核心部分が出てこなかったし、むしろここがこの問題の最大のキーポイントであるわけで。

ここからは自分でもうまく整理できていないのだが、これは「数学力」なんだろうか。僕が数学が苦手なのは自他ともに認めるところだが、いま大学院の授業についていくためにマセマの微分積分とかを解いてるけど、なんというかこう、公式あてはめて微分するとかとは違うわけじゃないですか。使われているのは中学生レベルの四則演算で、むしろ「数学的思考力」を問われているというか。

社会人になってプログラマとして10年ぐらいやっていて忘れかけていたけど、そういえば僕は勉強まったくできなかったな、受験敗者だったわ、忘れてた。僕には数学的思考力が致命的にない。こういうのをアルゴリズムクイズを解いていると嫌というほど思い出させられる。

今回の2題は非常に悔しい思いをしたので覚えたが、これを繰り返していけば僕のように頭がよくない人間も類題を解けるようになるのか、それともちょっとひねられるだけでまたまったくお手上げになってしまって悔し涙に枕を濡らすのか、いまは分からない。願わくは2年後に読み返したときに「あの時はあんなしょうもない問題で悩んでいたんだなぁ😅」と思えるようになっていたいが、どうやることやら。正月に書くような内容ではないな…

Amplify for Androidでより直感的になったGraphQLを試した

TL;DR

本エントリはAWS Amplify Advent Calendar 2019の24日目です!

今回は新しくリリースされたAmplify for Androidを使って、前回と同じく下のようなチャットアプリを作るので、興味のある方は前口上を飛ばして後半をお読みください🎄

f:id:fushiroyama:20191201023816g:plain

Amplifyとは

本題に入る前に、Amplifyを最近よく耳にするようになったけど何かよく分かっていないという人向けの説明をします。

Amplifyはモバイルバックエンドを爆速で作るためのサービスです。 https://aws.amazon.com/jp/amplify/

他のいわゆるモバイルバックエンドとの際立った違いのうち、個人的に強調したいのは以下2点です。

  • バックエンドはAWSのサービスであり、真の意味でスケールする
  • GraphQLのマネージドサービスを利用できる本日時点で唯一のプラットフォームである

Amplifyの良い点として、フロントエンドから利用する時はそれと意識せずに簡単に構築できるけれども、バックエンドはAWSが個別に提供するサービスで構成されているという点です。これは、バックエンドをキャパシティのよく分からないブラックボックスとして扱うことなく、必要であればAWSを使い慣れたインフラ部隊と協力して本当の意味でスケールするようにチューニング可能だということです。

さらに、AmplifyはGraphQLのマネージドサービスを提供する今日時点で唯一のプラットフォームです。GraphQLの良さは他の記事に譲りますが、これを自前で構築するのは中々骨が折れます。AmplifyでAPIを追加するときにGraphQLを選択すると、裏ではAWS AppSyncが使われますが、これはGraphQLのマネージドサービスであり、サーバのプロビジョニングやチューニングを気にすることなくGraphQLのメリットを享受できます。

Amplifyの構成要素

Amplifyは主に次の要素で構成されています

  • バックエンドプロビジョニングのためのCLI
  • ウェブ、iOS/Androidに組み込むためのフレームワーク
  • CI/CDや管理のためのコンソール

f:id:fushiroyama:20191229142819p:plain
AWS re:Invent 2019のセッション動画より引用

後述しますがAmplify for iOS/AndroidのリリースでXcode/Android Studioとの統合がすすみ、CLIを単独で使用することは今後かなり少なくなります。したがって、基本的には普段開発しているときのようにCocoaPodsやGradleでライブラリを導入するように使い始めることができます。具体的な使い方はこのエントリの後半で解説します。

Amplifyのカテゴリ

Amplifyはカテゴリという概念があり、

  • APIREST APIやGraphQL
  • Auth…認証と認可
  • Storage…ストレージ
  • Analytics…分析とユーザエンゲージメント
  • Predictions…AI/MLの組み込み
  • XR…AR/VR

のような機能のうち、使いたいものを好きなだけ選んで使うことができます。

これまでもモバイルからAWSのサービスを使うことは当然できました。これはAWS Mobile SDKによって簡単に実現できます。 ただ、これはどちらかと言えば「AWSのこのサービスを組み込みたい」というマインドセットで使います。対してAmplify for iOS/Androidでは「APIを組み込みたい」というように、ユースケースの側面から使うことができます。

f:id:fushiroyama:20191229144655p:plain
AWS re:Invent 2019のセッション動画より引用

前置きが長くなりました。ここまでがAmplifyの概要です。次からはAmplify for iOS/Androidについて解説します。

Amplify for iOS/Androidとは

先日のAWS re:Invent 2019で新しく発表された、Amplifyをモバイルからより簡単に使うためのアップデートです(公式ブログ)。

fushiroyama.hatenablog.com

先日、上記エントリで AWSAppSyncClient を使ってGraphQLにつなぐ方法を紹介しましたが、これが新しいAPIでどんな感じになるのか見てみましょう。

注意点

公式サイトに明示されていますが、Amplify for iOS/Androidはプレビューリリースで、本番環境での利用はまだ想定されていません。他の多くのOSSがそうであるように、開発者が想定していないと思われることをやろうとするとまだ粗削りな部分が見て取れますが、コントリビューションのチャンスでもあります!本エントリで興味を持ってくださった方は是非お試しください!

Androidクイックスタート

それではAmplify for Androidを使ってGraphQLの読み書きをしてみたいと思います。序盤は公式サイトの手順に準じます。

まずはプロジェクトの build.gradleプラグインをインストールします。

buildscript {
    ext.kotlin_version = '1.3.61'
    repositories {
        google()
        jcenter()
    }
    dependencies {
        classpath 'com.android.tools.build:gradle:3.5.3'
        classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
        // Amplify
        classpath 'com.amplifyframework:amplify-tools-gradle-plugin:0.1.0'
    }
}

// Apply Plugin
apply plugin: 'com.amplifyframework.amplifytools'

allprojects {
    repositories {
        google()
        jcenter()

    }
}

次にアプリケーションレベルの build.gradle にJava8の設定と依存関係のインストールをします。

android {
  compileOptions {
        sourceCompatibility 1.8
        targetCompatibility 1.8
    }
}

dependencies {
  implementation 'com.amplifyframework:core:0.9.0'
  implementation 'com.amplifyframework:aws-api:0.9.0'
}

次に、Android StudioをProjectビューにして amplify/backend/api/amplifyDatasource/schema.graphql を編集します。これはGraphQLのスキーマ定義で、これを元にAndroid用のモデルが自動生成されます。せっかくなので、以前のエントリと同じくリアルタイムチャットアプリを開発してみます。まずは、前回と同じ次の定義で作ってみましょう。

type Message @model {
  id: ID!
  username: String!
  content: String!
}

次に、Amplify CLIをインストールします。npmを使うのでNode.jsが入っていない人はインストールしてください。

$ npm install -g @aws-amplify/cli
$ amplify -v
4.7.0

初めてAmplifyを利用する場合は、CLIAWSのクレデンシャルを設定する必要があるので amplify configure コマンドを実行します。AWSのマネジメントコンソールにリダイレクトされるので、そこでユーザを作成してキーを発行してください。ここは次のチュートリアルに非常に詳細な解説があります。

この状態でAndroid Studioに戻ってくると、Build > Make Project でプロジェクトをビルドします。

f:id:fushiroyama:20191229153500p:plain

すると、Graldeメニューに modelgenamplifyPush のタスクが追加されます。

f:id:fushiroyama:20191229153610p:plain

まず、modelgen を実行すると先程のGraphQLのスキーマから com.amplifyframework.datastore.generated.model にメッセージを表すモデルの Message.java が生成されます。 次に amplifyPush を実行すると、Amplify CLIが必要なバックエンドをプロビジョニングしてくれます。この作業は数分かかることがあります。 完了したら、これらを使ってチャットのメッセージを読み書きしてみましょう。

最初にAmplifyを初期化するための設定をします。カスタムApplicationクラスを作成し、次の内容を追記します。

class MyApplication : Application() {
    override fun onCreate() {
        super.onCreate()
        try {
            Amplify.addPlugin(AWSApiPlugin())
            Amplify.configure(applicationContext)
        } catch (e: AmplifyException) {
            Log.e(TAG, e.message)
            throw RuntimeException(e)
        }
    }
}

MyApplicationAndroidManifest.xml に追加するのを忘れないでください。

    <application
        android:name=".MyApplication"
        android:allowBackup="true"
        android:icon="@mipmap/ic_launcher"
        android:label="@string/app_name"
        android:roundIcon="@mipmap/ic_launcher_round"
        android:supportsRtl="true"
        android:theme="@style/AppTheme">

あとは読み書きをするだけです!まずはミューテーションから。

modelgen によって作られたモデルは Message.builder() のようなビルダーがついてきます。ユーザの入力に合わせて簡単にモデルを組み立てることができます。 あとは Amplify.API.mutate メソッドを呼ぶだけです。結果を受け取りたい場合は ResultListener も一緒に渡します。

private fun mutation(
    username: String,
    content: String
) {
    val message: Message =
        Message.builder()
            .username(username)
            .content(content)
            .build()

    Amplify.API.mutate<Message>(
        message,
        MutationType.CREATE,
        object :
            ResultListener<GraphQLResponse<Message>> {
            override fun onResult(response: GraphQLResponse<Message>) {
                response.data?.let {
                  // do things with message
                }
            }

            override fun onError(e: Throwable) {
            }
        })
}

次はクエリです。こちらも Amplify.API.query メソッドを呼ぶだけです。 結果は同じように ResultListener で受け取れるので、これを RecyclerView で描画するといった使い方が一般的でしょう。

private fun query() {
    Amplify.API.query(
        Message::class.java,
        object :
            ResultListener<GraphQLResponse<Iterable<Message>>> {
            override fun onResult(response: GraphQLResponse<Iterable<Message>>) {
                val messages: MutableList<Message> = mutableListOf()
                for (message in response.data) {
                    messages.add(message)
                }

                // update RecyclerView
                updateMessages(messages)
            }

            override fun onError(e: Throwable) {
            }
        })
}

最後にサブスクリプションです。なんとこちらも Amplify.API.subscribe メソッドを呼ぶだけです。 これだけでメッセージの作成をリアルタイムに通知してもらえるので、チャットアプリのようなものがいとも簡単に作成できます。

private fun subscribe() {
    Amplify.API.subscribe(
        Message::class.java,
        SubscriptionType.ON_CREATE,
        object :
            StreamListener<GraphQLResponse<Message>> {
            override fun onNext(response: GraphQLResponse<Message>) {
                response?.data?.let {
                    // update RecyclerView
                    addMessage(it)
                }
            }

            override fun onComplete() {
            }

            override fun onError(e: Throwable) {
            }
        }
    )
}

めちゃくちゃ簡単ですね!ここまでが公式のチュートリアルに相当する内容です。

時系列でメッセージを取得

ここからは少し発展的な内容を扱います。

AmplifyでGraphQLを使う場合、デフォルトではAppSyncとDynamoDBが使われます。DynamoDBは扱うデータサイズや規模によらず、うまく設計すればミリ秒単位のパフォーマンスを維持できるKVSおよびドキュメントデータベースですが、一般的なRDBMSを1台だけで扱うときとは違う点を考慮する必要がある場合があります。たとえば「メッセージを投稿順で取得し、ページングさせたい」という場合には、DynamoDBではいくつか実現方法がありますが、

として設計するのがよくあるパターンです。ここではDynamoDBの詳細なデザインパターンには触れませんが、チャットサービスといえば普通はチャンネル(部屋)を複数持てるのが普通なので、各部屋を表現する roomId + メッセージの日付 createdAt を使ってメッセージを時系列に取得することにします。

まずは前出のGraphQLのスキーマを次のように変更します。

type Message
  @model
  @key(name: "SortByCreatedAt", fields:["roomId", "createdAt"], queryField: "listMessagesSortedByCreatedAt" )
{
  id: ID!
  username: String!
  content: String!
  roomId: String!
  createdAt: AWSDateTime!
  updatedAt: AWSDateTime
}

name: "SortByCreatedAt", fields:["roomId", "createdAt"] の部分で時系列に取得するためのキーを追加しています。 また queryField: "listMessagesSortedByCreatedAt" の部分で、このキーを使ったクエリをする際の名前を指定しています。

できたら、以前同様 modelgenamplifyPush を実行してモデルファイルを再生成し、バックエンドにプロビジョニングします。 これで、GraphQL的には次のようなクエリで時系列で取得できるはずです。

query GetPost(
  $roomId: String!
  $limit: Int!
) {
  listMessagesSortedByCreatedAt(
    roomId: $roomId
    sortDirection: ASC
    limit: $limit
  ) {
    items {
      id
      content
      username
      roomId
      createdAt
    }
  }
}

ただし、現状のAmplify for Androidではこのような任意のクエリを簡単に送る方法がないので、GraphQLRequest を利用します。 GraphQLRequest(query, variables, Message::class.java, GsonVariablesSerializer()) のように使います。

注意点として、スキーマcreatedAt: AWSDateTime! のように定義した場合、自動生成された Message クラスでは日付は java.util.Date 型として生成されますが、このモデルを透過的にGraphQLのクエリに変更してくれるライブラリ組み込みの GsonVariablesSerializerjava.util.Date を雑に yyyy-MM-dd に変換します。 これでは精度的に問題なので、ISO 8601 にするために独自の GsonVariablesSerializer を作成します。

class GsonVariablesSerializer : VariablesSerializer {
    override fun serialize(variables: Map<String, Any>): String {
        return GsonBuilder()
            .registerTypeAdapter(
                Date::class.java,
                DateSerializer()
            )
            .create()
            .toJson(variables)
    }

    internal inner class DateSerializer : JsonSerializer<Date?> {
        override fun serialize(
            date: Date?,
            typeOfSrc: Type,
            context: JsonSerializationContext
        ): JsonElement {
            val df: DateFormat = SimpleDateFormat(
                "yyyy-MM-dd'T'HH:mm:ssXXX",
                Locale.getDefault()
            )
            return JsonPrimitive(df.format(Date()))
        }
    }
}

あとは次のようにクエリを組み立てて、GraphQLRequest を作成して Amplify.API.query します。 残念ながらKotlinのraw stringsでは$自体の文字として扱えないので${'$'}のようにしています。

val query = """
    query GetPost(
      ${'$'}roomId: String!
      ${'$'}limit: Int!
    ) {
      listMessagesSortedByCreatedAt(
        roomId: ${'$'}roomId
        sortDirection: ASC
        limit: ${'$'}limit
      ) {
        items {
          id
          content
          username
          roomId
          createdAt
        }
      }
    }
    """.trimIndent()
val variables: Map<String, Any> = mutableMapOf(
    "roomId" to DEFAULT_ROOM,
    "limit" to DEFAULT_LIMIT
)
val request =
    GraphQLRequest(query, variables, Message::class.java, GsonVariablesSerializer())

val listener = object :
    ResultListener<GraphQLResponse<Iterable<Message>>> {
    override fun onResult(response: GraphQLResponse<Iterable<Message>>) {
        val messages: List<Message> = response?.data?.map { it }.orEmpty()
        // do things with messages
    }

    override fun onError(e: Throwable) {
    }
}
Amplify.API.query(request, listener)

ミューテーションもまったく同様です。

val query = """
    mutation CreateMessage(
      ${'$'}id: ID!
      ${'$'}content: String!
      ${'$'}roomId: String!
      ${'$'}username: String!
      ${'$'}createdAt: AWSDateTime!
    ) {
      createMessage(input: {
        id: ${'$'}id
        content: ${'$'}content
        roomId: ${'$'}roomId
        username: ${'$'}username
        createdAt: ${'$'}createdAt
      }) {
        id
        content
        roomId
        username
        createdAt
      }
    }
    """.trimIndent()
val variables: Map<String, Any> = mutableMapOf(
    "id" to id,
    "content" to content,
    "roomId" to roomId,
    "username" to username,
    "createdAt" to createdAt
)
val request =
    GraphQLRequest(query, variables, Message::class.java, GsonVariablesSerializer())

Amplify.API.mutate(
    request,
    object :
        ResultListener<GraphQLResponse<Message>> {
        override fun onResult(response: GraphQLResponse<Message>) {
        }

        override fun onError(e: Throwable) {
        }
    }
)

これで、時系列にメッセージを読み書きするための対応ができました。

改善を望む内容

最後に今回気付いたいくつかの今後改善されるであろう内容を取り上げます。

まず、Amplify.API.query, Amplify.API. mutate, Amplify.API.subscribe はいずれも現在のところブロッキングAPIとなっています。これは、LoaderObservable 等でラップして扱うことを意図しているのか、それとも単に設計上のミスなのか判然としません。とにかく、このままではUIをブロックしてしまうので、RxJavaの Single, Completable でラップして使いました。

// query
val source: SingleOnSubscribe<List<Message>> = SingleOnSubscribe {
    val listener = object :
        ResultListener<GraphQLResponse<Iterable<Message>>> {
        override fun onResult(response: GraphQLResponse<Iterable<Message>>) {
            val messages: List<Message> = response?.data?.map { it }.orEmpty()
            it.onSuccess(messages)
        }

        override fun onError(e: Throwable) {
        }
    }
    Amplify.API.query(request, listener)
}
return Single.create(source)

// mutation
return Completable.create { emitter ->
    Amplify.API.mutate(
        request,
        object :
            ResultListener<GraphQLResponse<Message>> {
            override fun onResult(response: GraphQLResponse<Message>) {
                response.data?.let {
                    emitter.onComplete()
                }
            }

            override fun onError(e: Throwable) {
                emitter.onError(e)
            }
        })
}

// subscription
return Single.create { emitter ->
    Amplify.API.subscribe(
        Message::class.java,
        SubscriptionType.ON_CREATE,
        object :
            StreamListener<GraphQLResponse<Message>> {
            override fun onNext(response: GraphQLResponse<Message>) {
                response?.data?.let {
                    emitter.onSuccess(it)
                }
            }

            override fun onComplete() {
            }

            override fun onError(e: Throwable) {
                emitter.onError(e)
            }
        }
    )
}

利用する側は次の通り subscribeOn(Schedulers.io()) とすればワーカスレッドで処理が実行されます。

compositeDisposable.add(
    mutation(
        name,
        editTextContent.text.toString(),
        DEFAULT_ROOM
    )
        .subscribeOn(Schedulers.io())
        .observeOn(AndroidSchedulers.mainThread())
        .subscribeBy(
            onComplete = {
                editTextContent.setText("")
            },
            onError = {
                Log.e(TAG, it.message, it)
            }
        )
)

APIとしてListenerを受け取る以上、設計としては非同期なコールバックとして意図されている気がします。この辺りは折角のオープンソースなので何らかのフィードバックを行いたいと思います。

それから、前回のエントリ同様に、AWSMobileClient を使うことで認証機能を簡単に追加することができますが、認証にAWSのCognito User Poolsを使った場合Subscriptionがうまく動かないようです。SubscriptionAuthorizationHeader.from の実装を見る限り、まだAPI Keyにしか対応していないのかも知れません。この辺り、やはりまだまだプロダクション・レディとは言えません。

まとめ

ということで、駆け足になりましたが、今月プレビューリリースされたばかりのAmplify for Androidをざっと触ってみた感じをご紹介しました。 ライブラリとしてはまだまだこれから成熟していくのに期待しますが、AmplifyはたとえばAppSyncのコンソールで非常に簡単にクエリを実行して確認できるなど、デベロッパー体験が非常によいです。Amplifyは正直もっと流行っていいと思っています。これからも情報発信してゆくのでどうかお楽しみに。

それではよいお年を。

社会人大学院で得たもの、失ったもの

こんにちは。本エントリは 社会人学生 Advent Calendar 2019 の第7日目です!

このエントリでは、社会人大学院で得たもの、失ったものについて思うところを思いつくままに書こうと思います。 特に、失ったものについては正直に書いておく必要があるでしょう。

自己紹介

僕のブログでは社会人大学院のことをたびたび書いており今更感もあるのですが、このアドベントカレンダー経由で本エントリを読んでくださる方も当然いらっしゃると考えるので、コンテキストの共有のために改めて自己紹介をさせてください。

白山と申します。36歳会社員です。妻と2歳6歳の女児を育てながらフルタイム会社員をしています。
現在は北陸先端科学技術大学院大学(通称JAIST)の修士課程で情報科学を専攻しています。元々いわゆる文系出身ですが、かれこれ10年以上もIT産業の片隅で禄を食みつづけておりました。去年〜今年の春にかけて1年と少し米国にいた経験から、大学院に進む決断をしました。その辺りの詳細は次の記事に寄稿しましたので良かったら読んでいただけたら幸いです。

engineer-lab.findy-code.io

また、特にJAISTに関してはこれらのエントリにまとめてあります。JAISTに興味のお有りの方はご参照ください。

fushiroyama.hatenablog.com

fushiroyama.hatenablog.com

社会人大学院で得たもの

それでは早速、良い面から書いていきましょう!

学位

すみません、まだまったく修了していないし目処も立っていないですが、これは書かないわけにはいかないですよね。学位。学位記。修士(博士)号。欲しい。

学位って何なんでしょうか。
前出のエントリには偉そうに「今ここに無い価値を創造する力を養うため(キリッ」みたいなことを書きましたし「米国の就労ビザが求める要件として関連学位が必要」とも書きました。しかしですね、力を養いたいなら別に大学院に行かなくたって夜中にシコシコ勉強すればいいんですよね。就労ビザだって、実のところ僕はIT業界で実務経験が12年ぐらいあるので、仮にBachelor of Artsでも弁護士の腕次第でH-1Bに申請できる可能性が高いです。

じゃあ何かって、ソシャゲのメダルみたいなものではないでしょうか。究極的には自己満足です。2〜3年間痛みに耐えてよく頑張った!感動した!それでいいじゃありませんか。その上で、就職にもうな重の山椒ほどの効果が上乗せできるなら上出来です。

よく言われるらしいですが、学部は参加賞、修士は努力賞だそうです。では博士は何賞なのか。 僕の指導教官が言っておりましたが、

「博士はね、いまも新規性は大事ではあるけど、最近では『自分で考えて』『計画を立てて』『表に出した』ということを『やれる子ですよ!』という認定に近いと思いますよ😌」

だそうです。なるほど認定証。欲しい!

モチベーションの高い同志

これは社会人大学院固有かもしれません。社会人大学院は非常にモチベーションが高い同級生がたくさんいます。 働きながら苦労してまで大学院に行こうという人たちなので、これは当然のことです。

20年前に修士を修了したけど、子供が独立したし役職定年で時間が出来たので博士課程に来ているお父さん。 教育学修士をすでに持っているけど、自分の幅を広げるためにさらに別の専攻に進んだ小学校の先生。 そして僕のように専攻も違うし学部しか出ていないけどキャリアアップのために修士にきてる人。 実に多種多様です。

企業の研究員も看護師さんもお医者さんも大学の先生まで同級生にいました。みんな色んな思いで大学に来ています。この情熱を分けてもらえるだけで通っている甲斐があります。

学力や能力

じゃあ大学院はやりがいや自己満足だけかというと、そんな訳ないですね。学力が普通に身につきます。 というか学力がないと単位が取れません。修了できません…

他のエントリにも書きましたが、JAIST情報科学は甘くないです。これだけ長いことIT産業にいて、しかも僕は競技プログラミングや数学、UNIXオペレーティングシステムなどの自習をかれこれ結構長いことやってきたのでまあ何とかなるかな?と高をくくっていたのですが、あんまり何とかなっていません。

勉強している内容は難しく、しかもその前提となる学部レベルの線形代数微積分学の勉強もまったく足りておらず、普通に座っているだけでは全然勉強についていけません。毎晩必死に勉強しています。自分の脳が、20年ぶりに呼吸をしているのを感じます。これが成長…!

他にも得たものはたくさんあると思うんですけど、あまり長くなっても読んでもらえないのでこの辺で。

社会人大学院で失ったもの

ここからは重く苦しい現実をお伝えしたいと思います。

子供との時間

JAIST(社会人コース)の情報科学は、金曜の夜と土日に開講しています。僕の場合は必修と修士論文を除けば、だいたい20単位取らないといけません。 JAISTは4学期制なので、1学期あたり2.5単位とれば2年で(単位としては)足りる計算です。3ヶ月ごとに1〜2講義です。

これがそれほど甘くありません。当初、この計算をしてみて「土日の1日ずつ差し出せば行ける!」と喜んだのですが、実際に蓋を開けてみると大学の授業はそんなに都合良くは開講しないので、授業によっては2コマずつ土日両方みたいな感じになります。 土日両方なので家族で旅行にもいけませんし、そもそも連日開講される授業の予習復習で土日も朝3時に起きて3時間資料に目を通すような生活がずっと続いています。

妻にも「大学院に行くのはいいよ・・・でも、『今』なの?」とため息まじりに言われました。何も言えない。

余暇

余暇などというものは何もなくなりました。1学期にたった1講義でも、予習復習で他のことを考えている余裕がありません。

そもそも僕は理系学部を出ていません。そうすると彼らが当然知っている線形代数や大学の微積分学をちゃんと身に付けていません。 ちょうど2年前ぐらいに機械学習が流行り始めて、僕も高校微積をひとりでコツコツやってたんですが、割と冗談抜きで大学の微積はレベルが違って、授業の板書を見返したときに式がなにをやってるか分からず半泣きで大学積分のテキストを開くというようなことを毎夜しています。

これはJAIST固有なのか理系固有なのかわかりませんが、出席点などというものはないので、基本的にテストができないと落第します。留年します。このプレッシャーは僕は未経験でした。酒を飲んでる余裕すらないんです。酒を飲むと朝起きて勉強ができない。勉強ができないと単位が取れない。 仕事終わりや休日に講義を受けて、みんなで楽しく議論してレポートを書いて、2〜3年で帽子🎓かぶって卒業なんて、とんでもない。そんな甘いものではなかった。

修士論文が始まると、おそらく行き帰りの電車の中ですらそれをずっと考える日々が始まると思います。大丈夫なのか不安です。

まとめ

本当に「今」大学院なのかというは未だに自問自答します。家族の信頼を揺るがしてまでやることなのかと。 僕は結構海外出張があるのですが、そこから逆算して合間を縫って取れる講義をとっているので、休日は大学に行ってていないし講義が少し途切れるタイミングで待っていたかのように一週間仕事で海外に行くような感じになるので、家族は「パパは休日家にいない人」みたいな感じになりつつあります。

結婚して、子供ができて、僕は一貫して定時で帰って家族と一緒に過ごすという日々を過ごしてきたので、これには忸怩たる思いもあります。 ただね、桜木花道じゃないですが、僕は「今」なんですよ…30代でエンジニアとしても脂の乗った「今」さらなる力をつけて飛翔したい…40歳までのあと4年の投資がその先の20年を左右すると思っているのです。それまで家族とのバランスを取りながらも前に進むしかない。ボーッと生きてたら人生終わってしまう!

また来年のアドベントカレンダーで進捗を共有したいと思います。また会うその日まで、じゃあの!