TECH PLAY

dely株式会社

dely株式会社 の技術ブログ

236

こんにちは、サーバーサイドエンジニアのjoooee0000です。 delyはデータ基盤としてAWS Athenaを使っており、ユーザーの行動ログからアプリケーションのアクセスログまで、様々なログがAWS Athena上に存在しています。AWS AthenaはS3上にあるデータソースをprestoのクエリ記法で引けるようになっていてとても便利です。 クラシルの分析基盤の歴史に興味がある方は、この記事にまとまっているので参照してみてください。クラシルの分析基盤はデータサイエンスチームが主体となって今も改善を続けています! tech.dely.jp 今回は、AWS SDK for Rubyを使ってAthenaのクエリを実行する記事が1本もなかったので書きました。また、本記事では並列処理ではなく、シンプルな1クエリを引くsync処理の場合について紹介します。 AWS Athena SDKの特徴 AthenaのSDKは少し変わっていて、実行待ちのポーリング処理を自前で実装する必要があります。処理の流れとしては、 クエリを実行するAPIを叩く クエリ実行のステータス問い合わせAPIを結果が SUCCEED になるまで叩く クエリの結果を返すAPIを叩く このように、2の処理の間ポーリング処理を実装する必要があります。 では、工程ごとに実装例を紹介していきます。 1. クエリを実行するAPIを叩く まずは、クエリ実行をリクエストする #start_query_execution APIを叩きます。 実装サンプルはこちらです。 client = Aws :: Athena :: Client .new({}) query_string = %Q{ SELECT * FROM "databasename"."tablename" limit 10; } client.start_query_execution( { query_string : query_string, output_location : ' s3:// ' + S3BUCKET_NAME } ) => #<struct Aws::Athena::Types::StartQueryExecutionOutput query_execution_id= " c0d4460b-xxxx-xxxx-9924-ede5c2d2b56b " > Clientのinitializeの引数は、AWSのaccess_key_id/secret_access_keyやregionを主に指定します。なにも指定しないと、他のAWS SDKの仕様と同様に Aws.config[:credentials] The :access_key_id, :secret_access_key, and :session_token options. ENV['AWS_ACCESS_KEY_ID'], ENV['AWS_SECRET_ACCESS_KEY'] ~/.aws/credentials ... の値が順番に参照されていきます。regionも同様です。環境に合わせて設定してください。 また、 #start_query_execution APIを叩く際は、クエリとresult_configurationを指定する必要があります。 result_configurationでは、クエリの実行結果の保存先としてs3のpathを指定します。APIでのクエリ実行も、AWSコンソールでのクエリ実行時と同じようにs3に実行結果を保持する仕組みになっています。指定したs3のpathに実行結果が蓄積されていきます。 また、APIのレスポンスとして、query_execution_idを取得できます。こちらのidは、実行結果の問い合わせや実行ステータスの問い合わせ、実行停止処理などに必要になります。 このAPIを叩いた時点では、処理を開始するリクエストを送っただけです。返り値としてクエリの実行結果が返ってくるわけではなくquery_execution_idという実行固有のidのみが返却されます。 2. クエリ実行のステータス問い合わせAPIを結果がSUCCEEDになるまで叩く 1で実行を開始したあと、クエリが走り終わるまで、結果を取得することはできません。しかし、1の工程ではクエリの実行完了を待たずにレスポンスがかえってきます。 そこで、現在の実行のステータスを知るための #get_query_execution というAPIが存在しています。そのAPIの返り値が SUCCEED になるまでステータスを問い合わせ続けなければなりません。つまり、ポーリング処理が必要となります。 今回はwhileを使ってポーリング処理をする代わりに、こちらのgemのwith_retriesを使用してポーリング処理を実装しました。 GitHub - ooyala/retries: A tiny Rubygem for retrying code with randomized, exponential backoff. こちらのgemは内部でexponential backoffを採用しています。exponential backoffとは、指数関数的に処理のリトライ間隔を後退させるアルゴリズムのことで、処理に時間がかかるほど再処理をする間隔が広くなっていきます。つまり、早く終わる処理には無駄な待ちがなく、時間がかかる処理には無駄なAPIのコールやCPUの負荷をかけずに済むようなアルゴリズムになっています。 実装サンプルはこちらです。 begin status = '' with_retries({ max_tries : 100 , base_sleep_seconds : 0.01 , max_sleep_seconds : 30 , rescue : [ Executing ] }) do | retry_count | state_result = client.get_query_execution({ query_execution_id : query_execution_id }) status = state_result.query_execution.status.state puts " [Athena Poling] fetching_count: #{ retry_count }" raise Executing if [ ' QUEUED ' , ' RUNNING ' ].include?(status) case status when ' FAILED ' , ' CANCELED ' # 処理が失敗した理由の取得 reason = state_result.query_execution.status.state_change_reason raise ExecutionError , reason end end status ensure # 成功時以外はクエリの停止リクエストを送信 unless status.present? && status == ' SUCCEEDED ' puts " クエリ停止処理 " client.stop_query_execution(query_execution_id) end end class Executing < Exception end class ExecutionError < Exception end with_retriesの引数である、max_triesやmax_sleep_secondsなどは、用途に合わせて調節してください。 ポーリング処理の精度検証 試しに実行時間が100秒弱のクエリのポーリング処理を下記の条件で実行した場合のCPU使用時間やAPIコール数を比較してみます。 シンプルなwhileでのポーリング処理(sleepなし) sleep(1)をはさんだwhileでのポーリング処理 with_retriesでのポーリング処理 CPU使用時間の計測には、Ruby標準ライブラリのbenchmarkを使用しました。 シンプルなwhileでのポーリング処理 APIコール数: 3470回 CPU使用時間: # benchmark結果 user system total   real 12.160000 1.320000 13.480000 ( 96.931696) sleep(1)をはさんだwhileでのポーリング処理 APIコール数: 94回 CPU使用時間: # benchmark結果 user system total real 0.350000 0.050000 0.400000 ( 97.002586) with_retriesでのポーリング処理 APIコール数: 15回 CPU使用時間: # benchmark結果 user system total real 0.090000 0.010000 0.100000 (110.306136) with_retriesの処理の方はwhile処理と比べぴったりに処理が終わらないため、少しreal timeが長くなっています。しかし、長い処理においてもCPUをほとんどつかっておらず、APIコール数も少ないのがわかります。 ほとんどCPUを使わないので平行で重い処理などが走っても心配ありません。(これくらいの差であればsleep(1)でも十分だと思いますが。) 停止処理について また、2番の処理の実装においてもう一つ大事なことは、途中で強制的に処理を終了した際などにしっかりクエリの実行を中断することです。想定外の長い処理が走ってしまったとき、コードを中断しても #stop_query_execution を叩かない限り裏側ではクエリの実行が走り続けてしまいます。なので、コードを強制終了させた場合などにも後処理として実行されるensure節でクエリ停止処理を書くことをおすすめします。 3. クエリの結果を返すAPIを叩く クエリの実行が完了したら、後は結果を引く工程のみです。 結果の量が多く、ページングが必要な処理に関してはnext_tokenを次のリクエストでなげる形で実装します。特に変わったことはしないのですが、一つあげるとしたら1ページ目の1行目にカラム行が返ってくるので、結果を返すときにそれを除外しています。 (カラム行をskipしてくれるoptionを探したのですが見つかりませんでした。。探せばあるかもしれません。) 本実装では、1ページにページに返ってくる上限を100件、すべての結果の上限を10000件に絞っています。 MAX_PAGE_RESULTS = 100 MAX_RESULTS = 10000 # next_tokenを受け取って次のページをリクエストする再帰処理 def get_all_results (query_execution_id, next_token = nil , results = []) rows, next_token = get_results(query_execution_id, next_token) results += rows results = results.flatten if results.count > MAX_RESULTS raise ExecutionError , ' 結果の上限数を超えています。 ' end if next_token.present? results = get_all_results(query_execution_id, next_token, results) end results end # 1ページ分の結果を取得する処理 def get_results (query_execution_id, next_token = nil ) results = client.get_query_results({ query_execution_id : query_execution_id, next_token : next_token, max_results : MAX_PAGE_RESULTS }) next_token = results.next_token # クエリの実行結果の取得 rows = results.result_set.rows # カラム一覧を取得 column = results.result_set.result_set_metadata.column_info.map(& :label ) result_rows = rows.map do | result | row = result.data.map(& :var_char_value ) # 初回はカラム行が返ってくるので除外 next if row == column column.zip(row).to_h end .compact [result_rows, next_token] end get_all_results(query_execution_id) まとめ 今回は、AWS AthenaをAWS SDK for Rubyで引く実装について紹介しました。 ポーリング処理を自前で書くような仕様が珍しいですよね。ポーリングを書く処理は、with_retriesを使ってみてはいかがでしょうか。また、停止処理はしっかりと行いましょう!
アバター
こんにちは。Androidエンジニアのうめもりです。 もう終わってからだいぶ経ってしまいましたが、今年のDroidKaigiもとても面白かったですね。自分は去年から参加し始めたのですが、去年と比べても 色々な部分で改善が見られ、運営の方々には頭が下がる思いです。来年以降も続いていくといいですね。 さて、自分は今年のDroidKaigiではこんな発表をしてきました。 ちなみに去年の発表はこんな感じなので、大分毛色が違う感じでしたね。(資料のアス比が…) 正直内容としてはそこまでレベルが高い感じもしなかったので、資料を作りながら(大丈夫かな…いやでもこれCfP通りだしな…いやでもな…)みたいな葛藤がありつつも、当日はそこそこ楽しんでいただけたようで良かったです。日本語から英語への同時通訳をしていただく中での発表は初めてだったのですが、終わった後にゆっくり喋ってもらったのが良かったとコメントをいただいて安心しました。(余談ですが、DroidKaigiの同時通訳の方はAndroid Specificな内容も理解しつつ的確に翻訳してくださる素晴らしい方々だったみたいです。どれだけ事前に勉強されたのでしょうか…。事前の打ち合わせでもお話をさせていただきましたが、安心して発表に臨むことが出来ました。) 発表内容の中でGoogle PlayへのAPK、Proguardのマッピングファイルの自動アップロードを行っているという話をチラッと出したのですが、登壇後のオフィスアワーでも、Twitter上でもどうやっているのかという質問をいただいたりしたので、こちらのブログで補足しておこうと思います。ちなみにAWS Lambdaからゴリゴリやっているので、ナウい感じのCIサービス(Bitriseとか)の話は出てこないことをご了承ください。 APKのアップロードをどのように自動化したか? タイトルでネタバレしてしまっていますが、delyのAndroidチームでは、Google Play Developer Publishing APIを使って APKのアップロード Proguardのマッピングファイルのアップロード リリースのドラフト作成(アルファ公開チャンネルに作成しています) のタスクを自動化しています。クラシルはRealmを使っていた関係でSplit APKを行っているので、APKのアップロードやProguardのマッピングファイルのアップロードは地味に面倒なタスクになっています。ファイルをアップロードするだけといえばアップロードするだけなのですが、リリース時の心理的な作業負担感が大分低減しているような実感があります。 Google Play Developer Publishing APIの叩き方 2019年2月21日現在でのGoogle Play Console上での説明なので、もしかしたらこの部分の説明は間違っている可能性があります。 まずはGoogle Play Developer Publishing APIを使うためのサービスアカウントを発行しましょう。今回の用途では各ユーザーの権限でAPIを呼ぶことは無いと思うので、サービスアカウントを使うのが簡単だと思います。 Google Play Developer Consoleの「設定」から、「APIアクセス」を開きます。(なお、この操作は適切な権限のあるアカウントでないと出来ませんのでご注意ください。) まだ1回もAPIを使ったことが無いのであれば、「新しいプロジェクトを作成」からプロジェクトを作成しましょう。 作成すると、画面の下にサービスアカウントというセクションが現れるはずなので、そこから「サービスアカウントを作成」をクリックします。 基本的にはそこに書いてある通りに操作を行えばいいのですが、Google API Consoleに移動して、「サービスアカウントを作成」から要求された項目を入力し、サービスアカウントを作成します。重要なのは、キーの作成を行って(JSONがいいと思います)、それを控えておくことです。 次に、Google Play Consoleの「ユーザーと権限」から、先ほど作成したサービスアカウントのメールアドレスを指定して、権限を与えます。「リリースマネージャー」にしておくのが手っ取り早くていいと思います。 以上で、先ほど作成したキーを使ってGoogle Play Developer Publishing APIを使うことが出来るようになりました。 Google Play Developer Publishing APIを呼び出す 先程作成したキーを使ってAPIを叩くのはGoogle APIs SDKを使うのが一番簡単です。弊社ではAWS Lambda上でGoを使ってAPIを叩いていますが、以降の説明はGoogleのAPIドキュメントを使って行います。(どのプラットフォーム用のSDKでも基本的には同じようにマッピングされているはずなので、適宜読み替えて使ってください。) さて、APKやProguardのマッピングファイルをアップロードするのにはまずEditsを作成する必要があります。Google Play Developer Publishing APIはAPIをまたいだトランザクションに対応しており、Editsはトランザクションの単位と考えれば間違えが無いと思います。 developers.google.com なお、こちらがGoogle Play Developer Publishing APIのドキュメントなので、こちらを見て分かる方はそちらを読んでいただいた方がいいと思います。 Editsを作成する まずは、 https://developers.google.com/android-publisher/api-ref/edits/insert POST https://www.googleapis.com/androidpublisher/v3/applications/{packageName}/edits こちらのAPIをコールしてEditsを作成します。{packageName}にはアプリケーションIDを入れます。 レスポンスはこのような構造になっています。 https://developers.google.com/android-publisher/api-ref/edits 重要なのはidで、こちらのidを使って以降のAPIをコールしてファイルのアップロードを行います。 APKをアップロードする 次に、APKのアップロードのやり方です。 https://developers.google.com/android-publisher/api-ref/edits/apks/upload POST https://www.googleapis.com/upload/androidpublisher/v3/applications/{packageName}/edits/{editId}/apks?uploadType={uploadType} こちらのAPIをコールしてAPKをアップロードします。先程作成したEditのidを{editId}に挿入します。{uploadType}は media か resumable を設定できますが、 今回は media を指定した場合の説明だけをしておきます。 Content-Typeは application/octet-stream か application/vnd.android.package-archive を設定し、ファイルのバイナリ列をリクエストボディとして送信しましょう。 なお、弊社ではAPKごとにゴルーチンを立ち上げて並列アップロードしています。 Proguardのマッピングファイルをアップロードする 次に、Proguardのマッピングファイルをアップロードします。 https://developers.google.com/android-publisher/api-ref/edits/deobfuscationfiles/upload POST https://www.googleapis.com/upload/androidpublisher/v3/applications/{packageName}/edits/{editId}/apks/{apkVersionCode}/deobfuscationFiles/{deobfuscationFileType}?uploadType={uploadType} 基本的にはAPKと同じ要領ですが、どのAPKのマッピングファイルなのかを指定する必要があります。{apkVersionCode}には、対応するAPKのVersionCodeを挿入します。{deobfuscationFileType}には現在は proguard しか指定できませんので、それを指定しましょう。 Content-Typeには application/octet-stream を指定し、マッピングファイルのテキストをリクエストボディに入れて送信しましょう。 余談ですが、難読化を意味するobfuscationはネイティブの方でも馴染みのない単語みたいですね。事前打ち合わせでその話題が出てきて、確かに日本語でも難読化なんて言葉そんなに使わないわ…と思った記憶があります。 TracksをUpdateする 最後に、どのトラックにアップロードするかを指定します。 https://developers.google.com/android-publisher/api-ref/edits/tracks/update PUT https://www.googleapis.com/androidpublisher/v3/applications/packageName/edits/editId/tracks/{track} {track}には alpha , beta , production , rollout , internal を指定しましょう。弊社では毎回 alpha でアップロードしています。( internal の方がGoogle Playへの反映が早いですし、そちらでアップロードすることを検討してもいいかもしれませんね…。) Content-Typeは application/json を指定しましょう。 リクエストボディのフォーマットは https://developers.google.com/android-publisher/api-ref/edits/tracks#resource こちらに書いてあります。必須なのは track と releases[].status 、 releases[].versionCodes です。versionCodesにはアップロードしたAPKのVersionCodeを指定しましょう。statusは completed , draft , halted , inProgress ですが、弊社では draft でアップロードし、リリースノート等は後で入力するという運用にしています。 Editsをcommitする https://developers.google.com/android-publisher/api-ref/edits/commit POST https://www.googleapis.com/androidpublisher/v3/applications/{packageName}/edits/{editId}:commit 上記APIをeditIdを指定して呼び出せば、今まで行った全ての操作がGoogle Playに反映されます。リクエストボディは指定する必要はありません。 注意点 一つだけ注意点ですが、Editsを編集している際にはGoogle Play上でリリースを操作するのはやめましょう。トランザクション外で操作が発生していると、commitする段階でAPIリクエストが失敗します。 まとめ 以上がGoogle PlayへのAPKのアップロード方法です。少しAPIに癖はありますが、Editsの操作さえ分かってしまえば他のAPIも同じように呼ぶことが出来るはずです。皆さんも良いGoogle Playライフを。 この記事の内容への質問等があればこちらまで気軽にどうぞ。 https://twitter.com/kr9ly 来年のDroidKaigiの話 ちなみに、来年のDroidKaigiでもCfPを出そうと思っているのですが、Kotlin Coroutinesで状態遷移を可視化して管理するみたいな話をしたいと思っています。Kotlin Coroutinesの無限のパワーをどう生かすかという話は来年はいっぱいCfPが出てきそうですが、今のうちにしっかり準備してめちゃくちゃ面白い発表にしたいと思っています。(通るといいな…)
アバター
はじめに こんにちは。 機械学習エンジニアの辻です。 2/6(水)AWS Loft Tokyoでイベント開催します!ご興味のある方はぜひご応募ください! bethesun.connpass.com さて本日は「クラシル・パーソナライゼーションの歩み」ということで、クラシルをよくご利用頂いているユーザに対してよりいっそう良いコンテンツを提供していくために、 パーソナライゼーション の取り組みに力をいれています。そこで、これまでに取り組んできたさまざまな施策に関して考えてきたことやフィードバックから学んだこと、そして、今後どのように進めて行こうとしているのかということについて、過去から未来への歩みとして少しご紹介したいと思っています。 目次 はじめに 目次 パーソナライゼーション以前の課題感 エコシステム化 クラシルというブランドを理解する データ分析に関する社内への取り組み レシピを考え、作るというプロセス レシピ動画評価 献立の最適化問題 まとめ さいごに パーソナライゼーション以前の課題感 昨年6月までクラシルで配信しているレシピ動画として主におすすめしていたものは、いわゆるルールベースの「 Most Popular推薦 」という選出方法によるものだけでした。このMost Popular推薦とは、非常にシンプルなスコアリングモデルで、たとえばクリック数やお気に入り数の集計結果をベースに、いくつかの独自ルールを盛り込んでスコア化し、そのスコアの高いものから順に選出していくというもので、推薦されるレシピは全ユーザーに対して同じものとなります。この方法をざっくりいえば、「たくさんの人が好きなレシピは、たくさんの人が好きなはずでしょ?」ということなので、ある意味で理にかなっているといえます。しかしこれだけですと、クラシルをよく利用して頂いているヘビーユーザにとっては、 代わり映えしない提案 だったり、 お気に入り済みなのに何度も勧めてきてクドいなぁ と感じられることも多々あるかと思います。あるいはまた、何か苦手な食材があるユーザに対してまったく故意ではないにせよ、その苦手食材を毎回おすすめしてしまっていては、続けて使って行こうなんてきっと思って頂けないと思います。 それから、このMost Popular推薦のルール変更についても当時は定性的な判断によるもので、例えばこのルールを追加したらCTRが0.3%向上した、あるいはこの施策によってCTRが0.5%下がったなどといったように、ユーザ行動における詳細な相関分析や因子分析を行わないまま、 微細な数値の増減だけに翻弄される日々 を過ごしていました。今にして思えば、これこそまさに ノーフリーランチ定理 だったのです。すべてのユーザ標本にとって最大極値を探索するような汎用アルゴリズムは、全ての可能なコスト関数に適用した結果を平均するのと同じ性能になってしまっていたわけです。 ノーフリーランチ定理 この状況を打開すべく、まずは特殊用途に最適化するために全ユーザ標本に対してクラスタリングを行い、最適化すべき定義域を局所化することから始めました。これにより、分類した各クラスターの基礎統計を観察してみたところ、それぞれに特色のようなものが現れ始めたので、この特色を定量化すべく主成分分析や因子分析を行い寄与率の高い特徴量を探索して絞り込んでいくことができました。 そして、このクラスター毎のMost Popular推薦のルールを作成しそれぞれに適用することで、全ユーザ標本に対してのMost Popular推薦と比較しても格段に高い結果を得ることができました。さらにまた、強調フィルタリングを用いてユーザと動画のスコアリングを行うことでレコメンドエンジンを作成し、パーソナライズド・レコメンドを部分的に適用することができました。(一部のクラスタではCTRが下がるという結果が得られたのですが、そのクラスタには効果がないということが判断できたので、それもまた発見でした。) 中でも、顕著な特性として出てきたのが新奇性に対する反応の違いでした。新奇性とは、目新しさや物珍しさに対する反応のことで、ヘビーユーザの中には新しいレシピを待っていて、出るとすぐにお気に入りするという使い方をされている方がいらして、その方々のパーソナライズド・レコメンドに対する反応が顕著に見られました。しかしその方々の反応は長期間継続せず、それはこのクラスタの方々にとってのレシピの鮮度というのが、配信後からお気に入りするまでの比較的短い期間であるためであることがわかりました。そのため一度お気に入りしてしまうと、おすすめレシピへの興味は急激に減少しCTRが激減するという傾向があったのです。 (以下のグラフでは、緑が各クラスタ、青が全体の平均CTRを現しています) 新奇性が高いクラスターのレコメンドに対する反応遷移 その一方、調理を重視してクラシルを利用されている方々にとっては新奇性の影響はあまりなく、その反面、パーソナライズド・レコメンドに対する反応もそこまで高くはないという面が見られました。 調理を重視しているクラスターのレコメンドに対する反応遷移 このような、クラスタの特色を踏まえて理想のレシピ提案を行っています。こちらについては、今後もさらなる精度向上を目指しています。 エコシステム化 さて、ここまで課題感としてあったレシピ動画のおすすめ提案についての事例をご紹介しましたが、実はレコメンデーション自体は目的ではなく、パーソナライゼーション全体においてはほんの一部に過ぎないと考えています。それというのも、パーソナライゼーションの取り組みを進めていくことで、このおすすめ提案以外にも、様々な機能により利用して頂けば頂くほどユーザからのフィードバックを得られ、より良いサービス提供が可能になると信じているからです。ですので、ここで結果を焦り過ぎてはいけません。まずはこの フィードバックが全体に循環するエコシステム 作りこそが優先で、これなくして一時の場当たり的な改善に一喜一憂しては何も得られません。 リーン開発サイクルとフィードバックエコシステム クラシルというブランドを理解する エコシステム化を進めるにあたって、その根幹にはクラシルというアプリの存在意義があります。この点でクラシルには「ブランドガイド」という指針があり、このコンセプトに反するようなイメージを受け入れることはできません。では、どうでしょうか?機械学習やAIという言葉から受ける印象と、クラシルから受ける印象とは親和性があるでしょうか?これについて定性的な判断は不可能ですが多くの人があまり親和性が高いとはいえないとお考えになるのではないでしょうか?それであれば積極的に全面に出るよりも「あたたかくて、おいしい」にそっと寄り添うようなアプローチを目指すほうが良いと判断しています。(あくまで現時点の個人的な所感に過ぎませんが。) データ分析に関する社内への取り組み 先程、ノーフリーランチ定理に触れましたが、やはり、なかなかそれを理解してもらえないという状況もあります。過去の経験やドメイン知識に基づく判断によってルールを場当たり的に変更していけば、いつかそのうちCTRが向上すると頑なに信じている人も中にはいます。仮に過去データに基づき統計的手法で算出した数値を根拠にいくら定量的な検定結果を共有したところで、 難しい とか 経験によってうまく行ったというハロー効果 はなかなか覆し難いのも事実です。このような状況では、分析者側からいたずらに対立関係を作るのではなく、根気強く 納得してもらえるまで取り組みに協力 して、現実を検証し続けることが大切だと思っています。それでもし運良くすばらしい結果が出れば、より良いMost Popular推薦のルールが発見されたのですからそれはそれで良いことなのです。 こちらの「ファスト&スロー あなたの意思はどのように決まるか?」を読むと統計学の研究者でさえ誤りを犯すことがあるほど、意思決定がいかに曖昧なものか理解できます。 ファスト&スロー あなたの意思はどのように決まるか? 文庫 (上)(下)セット 作者: メディア: セット買い また、社内では以下のような取り組みによって、定量的なデータ分析に基づく共通認識を深めています。 サンプルサイズの算出方法を社内共有+推定値の自動算出 分析基盤の構築+運用+改善 ユーザ行動ログおよびレシピデータに基づいたEDA分析 ダッシュボードによるビジュアライズ 統計学、多変量解析の実践方法を社内にレクチャー SQL勉強会の開催 レシピを考え、作るというプロセス 弊社では クラシルシェフ と呼ばれる料理人の方たちによって日々新しいレシピが考案されています。この レシピを考えるという作業 はそれ自体が非常に複雑な最適化問題であるといえます。旬の食材や価格、あるいは余り物があれば優先して使いたいし、家族に子供がいる場合と高齢者がいる場合など家族構成によって様々な配慮が必要です。それに加えクラシルシェフの場合は、世間のトレンドや検索キーワードなど様々な外的要因も考慮しなければならず、また過去に作った多くのレシピともかぶらないものにしなければならないので、レシピの考案まで極めて多くの制約があります。その複雑な作業を少しでもお手伝いできないかと考えて、これらの機械学習を用いたプロセスの改善に取り組んでいます。 レシピ考案をお手伝い:いくつかの説明変数からレシピをヒントとして推論する レシピ手順の評価:レシピの手順がネガティブ・ポジティブかを判定して手順を記述する際の判断材料にしてもらう レシピの素性抽出を自動化:レシピに関する様々な素性をルールベースで導出、あるいは推論により抽出し更新・保存する レシピ動画評価 良いコンテンツは再現したいものの、このコンテンツの良し悪しというのは外的要因に左右されることも多く、また様々なコンテキストによって目的が異なります。再生数が多いほどよいのか?より美味しそうな方が良いのか?あるいは簡単なほどよいのか?など一概に判断が難しいところです。しかしこの「良い動画」をとあるコンテキストにおいて局所的な定量評価することで、より良いコンテンツ作りのサポートができると考えて様々な角度から取り組んでいます。 献立の最適化問題 クラシルでは、去年の11月に 献立機能 をリリースしました。レシピでさえ考えるのが複雑であるにもかかわらず、主菜+副菜+汁物という 献立 を考えるというのは本当に困難な家事と言えます。ですので、この献立についても、主菜に合う副菜、汁物が最適な組み合わせとなるように現在取り組んでいます。 献立画面 初回のトレーニングデータのラベル付けについては専門家であるクラシルシェフの皆さんと調理栄養士の方を中心に人海戦術で行いました。現在ではそのトレーニングデータをもとに作成したモデルから最適な組み合わせを推論しています。組み合わせアルゴリズムはナップサック問題のアルゴリズムをベースにした独自実装となっています。 ナップサック問題 今後はさらに、旬食材や冷蔵庫の余り物、あるいはアレルギー体質などにも考慮し、さらにご利用頂くユーザに寄り添う献立を様々な形で提案していきたいと考えています。 まとめ いかがでしたでしょうか? クラシルにおけるパーソナライゼーションの歩みについてご紹介させていただきました。機械学習やAIというとなんとなく機械的に提案されたレシピを食べるのは嫌だなぁと抵抗のある方もいらっしゃるかもしれませんが、最終的にご提案するのは、クラシルシェフの作った「あたたかくて、おいしい」レシピであって、機械学習やAIはそれにちょっとだけプラスアルファすることで、ご利用頂くユーザのライフスタイルにもっと最適なご提案ができるようなサポート的な存在として寄り添っていきたいと思っております。 さいごに 繰り返しになりますが、 2/6(水)にこのような機械学習のイベントを開催します。今回紹介しました内容以上に実践的なお話ができるかと思いますので、ご興味のある方はぜひお申込みください! ご来場頂いた方には、弊社の取り組みの中で試行錯誤した 「SageMakerの便利スニペット集」 をプレゼント致します!こちらのスニペットに対する質問も随時受け付けますので奮ってお申込みください! bethesun.connpass.com
アバター
こんにちは。delyのTech Leadのうめもりです。 これはdely Advent Calendarの25日目の記事です。ほかの記事についてはこちら qiita.com adventar.org をご覧ください。 昨日はプロダクトマネージャー兼開発部ジェネラルマネージャーをしている奥原 (@okutaku0507)が tech.dely.jp という記事を書いてくれました。ご興味あればそちらも是非ご覧ください。 25日目の記事は、みんな大好き技術的負債の話をしたいと思います。 はじめに 「技術的負債」 我々プログラマーからすればうまく付き合っていく必要のある厄介な存在であり、「何故技術的負債を解消していかないといけないのか?」というトピックは定期的にプログラマー界隈でも話題になりやすいものです。 時にはビジネスサイドに技術的負債の存在やその厄介さについて説明する必要が生じることもあり、その説明の難儀さに苦労した方も多いのではないかと思います。 今日はプログラミング、プロダクト開発の不確実性というテーマから、技術的負債についての説明をしてみたいと思います。 そのためにまずは、我々は何故プロダクト開発をするのか?というところに立ち返ってみましょう。 我々は何故プロダクト開発をするのか? BtoC、CtoC、様々なビジネス領域においてプロダクト開発という業務は存在していますが、仮にあなたがどんなプロダクトを作っていたとしても 「我々は何故プロダクト開発をするのか?」という問いに対する答えは基本的には同じはずです。 我々は、未来のマーケット、未来のユーザーに価値を提供するためにプロダクト開発をしている。 これが我々が何故プロダクト開発をするのか?ということに対する答えになると思います。 注意しなければならないのは、現在のマーケット、現在のユーザーに対してのものではないということです。何故ならばそのプロダクトが出来上がるまでには多かれ少なかれ時間がかかるはずであり、その頃にはマーケット、ユーザーは多少なりとも変化しているはずですから。 未来を確実に予測することはできないという前提 未来のマーケット、未来のユーザーに価値を提供するということを考える際に、最も重要な原則があります。それは 未来を確実に予測することはできない ということです。 どんなに注意深くマーケット、ユーザーの情報を集めたとしても、100%未来がこうなると予測することは現在の技術ではできません。つまり、未来のマーケット、未来のユーザーに対して開発する予定のプロダクトが、本当に価値を提供できるかどうかを確実に予測するすべはないということです。プロダクト開発とは本質的に不確実性を持っているものであり、我々はプロダクト開発がもたらす不確実性とどう付き合っていくか、ということを考える必要があります。 もしそのプロダクトを使うユーザーはあなたがよく知っている人間であるとしても、その不確実性を排除することはできません。プロダクトを欲しがっているユーザーと、プロダクトを前にしたユーザーはもはや他人であると考えるべきです。(多かれ少なかれ皆さんも経験があることではないでしょうか?) そして、一部の例外を除くほとんどのプロダクトについては、今プロダクトを使っていない未知のユーザーに届ける必要性があるものだといえます。勝手知ったるユーザーにプロダクトを提供することに不確実性があるなら、今あなたが全く知らないユーザーに対してプロダクトを提供することについては言うまでもないでしょう。 不確実性を味方にするたった一つの方法 では、我々はプロダクト開発がもたらす不確実性とどう付き合っていけばいいのでしょうか? 未来を予測する最も確実な方法は、それを発明することだ - アラン・ケイ 我々は未来を確実に予測できなくても、過去のプロダクトがマーケット、ユーザーにとって価値があったかどうかを検証することはできます。 あらゆるプロダクト開発は、マーケット、ユーザーにとってそのプロダクトが価値があるかどうかを検証するために行われるものだと言っても過言ではありません。 プロダクト開発が成功した場合においても、失敗した場合においても、我々はそのプロダクトのもたらす価値という情報を得ることになります。 プロダクト開発が成功した場合に得られる情報は、それはそのプロダクトが価値がある、という情報です。 プロダクト開発が失敗した場合に得られる情報は、それはそのプロダクトが価値が無い、という情報です。 大抵の場合、そのプロダクトに価値があるかどうかは100%か0%ではなく、ある部分は価値があり、ある部分は価値がないという情報が得られるでしょう。 プロダクト開発がもたらす不確実性を味方にするたった一つの方法、それはプロダクト開発が失敗した場合に被る損失を最小にし、プロダクト開発が成功した場合に得られる利益を最大化するという方法です。 プロダクト開発の失敗とどう向き合うのか? プロダクト開発の成功から得る利益を最大にする方法、それはプロダクトの価値のある部分を破棄せずに使い続けることです。 プロダクト開発の失敗から被る損失を最小にする方法、それはさっさとプロダクトの価値のない部分を破棄して、作り直すことです。 ここで一つ気を付けなければならないのは、コードの改修は大抵の場合はこの破棄して作り直す、ということを意味するということです。破棄する範囲が広いか狭いか、という違いだけがそこには存在します。 価値のある部分を残し、価値のない部分を捨てるためには、それらを区別することができることが必須条件です。 もし価値のある部分と価値のない部分の区別がつかなければ、価値のない部分だけを捨てるという判断が出来ません。そうやってプロダクト自体の価値のない部分が時間とともに増えていけば、いずれプロダクト全体を捨てなければならないという破綻を招くことになります。 疎結合であり、単純明快で意味のある構造を実現し続けること プロダクトの価値のある部分を残し、価値のない部分を捨てられるようにするための基本的なアイディアとして、コードを疎結合な状態に保つということがあります。疎結合とは単純にコードが分割されている、というだけではなく、コードの意味として分割されていることで、分割されたお互いのモジュールの実装の詳細を知らなくてもそれぞれの機能が提供できる、という状態のことです。 例えばプロダクトがAという機能とBという機能を提供していたとして、Aという機能を提供している部分、Bという機能を提供している部分がコード上での構造としても明確であり、Bという機能だけを検証の結果として破棄することになったとしてもすんなり破棄できる状態であるのが疎結合になっているという状態です。これが、Aという機能がBという機能の実装を深く理解していないと提供できない、あるいはそもそも不可分である、という状態になっていると、そう簡単にBという機能だけを破棄することが出来ないということになります。Bという機能を外から不可視にすることは簡単にできるかもしれませんが、システムとしては本来必要のないBという機能の実装を理解した上で、今後の機能実装を進める羽目になるでしょう。そういった破棄できない部分が増えていけば、前述したようにいずれコードを丸ごと破棄するしかなくなるという結末が待っているでしょう。 コードを疎結合に保つ、ということはそうでない場合に比べて実装コストがかかりやすいものですし、そもそも困難なことです。大抵の場合は密結合に作る方が簡単ですし、実装コストも安く済むでしょう。ただし、それは実装した部分全てが有用であり、変更する必要が無いということが前提になっているか、そもそも一回きりの実装でコードを丸ごと破棄することに問題がない場合だけでしょう。(例えばプロトタイプ開発とか) 我々はプロダクトを実装した結果から学習し、不要な部分を破棄した上で先に進み続ける必要があります。古くからそのための様々な考え方がありますが、今回はその文脈でよく出てくるDRY原則と、SOLID原則についてもう一度振り返ってみましょう。 DRY原則をコードを破棄するという観点から振り返る DRY(Don’t Repeat Yourself)原則とは、情報の重複を避けるという考え方です。特定の機能の実装がバラバラにコードの中に入っていると、その機能を破棄する際にコードの実装を細かく調べる必要があり、破棄することが困難になります。例えばそれらが同一の関数あるいは同一のメソッドあるいは同一のクラスで表現されていれば、破棄は容易になるはずです。一つにまとめたうえで、分かりやすい名前がついていればより簡単にコードを捨てられるようになるでしょう。 ここで気を付けなければならないのは、コードの重複を排除して一つにまとめることが、コードを破棄しにくくすることにつながってしまうことも往々にしてあるということです。一見それは重複であっても、機能上の意味として異なる場合にはそれらの機能をまとめてはいけません。 DRY原則は有用な考え方ですが、プロダクトから学習してプロダクトを改善し続ける際には、注意深く適用しなければならない考え方です。 SOLID原則をコードを破棄するという観点から振り返る SOLID原則とは、オブジェクト指向言語(今日の実用的なプログラミング言語は多かれ少なかれオブジェクト指向的な要素を持っていますね)における、5つのプログラミング上の原則をまとめて頭文字をとったものです。 Single Responsibility Principle(単一責務の原則) Open/closed principle(開放/閉鎖の原則) Liskov substitution principle(リスコフの置換原則) Interface segregation principle(インターフェース分離の原則) Dependency inversion principle(依存性逆転の原則) 今回はそのすべてを振り返ることはしませんが、コードを破棄するという観点から考えるとどれも有用な考え方です。そのうちの二つ、単一責務の原則と開放/閉鎖の原則について振り返ってみます。 単一責務の原則 Single Responsibility Principle(単一責務の原則)とは、「クラスはただ一つの理由で変更すべきであり、一つの機能だけを持っているようにするべきである」という原則のことです。コードを破棄するということから考えると、とても自明なことであると言えます。機能がクラスごとに明確に分割されており一対一でマッピングされる状態であるなら、それらを破棄することはとても容易なはずです。 開放/閉鎖の原則 Open/closed principle(開放/閉鎖の原則)とは、「クラスは拡張に対して開いていなければならず、修正に対して閉じていなければならない」という原則のことです。機能を追加する際にクラス自体を修正する必要が無いようになっているということも重要ですし、その他クラス外の何か変更によって、クラスの挙動が破壊されないということが保証されているならば、安心してコードの破棄を行うことができるようになるはずです。 まとめ:「技術的負債」とは何だったのか? さて、プロダクトから得る価値を最大化するためにコードを破棄するという観点からDRY原則、SOLID原則の一部を振り返ってみました。 「技術的負債」という言葉をここまで使いませんでしたが、この言葉をプロダクトから得る価値を最大化するためにコードを破棄するという観点から整理し直すと、 マーケット、ユーザーにとって価値のないコードそのもの(いずれ破棄しなければならないという債務を抱えている) コードを破棄することを妨げるコード(負債への対応を先送りし、プロダクト全体の破綻を招くコード) プロダクトの不確実性に耐えられない、脆い部分 が技術的負債である、ということが言えるのではないでしょうか。 常にそれは時間とのトレードオフになりますが、我々はプロダクトから学習しプロダクトを成長させ続けるために、技術的負債にいかに対応していくかということを考えていく必要があります。 参考文献 反脆弱性[上]――不確実な世界を生き延びる唯一の考え方 https://www.amazon.co.jp/dp/B072PXY32Q/ エンジニアリング組織論への招待 ~不確実性に向き合う思考と組織のリファクタリング https://www.amazon.co.jp/dp/B079TLW41L/ 最後に dely Advent Calendarは今年初めて行った試みですが、今日で25日分全て公開できました。(本来のアドベントカレンダーは24日分であることが多いと思うのですが、何故ブログは25日分書くことになっているのかは割と謎ですよね) 今までdelyの開発部はあまり情報を外に出していなかったのですが、今回は技術的な情報を外に出していくといういいきっかけになったと思います。今後も定期的に色々な情報を発信していきますので、dely engineering blog, dely design blogを引き続きよろしくお願いいたします。 最後に、師走で業務も忙しい中しっかり記事を書いてくれた開発部の皆さんに感謝を。どうもありがとうございました。
アバター
こんにちは: ) sakura( @818uuu )です。 本記事はdely Advent Calendar 2018の15日目の記事です。 Qiita : dely Advent Calendar 2018 - Qiita Adventar : dely Advent Calendar 2018 - Adventar 前日は、検索エンジニアの仕事内容を紹介しつつ1年間取り組んだことをご紹介しました。 tech.dely.jp はじめに 本記事は、delyの開発部の中からみた開発部の雰囲気をお伝えしようと思います。 技術ブログですが、現状開発部の雰囲気を知る機会や文章はあまりないのでこういう記事が一つくらいあってもいいかなと思って書かせていただきました。 あくまで個人の主観による意見が大きいですがそこはご了承願います。 自分なりに精一杯言語化したのですが伝わりにくいことがあったらすみません。 これがdelyに入社しようと考える人のご参考になれば幸いです。 変化に柔軟に対応するのが上手い delyは会社が急成長していることもあり、どんどん環境が変化していっています。 人でも考え方でもそうですが、環境の変化にはそれ相応の対応が必要になってきます。 環境の変化に対応することは一般的にはとても難しいことだと思うのですが、delyの開発部の人は上手い人が多い気がします。 以下の記事に書いてあるプロダクト改善のプロセスは、柔軟に変化してきた一番わかりやすい好例だと思います。 とても為になると思うのでぜひご一読してみてください。 tech.dely.jp 他には、CTOの大竹さんの『 越境型スキルのすゝめ 』もまさに変化に柔軟に対応していくことが書かれた記事だと思います。 優しい人が多い 性格的に優しい人が多いです。 一見なんてこともない特徴かもしれないですが、実はとても重要な特徴だと思います。 第一に細かな気遣いをしてくれる方がとても多いです。 他には、 ・わからないことを聞いたら丁寧に説明してくれる(環境構築とか特にわからないことが多いですよね) ・物腰がやわらかい ・よく感謝する(ありがとスタンプがよくslackで押されます) ・誰かがやらなきゃいけない仕事を自ら引き受けてくれる ・耳の痛いことをあえて言ってくれる などとにかく細かなところで優しい人が多いです。 話を聞くのが上手い 話を聞くのが上手い人が多いです。 話しやすい雰囲気、というのもそうなんですが理解してくれるのがはやいし上手いです。 「一を聞いて十を知る」ということわざがありますが、まさにそんなかんじです。 私は自分の思い通りに言葉を伝えるのが少し苦手なので、開発部の人に相談させてもらうときにすごく助かっています。 他には - 落ち着いている - 和やか - 人の意見を尊重する - 難しいことをいいかんじに対応してくれる - 哲学好き などの特徴をもった人が多いと思います。 さいごに 少しでも開発部の雰囲気が伝わったでしょうか? よく採用広報で「こんな人を求めています!」や「〇〇はこんな雰囲気です」と書かれていることがありますが、 入ってみないと正直わからないことが多いですよね。 この記事でその溝を少しでも埋めることができたら何よりです。 最後に採用情報です。クラシルを一緒に作る仲間を募集しています。 もしも就職活動や転職活動で悩んでいるならdelyを選択肢の候補にいれてみるのはどうでしょうか。また、この記事に書かれている特徴に当てはまりそうな人もぜひ検討してみてください。 よろしくお願いします! www.wantedly.com 次回予告 明日は弊社のプロダクトデザイナーのミカサ トシキ(@acke_red)による「 Fluid Interfaces実践 - なめらかなUIデザインを実現する 」です。 ぜひご覧ください。
アバター
こんにちは! dely, Inc.でプロダクトマネージャー兼開発部ジェネラルマネージャーをしている奥原 ( @okutaku0507 ) といいます。この記事は dely Advent Calendar 2018 の24日目の投稿です。明日は待ちに待ったクリスマスですね。 先日は、弊社CTOの大竹 ( @EntreGulss ) から「越境型スキルのすゝめ」というタイトルで投稿がありました。自分の立ち位置を理解して、スキルにレバレッジをかけ付加価値つけながら、時代の急激な変化の中で日々戦っているたけさんの考えが書かれていて、とても面白い記事になっています。 さて、Advent Calendarも終盤に差し掛かった今回は「 クラシル で実践しているプロダクト改善プロセスのすべて」という題で、弊社が運営しているクラシルで実践しているリーンなプロダクト開発を一つ一つの事細かに紹介します。そのため、とても長い記事になっています。ですが、 この記事を読んでいただき、不確実性に立ち向かう組織が増え、より良いサービスが世の中に増えたらいいな と思います。 また、弊社が運営しているレシピ動画サービスであるクラシルのことはよく知っているけれども、delyという会社あるいはそのエンジニアやデザイナーのことはわからないという方に、これからご紹介するプロダクト改善プロセスを通して弊社の開発部のことを少しでも知っていただけたら幸いです。 僕らは生活のインフラになるようなサービスを再現性高く世の中に提供していきます。クラシルだけではもちろんありません。クラシル規模のサービスがどんどん出てくる組織を目指して、この不確実性に向き合う開発体制を確立しています。現在、募集しているプロダクト開発部の職種です。一緒に良いプロダクトを創っていける仲間を探しています! iOSエンジニア Androidエンジニア サーバーサイドエンジニア(Ruby on Rails) SRE MLテクノロジスト ストリームパイプラインエンジニア データサイエンティスト UI/UXデザイナー UXリサーチャー Webグロースディレクター   目次 目次 1. リーンなプロダクト開発が必要になった経緯 2. クラシル版リーンなプロダクト開発の概略 3. どの課題を解くかで全てが決まる バケツを大きくする施策 バケツの穴を塞ぐ施策  4. 課題に対する解決策を考える 5. プロトタイピング 6. アイデア検証 7. 学びを整理する 8. 意思決定者の決定を仰ぐ 9. 要件定義を詰め、実装仕様書を作成する 10. ゴリゴリ実装 11. デバックテストを行う 12. まとめ 13. 終わりに   1. リーンなプロダクト開発が必要になった経緯 クラシルでは新規のサービスや機能改善にリーンなプロダクト開発を取り入れています。業界的にそれらが必要になった経緯がまとまった素晴らしい記事があるので、リンクを貼っておきます。現在、メルペイのPMをされている川嶋一矢さん ( @tsumujikaze ) の記事です。 note.mu クラシルではプロダクトの立ち上げから2017年の終わりにかけて記事の中にある、通常のアジャイル開発 (図左) を行っていました。 なぜモダンなプロダクトチームによるリーンなプロダクト開発が必要なのか ( https://note.mu/tsumujikaze/n/n8b5f9cfec2c9 ) もちろん、ここでいう通常のアジャイル開発自体は広く一般的で、それで成功している会社も多いと思います。クラシルでも、基本的な機能でさえ不足していた初期では、この開発手法の方が上手く回っていました。しかしながら、幸いなことに多くの方にサービスを利用していただき、長期にわたり運営してきて、それらが上手くはまらなくなってきたのです。 僕らが陥った具体的な問題として大きいのは、頑張って開発した機能がほとんど使われないということが度々あったという問題でした。世の中には「この機能はリリースしてみないと使われるかわからない」という不確実性が存在します。その問題をどのように解決すればいいのか。僕らは四苦八苦していました。 UXデザインへの理解を深める〜これからのデザイナーがすべきこと〜 ( https://goodpatch.com/blog/about-basic-uxdesign ) Ruby on RailsやLaravelなどweb開発で使用する充実したフレームワークが枯れた技術となり、Herokuなど簡単にインフラの環境を構築することができるPaaSが普及した現代において、アイデアはすぐにコピーされ、機能的価値はすぐにコモディティ化します。何かをweb上でできることはもはや当たり前になったのです。 僕らが開発しているクラシルですが、運営している身からしてこんなことを言ってはいけないと思うのですが、例えば「ハンバーグ」というレシピの作り方が知りたかったら、ユーザー視点で考えると正直なところ別にクラシルではなくても他のレシピサービスでも良いわけです。多分ですが、どのレシピサービスを利用したとしても美味しいハンバーグを作ることができるでしょう。一つ、クラシルの良いところをあげるとしたら、動画でわかり易いというのが強みであり、それがマーケットに刺さったポイントでもあります。しかしながら、クラシルをはじめとして他のレシピサービスは作りたい料理のレシピを知りたいと思った時に引く辞書的なレシピサービスとしては完全にコモディティ化しています。 では、クラシルをこれからさらに伸ばし、より多くのユーザーに使われ世の中に幸せを届けるためにはどうすればいいのか。僕らが出した答えは、噛み砕いた言い方をすれば「 ユーザーが欲しいと (潜在的に) 考えているプロダクトを、しっかりと届ける 」ことでした。 僕らはこれからクラシルを通してモノが買われる世界を創り、食に関わる今まで解決されてこなかった課題を解決することで、日本で一番使われる生活のインフラとなるサービスにしたいと思っています。さらには、プロダクトの再現性を高めて、クラシルを越えるようなサービスを何度も創っていき、数年後にはdelyから出るサービスは全部めちゃくちゃ流行るという世界を実現したいと考えています。その文脈において、現在どこの会社も採用には難航していると思うのですが、優秀な人材が足りていません。とりわけ、開発リソースは極限まで枯渇しています。 jp.techcrunch.com そのため、少ない開発リソースを確実に成果に結びつけるために、リリースして使われない機能にリソースを1秒でも割くことは避けなければなりません。しかしながら、僕らが当たると確信した機能でもリリースしてみたら全く使われないという厳しい現実があります。ですが、その「この機能はリリースしてみないとわからない」という不確実性を実際にユーザーに使ってもらう前に下げる手法は存在します。それが、これから紹介するリーンなプロダクト開発なのです。 少々長くなりましたが、経緯をまとめると、 レシピサービスとしてのコモディティ化 開発リソースの枯渇 という切実な現実を背景として、不確実性をいかに下げていき「これが欲しかった」を再現性高く実現させ、枯渇した開発リソースを着実に成果に転換していく方法として、リーンなプロダクト開発がその答えとなりました。   2. クラシル版リーンなプロダクト開発の概略 リーンなプロダクト開発は川嶋さんの記事にあるような形が原型ですが、クラシルで実際に取り入れていくに伴い、組織のフェーズや形態、プロダクトのフェーズや性質によってより現実に則した形に変わっていきました。それが下の図です。  Design by はしもん (@oyasumi_yayaya) このリーンなプロダクト開発の本質は、 要件定義 (何を創るべきか) のアジャイル化、デザインフェーズの導入 です。図でもあるようにデザインフェーズと実装フェーズが分かれています。 今までのプロダクト開発を鑑みて、使われない機能がリリースされてしまう今までの開発プロセスの大きな原因として、 ユーザーやビジネス上の課題設定は正しいけれども、解決策が間違っている ということに気がつきました。これは、今までのアジャイル開発において、要件定義をPOあるいはPMとデザイナーが一気に課題選定から解決策、そしてUI作成までを行ってしまっていたから生じたのではないかという考えに至りました。この、課題と解決策が異なるというミスマッチを防ぐ方法として要件定義もアジャイル化して、課題に対する解決策として正しいのかを実際のユーザーに当てて何度もイテレーションを回すことで確かめていきます。 つまり、上の図であるように、今までの開発においては紫色の線のように、機能をバーンとリリースした時に一気にその機能が課題に対する解決策なのかという不確実性の学びを得るのに対して、リーンなプロダクト開発では、要件定義をアジャイル化することで、イテレーション毎に不確実性を減らすような学びを得るため、その都度何を創れば良いのかを学ぶ方向修正を行います。こうすることで、一生懸命に開発工数を使った機能が大ハズレすることのリスクが減っていくのです。 これからさらに細かく一つ一つをみていきます。長くなりますが、どうぞお付き合いください。   3. どの課題を解くかで全てが決まる プロダクト開発において、最も大事になるのが解くべき課題を決めること です。 イシューからはじめよ―知的生産の「シンプルな本質」 作者: 安宅和人 出版社/メーカー: 英治出版 発売日: 2010/11/24 メディア: 単行本(ソフトカバー) 購入 : 48人 クリック : 660回 この商品を含むブログ (145件) を見る   この本にもあるように、プロダクト開発においてもやりたいことが無限にあると思います。しかしながら、先ほどもあったように開発リソースが枯渇している現状では、課題を間違えてしまえば、余裕で開発者の一ヶ月くらいをムダにすることになります。競合ひしめくベンチャーにおいてそれは命取りになります。 ユーザーあるいはビジネスの課題は大きく分けて二つに大別すると考えています。 バケツを大きくする施策 バケツの穴を塞ぐ施策  これらのどちらも欠かすことができません。 バケツを大きくする施策 バケツを大きくする施策で大切にしている考え方は、自分たちの枠組みで考えないことと、プロダクトを未来の達成したい目標から逆算して考えることです。 人間の思考はついつい自分の枠組みに束縛されてしまいます。具体的には、僕は元エンジニアなので、エンジニアリングに強みを持っていて、DAUが思うように伸びないという課題があった時に、とりあえず継続率をあげる施策を考えます。しかしながら、いくら継続率をあげたところで入っていくユーザー数にもコスト的な視点で限りがあるので、流入と離脱が釣り合う時がきます。その課題解決のアプローチは間違いではないのですが、継続率をあげることも大事だけれども、マーケコストに依存しない大きな流入経路を確立させることができれば、既存の継続率でも十分にユーザーは溜まっていくでしょう。 また、当たり前ですが事業計画上で達成しなければならない数字があります。それらをプロダクト側の人間が無視して開発を行うことは良くありません。プロダクト側の数字で達成しなければならないDAUやサブスクリプションの契約数があります。それから逆算した際に、今のCVRでは全く達成できないことがわかるかも知れません。その場合、今の方法を行っていれば確実に実現したい未来はこないでしょう。そのため、方法を変えるという意思決定をする必要があります。これは、定量的な側面ですが、プロダクトとして実現したい未来という定性的な側面でも同様のことが言えます。数年後にプロダクト上で実現したいユーザー行動があるならば、その行動変容を起こすようにプロダクトに対して変更を加えて行かなければなりません。ある機能をリリースしたからといって、既存のユーザー行動が一気に変わることは滅多にありません。 そして、バケツを大きくする施策で具体的なプロダクトへの変更を考える際には、実際のUXリサーチなどを行いながら、ユーザー行動から課題を発見してそれらに対する解決策を実装していきます。このようなアプローチが人間中心設計やジョブ理論ということだと考えています。 ジョブ理論 イノベーションを予測可能にする消費のメカニズム (ビジネスリーダー1万人が選ぶベストビジネス書トップポイント大賞第2位! ハーパーコリンズ・ノンフィクション) 作者: クレイトン M クリステンセン,タディホール,カレンディロン,デイビッド S ダンカン,依田光江 出版社/メーカー: ハーパーコリンズ・ ジャパン 発売日: 2017/08/01 メディア: 単行本 この商品を含むブログ (6件) を見る   クラシルにおいては料理という一連のユーザー体験を考えると、料理は調理だけではないわけです。残念ながら、該当するツイートは消えてしまっているのですが、以下の記事で紹介されているツイートでは、調理という行動は料理の中では氷山の一角であることがわかります。 料理をしない人に知って欲しいことを「絵」にしてみたら、想像以上に壮絶だと分かった ( http://netgeek.biz/archives/76864 ) 今まで、クラシルはレシピが動画でわかりやすいということがユーザーに刺さり伸びてきたサービスですが、献立機能を開発した背景には、このように料理を毎日のようにしているユーザーには調理だけではなく、もっと大きな問題が山積しているという現実があり、その課題をクラシルでどう解決していくのかということを考えています。 prtimes.jp バケツの穴を塞ぐ施策  バケツの穴を塞ぐ施策は、基本的には既存のプロダクトの実装されている機能の改善を行うことです。そのため、溜まっているユーザーの行動データを定量的に分析して行きます。現在では Firebase Analyticsなどを入れておけば、基本的なアプリの行動は把握することはできますが、もっと詳細に追いたい場合や自分たち用にカスタマイズしたいという要件がある場合は自社でデータ基盤を構築する必要があります。クラシルでもFirebaseなどは導入していますが、自社でデータ分析基盤を持っています。 tech.dely.jp これらを駆使し、既存の機能に対して、その機能は本当に使われているのか、KPIを達成するためにはどのような変更が必要なのかを定量的な側面からアプローチしていきます。   4. 課題に対する解決策を考える 定性的、定量的両軸で検討した際に、今僕らが解決すべきもっとも大切な課題を設定することができたとします。次のステップでは、設定した課題に対する解決策を考えます。アイデア出しの着想としては「SPRINT 最速仕事術」という本で紹介されている、Googleが実践しているDesign Sprintで、本来であれば5日間で実践する内容を、エッセンスだけを取り入れて、僕らで必要なことだけを行っています。 SPRINT 最速仕事術――あらゆる仕事がうまくいく最も合理的な方法 作者: ジェイク・ナップ,ジョン・ゼラツキー,ブレイデン・コウィッツ,櫻井祐子 出版社/メーカー: ダイヤモンド社 発売日: 2017/04/13 メディア: 単行本(ソフトカバー) この商品を含むブログ (3件) を見る    これが実際の様子です。 弊社ではこのように、会議室に数時間こもってアイデア出しをします (「仕様が決まるまで帰れまてん」と呼んでいます) 。この時大事になってくるのが、この会議に誰をアサインすべきかです。少なすぎても多すぎても会議自体が機能しなくなります。 POまたはPM デザイナー エンジニア (プロトタイプエンジニア) 絶対に意思決定者であるPOあるいはPMを参加させてください。これらの役職についている人は基本的にめちゃくちゃ忙しい身かと思いますが、プロダクトの明るい未来のために時間を取ってもらいましょう。意思決定者が不在だと、この会議で決まったことが後々になって覆ることがあるからです。プロダクトに関わるみんなで行った民主的な意思決定だったとしても、プロダクトに変更を加える最終的な意思決定を行うためにPOやPMが存在しているわけで、そこにいるメンバーが知るよしもない施策が走っていたりするので、プロダクト開発における組織全体の情報を持っている者には判断を仰ぐことを怠ってはいけません。たとえ、権限が完全に委譲されていたとしてもちょっと確認するくらいはした方がいいと個人的には思っています。 そして、この会議ではメンバーががっつり時間を使って議論できることに価値があります。これを短縮しては良いアイデアは出てきません。なぜなら、限られたメンバーで情報を瞬時に共有して知識レベルを揃え、過学習した結果として、良いアイデアが生まれやすいからです。アイデアはいくつもあれば良いわけではありません。考え尽くされた一つでも十分に成果を上げることは可能です。大事なのはちゃんとあらゆることを考え尽くした結果として出てきたアイデアなのかです。 また、この段階で次のステップに必要な全てを決めてしまいます。具体的には、どこまで精密なプロトタイプを作るかと、どのように機能的価値を検証し、ユーザービリティテストを行うかです。   5. プロトタイピング 次にプロトタイプを作成します。この時に大事になってくるのが Fidelity (忠実度) という概念です。忠実度とは、ざっくりいうとどれだけ現実に則したプロトタイプを作るかの尺度です。High-FidelityとLow-Fidelityのプロトタイプが存在します。 High-Fidelity = コーディングによるプロトタイプ (Xcode)
 Low-Fidelity = ペーパープロトタイプ or ワイヤーフレーム (Sketch) が具体的な産物になります。 当たり前ですが、High-Fidelityなプロトタイプほど実装の難易度と工数が嵩みます。しかしながら、とても大事な考え方として「 考えたアイデアが本当に課題に対する解決策として適しているのかという不確実性を最も下げることができるプロトタイプ 」かどうかがあります。工数が嵩むからと言って、中途半端なプロトタイプでは、実際にその不確実性を下げることができず、本末転倒ということはよくあります。 なぜなら、中途半端なプロトタイピでは、動かない所などが多々ありユーザーの体験を阻害し、開発サイドの説明なしには動かすこともままならなくなってしまうからです。それでは、本当に欲しかった機能的価値検証やユーザービリティテストを僕らから誘導する形で期待した答えを言うようにユーザーを導いてしまうこともあるのです。 弊社のプロトタイピングのことが書かれた記事を貼っておきます。時間がある際に読んでいただけると幸いです。   6. アイデア検証 プロトタイプ ができたら、次はそれが表現するアイデアを検証します。 このように社内のクラシルユーザーを招いて、上から録画しつつ設問を当てて行きます。検証することは以下の二つです。 ユーザービリティ 機能的価値 まずは、機能的価値の検証では、本当にそのアイデアが課題を解決できる方法なのかを検証します。機能的価値とユーザビリティを一度に試験することができる時もあれば、そうではない時もあります。それは、機能的価値がその場で検証しにくい性質を持っているからです。なぜなら、その機能は必要な時が来ないとなんの役にも立たないからです。例えば、被験者に対して「ここはキッチンです。あなたはこれから料理をしようとしています。それらを想像しながら以下の設問に答えてください。」という設定は、あくまでも想像でしかないので、本当に機能的価値を検証できるとは限らないのです。そのため、機能の特性によって機能的価値を検証する方法は異なりますが、それが現実的に可能ではない場合は、想像で答えてもらう場合もあります。 次にユーザービリティテストでは、使いやすさのテストを行います。実際のユーザーは様々なバックグラウンドを持っていて、持っている認知バイアスはそれぞれ異なります。そのため、プロトタイプを使っている際に手が止まるポイントや理由が異なるのです。それが、その個人による局所的なものなのか、それともユーザー全般的に影響を及ぼすものなのかを見極める必要があります。設問を当てながらそれらを瞬時に見抜くことは人間の脳には困難なため、被験者の承諾の上でテスト内容を録画をしています。その認知バイアスの話で弊社CTOのいい感じのスライドがあるので、引用しておきます。 speakerdeck.com   7. 学びを整理する 機能的価値の検証およびユーザビリティのテストが済んだら、デザインフェーズの最後に得られた知見を共有し、整理します。この時に大事になってくるのが以下のことです。 必ずデザインフェーズに関わる全員が参加する 検証を実施した日に行う 小さな問題も見逃さない 得られたインサイトを深く掘って本質を見抜く 次のイテレーション時のことを全て決める まず大事なのは、検証を実施した日に、デザインフェーズに関わった全員がこの学習の場に参加することです。人間の記憶は短命です。数分前のことでさえ忘れてしまう場合があります。後日にしては、せっかく検証の際に色々考えていたことが揮発してしまうので、絶対にその日に知識を整理してください。また、デザインフェーズに関わる人全てが参加する必要があるのは、次のイテレーション時のことを全て決めておく必要があるからです。そもそも、これでデザインフェーズを終えるのか、まだイテレーションを回す必要があるのかを議論して決定する必要があります。 この学びを得るステップでは、必要あらば録画した動画を見直し、ユーザーが手を止めてしまった原因、それを解決するための情報アーキテクチャやUIの改善をどうするかを決めます。また、今回のアイデアは課題の解決策としてどこまで作り込むべきかもここで話す必要があります。 プロトタイプへの改善策が決まれば、再度デザインフェーズを回し、もう必要ないと判断したならば次のステップへ進んでいきます。   8. 意思決定者の決定を仰ぐ プロダクト開発において、様々な組織形態があると思いますが、弊社では意思決定者として、POあるいはPMを設置しています。現在では、僕が兼務している形ですが、会社の未来を左右するような機能の場合はCEOである堀江 ( @santamariaHORI ) の決定を仰ぐ場合もあります。 基本的に、意思決定と責任はセットである必要があると考えています。全員が意思決定者となり、プロダクトに変更を加えて行くことがベンチャーあるいはスタートアップの醍醐味と思われているかも知れません。それで使われるプロダクトが創れるならばそれが良いと思うのですが、不幸なのは使われない機能が世に出てしまうことと、会社としての方向性が開発部だけ異なる方向に向かっていることです。会社を成長させ、社員全員を盛り上げて行くためにも、プロダクトの成功は絶対に欠かせません。それに対して、責任を負っているのに、意思決定権がメンバー全員に分散しているのは組織構造としてよくありません。そのため、デザインフェーズの締めくくりとして、これが課題に対しての最適な解決策で、世に出して良いものかを意思決定者に確認を仰ぐのです。実質的に一つ前のステップである「学習」にも意思決定者が参加しているはずなので、そこで判断されることが多いですが、とても重要なことなので明確なステップとして切り出しています。   9. 要件定義を詰め、実装仕様書を作成する デザインフェーズを終えてブラッシュアップされたアイデアが世に出して良いと判断されたら、次は今までの要件定義を整理して実装仕様書を作成します。 実装仕様書とは、1px単位で詰められたUIと、どのAPIを叩くのか、どのようなレスポンスが返ってくるのか、必要なパラメータは何かなどを事細かに書いたドキュメントです。もちろん、ボタンを押した際にどのようなインタラクションを行うかなども記述します。 この実装仕様書を読めば誰でも同じ品質で実装を行うことができる ことを目指します。 この実装仕様書が必要になった背景として、アプリを開発している会社で問題になりがちなのが、iOSとAndroidで意図した挙動が異なるように実装されてしまうことがあります。それぞれ、 Material Design と Human Interface Guidelines にあるようにデザイン原則が異なるので、意図した差異であれば良いのですが、仕様上では一緒のはずなのに実装の際にそれらの意図が明確に伝わらずに、確認漏れもあってリリースされてしまったというケースが頻発していました。そのため、誰が見ても均質な実装が行えるようにソースを一つとして、このドキュメントさえ読めば大丈夫というように仕組みで解決しました。   10. ゴリゴリ実装 さて、要件定義も煮詰まったら実装フェーズに突入します。この時に大切になるのは、以下のことです。 どのように実装するかはエンジニアの裁量に任せる 実装仕様書は常に最新状態を維持する 原則としてデザインフェーズまで回帰することはない 今までの文脈では、実装フェーズのエンジニアは「何を創るべきか」という所には参加していませんでした。それらは全てデザインフェーズにいる人たちで決めています。何を創るか決まったものを実装するなんてつまらないという人も、もちろんいると思います。先ほどもありましたが、全ての人が意思決定者となってプロダクトに変更していくことは組織が拡大していくことでカオスになり、必要ではない機能がリリースされる、その結果として枯渇状態の開発リソースが割かれてしまうという問題が生じることがよくあります。何度も伝えたいのですが、プロダクトが成長しないことが最も不幸なことです。このリーンなプロダクト開発は、より確実に確度が高い機能をリリースさせ、プロダクトを成長させるためにとってる開発プロセスなので「何を創るべきか」に関心があるならば自分自身の組織における役割を変え、デザインフェーズに入っていくことで開発できます。もちろん、リソースに余裕があれば、デザインフェーズで固まった要件をそのまま自分で実装するケースもあると思います。そこに垣根はありません。 実装フェーズにおいて、エンジニアはどのように実装するかと期日に間に合わせるためにはどうするかという所に責任を負います。どのように実装するかを考えることは、非常に難易度が高いことです。プロダクトが成熟し、組織が拡大すると、基本的にはソースコードは複雑性を増していきます。複雑性を増すことは、開発におけるスピードを落とす原因にもなりかねません。 そのメタファとして、ジェンガが面白いです。開発初期では、ジェンガの中から一つを取り出すことは簡単です。しかしながら、開発が進んでくると一つのブロックを取り除き、どこに置くかはとても難しくなります。これが開発でも起こりうるのです。そのため、成熟したプロダクトの開発こそ、より良いアーキテクチャや読みやすく、理解しやすく、変更に強いコードを書くことはとても重要です。長期的な生産性の高さやテストのしやすさなどに必ず効いてきます。より良いコードを実現するために、コードの全体像を理解して、メンテナンス性が高い設計をすることに実装フェーズのエンジニアは責務があるわけです。 また、実装上にも不確実性が存在します。実装を進めていく上で、難しい実装や意図してなかった仕様の穴が潜んでいるわけです。それが発覚した時点で、先ほど作成した実装仕様書に変更を加えて、関係者に伝達する必要があります。それに応じて、リリース日の再検討などが発生するため、PMはステークホルダーに伝達する必要があります。 そして、原則としてその機能を開発すべきということはデザインフェーズで議論し尽くされたという前提があるため、全てを覆す事実がない限りはデザインフェーズまで立ち返ることはありません。その機能の必要性が納得できていないメンバーいるならば、それはPMに納得いくまで説明する責任があります。ここで大事なのは、 全員が納得した状態で開発に挑む ことです。僕らエンジニアは決まったものを実装するためにいるのではありませんし、ユーザーに価値あるプロダクトを届けるために、役割を分担して良いサービスを創っているので、全員が同じ方向を向いていることはとても大切なことです。   11. デバックテストを行う 実装フェーズの最後のステップとして、デバックテストがあります。ちゃんとQAエンジニアがいれば良いのですが、そのような充実した開発体制が整っているところも多くはないと思います。 弊社では、検証可能なバージョンをDeployGateで配布して、各々が持っている端末あるいは検証用に用意された端末でデバックを行なっています。その際に出た小さい実装漏れやクラッシュを引きこすようなバグをその再現方法などを専用のチャンネルでやりとりしています。大きな機能によっては、会議室を抑えてみんなでデバックしています。この時 大事になことは、どれだけ小さなことでも発言すること です。小さいことが実は大きなバグの引き金となることは歴史が証明してくれています。誰が実装したかなどは関係なく、発言することが大事です。 ここで修正すべき問題があれば、そのフィードバックをもとに実装仕様書を変更して、実装フェーズのイテレーションを回します。問題ないことが確かめられたら、晴れてリリース 🎉 となるわけです。   12. まとめ 今まで読んできていただいたように、このリーンな開発プロセスはとても長いですし、デザインフェーズに関わるメンバーも工数がかなりかかっています。そのため、 全ての課題に対してこのリーンなプロダクト開発を適用することはおすすめしません 。この開発プロセスを適用する判断の軸として「 そのアイデアは課題に対して本当に最適な解決策となっているという不確実性が高いか、あるいはその課題が本当に今取り組むべき課題なのか 」を持っておくと良いと思います。極論をいえば、文言をちょっと変えるだけの実装にデザインフェーズは不要な場合が多いです。 少ない開発リソースでデザインフェーズを適用するかどうかも、そのメンバーで実装できたことのトレードオフの上に成り立っているということを忘れてはなりません 。 クラシルの開発プロセスにこのリーンなプロダクト開発を取りいれたら、世にでる施策の精度は一気に高まりました。全て当たったと思うほどの威力を発揮します。裏を返せば、今までリリースされてきた不要な機能のように確度が低いものが世に出なくなったということでもあります。 プロダクト開発はマラソンです。リズムが大切です。一見、煩雑に見れるこの開発プロセスを体験してみると、実は今まで以上に開発スピードが上がっていることに気がつくと思います。それは、今まで要件定義がバーンとなされていたことで、仕様の雑さなどから、手戻りなどが多く発生し、仕様の共有ができてなかったり様々な要因で開発が阻害されていたからです。それらが積み重なれば、エンジニアの心理的な余裕がなくなり、バグが発生してしまう要因にもなり得ます。 リーンなプロダクト開発のように、確度が高い施策を常に世に出せる開発体制は、成功体験を積みやすく、モチベーションにも還元されてきます。結局はプロダクトの成功が全てを癒してくれるのです。 もちろん、僕らもまだまだ完璧ではないし、多くのミスを犯してしまいます。しかしながら、リーンなプロダクト開発は僕らに不確実性に向き合うための勇気をくれたと信じています。   13. 終わりに 長い長い記事を読んでいただき、本当にありがとうございます。 明日は弊社Androidのエンジニア兼テックリード兼エンジニアリングマネージャーをしている梅森より「不確実性とうまくやっていくためのプログラミング設計論」というタイトルで記事が出ます。是非お楽しみにしてください。 このように僕らは、不確実性に向き合う組織を作って、ユーザーから「これが欲しかった!」をいただけるように日々開発を行なっています。自分たちが開発しているプロダクトを通して、世の中がより良くなっていくことはエンジニアの冥利に尽きます。しかしながら、現在開発部を含めてすべてのポジションで人が足りていません。僕らと一緒に世の中を変えるプロダクト創りに全力を出せる人を探しています。 iOSエンジニア Androidエンジニア サーバーサイドエンジニア(Ruby on Rails) SRE MLテクノロジスト ストリームパイプラインエンジニア データサイエンティスト UI/UXデザイナー UXリサーチャー Webグロースディレクター www.wantedly.com 全てのプロダクト開発に幸あれ! 良いクリスマスを!
アバター
はじめに こんにちは。 機械学習エンジニアの辻です。 本記事はdely Advent Calendar 2018の22日目の記事です。 dely Advent Calendar 2018 - Adventar dely Advent Calendar 2018 - Qiita 昨日は弊社のサーバサイド・エンジニアの山野井が「【Vue.js】算出プロパティの仕組みについて調べてみた」という記事を書きました! とてもわかり易く解説しているので興味のある方は是非読んでみてください。 tech.dely.jp さて本日は「Lispの車窓から見た人工知能」と題しまして、プログラミング言語 Lisp から見た人工知能の風景を眺めていきたいと思っています。ぼくはEmacs使いのLisperですが、Lispを書くのは自分用のスクリプトや、Emacs Lispの設定変更といったものだけで、ふだんの機械学習に関するプロダクションコードでは一切使っていません。ではなぜそんなLisp本職でないぼくが、Lispの歴史から人工知能の風景を眺めるかといいますと、実はLispと人工知能とはとても縁の深い関係にあるからなんです。昨今では、機械学習といえばPythonやC++を用いるのが主流となっていますが、黎明期には実はLispがその役割を担っていました(Lisp専用のハードウェアがふつうに販売されていたほどです)。その辺りの流れや様子などの風景を、象徴的な書籍などを少しずつかいつまみながら気楽にご紹介していけたらなぁと思っています。 目次 はじめに 目次 Lispと人工知能の父 Lispの概略 Lispの特徴 on Lisp 最初のAIの春(1956−1974) ELIZA AIの最初の冬 (1974−1980) Gödel, Escher, Bach「GEB」 (1979) 2度目のAIの春 (1980–1987) Structure and Interpretation of Computer Programs「SICP」(1985) AIとLispの冬 (1987−1993) HAL 9000 はどこに? 実用Common Lisp Deep Learningの芽生え まとめ・そしてHylangという選択 Hylang さいごに Lispと人工知能の父 この方は言わずと知れたジョン・マッカーシー(John McCarthy)ですね。 人工知能(Artificial Intelligence、以下はAIと称します)という言葉の提唱者であると同時にLispの生みの親でもあります。つまり、その点においてはLispと人工知能は同じ親を持つ兄弟のような関係ともいえるわけです。 マッカーシーのAIに関する功績は偉大過ぎるほど偉大で、遡ること今からなんと60年以上も前、1956年にAIに関する世界初の国際会議( ダートマス会議 )を主催されたのでした。実は「人工知能の父」と称されるあのマービン・ミンスキーもこの会議を機にAI研究者を志し、その3年後にはMITでマッカーシーを師事することになったのです。 また、AIにおいてしばしば議論の的となる「有限の情報処理能力しかないロボットには、現実に起こりうる問題全てに対処することができない」という、この フレーム問題 についても、パトリック・ヘイズとともに最初に提唱されたのがやはりマッカーシーでした。 その一方で、コンピューターサイエンスに関するマッカーシーの功績も超絶偉大で、実はいまでは当たり前のように用いられている「ガベージコレクション」技法を発明されたのもマッカーシーなんですね。そして、タイムシェアリングシステム(メインフレームからエンジニアをはじめたぼくにとってはTSSはとても馴染みがありますが、一般的にはどうなんでしょうか?)の技術によって(水道や電力のように)コンピュータの能力や特定のアプリケーションを販売するビジネスモデルを生み出すかもしれないともマッカーシーは提唱されましたが、いかんせん当時のハードウェアもソフトウェアも通信技術も未熟であったために1970年代中ごろには消えてしまいました。しかし恐るべきことに、21世紀になってからこのTSSの考え方はアプリケーションサービスプロバイダにはじまり、グリッド・コンピューティングを経て クラウドコンピューティング へと昇華し再浮上してきたことを考えると、まぁなんとういう先見だろうとただただ驚愕してしまいます。 さて、Lispはと言いますと、マッカーシーが最初にLispを発表したのはダートマス会議から4年後(1960年)のことで、それはまさしく AIアプリケーションのための専用プログラミング言語 としての誕生でした。 (出典: ジョン・マッカーシー - Wikipedia ) Lispの概略 ガイ・スティール(Guy L. Steele, Jr) ここでもしかすると、Lispという言語をあまりよく知らないという方のためにほんの少しだけご紹介したいと思います。Lispは上述の通りマッカーシーによって1960年に生み出されたのですが、実装はというと、当時マッカーシーのもとで大学院生だったスティーブ・ラッセルがマッカーシーのLispに関する論文を読み、それを僅かな期間で機械語で実装してみせてマッカーシーを大変驚かせたという逸話が残っています。そしてこの時こそが、 Lispインタプリタが誕生した瞬間 だったのです。誕生から幾年月が流れ、1980年代から90年初頭には非常にたくさんのLisp方言が乱立することになりました。それはある意味ではLispという言語仕様の、その実装の容易さこそが仇となった結果と言えるのでした。LispにはLisp自体を拡張する(すなわちon Lispする) マクロ という機能が備わっていて、そのマクロを用いることで既存のLispの文法構造自体を拡張することが可能であるため、利用者ごとにカスタマイズした方言を生み出すことができます。しかしそれでは管理が大変だということで、乱立するLisp方言たちを一つの言語に統合していかなければという動きがガイ・スティール(Guy L. Steele, Jr)を先頭として始まり、その結果として設計された新しい言語「 Common Lisp 」は、基本的にそれらの方言たちのスーパーセットであり、それらの方言たちを置き換え統合することになったのでした。そして試行錯誤の末の1994年、ついにANSIはCommon Lispの標準仕様「ANSI X3.226-1994 American National Standard for Programming Language Common LISP」を出版したのでした。ところが悲しいかな、その時には既にC言語など新たな言語の潮流に押し流され、Lispはもはや全盛期に比べると市場のほとんどが失われていたのでした。 (出典: LISP - Wikipedia ) COMMON LISP 第2版 作者: Guy L.Steele Jr. 出版社/メーカー: 共立出版 発売日: 1992/06/10 メディア: 単行本 言語の登場を簡易的に示した図 図中の赤のやじるしは影響を与えた言語を示しています。 Lispの特徴 さらに続けまして、Lispの構文についても少しご紹介しておきたいと思います。 Lispという言語構文の特徴といえば何といってもカッコ、 S式 です。Lispはすべての構文をこのS式で記述できるので 式と文とが区別されず 、すべてのコードとデータは式としてシームレスに書き下すことができます。コンパイラによって式が評価されたとき、それは値(または値のリスト)として生成されます。ところで、S式はあまりにカッコを大量に使用するため、その見た目上読みづらい(他の言語構文の仕様と乖離があるため)という批判を受けることがままあります。「Lispは 『lots of irritating superfluous parentheses』(過剰でいらいらさせる大量の括弧)に由来する」というジョークもあるほどです。しかしこのS式によるすべてのコードとデータの 同図像性(homoiconic) こそがLispの他言語にない能力を生み出す要因ともなっているわけです。特にLispの関数はそれ自身がリストとして書かれているのでデータとまったく同様に扱うことができます。 果たして、良いコードとは一体なにか、その答えは千差万別で、状況次第なわけですし、これという正解をぼくは知りません。可読性の対象とは人間なのか、否、機械なのか、それともまた他の何かなのか...まあその議論はここでは置いておきましょう。それでは、ほんの少しだけS式構文の例をCommon Lispでご紹介したいと思います。 Lispの構文的特徴としてはカッコもそうですが、 前置記法 が顕著なものとしてあります。こちらの例でカッコで閉じられた先頭の list は文字列ではなくリストを返す関数です。 ( list 1 2 "foo" ) ;; => (1 2 "foo") そして、このようにカッコの前にシングルクォーテーションをつけても上のlist関数と同じ意味となりリストを返します。 '( 1 2 "foo" ) ;; => (1 2 "foo") +オペレータを使うと、このように、後続の整数をすべて足し合わせることもできます。 ( + 1 2 3 4 ) ;; => 10 ラムダ式を記述する場合もこのようにlambdaと記述できて直感的に表現できます。 (( lambda ( x y ) ( + x y )) 3 4 ) ;; => 7 また、関数を定義する場合はこのように、 defun を用いて定義します。 再帰的に階乗を計算する場合はこのように定義します。 ( defun factorial ( n ) ( if ( <= n 1 ) 1 ( * n ( factorial ( - n 1 ))))) (出典: LISP - Wikipedia ) on Lisp 先ほど少し触れたマクロについても少しだけご紹介しておきたいと思います。 Lispといえばこの方、ポール・グレアムは著書「on Lisp」の前書きの中でこのようにおっしゃっています。 「プログラムをただLispで書くだけでなく、独自の言語をLispを基に(on Lisp)書くことが可能であり、プログラムはその言語で書くことができる。~中略~ Lispは開発当初から拡張可能なプログラミング言語となるよう設計されていた。言語そのものの大半がLispの関数の集合体であり、それらはユーザ自身の定義によるものと何も違わない。それどころかLispの関数はリストとして表現できるが、リストはLispのデータ構造なのだ。このことは、ユーザがLispコードを生成するLisp関数を書けるということだ。」 S式が同図像性(homoiconic)構文であるがゆえに、マクロを使って元の構文を自由に変更したり追加することが可能となります。ほんの少しだけマクロの例についてもCommon Lispを例に触れたいと思います。 マクロ定義はこのように記述します。 ( defmacro macro-name ( args1 args2 ) ( list args1 args2 )) この場合、呼び出す際にはこのように記述します。 ( macro-name args1 args2 ) あまり意味はないですが、ifを拡張してmy-ifを作る場合はこのようになります。 ( defmacro my-if ( condition then else ) `( cond ( , condition ,then ) ( t ,else ) ) ) ( defun fak ( n ) ( my-if ( = n 0 ) 1 ( * n ( fak ( - n 1 ))))) リテラルデータをテーブルにputするのにdefmapマクロを作成する場合はこのようになります。 ( defmacro defmap ( name options &rest entries ) ( declare ( ignore options )) ( let (( table ( make-hash-table ))) ( dolist ( entry entries ) ( setf ( gethash ( car entry ) table ) ( cadr entry ))) `( defvar ,name ' ,table ) )) このように、マクロによってLispの構造をどんどん拡張することができます。しかし実際のところ関数で事足りる場合がほとんどです。マクロを使う場合として、ポール・グレアムはこのように示しています。 「ある関数が本当にマクロであるよりも関数であった方が良いというのは、 どうしたら分かるだろうか? マクロが必要な場合とそうでない場合の間には大抵明確な区別がある。基本的には関数を使うべきだ。関数で間に合う所にマクロを使うのはエレガントでない。マクロが何か特定の利益を与えるときのみそれを使うべきなのだ。それはいつだろうか? マクロで実現できることの大部分は関数ではできない。」つまり、関数を書いていて実現できないことをマクロで書くとそういうことですね。ボトムアップなコーディングを推奨されるLispにとってそれはある意味自然なことです。 On Lisp 作者: ポール グレアム , 野田 開 出版社/メーカー: オーム社 発売日: 2007/03/01 メディア: 単行本 こちらは、さらにマクロを過激に啓蒙する賛否ある書籍です。とても刺激的な内容になっています。 Let Over Lambda 作者: Doug Hoyte 出版社/メーカー: Lulu.Com 発売日: 2008/05/07 メディア: ペーパーバック 最初のAIの春(1956−1974) ここでLispを一旦離れまして、車窓から見えるAIの風景へと目を移していきたいと思います。何と言っても、ダートマス会議後の数年はジョン・マッカーシーを中心としたAIに対する 期待と発見の時代 であり、新たな地平を疾走する機関車のような勢いでAIは発展を遂げていきました。しかし今から考えると、この時代に開発されたプログラムのほとんどは推論と探索に頼っていて、どんなに巨額を投じて開発した当時の最高峰のコンピュータだろうと、処理可能な計算量はごく僅かであり、非常に限定的な領域の問題しか解くことはできませんでした。しかし、それでも当時の人々にとって、見た目的には まるで人間の様に振る舞う機械 は非常に「 驚異的 」に写っていたことでしょう。 ELIZA そんな黎明期の象徴的な出来事として、ジョセフ・ワイゼンバウムは、 ELIZA と呼ばれる単純な自然言語処理プログラムを公表されました。それはLispが誕生してわずか6年後の1966年のことでした。ELIZAは診療カウンセラーを装って人間と対話できるプログラムとして紹介され、あたかも人間の返答を理解したような対話をするために、今ではとても単純なパターン照合プログラムが適用されていました。つまり、ELIZAはいわゆる「チャットボット」の先駆けといえます。特に日本では 人工無脳 という名前で知られていて独自の進化を遂げました。 ちなみに、弊社では毎日こちらの哲学SlackBotからありがたいお言葉を頂いております。 tech.dely.jp ワイゼンバウムは、ELIZAが多くの利用者や観衆に非常に重大な影響を及ぼしていく様子や、人々がELIZAに心を開く様子を見ていく中で徐々に不安に苛まれるようになりました。そして、あらためてAIという意味について冷静に考えはじめ、後にはAIに対する主要な批評家の1人に転じられることとなったのでした。 有名な1976年の書籍「コンピュータ・パワー―人工知能と人間の理性( Computer Power and Human Reason)」の中で、ワイゼンバウムはコンピュータ技術についてのアンビバレンスを示されています。それはすなわち、「AIは可能であろう。しかし、コンピュータは常に同情や知恵などの人間性を欠くため、人間は決してコンピュータに重要な決定を任せるべきでない」というものでした。 8章人工知能学にはこのような一節があります。「私の主張したいことは、知性についてあまりにも単純すぎる考え方が大衆ならびに科学会の思考を支配し、 部分的にはこの考えがもとになって 、人工知能学において 誤った雄大なる幻想 が育まれることになった、ということである。」このような警鐘を鳴らしています。 ちなみに、ワイゼンバウムがELIZAを実装された際のオリジナルはSLIPという独自の言語で実装されていたのですが、これを Bernie Cosell が後にLispに移植されました。(あとで紹介するピーター・ノーヴィグの「実用Common Lisp」にその実装が掲載されています) (出典: ジョセフ・ワイゼンバウム - Wikipedia ) コンピュータ・パワー―人工知能と人間の理性 作者: ジョセフ・ワイゼンバウム 出版社/メーカー: サイマル出版会 発売日: 1979/01 メディア: 単行本 余談ですが、Emacsで「M-x-doctor」としてみるとこのようなELIZAと謎の対話を始めることができます。 I am the psychotherapist. Please, describe your problems. Each time you are finished talking, type RET twice. me> Hi ELIZA> How do you do? What brings you to see me? me> I am hungry ELIZA> Why do you say you are hungry? me> You've got to say what? ELIZA> Is it because of your plans that you say I have got to say what? ELIZA> What do you think? SiriにELIZAのことを尋ねると... お知り合いでした。 AIの最初の冬 (1974−1980) 1970年に入ると、それまで破竹の勢いで発展してきたAIでしたが、様々な批判に晒され、その結果として資金も縮小され発展に陰りが見え始めました。その要因として、ワイゼンバウムの警鐘も虚しく、当時のAI研究者たちの過度な期待によって、直面している問題の難しさに対して、正しく評価できなかったことが大きかったのでした。その期待ゆえに楽観的な彼らが予想する成果へのさらなる期待の高まりは、他の研究者や出資者の間で飽和していたにもかかわらず、実際の結果としてその期待に応えるどころか落胆の連鎖を生み、次第にAI研究への出資はほとんど無くなっていきました。今から考えると、1970年代前半のAIプログラムの能力は非常に限定的で、その実態を出資者から見れば、どのプログラムも単なる「おもちゃ」に過ぎませんでした。そしてさらには、その実用化に当たっては、ハード面でのメモリ容量や速度の不足といった性能の限界は深刻な課題でした。 また、AIそのものに対する他学界からの批判が激化し、一部の哲学者はAI研究者の楽観的な主張に強く反論し、 ゲーデルの不完全性定理 が形式体系(コンピュータプログラムなど)では、 人間が真偽を判断できることも判断できない場合がある ことを示していると主張したのでした。 Gödel, Escher, Bach「GEB」 (1979) さて、ゲーデルといえば、そんな最初の冬が終わりを告げる頃、ダグラス・ホフスタッター(Douglas Richard Hofstadter)によって執筆されたゲーデル・エッシャー・バッハ(Gödel, Escher, Bach)が出版されたのでした。この本は人工知能の問題に関する書籍としては世界中で一般の人々に読まれた最初の古典と言えるのではないでしょうか。当時、ホフスタッターはまだ20代後半という若さでしたが、人工知能問題を高エネルギー物理学、音楽、芸術、分子生物学、文学、といった多彩なテーマに絡めて非常に豊かに記述され当時大変な話題となりました。この本がきっかけになって、人工知能分野へ進むことを決めた学生も大勢いたと言われています。 未だファンの多い書籍であり、内容も多岐に渡るので概略をご紹介するのは恐れ多いことですが、気楽に、誤解を恐れず一ファンの個人的な感想として紹介させていただくと、この本のエッセンスは、バッハのカノンのパターンとエッシャーの『描く手』に現れていると言えるのではないかと思います。 それは、右手が左手を、左手が右手を描いているという極めて奇妙な絵です。(『描く手』の実物を、今年上野で「エッシャー展」が開かれましたので見て感激しました) この絵を「手」そのものの次元で観察したときには、どちらが描く方で、どちらが描かれている方なのか、判断することは不可能です。つまり、互いに描きあう手は自己言及を繰り返しその環の中で閉じているといえます。しかし、一方で観察者が「絵」としての次元で観察したときには、その背後の、右手、左手の両方の創造者である、エッシャー自身の描かれてない「描く手」がメタ的に控えています。そしてさらには、この絵を描くエッシャーを鏡に映したなら、エッシャーの絵をさらに鏡の向こうの環に閉じるという連続が無限に続いています(『3つの球体』の試み)。このように互いに描き合う「手」を起点として渦巻く無限ループからいったん離れ、自分のしていることをメタ的に眺めることができるのは、人と機械(AI、論理構造、システムetc...)の最大の違いであるという点にホフスタッターは言及するのでした。(こうして今「手」について記述しているぼく自身がしている行為も無限ループの一環に閉じているのかもしれません。) この、人間と機械の違いというのは、「矛盾」の取り扱いに対して如実に現れ、人は自分の考えに矛盾した点を見つけても、全精神が崩壊したり、思考活動を停止してエラーを吐き出すようなことにはなりません。その代わりに、矛盾を引き起こしたと思われる信念や前提、推論方法を振り返り吟味しはじめます。すなわち、その中で矛盾が生じたと思われるシステムから外に出て、それをメタ的に修復しようと試みること、この「アナロジー」としてエッシャーのこの「描く手」やバッハのカノンのパターン、あるいは、特徴的な「アキレスと亀」の掛け合いを用いて議論が進行していきます。 ちなみに、書籍中の数ページではLispについて言及されている箇所もあって、「すべてのコンピュータ言語の中でもっとも重要でしかも魅力的なもののひとつ」として紹介されています。とくに面白いのが、『描く手』になぞらえて「自分自身の構造の中に入り込み、それを変更するように設計されたLispプログラムについても、これに比類できる両義性が生じうる。もしそれを「Lisp」そのものとして眺めれば、自分自身を変更しているといえるだろう。しかし、レベルを移して、LispプログラムをLispインタープリタのデータと見るなら、事実動いている唯一のプログラムはインタープリタであり、なされた変更は単にデータ片の変更のみでインタープリタ自身の変更は免れている」このように述べられています。 ゲーデル、エッシャー、バッハ―あるいは不思議の環 20周年記念版 作者: ダグラス・R. ホフスタッター 出版社/メーカー: 白揚社 発売日: 2005/10/01 メディア: 単行本 メタマジックゲームは、「サイエンティフィック・アメリカン」でホフスタッターが連載していたコラムを中心に一冊にまとめられた書籍でGEBよりも具体的なテーマを扱っています。17章ではまるっと1章「人工知能言語Lispの楽しみ」と題されたLispに関して記載されています。「私個人としては、setqや副作用のある関数を徹底的に嫌っているわけではない。関数型プログラミングのエレガンスも捨てがたいが、これだけで大きな人工知能プログラムを組むのはちょっと無理だと思う。」実に当時から関数型プログラミングの議論の論点は変わっていないみたいです。 メタマジック・ゲーム―科学と芸術のジグソーパズル 作者: ダグラス・R. ホフスタッター 出版社/メーカー: 白揚社 発売日: 2005/10/01 メディア: 単行本 こちらは今年翻訳が出版された書籍で、GEBから40年以上の年月を経た今、ホフスタッターの考える「人間の認知」についての考察がGEB同様様々な視点からつづられています。非常に興奮して一気に読み終えました。 わたしは不思議の環 作者: ダグラス・ホフスタッター 出版社/メーカー: 白揚社 発売日: 2018/07/24 メディア: 単行本 2度目のAIの春 (1980–1987) ゲーデル・エッシャー・バッハ(Gödel, Escher, Bach)の出版を皮切りにして(因果関係があるかどうかは言及しません)、1980年代に入ると再びAIが脚光を浴び始めます。この頃から、AIプログラムの一形態である「エキスパートシステム」が世界中の企業で採用されるようになり、その知識表現がAI研究の中心となっていきました。エキスパートシステムとは、特定領域の知識について質問に答えたり問題を解いたりするプログラムのことで、専門家の知識から抽出した論理的ルールを使用して解を導いていきます。エキスパートシステムはあえて扱う領域を狭くし(それによって1回目の冬の轍を踏まないよう常識的知識の問題を回避する)、単純な設計でプログラムを構築しやすくすると同時に運用中も修正が容易となりました。このエキスパートシステムは実用的なプログラムであり、それまでのAIが到達できていなかった段階にまで到達していくことが可能となったのでした。 Structure and Interpretation of Computer Programs「SICP」(1985) この頃の書籍の代表としてはやはりこちらでしょう。 SICPこと『計算機プログラムの構造と解釈』(Structure and Interpretation of Computer Programs)が刊行されたのが1985年のことでした。この本の中で用いられている例にはすべてScheme(主要なLisp方言の一つ)が用いられていて、抽象化、再帰、インタプリタ、メタ言語的抽象といった計算機科学の概念の真髄が説明されています。 こちらも序文から当時の様子が伺える箇所を少し抜粋してみたいと思います。「Lispは人工知能のため、重要な応用分野でプログラミングの要求を支えてきた。これらの領域は重要であり続け、LispとFortranは少なくとも次の四半世紀では活発に使われるよう、そこでのプログラマは2つの言語に専念しよう。~中略~ これは人工知能の研究の準備に使われるほとんどの書籍と違いプログラミングの教科書であることに注意しよう。つまり、ソフトウェア工学と人工知能におけるプログラミングの重要な関心事が、検討するシステムが大きくなるにつれて合体する傾向なのである。これが人工知能以外でもLispへの関心が増しつつあることへの説明だ。」 最初の苦い冬の経験を経て、人工知能の研究がもたらした様々な課題への取り組みは実用的なプログラミングの問題解決に活用できる汎用的な成果であるということが窺い知れます。 計算機プログラムの構造と解釈 第2版 作者: ハロルド エイブルソン , ジュリー サスマン , ジェラルド・ジェイ サスマン 出版社/メーカー: 翔泳社 発売日: 2014/05/17 メディア: 大型本 AIとLispの冬 (1987−1993) 1980年代後半となり、再びAIに冬の時代が訪れます。そしてこの冬はAIとともにLispにとっても過酷な冬の時代でした(四半世紀で活発に使われることを望んでいたのですが)。 その最初の兆候となったのは、1987年にAI専用ハードウェアの市場が突然崩壊したことでした。その背景にはアップルやIBMのデスクトップコンピュータの性能が徐々に向上していったことがあり、当時非常に高価だったAI専用のLispマシンは性能的に凌駕され、Lispマシン5億ドルの市場があっという間に消えてしまったためでした。 それから1990年代初頭にかけて、AIには確率と決定理論がもたらされ大きな影響を受けることとなりました。ここで多くの実用化されたツールが、ベイジアンネットワーク、隠れマルコフモデル、情報理論、確率的モデリング、古典的最適化などを活用し始めることになりました。 またこの頃から、ニューラルネットワークや進化的アルゴリズムといった「計算知能」パラダイムのための正確な 数学的記述 も発展してきたのでした。この発展によりもたらされた再現性の恩恵によって、元来はAI研究者が開発したアルゴリズムだったプログラムなどが大規模システムの一部として使われ始めたのでした。 この流れによって、SICPでも述べられていた通り、 実はそれまでAI研究を通じて様々な非常に難しい問題が解決されてきたという事実 、そして、 その解法は極めて実用的であったことが証明されはじめた のでした(例えば、データマイニング、産業用ロボット、物流、音声認識、銀行のソフトウェア、医療診断、Googleの検索エンジンなどがまさにその例です)。ところが残念なことに、こういった良い流れがありながら、それらの産業における実用的成功が、実はAIのおかげだという事実が世間的に知られることはほとんどありませんでした。それらの技術革新の多くは、達成と同時に計算機科学のありふれたアイテムの一つとして扱われたのです。一体なぜなのでしょうか? ニック・ボストロムはこれを「 AIの最先端の多くは、十分に実用的で一般的になったとたんAIとは呼ばれなくなり、一般のアプリケーションに浸透していく 」と的確に説明されています。そういった背景にあって、1990年代の当時の多くのAI研究者は、意図的に自らの仕事をAIとは別の名前で呼んでいたのでした(例えば、インフォマティクス、知識ベース、認知システム、計算知能など)。その理由の一部には、確かに彼らが自分の研究をAIとは全く異なるものだと思っていたということもありますが、背景的にこのような新しい名前をつけることで資金提供を受けられるという面も少なからずあったのです。 とくに産業界では「最初のAIの冬」がもたらした失敗の影が依然として払拭されておらず、ニューヨークタイムズ紙は「 無謀な夢を見る人とみなされることを恐れ、計算機科学者やソフトウェア工学者は人工知能という用語の使用を避けた 」そのように評しました。 HAL 9000 はどこに? 2001年宇宙の旅 さて話はころっと変わりますが、1968年、アーサー・C・クラークとスタンリー・キューブリックは、2001年(もうだいぶ昔に感じますが...)には人間並みか人間を越えた知性を持ったマシンが存在するだろうと想像していました。彼らが創造したHAL 9000は、当時(最初のAIブームの頃)のAI研究者が2001年には存在するだろうと予測していたものだったのです。(このエピソードだけでもだいぶ楽観的だなという感じがします..)これに対して、マービン・ミンスキーは、「 そこで問題は、なぜ我々は2001年になってもHALを実現していないのかだ 」と問題提起したのでした。 実用Common Lisp 1991年にピーター・ノーヴィグによって発表されたこちらの書籍は、人工知能とCommon Lispにおける古典と位置づけられている名著です。 この本が扱っているトピックは、人工知能(AI)、コンピュータプログラミング技術、プログラミング言語Common Lispの3つです。この本をていねいに読めば、AIに対する多くの疑問が解けると同時に、重要なAIの技法も理解できます。実例として、AIの研究で実際に使用されるテキストが多く含まれています。これらは、かつてのAI領域の重要な問題を解決へと導いた応用範囲の広い技法を使用したプログラムの一部でもあります。GPS問題、ELIZA、エキスパートシステムなどAIに2度目の春をもたらした当時の息吹を感じとることができます。 実用Common Lisp 作者: ピーター・ノーヴィグ 出版社/メーカー: 翔泳社 発売日: 2015/06/02 メディア: Kindle版 Deep Learningの芽生え 2度目の冬は果てしなく長く続きました。 そしてようやく2000年代となり、今ではよく知られている制限ボルツマンマシンやコントラスティブ・ダイバージェンスの提案が徐々に行われ始めました。これらの提案によって、あのディープラーニングの発明に向かう道筋がつくられていくことになっていくのでした。そしてついに、2006年に ジェフリー・ヒントン によるオートエンコーダを利用したディープラーニングが発明されました。この発明は支持され、とくに人手を介さず特徴量を抽出できる点で、人間による知識表現の必要が無くなり、近年のAIにおける大きなブレイクスルーをもたらすこととなるのでした。そしてこの瞬間に、長らく暗黒時代を迎えていたコネクショニズムが突如として復活することとなりました。また同時に、人間が知識表現を行うことで生じていた記号設置問題も解決されたのでした。 ハード面でも、再び春の到来を待ちわびていたかのように、2010年頃にはインターネット上のデータ転送量の指数関数的な増大を受けて、ビッグデータという用語が誕生しそれを取り扱うハードウェアの発展への兆しを見せ始め、そしてまた後を追うようにして、2012年の物体の認識率を競うILSVRCにおける、GPU利用による大規模ディープラーニング(ジェフリー・ヒントンの率いる研究チームがAlex-netで出場した)の大幅な躍進があり、同年のGoogleによるディープラーニングを用いたYouTube画像からの猫の認識成功の発表など、世界各国において再び人工知能研究に注目がぐっと集まり始めたのでした。これ以降のディープラーニングの目覚ましい発展については記憶に新しいところです。 こちらの図はガートナー社が今年発表した「先進テクノロジーハイプサイクル」です。ディープラーニングを見ると、過度な期待のピーク期にあるとされていて、いずれは幻滅期へと向かうとされています。しかし、AIは幾度の春と冬とを乗り越えた結果として現在に至り、轍を踏むことなく過度な期待を慎重に実用に転換してきました。「ディープラーニング活用の教科書」の中で、ディープラーニングは今後『ジェネラル・パーパス・テクノロジー(GPT)となっていく』という松尾先生の視点が示されています。GPTというのは、汎用的な目的に利用できる技術のことで、古くは車輪の開発や内燃機関の発明から、最近ではインターネットやナノテクノロジーといった汎用的な技術を指します。かつて2度目の冬では、あえてAI研究を隠していたわけですが、そういうネガティブな意味ではなく、ニック・ボストロムのいう「AIとは呼ばれなくなり、一般のアプリケーションに浸透していく」状態がごく自然な形で浸透して、この先の20年では春や冬という尺度ではないより高次元の汎用的な技術の一つとして扱われることで、これまで以上の驚くべき発展を遂げていくのではないでしょうか? ディープラーニング活用の教科書 作者: 出版社/メーカー: 日経BP 発売日: 2018/10/25 メディア: 単行本 まとめ・そしてHylangという選択 いかがでしたでしょうか? 「Lispの車窓から見た人工知能」ということで、現在における人工知能の源流から現在に至るまでの風景をLispというプログラミング言語の視点からご紹介してきました。Lispと人工知能がどちらもジョン・マッカーシーを父に持つ存在として誕生し、幾度となく春と冬の季節の移り変わりを経て、Lispマシン=AIと認知されていた頃に比べると、今ではLispは数あるプログラミング言語の一つとして、人工知能はGPTになりうる技術として、それぞれ別々の道を進むこととなりました(もちろんLispで機械学習のプログラミングができないという意味ではないです)。マッカーシーを始めとした多くの先人たちは、天国から今の様子をどんな思いで眺めていらっしゃることでしょうか。この先、HAL 9000が誕生する日は果たしてやってくるんでしょうか。今回こうしてLispの車窓から人工知能の風景を眺めてみたことで、ぼく自身も広義においてはAI分野を生業にしている端くれである以上、今後も近い場所でこの発展を眺めていけるように日々技術の研鑽や知識のアップデートに努めていきたいと改めて感じることができてとても有意義であったと思っています。 Hylang おまけですが、それでもS式を書きたい!そういう方のための選択肢として Hylang をご提案したいと思います。「Lisp and Python should love each other.」ということで、pythonの資産をすべからくS式で利用できるというプログラミング言語なっています。 github.com ↓こちらから面白いreplが試せます。(powered by Symbolics, Inc.!!) try-hylang インストールはpipで普通にできます。 pip install hy jupyter notebook のカーネルとしてhyを利用したい場合はcalysto_hyをインストールします。 pip3 install git+https://github.com/ekaschalk/jedhy.git pip3 install git+https://github.com/Calysto/calysto_hy.git python3 -m calysto_hy install Common Lispの例で示した階乗計算も、hyではこんな感じで同じように書けます。 ( defn fact [n] ( if ( = n 0 ) 1 ( * n ( fact ( - n 1 ))))) マクロの定義もこのように書けます。 ( defmacro incf [var &optional [diff 1 ]] `( setv ~var ( + ~var ~diff ) ) ) ( defn plus [&rest args] ( let (( sum 0 )) ( for [i args] ( incf sum i )) sum )) letがないのでletをマクロで追加します。 ( defmacro let [var-pairs &rest body] ( setv var-names ( list ( map first var-pairs )) var-vals ( list ( map second var-pairs ))) `( ( fn [~@var-names] ~@body ) ~@var-vals ) ) ↓詳しくはチュートリアルを参照してください。 Tutorial — hy 0.18.0 documentation では、簡単なMNISTをTensorflowでトレーニングするチュートリアル・プログラムをちょっとだけhyで示したいと思います。 まず、importから ( import [tensorflow.examples.tutorials.mnist [input_data]] ) ( import [tensorflow :as tf] ) MNISTデータを取得します。 ( setv mnist ( input_data.read_data_sets "MNIST_data/" :one_hot True )) 設定やデータ加工をもろもろおこないまして、 ( setv sess ( tf.InteractiveSession )) ( setv x ( tf.placeholder tf.float32 [None 784 ] )) ( setv y_ ( tf.placeholder tf.float32 [None 10 ] )) ( setv W ( tf.Variable ( tf.zeros [784 10 ] ))) ( setv b ( tf.Variable ( tf.zeros 10 ))) ( sess.run ( tf.global_variables_initializer )) ( setv y ( + ( tf.matmul x W ) b )) ( setv cross_entropy ( tf.reduce_mean ( tf.nn.softmax_cross_entropy_with_logits :labels y_ :logits y ))) ( setv train_step ( .minimize ( tf.train.GradientDescentOptimizer 0.5 ) cross_entropy )) ( for [_ ( range 1000 ) ] ( setv batch ( mnist.train.next_batch 100 )) ( sess.run train_step :feed_dict {x ( get batch 0 ) y_ ( get batch 1 ) } )) ( setv correct_prediction ( tf.equal ( tf.argmax y 1 ) ( tf.argmax y_ 1 ))) ( setv accuracy ( tf.reduce_mean ( tf.cast correct_prediction tf.float32 ))) ;; (print (accuracy.eval :feed_dict {x mnist.test.images y_ mnist.test.labels})) 必要な関数をいくつか定義します。 ( defn weight_variable [shape] ( setv initial ( tf.truncated_normal shape :stddev 0.1 )) ( tf.Variable initial )) ( defn bias_variable [shape] ( setv initial ( tf.constant 0.1 :shape shape )) ( tf.Variable initial )) ( defn conv2d [x W] ( tf.nn.conv2d x W :strides [1 1 1 1 ] :padding "SAME" )) ( defn max_pool_2x2 [x] ( tf.nn.max_pool x :ksize [1 2 2 1 ] :strides [1 2 2 1 ] :padding "SAME" )) 関数を用いて変数にデータを入れていきます。 ( setv W_conv1 ( weight_variable [5 5 1 32 ] )) ( setv b_conv1 ( bias_variable [32] )) ( setv x_image ( tf.reshape x [-1 28 28 1 ] )) ( setv h_conv1 ( tf.nn.relu ( + ( conv2d x_image W_conv1 ) b_conv1 ))) ( setv h_pool1 ( max_pool_2x2 h_conv1 )) ( setv W_conv2 ( weight_variable [5 5 32 64 ] )) ( setv b_conv2 ( bias_variable [64] )) ( setv h_conv2 ( tf.nn.relu ( + ( conv2d h_pool1 W_conv2 ) b_conv2 ))) ( setv h_pool2 ( max_pool_2x2 h_conv2 )) ( setv W_fc1 ( weight_variable [ ( * 7 7 64 ) 1024 ] )) ( setv b_fc1 ( bias_variable [1024] )) ( setv h_pool2_flat ( tf.reshape h_pool2 [-1 ( * 7 7 64 ) ] )) ( setv h_fc1 ( tf.nn.relu ( + ( tf.matmul h_pool2_flat W_fc1 ) b_fc1 ))) ( setv keep_prob ( tf.placeholder tf.float32 )) ( setv h_fc1_drop ( tf.nn.dropout h_fc1 keep_prob )) ( setv W_fc2 ( weight_variable [1024 10 ] )) ( setv b_fc2 ( bias_variable [10] )) ( setv y_conv ( + ( tf.matmul h_fc1_drop W_fc2 ) b_fc2 )) ( setv cross_entropy ( tf.reduce_mean ( tf.nn.softmax_cross_entropy_with_logits :labels y_ :logits y_conv ))) ( setv train_step ( .minimize ( tf.train.AdamOptimizer 1e-4 ) cross_entropy )) ( setv correct_prediction ( tf.equal ( tf.argmax y_conv 1 ) ( tf.argmax y_ 1 ))) ( setv accuracy ( tf.reduce_mean ( tf.cast correct_prediction tf.float32 ))) そして、最後にトレーニングを実行します。pythonで書く場合とほとんど同じですね。 ( with ( sess ( tf.Session )) ( sess.run ( tf.global_variables_initializer )) ( for [i ( range 20000 ) ] ( setv batch ( mnist.train.next_batch 50 )) ( when ( = ( % i 100 ) 0 ) ( setv train_accuracy ( accuracy.eval :feed_dict {x ( get batch 0 ) y_ ( get batch 1 ) keep_prob 1.0 } )) ( print ( .format "step {0}, training accuracy {1:.2f}" i train_accuracy ))) ( train_step.run :feed_dict {x ( get batch 0 ) y_ ( get batch 1 ) keep_prob 0.5 } )) ( print ( .format "test accuracy {:.3f}" ( accuracy.eval :feed_dict {x mnist.test.images y_ mnist.test.labels keep_prob 1.0 } )))) さいごに 明日は、お待ちかね弊社CTO大竹が「越境型スキルのすゝめ」というタイトルで投稿します! お楽しみに!
アバター
この記事はdely Advent Calendar 2018の21日目の記事です。 Qiita: https://qiita.com/advent-calendar/2018/dely Adventar: https://adventar.org/calendars/3535 前日は、iOSデザインエンジニアの John が デザインについてエンジニアなりに意識していること という記事を書きました。 デザイナーとエンジニアがどううまく連携していくかがわかりやすく書かれています。 はじめに こんにちは、delyでサーバサイドエンジニアをやっている山野井といいます。 普段の業務ではweb版 kurashiru( www.kurashiru.com )のサーバサイド周りを主に担当していて、たまにフロントエンドを触ったりもしています。 web版 kurashiruでは Javascript のフレームワークに Vue.js を使用しています。 通常、Vue.jsは内部実装をそこまで深く理解する必要がなくとも十分に扱えますが、自分のスキルアップの為や、普段の業務で何かヒントになりそうだと思い調べてみました。 本記事では、算出プロパティ(以下computedプロパティ)がどのように結果をキャッシュして依存された値の変更を検知しているか調べて分かったことを、解説していきたいと思います。 (注)解釈が間違っている可能性もございますが、温かい目で見守っていただけると嬉しいです。   今回解説に使用するサンプルコード 今回は、下記のサンプルコードをベースに解説していきます。 Vue.js@2.5.17 このコードは、マウントしたエレメント(#sampleApp)に対して computed_message を表示するようなシンプルなサンプルになっています。 < html >    < head >      < script src = "https://cdn.jsdelivr.net/npm/vue@2.5.17/dist/vue.js" ></ script >    </ head >    < body >      < div id = "sampleApp" ></ div >          < script >        var vue = new Vue ( {         data: {           message: 'Hello, World'          } ,         computed: {           computed_message () {              return `Computed $ {this .message } `            }          } ,         render ( h ) {            return h ( 'div' , this .computed_message )          }        } )       vue.$mount ( '#sampleApp' )      </ script >    </ body > </ html > このコードをブラウザで開くと、'Computed Hello, World'という文字列がブラウザ上に表示されます。 作成された Vueインスタンス の $data.message を devTool 等で書き換えると、同時に computed_message の値も更新され、画面上の文字も更新されることがわかると思います。 computedプロパティの基本の動き computedプロパティは作成されると Vueインスタンス に同名のプロパティが定義されます。 サンプルコードでは computed_message が定義されているため、Vueインスタンスに computed_message という名前のプロパティが生えます。 (this.computed_message として Vueインスタンス内からアクセスすることができるようになります。) また computedプロパティ は一度アクセスされると、依存されたプロパティが変更されない限りは常に同じ値を返し続けます。 つまりどういうことかというと、computed_message は一度実行された後その結果("Computed Hello, World")を内部で保持しておき、2回目以降はその保持した結果を返し続けます。 これにより、高速に処理を行うことができる様になっています。 依存しているプロパティ(message)の値が "Hello, World" から "hogehoge" に変更されたら結果を更新する必要があるため、再度関数を評価し、その結果("Computed hogehoge")を保持し、返しているわけです。​ そのため computedプロパティ は依存しているプロパティの変更を逐一知る必要があります。 computedプロパティが data の変更を知る仕組み Vue.js ではこの変更を知る仕組みをデザインパターンで言うオブザーバーパターンで実装されていて、dataプロパティ の各値が更新されると Watcherクラス(src/core/observer/watcher.js)  のインスタンス(以下watcher)へと通知される仕組みになっています。 各 computedプロパティ は  Vueインスタンス を生成する際にこの watcher を生成しています。 今回のサンプルコードでは computedプロパティ である computed_message が watcher を生成し、message の変更はこの watcher へと通知されます。 通知を受け取った watcher は update() を実行します。 update() 内では dirty を true にして依存されたプロパティに変更があった事を保持しておきます。 export default class Watcher {   ...      update () {      /* istanbul ignore else */      if ( this .lazy) {         this .dirty = true      } else if ( this .sync) {        this .run()      } else {       queueWatcher( this )      }    }     ... }   今回 message の変更を通知する先は computed_message が所持する watcher のみになりますが、message に依存している複数の computedプロパティ が定義されている場合は全ての watcher に対して mesage の変更を通知させる必要があります。 そこで依存関係を構築する Depクラス(src/core/observer/dep.js ) が登場します。 Depクラス は下記の様な、ユニークidと subs という watcher の配列を持ったクラスになります。   export default class Dep {     static target: ?Watcher;     id: number;     subs: Array <Watcher>;       ...     ...     ...        notify () {       const subs = this .subs.slice();        for ( let i = 0, l = subs.length; i < l; i++) {         subs [ i ] .update();        }      }    }  data の各プロパティは Vueインスタンス を生成する際に Depクラスインスタンス(以下dep) を生成します。 サンプルコードでは message が data として定義されているため、message の dep が生成されます。 この dep の subs に 通知すべきwatcher を格納することで data と 複数の watcher の対応関係を作ることができます。  data(message) が更新されると生成された dep の notify() を通して依存している全ての watcher へ通知されます。 dataプロパティの変更を検知する  そもそも dataプロパティ の変更の検知自体はどの様に行われているかと言うと Object.defineProperty を用いて実現されています。 Object.defineProperty を使用すると任意のオブジェクトに対して独自の getter や setter を生やすことができます。 data = {} Object .defineProperty(data, 'message' , {   enumberable: true ,   configurable: true ,   get: function () {     console.log( 'called getter' )      return this ._message    } ,   set: function (value) {     console.log( 'called setter' )      this ._message = value      return    } } );   data.message = 1 // called setter data.message  // called getter これを使用して、各dataプロパティの setter に 値をセットした後にwatcher に通知する処理を書いてあげることで値の変更通知処理を実現することができます。 実際のコードは下の様になっています。 src/core/observer/index.js export function defineReactive (   obj: Object ,   key: string,   val: any,   customSetter?: ? Function ,   shallow?: boolean ) {    const dep = new Dep()      const property = Object .getOwnPropertyDescriptor(obj, key)    if (property && property.configurable === false ) {      return    }      // cater for pre-defined getter/setters    const getter = property && property.get    const setter = property && property.set      let childOb = !shallow && observe(val)    Object .defineProperty(obj, key, {     enumerable: true ,     configurable: true ,     get: function reactiveGetter () {        const value = getter ? getter.call(obj) : val        if (Dep.target) {         dep.depend()          if (childOb) {           childOb.dep.depend()            if ( Array .isArray(value)) {             dependArray(value)            }          }        }        return value      } ,     set: function reactiveSetter (newVal) {        const value = getter ? getter.call(obj) : val        /* eslint-disable no-self-compare */        if (newVal === value || (newVal !== newVal && value !== value)) {          return        }        /* eslint-enable no-self-compare */        if (process.env.NODE_ENV !== 'production' && customSetter) {         customSetter()        }        if (setter) {         setter.call(obj, newVal)        } else {         val = newVal        }       childOb = !shallow && observe(newVal)       dep.notify()      }    } ) } setter である reactiveSetter の中で dep.notify() を呼んでいるのが分かると思います。 依存関係の構築 ここまでで message が変更された時、message の持つ dep を通して subs に対して通知を送れる様になりました。 最後に computedプロパティ と dataプロパティ がどのようにして依存関係を構築しているのかを見ていきます。 これは watcher が computedプロパティ の getter を評価する時と、先程の dataプロパティ の getter である reactiveGetter 内に出てきた dep.depend() が関係してきます。 まずは computedプロパティ の初期化の部分から追ってみます。  computedプロパティ の初期化は Vueインスタンス の初期化フローにて行われます。 src/core/instance/state.js function initComputed (vm: Component, computed: Object ) {    const watchers = vm._computedWatchers = Object .create( null )    // computed properties are just getters during SSR    const isSSR = isServerRendering()      for ( const key in computed) {      const userDef = computed [ key ]      const getter = typeof userDef === 'function' ? userDef : userDef.get      if (process.env.NODE_ENV !== 'production' && getter == null ) {       warn(         `Getter is missing for computed property "${key}" .`,         vm       )      }        if (!isSSR) {        // create internal watcher for the computed property.       watchers [ key ] = new Watcher(         vm,         getter || noop,         noop,         computedWatcherOptions       )      }        // component-defined computed properties are already defined on the      // component prototype. We only need to define computed properties defined      // at instantiation here.      if (!(key in vm)) {       defineComputed(vm, key, userDef)      } else if (process.env.NODE_ENV !== 'production' ) {        if (key in vm.$data) {         warn(`The computed property "${key}" is already defined in data.`, vm)        } else if (vm.$options.props && key in vm.$options.props) {         warn(`The computed property "${key}" is already defined as a prop.`, vm)        }      }    } }   for文で定義されている computedプロパティ を1つずつ取り出して Watcherインスタンス を作成しています。  Watcher を new する際に Vueインスタンス である vm , computedプロパティ の関数本体である getter , その他オプションを渡しています。 その後呼び出されている defineComputed のコードは以下になります。 export function defineComputed (   target: any,   key: string,   userDef: Object | Function ) {    const shouldCache = !isServerRendering()    if ( typeof userDef === 'function' ) {     sharedPropertyDefinition.get = shouldCache       ? createComputedGetter(key)       : userDef     sharedPropertyDefinition.set = noop    } else {     sharedPropertyDefinition.get = userDef.get       ? shouldCache && userDef.cache !== false         ? createComputedGetter(key)         : userDef.get       : noop     sharedPropertyDefinition.set = userDef.set       ? userDef.set       : noop    }    if (process.env.NODE_ENV !== 'production' &&       sharedPropertyDefinition.set === noop) {     sharedPropertyDefinition.set = function () {       warn(         `Computed property "${key}" was assigned to but it has no setter.`,          this       )      }    }    Object .defineProperty(target, key, sharedPropertyDefinition) }   function createComputedGetter (key) {    return function computedGetter () {      const watcher = this ._computedWatchers && this ._computedWatchers [ key ]      if (watcher) {        if (watcher.dirty) {         watcher.evaluate()        }        if (Dep.target) {         watcher.depend()        }        return watcher.value      }    } }   先程登場した Object.defineProperty で target(vm) に対して computedメソッド名 をキーにしてプロパティを定義しています。 これは'computedプロパティの基本の動き'で述べた computedプロパティは作成されると Vueインスタンス に同名のプロパティが定義されます。 の正体です。 さてこれで Vueインスタンス内から this.computed_message と呼び出すことができるようになったのですが this.computed_messageと呼び出された時の処理は Object.defineProperty によって以下のようになっていますね。 sharedPropertyDefinition.get = userDef.get       ? shouldCache && userDef.cache !== false         ? createComputedGetter(key)         : userDef.get       : noop   function createComputedGetter (key) {    return function computedGetter () {      const watcher = this ._computedWatchers && this ._computedWatchers [ key ]      if (watcher) {        if (watcher.dirty) {         watcher.evaluate()        }        if (Dep.target) {         watcher.depend()        }        return watcher.value      }    } } watcher.dirty の場合 watcher.evaluate() を実行しています。 watcher.evaluate() は watcher の作成時に渡したgetter(computedプロパティの関数本体)を評価し、結果を value に代入しています。 一度実行した結果を value に保持していて、 watcher.dirty な状態にならない限りは watcher.value を返し続けています。 こちらも最初に述べました また computedプロパティ は一度アクセスされると、依存されたプロパティが変更されない限りは常に同じ値を返し続けます。 の部分になります。 このdirtyがtrueになる時というのは watcher.update() が呼び出された時、つまり dataプロパティ に変更があり、 watcher へ通知された時となります。 下の図は message に変更があった時に this.computed_message が呼び出された時の様子です。 watcher.evaluate() で行っている処理は大きく分けて以下の4ステップになります。 pushTarget getter.call popTarget dirty=falseにする 1.pushTarget staticな値 Dep.target に対してwatcher自身を代入します。 2.getter.call getter.call をしてcomputedプロパティの関数を評価します。 関数を評価するということはその中で this.message が呼び出され、this.message の getterの中に定義されていた dep.depend() も呼び出されます。 ...   depend () {   if (Dep.target) {     Dep.target.addDep( this )   } } ... dep.depend() は Dep.Target が存在する時に、 Dep.target の addDep() を自身を引数にして呼び出します。 初めに Dep.target にはステップ1にて watcher を格納していたため、その watcher の addDep() が呼び出されます。 src/core/observer/watcher.js class Watcher { ...  addDep (dep: Dep) {   const id = dep.id   if (! this .newDepIds.has(id)) {     this .newDepIds.add(id)     this .newDeps.push(dep)     if (! this .depIds.has(id)) {       dep.addSub( this )     }   } } .... } src/core/observer/dep.js class Dep { ... addSub (sub: Watcher) {   this .subs.push(sub) } ... } 渡された dep が watcher の newDepIds にまだ存在しない場合は追加して、その後 dep.addSub(this) としています。 これで dep.subs = [watcher] の依存関係を作ることができました。 例えば  computed_message が次の様に定義されているとすると new Vue( { data: {    message: 'Hello, World' ,     name: 'tarou' ,    } ,   computed: {    computed_message() {       return `Computed $ {this .message } by $ {this .name } `       }    } , } ) 下の図のようにそれそれのdepがwatcherと依存関係を構築します。 そして以下の様な依存関係が構築されることになります。 message => dep => [watcher] name    => dep => [watcher] 1つのdataプロパティが複数の computedプロパティ に依存している場合は new Vue( { data: {    message: 'Hello, World' ,   } ,   computed: {    computed_message() {      return `Computed $ {this .message } `      } ,     introduction() {     return `My First $ {this .message } `     } ,    } , } ) 以下の様な依存関係になります。 message => dep => [watcher(computed_Message), watcher(introduction)] この様にして Vue.js では computedプロパティ の依存関係を構築しています。 3.popTarget Dep.targetに代入されいてたwatcherを取り除きます。 4.dirty = false にする 次回 this.computed_message にアクセスがあった時に計算済みの値 value を返すようにするため、dirtyをfalseにします。 watcher.evaluate() の実行を経て、 data プロパティと watcher の依存関係を構築することができました。 まとめ 長くなりましたが、解説は以上となります。 実際にソースコードレベルで調べることでよりフレームワークへの理解を深めることができたような気がします。 Vue.js 3.0では今回解説したObject.definePropertyを用いた監視方法から変更になるようなので機会があればそちらも解説できたらと思います。 最後までお付き合いありがとうございました。 明日は、弊社の機械学習エンジニアの辻より'Lispの車窓から見る人工知能'の記事がアップされる予定です。 こちらもぜひご覧ください!  
アバター
本記事はdely Advent Calendar 2018の19日目の記事です。 Qiita : dely Advent Calendar 2018 - Qiita Adventar : dely Advent Calendar 2018 - Adventar 前日は、弊社でSREをしている井上がkurashiruのデプロイについて記事を書きましたので是非読んでみてください! tech.dely.jp はじめに こんにちは。サーバーサイドエンジニア兼、新米slackBot整備士のjoe (@joooee0000) です。 前回の11日目の記事では、くだらないslackBotを作るモチベーションやBotアプリの作成方法について書かせていただきました。 tech.dely.jp 本記事では、 InteractiveMessages (InteractiveComponents)を用いた哲学slackBotの実装面について紹介させていただきます。 また、slackでは ボタン プルダウンボックス アクション をつけることができますが、今回はボタンを使っています。 (こちらのような見た目です) (深い。。。) 機能としては、 数百種類の哲学名言の中から毎朝9:30に1つをつぶやく メンバーがボタンを押して哲学を評価する 誰がどのように評価をしたかがslackにポストされる というシンプルなBotです。これだけでも、みんなが参加してくれて盛り上がったりします! 1つの哲学に対してみんなの受け取り方が全く違ったりするので、今やとても興味深いです。 構成図 インタラクティブなBotを構成する要素は二種類あります。 ボタン付きのメッセージをチャンネルにポストする ボタン付きのメッセージへのユーザーのリアクションを受け取り、処理する こちらの二種それぞれについて書いていきます。 また、slackBotの実装を始めるには、slackのアプリを作成します。アプリの作成に関しては、 11日目の記事 に書いてあるので、参照してください。 ボタン付きメッセージをポストする 最初に、ボタン付きメッセージを決まった時間にチャンネルにポストする部分について書きます。 組み合わせは、下記のシンプルな構成です。 Go言語 DynamoDB AWS SAM AWS Lambda AWS CloudWatch 哲学名言はクロールしてDynamoDBにデータを保持しています。クロールの部分やDynamoDBへのデータ保持について話すと長くなってしまうので、あらかじめデータが入ったDynamoDBが用意されている前提で話します。 1. Go言語でメッセージをポストするコードを書く コードは、Lambdaで実行する用に書いていきます。 こちらが簡略化したサンプルコードです。長くなりすぎないように、DynamoDBから哲学用語を取得する部分などは省略しています。 package main import ( "errors" "log" "math/rand" "strconv" "syscall" "time" "github.com/aws/aws-lambda-go/lambda" "github.com/aws/aws-sdk-go/aws" "github.com/aws/aws-sdk-go/aws/session" "github.com/aws/aws-sdk-go/service/dynamodb" "github.com/aws/aws-sdk-go/service/dynamodb/dynamodbattribute" "github.com/nlopes/slack" ) type PhilosophicalWord struct { PhilosophicalWordId int `dynamodbav:"philosophical_word_id"` FormedText string `dynamodbav:"formed_text"` PlainText string `dynamodbav:"plain_text"` ScholarName string `dynamodbav:"scholar_name"` } func getRandomPhilosophyWordByDynamoDB() (result *PhilosophicalWord, err error ) {    // DynamoDBから哲学名言を取得する処理 return } func getIconEmoji(scholarName string ) (emojiStr string ) {    // 絵文字の文字列を学者の名前から取得する処理 return func handleSendPayload(client *slack.Client, channelId string , sendText string , scholarName string ) error { actionName := "philosophical_value" attachment := slack.Attachment{ Color: "#42cbf4" , CallbackID: "philosophical" , Fields: []slack.AttachmentField{ { Title: "Please evaluate the word!! :smirk_cat:" , }, }, Actions: []slack.AttachmentAction{ { Name: actionName, Text: "すごく微妙" , Type: "button" , Style: "danger" , Value: "minus2" , }, { Name: actionName, Text: "微妙" , Type: "button" , Style: "danger" , Value: "minus1" , }, { Name: actionName, Text: "普通" , Type: "button" , Value: "zero" , }, { Name: actionName, Text: "良い" , Type: "button" , Style: "primary" , Value: "plus1" , }, { Name: actionName, Text: "すごく良い" , Type: "button" , Style: "primary" , Value: "plus2" , }, }, } params := slack.PostMessageParameters{ IconEmoji: getIconEmoji(scholarName), Username: scholarName, }    // メッセージの本文を定義する処理 msgOptText := slack.MsgOptionText(sendText, true )    // 必須項目を定義する処理 msgOptParams := slack.MsgOptionPostMessageParameters(params)    // ボタンを定義する処理 msgOptAttachment := slack.MsgOptionAttachments(attachment)    // メッセージ送信処理 if _, _, err := client.PostMessage(channelId, msgOptText, msgOptParams, msgOptAttachment); err != nil { log.Println( "Slack PostMessage Error" ) return err } return nil } func LambdaHandler() error { oauthAccessToken, found := syscall.Getenv( "OAUTH_ACCESS_TOKEN" ) if !found { log.Print( "OAuth Access Token Not Found" ) return errors.New( "OAuth Access Token Not Found" ) } channelId, found := syscall.Getenv( "CHANNEL_ID" ) if !found { log.Print( "Channel Id Not Found" ) return errors.New( "Channel Id Not Found" ) } result, err := getRandomPhilosophyWordByDynamoDB() if err != nil { log.Print( "DynamoDB Error" ) return err } sendText := result.FormedText scholarName := result.ScholarName client := slack.New(oauthAccessToken) handleSendPayload(client, channelId, sendText, scholarName) return nil } func main() { lambda.Start(LambdaHandler()) } slackのAPI Clientには、 nlopes/slack というpackageを使用しています。 今回はメセージをチャンネルに送ることが目的なので、こちらのpackageの中でも chat.go の PostMessage 関数を使ってslackチャンネルにメッセージを送信しています。 nlopes/slack の PostMessage は、要件をslackメッセージの要素ごとに分解して設定できるようになっています。 ざっくり言うと、 PostMessageParameters : メッセージ送信時の必須パラメータ部分(ピンク部分) ユーザー名やアイコンの設定など MsgOptionText : メッセージ本文(黄色部分) MsgOptionAttachments : アタッチメント部分(青部分) ボタンの設定 という構成です。 nlopes/slack では、上記であげたそれぞれの要素が構造体として定義されています。 PostMessage の引数は可変引数となっており、必要な型だけを引数として指定することができます。 例えば、ユーザー名やアイコンの設定はデフォルトでよく、ボタンも不必要なメッセージのみを送る場合は、 msgOptText := "適当" client.PostMessage( "your-channel-name" , msgOptText) だけでシンプルなメッセージを送信することができます。 また、アタッチメント部分(今回だとボタンを定義している部分)もいくつかの構造体の入れ子になっており、 AttachmentField (アタッチメント部分に記載できるテキストなど)や AttachmentAction (ボタンやプルダウンメニューなどのアクションの具体的な内容を指定するところ)を指定できるようになっています。ここら辺の値をやりたいことに対して柔軟に変えることでカスタマイズしていきます。 ここに書いたこと以外にも、 nlopes/slack をつかって様々なことができるので、詳しくはコードを読むか、 docs を参照してください。 2. AWS SAMで定期実行するLambdaを構築する AWS SAMの設定ファイルを作成する AWS SAMとは、サーバーレスアプリケーションモデルの略で、AWSでサーバーレスアプリケーションを構築するために使用することができるオープンソースフレームワークです。テンプレートに必要な情報を記入することで、サーバーレスアプリケーションの構築を手軽に行うことができます。(本当に手軽にできます) 下記のサンプルは、 Lambdaを動かすためのIAM Roleの作成 毎日朝9:30に稼働するメッセージポスト用Lambdaの設定(1で作成したLambdaコード) が記述されています。 哲学Botは、DynamoDBに哲学用語をためているため、DynamoDBへのアクセス権限もつけています。 また、ソースコードに載せられないセキュアな情報は環境変数にしてLambdaから呼び出すようにしています。 Systems Manager パラメータを使って設定した変数をLambdaのコード内から呼び出せるように、Parametersという項目を指定します。 今回は、 チャンネルID (slackのチャンネル名) OAuth Access Token (slackのAPIを呼び出すためのトークン) をパラメータ化して環境変数として呼び出せるようにしています。 Systems Manager パラメータの設定の仕方は、 こちら を参照してください。 # template.yml AWSTemplateFormatVersion : '2010-09-09' Transform : AWS::Serverless-2016-10-31 Description : Create Lambda function by using AWS SAM. Parameters : ChannelId : Type : AWS::SSM::Parameter::Value<String> Default : /philosophy_bot/channel_id OauthAccessToken : Type : AWS::SSM::Parameter::Value<String> Default : /philosophy_bot/oauth_access_token Resources : LambdaIamRole : Type : AWS::IAM::Role Properties : AssumeRolePolicyDocument : Version : "2012-10-17" Statement : - Effect : Allow Principal : Service : lambda.amazonaws.com Action : "sts:AssumeRole" Policies : - PolicyName : "philosophy-slack-bot" PolicyDocument : Version : "2012-10-17" Statement : - Effect : "Allow" Action : "dynamodb:*" Resource : "*" - Effect : "Allow" Action : "cloudwatch:*" Resource : "*" PhilosophySlackBot : Type : AWS::Serverless::Function Properties : Handler : philosophy-slack-bot Runtime : go1.x Role : !GetAtt LambdaIamRole.Arn Environment : Variables : CHANNEL_ID : !Ref ChannelId OAUTH_ACCESS_TOKEN : !Ref OauthAccessToken CodeUri : build Description : 'Post philosophical word to slack' Timeout : 30 Events : Timer : Type : Schedule Properties : Schedule : cron(30 0 * * ? *) # JST 09:30 デプロイする デプロイするにあたって、下記を済ませておく必要があります。 IAM Roleの設定 こちらで言う所のIAM Roleの設定は、AWS SAMを動かすための権限付与 ( 正しく解説できる自信がな 記事が長くなりすぎるため詳細の説明を省略) AWS CLIのセットアップ 詳しくはこちら 先ほど作成したAWS SAMのtemplate.ymlを使って、aws-cliのコマンドでデプロイすることができます。 # deploy.sh #!/usr/bin/env bash cd ./slack_bot GOARCH =amd64 GOOS =linux go build -o ../build/philosophy-slack-bot cd ../ zip build/philosophy-slack-bot.zip build/philosophy-slack-bot aws cloudformation package --profile yourprofile \ --template-file template.yml \ --s3-bucket serverless \ --s3-prefix philosophy-slack-bot \ --output-template-file .template.yml aws cloudformation deploy --profile yourprofile \ --template-file .template.yml \ --stack-name philosophy-slack-bot \ --capabilities CAPABILITY_IAM こちらのスクリプトでは、前半の部分でGoのコードのデプロイパッケージ(コードと依存関係で構成される .zip ファイル)を作成しています。 windowsにおけるGoのデプロイパッケージの作り方が少し違うようなので、デプロイを実行するOSによって書き方を変える必要があります。こちらのサンプルコードは、MacOSで作成する場合のサンプルとなっています。詳しくは、 こちら をご覧ください。 また、ここで紹介しているサンプルでは、下記のようなファイルの階層を想定しています。 . ├── build ├── deploy.sh ├── slack_bot │ └── main.go └── template.yml これで、deploy.shを実行するだけでLambdaが指定したevent通りの時間に定期実行されるようになります。 ここまでで、メッセージの見た目はslackチャンネルにポストできるようになりました!! (この時点では、ボタンを押してもなにも起こらない) ボタン付きメッセージへのユーザーのリアクションを受け取る ボタンをおしたらアクションが起こるようにしていきます。 1. Go言語でユーザーのリアクション情報を受け取るコードを書く ユーザーがボタンを押すと、設定したURLにPOSTリクエストが届きます。 なので、常にリクエストを待ち受けるAPIにしておく必要があります。 構成は、下記のようなシンプルなものです。 Go言語 DynamoDB AWS SAM AWS Lambda AWS API Gateway 最初に、API Gatewayにslackからリクエストがきた時のLambdaの処理を書いていきます。 処理は、下記のような手順で行います。 callbackレスポンスをParseする VerificationTokenをチェックする 結果を確認し、所望の処理をする リクエストのレスポンスとして、callbackレスポンスの中のoriginal_messageと同じ形式のレスポンスを返す 1. callbackレスポンスをParseする リクエストをJSON形式にParseすると、このような形式のレスポンスが返ってきます。 返ってきたレスポンスを、 nlopes/slack packageが定義してくれている slack.InteractionCallback 型にマッピングします。 { " type ":" interactive_message ", " actions ": [ { " name ":" philosophical_value ", " type ":" button ", " value ":" plus1 " } ] , " callback_id ":" philosophical ", " team ": { " id ":" xxx ", " domain ":" xxx " } , " channel ": { " id ":" xxx ", " name ":" xxx " } , " user ": { " id ":" xxx ", " name ":" joe " } , " action_ts ":" 1544247183.026560 ", " message_ts ":" 1544247178.002000 ", " attachment_id ":" 1 ", " token ":" DUMMYDUMMYDUMMY ", # VerificationToken " is_app_unfurl ": false , " original_message ": { " type ":" message ", " subtype ":" bot_message ", " text ":" 言い違い、聞き違い、読み違い、書き違いは受ける側の願望を表わしてる。 - ジークムント・フロイト- ", " ts ":" 1544247178.002000 ", " username ":" ジークムント・フロイト ", " icons ": { " emoji ":" :tetsu_freud: " } , " bot_id ":" xxxxxx ", " attachments ": [ { " callback_id ":" philosophical ", " id ": 1 , " color ":" 42cbf4 ", " fields ": [ { " title ":" please score the word!! :smirk_cat: ", " value ":"", " short ": false } ] , " actions ": [ { " id ":" 1 ", " name ":" philosophical_value ", " text ":" すごく微妙 ", " type ":" button ", " value ":" minus2 ", " style ":" danger " } , { " id ":" 2 ", " name ":" philosophical_value ", " text ":" 微妙 ", " type ":" button ", " value ":" minus1 ", " style ":" danger " } , { " id ":" 3 ", " name ":" philosophical_value ", " text ":" 普通 ", " type ":" button ", " value ":" zero ", " style ":"" } , { " id ":" 4 ", " name ":" philosophical_value ", " text ":" 良い ", " type ":" button ", " value ":" plus1 ", " style ":" primary " } , { " id ":" 5 ", " name ":" philosophical_value ", " text ":" すごく良い ", " type ":" button ", " value ":" plus2 ", " style ":" primary " } ] } ] } , " response_url ":" https: \/\/ hooks.slack.com \/ actions \/ DUMMYDUMMY \/ DUMMYDUMMY \/ DUMMYDUMMY ", " trigger_id ":" xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx " } 2. VerificationTokenをチェックする さきほどParseしたリクエストに、 token というkeyがはいっています。こちらのトークンを使って、不正なリクエストでないかを判定することができます。 こちらのtokenと作成したアプリの管理画面で参照できるVerificationTokenが一致するかをチェックします。 3. 結果を確認し、所望の処理をする 哲学Botは、DynamoDBにcallbackとして送られてきた評価を記録していますが、割愛します。 4. リクエストのレスポンスとして、callbackレスポンスの中のoriginal_messageと同じ形式のレスポンスを返す ParseしたJSONを見ると、original_messageという項目が返ってきているのがわかります。 こちらのJSONを希望のレスポンスに加工して返すことで、slackに反映させることができます。 また、response_type/replace_originalというkeyをoriginal_messageのJSONに追加して返すことで、下記のような様々な見た目のメッセージの反映ができます。 アクションしたユーザーだけが見れる(Only visible to you) response_type: ephemeral (default) メッセージの上書き response_type: in_channel replace_original: true 新しいメッセージのポスト response_type: in_channel レスポンスの選択肢に関しては こちら を参照しました。 サンプルコード 本記事で紹介している哲学Botのレスポンス形式は、ユーザーのアクションを検知したら、前のメッセージをうわ書かずに新しいメッセージをチャンネルにポストする方式です。 package main import ( "context" "encoding/json" "net/url" "strings" "github.com/aws/aws-lambda-go/events" "github.com/aws/aws-lambda-go/lambda" "github.com/nlopes/slack" ) func handleRequest(ctx context.Context, request events.APIGatewayProxyRequest) (events.APIGatewayProxyResponse, error ) {    // 1. callbackレスポンスをParseする str, _ := url.QueryUnescape(request.Body) str = strings.Replace(str, "payload=" , "" , 1 ) var message slack.InteractionCallback if err := json.Unmarshal([] byte (str), &message); err != nil { return events.APIGatewayProxyResponse{Body: "json error" , StatusCode: 500 }, nil }    // 2. VerificationTokenをチェックする verificationToken, found := syscall.Getenv( "VERIFICATION_TOKEN" ) if !found { return events.APIGatewayProxyResponse{Body: "NoVerificationTokenError" , StatusCode: 500 }, nil } if message.Token != verificationToken { return events.APIGatewayProxyResponse{Body: "InvalidVerificationTokenError" , StatusCode: 401 }, nil }    // 3. callbackの中身をみて所望の処理をする var score string value := message.ActionCallback.Actions[ 0 ].Value switch value { case "plus2" : score = "すごく良い" case "plus1" : score = "良い" case "zero" : score = "普通" case "minus1" : score = "微妙" case "minus2" : score = "すごく微妙" default : score = "0" }       // 4. リクエストのレスポンスとして、callbackレスポンスと同じ形式のレスポンスを返す userName := message.User.Name resMsg := userName + "さんが" + "「" + score + "」" + "と評価しました" orgMsg := message.OriginalMessage orgMsg.Text = ""    // 今回はメッセージを上書きせず、チャンネル全体に投稿する orgMsg.ResponseType = "in_channel" orgMsg.Attachments[ 0 ].Color = "#f4426e"    // ボタンを空にする orgMsg.Attachments[ 0 ].Actions = []slack.AttachmentAction{}    // 返したいレスポンスを定義する orgMsg.Attachments[ 0 ].Fields = []slack.AttachmentField{ { Title: resMsg, Value: "" , Short: false , }, } resJson, err := json.Marshal(&orgMsg) if err != nil { return events.APIGatewayProxyResponse{Body: "JsonError" , StatusCode: 500 }, nil } return events.APIGatewayProxyResponse{Body: string (resJson), StatusCode: 200 }, nil } func main() { lambda.Start(handleRequest) } 2. AWS SAMでslackからのリクエストを受け取るAPI Gatewayを構築する AWS SAMの設定ファイルを作成する こちらの設定ファイルは、先ほどのボタン付きメッセージをslackにポストする部分も一緒に含まれています。 前回と同様に、VerificationTokenなどのセキュアな情報はSystems Manager パラメータで設定したものを呼び出しています。 AWSTemplateFormatVersion : '2010-09-09' Transform : AWS::Serverless-2016-10-31 Description : Create Lambda function by using AWS SAM. Parameters : ChannelId : Type : AWS::SSM::Parameter::Value<String> Default : /philosophy_bot/channel_id OauthAccessToken : Type : AWS::SSM::Parameter::Value<String> Default : /philosophy_bot/oauth_access_token VerificationToken : Type : AWS::SSM::Parameter::Value<String> Default : /philosophy_bot/verification_token Resources : LambdaIamRole : Type : AWS::IAM::Role Properties : AssumeRolePolicyDocument : Version : "2012-10-17" Statement : - Effect : Allow Principal : Service : lambda.amazonaws.com Action : "sts:AssumeRole" Policies : - PolicyName : "philosophy-slack-bot" PolicyDocument : Version : "2012-10-17" Statement : - Effect : "Allow" Action : "dynamodb:*" Resource : "*" - Effect : "Allow" Action : "cloudwatch:*" Resource : "*" - Effect : "Allow" Action : - "logs:CreateLogGroup" - "logs:CreateLogStream" - "logs:DescribeLogGroups" - "logs:DescribeLogStreams" - "logs:PutLogEvents" - "logs:GetLogEvents" - "logs:FilterLogEvents" Resource : "*" # ボタンつきメッセージのポスト PhilosophySlackBot : Type : AWS::Serverless::Function Properties : Handler : philosophy-slack-bot Runtime : go1.x Role : !GetAtt LambdaIamRole.Arn Environment : Variables : CHANNEL_ID : !Ref ChannelId OAUTH_ACCESS_TOKEN : !Ref OauthAccessToken CodeUri : build Description : 'Post philosophical word to slack incoming webhooks' Timeout : 30 Events : Timer : Type : Schedule Properties : Schedule : cron(30 0 * * ? *) # JST 09:30 # ボタン付きメッセージのレスポンスAPI PhilosophySlackBotInteractiveApi : Type : AWS::Serverless::Function Properties : Handler : philosophy-slack-bot-interactive-api Runtime : go1.x CodeUri : build Timeout : 300 Role : !GetAtt LambdaIamRole.Arn Environment : Variables : VERIFICATION_TOKEN : !Ref VerificationToken Events : Post : Type : Api Properties : Path : /slack Method : post Lambdaを定期的に動かすタイプの設定と違うところは、 Events のところをAPI Gatewayの設定に変更するだけです。 Events : Post : Type : Api Properties : Path : /slack Method : post それだけで、API Gatewayが立ち上がり、pathに指定したエンドポイントにアクセスすることができます。 https://{restapi_id}.execute-api.{region}.amazonaws.com/{stage_name}/ このようなエンドポイントが、/Stage(ステージング用)と/Prod(プロダクション用)それぞれ用意されます。 エンドポイントはデプロイ後、AWSコンソールのAPI Gatewayの画面で確認することができます。 デプロイする こちらのデプロイスクリプトに関しても、ボタン付きメッセージをポストする部分が含まれています。 説明はボタン付きメッセージの時とほぼ一緒なので割愛しますが、2つのデプロイパッケージをつくって同時にデプロイすることが可能です。 #!/usr/bin/env bash cd ./slack_bot GOARCH =amd64 GOOS =linux go build -o ../build/philosophy-slack-bot cd ../ cd ./slack_interactive_api GOARCH =amd64 GOOS =linux go build -o ../build/philosophy-slack-bot-interactive-api cd ../ zip build/philosophy-slack-bot.zip build/philosophy-slack-bot zip build/philosophy-slack-bot-interactive-api.zip build/philosophy-slack-bot-interactive-api aws cloudformation package --profile yourprofile \ --template-file template.yml \ --s3-bucket serverless \ --s3-prefix philosophy-slack-bot \ --output-template-file .template.yml aws cloudformation deploy --profile youprofile \ --template-file .template.yml \ --stack-name philosophy-slack-bot \ --capabilities CAPABILITY_IAM 想定する階層構造 . ├── build │ ├── philosophy-slack-bot │ ├── philosophy-slack-bot-interactive-api │ ├── philosophy-slack-bot-interactive-api.zip │ └── philosophy-slack-bot.zip ├── deploy.sh ├── slack_bot │ └── main.go ├── slack_interactive_api │ └── main.go └── template.yml ./deploy.shをしていただければAPI Gateway/Lambdaに先ほど書いたコードがデプロイされます。 これで、晴れて、インタラクティブなslackBotが完成しました!! まとめ 初めてのことが多かったので色々な記事を参考にさせていただきました。 こちらの記事も、少しでもslackBotの運用をするきっかけとなれば幸いです。 今後の展望ですが、ランキング機能をつけるなどの拡張を考えるとRDBの方が使い勝手がいいので、近々Aurora Serverlessに載せ替えたいと思っています! 明日はデザインiOSエンジニアのJohnが「デザインについてエンジニアなりに意識していること」というタイトルで投稿します!お楽しみに!
アバター
はじめに 本記事はdely Advent Calendar 2018の18日目の記事です。 Qiita : dely Advent Calendar 2018 - Qiita Adventar : dely Advent Calendar 2018 - Adventar 昨日は弊社iOSエンジニアの堀口( @takaoh717 )が「エンジニアがCSと上手く連携するためのコミュニケーション」をタイトルに記事を書きましたので是非読んでみてください。 tech.dely.jp こんにちは!delyでSREをしている井上です。 本記事では現在kurashiruで運用しているECSとデプロイについてご紹介したいと思います。 delyではインフラにメインでAWSを利用しており、kurashiruのインフラは主にECSで構築されています。直近の半年では、より高速にデプロイが完了できるようにDockerのbuildを非同期で行ったり、コンテナインスタンスのCPUとメモリが効率的に使えるようにタスクの定義情報の最適化を行ったりなどいくつかの改善を行いましたが、現状のフェーズにおいては安定して運用できているので今回紹介したいと思います! EC2とECSについて イメージが伝わりやすくなるようにかなり簡略化していますが下記のような構成になっています。 コンテナインスタンス コンテナインスタンスはオンデマンドインスタンスとスポットインスタンスを併用することでコストを抑えています。 オンデマンドインスタンスのオートスケールはオートスケーリンググループを利用して行っており、スポットフリートのキャパシティはオンデマンドインスタンスの数に応じて増減するようにLambdaを使って制御しています。 また、スポットインスタンスは中断される可能性があるため、中断のイベントをCloudWatchで拾ってLambdaで該当のコンテナインスタンスをドレイン状態にするという制御を行っています。 https://docs.aws.amazon.com/ja_jp/AWSEC2/latest/UserGuide/spot-interruptions.html docs.aws.amazon.com ちなみにデータプレーンにEC2ではなくFargateを使うという選択肢もありますが、下記の理由から本格的な導入は行っていない状況です。 EC2と比較して料金が割高 起動が遅い(Dockerのレイヤキャッシュが効かない) CPU・メモリが柔軟に設定出来ない 特にコスト面はスポットと比較するとかなり割高になってしまうので、現状だと全てをFargateに乗り換えるという選択は厳しいです・・・ Fargateにもスポットの対応が待ち望まれますね! ECS ECSにおいては、WEBやAPIといったドメイン単位でターゲットグループを分けて構築しています。 また、ターゲットグループ毎にred, yellow, greenの3つでサービスを分けていて、タスク定義は同一のものを設定しています。 3つのサービスのうちgreenだけはオートスケール設定を行っていて、トラフィックに応じてタスク数が増減するようにしています。 また3つに分けることによって、デプロイ時に新バージョンをred -> yellow -> greenといったように段階的な反映を可能としており、red、yellowに反映させた状態でエラーの有無やログの確認を行い、問題がなければgreenに反映するという運用を行っています。 デプロイについて フロー こちらもイメージが伝わりやすいようにSNSなどは省略して記載しています。 CodePipeline デプロイは内部的にはCodePipelineを使って制御しています。 サービスredのデプロイはCodePipelineからECSに直接デプロイする機能を使っていますが、サービスyellow、greenはLambdaを使ってデプロイしています。3つのサービス全てに対してCodePipelineのECSデプロイ機能を利用した場合、タスク定義のバージョンが3つのサービスで一つずつずれてしまいます。そのためサービスredのタスク定義のバージョンと同じバージョンでデプロイするためにyellow、greenのデプロイはLambdaを使っています。 また各サービス(red, yellow, green)のデプロイの直前にCodePipelineの承認アクションを設定しています。これによって開発者がそれぞれのサービスにデプロイするタイミングをコントロール出来るようにしています。 Slackを利用したデプロイ デプロイは開発者がSlackのChatBotから行っています。 Slackを利用したデプロイの流れ(例)を紹介します。 開発者がSlackのチャンネルにおいて下記のように送信すると、 サービスredのデプロイ開始待ちになります。 デプロイを開始するタイミングをコントロールできるようにSlackのボタンでCodePipelineの承認アクションを制御しています。 Approveをクリックすると、 サービスredのデプロイが開始されます。 サービスredのデプロイが完了するとサービスyellowのデプロイ開始待ちになります。 このタイミングで新バージョンでのエラーや怪しいログが発生していないか確認します。 確認後、Approveをクリックすると、 サービスyellowのデプロイが開始されます。 サービスyellowのデプロイが完了するとサービスgreenのデプロイ開始待ちになります。 Approveをクリックすると、 サービスgreenのデプロイが開始されます。 デプロイが正常終了するとデプロイ完了の通知が届きます。 さいごに 簡単にはなりますがkurashiruで運用しているECSとデプロイについてご紹介しました。オーケストレーションツールはKubernetes一強ですが、現段階はECSで運用しています。組織やプロダクトの成長に伴って、インフラは何を求められ、提供できるのかを考え今後オーケストレーションツールに何を採用するのかを見極めていきたいと思っています。 ちなみに先日行われたAWS re:Invent 2018ではたくさんの機能追加や新しいサービスが紹介されました。その中でも今回紹介した構成の中で利用できそうなものがあったので抽出してみました! CodePipelineがソースをECRに対応 aws.amazon.com オートスケーリンググループ内でオンデマンドインスタンスとスポットインスタンスを混在させられる aws.amazon.com CodeDeployがECS対応 aws.amazon.com まだ手がつけられていないので、時間を見つけて検証し、より使いやすいデプロイ環境にしていこうと思っています! 明日は新米slackBot整備士のjoe (@joooee0000) が「サーバーレス+Go言語で作るインタラクティブな哲学slackBot」というタイトルで投稿します!お楽しみに!
アバター
この記事はdely Advent Calendar 2018の17日目の投稿です。 昨日は、プロダクトデザイナーのミカサ トシキ( @acke_red )が「 Fluid Interfaces実践 - なめらかなUIデザインを実現する 」というタイトルで投稿しました。 Qiita: https://qiita.com/advent-calendar/2018/dely Adventar: https://adventar.org/calendars/3535 はじめに こんにちは、delyでiOSエンジニアをしている堀口( @takaoh717 )です。 今日は、普段僕が行っているCS(カスタマーサクセス)担当者との取り組みについてご紹介しようと思います。 僕の普段のメイン業務はiOSアプリの開発ですが、CSの技術的なサポートも毎日行っています。 内容としては、主にクラシルの利用に関する問い合わせが来た際にCS担当者が分からないことの解説を行ったり対応方法を教えてあげたりしています。 今回は1年ほどCS担当者とのやり取りを行った中で、エンジニアがこういうことを意識してコミュニケーションすると良いなと感じたことを挙げてみたいと思います。 CSとのコミュニケーションで意識して良かったこと ボールを宙に浮かせない 弊社ではクラシルの問い合わせ対応においてはCS用のツールを特に使用していません。 現在は主に以下のツールのみで運用を行っています。 (掲載しているレシピに関する質問への対応は社内ツールを使用しています。) Gmail ユーザとのやり取り Slack 社員同士のやり取り Qiita:Team 対応テンプレ作成やナレッジの蓄積 基本的にはエンジニアとCS担当者とのやり取りはSlack上で行われますが、管理ツールを使用していないと、たまに以下のようなやり取りが発生します。 CS < ユーザからアプリが正しく動作しないと問い合わせが来ましたが、何か分かりますか? エンジニアA < うーん、こちらでは再現しませんね・・・ エンジニアB < 最近ここ特に触ってないですよね・・・ エンジニアA < そうですね、謎ですね・・・ このまま問題が放置されて、しばらく時間が経ってしまうということが以前はたまに起きてました。 これだと、CS担当者も対応が分からず不安なままですし、何よりユーザを待たせてしまいます。 このように、原因がすぐに特定できない場合は、CS担当者にその旨を伝えてユーザに返信をしてもらうようにします。 ↓改善後はこのようなコミュニケーションになります CS < ユーザからアプリが正しく動作しないと問い合わせが来ましたが、何か分かりますか? エンジニアA < うーん、こちらでは再現しませんね・・・ エンジニアB < 最近ここ特に触ってないですよね・・・ エンジニアA < じゃあ一旦ユーザには調査する旨を伝えてIssue作成しておきます。        @CS ユーザさんに調査する旨をお伝え下さい。 質問しやすい状態を作る CSの対応ではスピード感がとても重要だと思います。 内容にもよりますが、ユーザの問題の解決は早いに越したことはありません。 そして、素早い対応を行うには、躊躇せずに質問ができる状態を作っておくことが大切です。 そこで、どういう風にすれば素早く質問をしてもらえる状況を作れるかを考えて以下のようなことを行っています。 質問の仕方をフォーマット化する 質問したい問い合わせのユーザのメールアドレスのみをSlackに投稿し、スレッド内に聞きたいことを書く フォーマットが自由になっていると、「お忙しい所すみませんが・・・」みたいなやり取りが無駄に発生しがちです。簡潔なフォーマットが決まっていれば、無駄なやり取りに気を使う精神的コストや文章を考える負担をかけることがありません。 週1回は対面でのコミュニケーションの場を設ける 問い合わせが来たわけではないけれど気になっていることなどをここで質問してもらう オンラインのやり取りだと中途半端な理解で済ませがちなことも、きちんと納得がいくまで説明する機会が作れます。 サービスに関する説明は画面を見せながら説明すると理解しやすいことが多いです Slackでやり取りするときは絵文字や!などをなるべく使う テキストのみでの会話だと感情が読み取りにくいため、相手の顔色を伺いながら話しかけるような状態が生まれやすく、生産性の低下に繋がります。 対応方法だけではなく、きちんと仕組みを説明する 不具合があったときに、非エンジニアの人でも理解できる言葉を使って説明することも意識しています。 サーバー や データベース などの技術的な単語は使わずになるべく一般の人が理解できる言葉に置き換えながら説明を行います。 その一例として、クラシルで実際に起こった例をご紹介します。 起きたこと: iOSアプリの内部のデータベースの一部のデータの保存先を変更した際に、変更する予定じゃなかったデータ(お気に入り)の読み書き先も変わってしまい、データが表示されなくなってしまった。 この現象を担当者に理解してもらうために以下の説明をしました。 クラシルのお気に入りはアプリの中にデータを保存しています。 イメージとしては、アプリの内部にWindowsのマイドキュメントのようなデータを保存する仕組みがあると思ってください。 マイドキュメントの中には「お気に入りフォルダ」があります。ユーザがお気に入りボタンを押したときはレシピがこのお気に入りフォルダに入ります。 これが普段のクラシルの状態です。 今回のパターンを説明します。今回はお気に入りとは違うデータを保存しないといけなくなりました。 そこで、マイドキュメントの中に「新しいフォルダ」を作成して、ここにデータを保存するようにしました。 しかし、開発上のミスで、ユーザがお気に入り一覧画面を開いたときに、今までは「お気に入りフォルダ」を開いていたんですが、 「新しいフォルダ」を開くようになってしまいました。 これをちゃんと行うことで以下のような効果があると思います。 きちんと仕組みを理解してもらうことで、 納得感を持ってユーザに説明する ことができるようになる 問い合わせ対応の質(説明の内容やスピード)が向上する 問題がどういう状態のものか理解することで、 似ているけれど違う要因の問い合わせ がきたときに判別ができる 内容は同じだけど、問い合わせの文章が異なっていて同じ要因かどうか判別しづらいものが判別できるようになる 対応方法を共有する場合は、自分がその解に至った経緯やロジックを共有する 何らかのサポートをするときに解決方法を共有しただけでは、次に同じ問題が発生した場合に解決することができない可能性が高いです。 そのため、自分がどうやってその解を導き出したかという経緯も共有してあげると良いと思います。 例えば、こういう感じでコミュニケーションをしています。 「ユーザさんが「昨日から〜〜ができない」と仰っているので◯◯ではなくて、△△に該当すると思います」 「Slackやドキュメントで「〇〇」で検索をしたら、こういう結果が出てきたので△△の対応が適切だと思います」 こういった共有を行うことで、次に同じような問題が発生した際に対応方法は分からなくても調査を行うことができますし、 全く別の問題が発生した場合の調査方法の幅も広がります。 まとめ 以上、自分としてもまだまだ改善できることはあると思っていますが、やってみるとチーム的にもCSの対応としても良くなったんじゃないかなと思っています。 ここで書いた内容に関して、まとめると総じて重要なことは以下のことだと思います。 ・ちゃんと納得感を持った上でユーザへの対応行う ・素早く的確にユーザの問題を解決できるようにする 明日はSREの井上が投稿します。こちらもぜひご覧ください!
アバター
こんにちは、検索エンジニアのsakura ( @818uuu ) です。 先日、営業さん向けにSQL勉強会を行いました。開催してみて難しかったことや得た知見などを紹介します。 この記事はdely Advent Calendar 2018の14日目の記事です🎅🎄 Adventar : dely Advent Calendar 2018 - Adventar Qiita : dely Advent Calendar 2018 - Qiita 13日目の記事は、Androidエンジニア kenzoさんの「【Android】ViewPagerのページ切り替えをいい感じにする 」でした。ぜひ読んでみてください。 tech.dely.jp なぜやろうと思ったか 勉強会を開催しようと思ったきっかけは、営業さんの日報を見ていると「SQLや分析に興味がある」と時々書かれていてなにか助けになれないかなーと思ったからです。 エンジニア以外でもSQL学ぶことのメリットはこちらにまとまっていましたのでよければご覧ください。 paiza.hatenablog.com 行う目的やゴールを明確にする 勉強会の事前準備をしてる中で教えていただいたことがあります。 それは 「この勉強会の目的はなにか。ゴールをどういう指標にするのか。」 を明確にするということです。 そうすることで 、 ・参加者にとって参加するか/しないかを決める判断指標の1つになる ・スピーカーにとって何を伝えたらいいのかを明確にできる になるからです。 いままで個人で勉強会を開催したことはあるのですが、参加者のゴールは決めたことがなかったのでとても参考になりました。 これから勉強会を行う際も気をつけていこうと思います。 今回の勉強会のゴール 開催概要 勉強会には営業さんを中心に約10名にご参加していただきました🎊 そして、開発部やマーケティング部の方にご協力いただき計3名がスピーカーをしました。 参加者の皆さんは積極的に質問していただいたり、スピーカーの方はとても参考になる資料作成をしていただいたり、コーポレート部さんは自発的に勉強会の様子を動画で撮影してくださいました。 勉強会は色んな方の協力があってこそ成立するんだなと改めて思いました。 勉強会で紹介した資料の一部 知見まとめ 勉強会を開催したことによって学んだ知見をslackにまとめました。 いい知見をたくさん得ることができたので次の勉強会を開催する際に活かします。 これからのdely Advent Calendar 2018もぜひお楽しみにしていてください〜!
アバター
こんにちは。delyでAndroidのエンジニアをしているkenzoです。 この記事はdely Advent Calendar 2018の13日目の記事です。 Qiita:  https://qiita.com/advent-calendar/2018/dely Adventar:  https://adventar.org/calendars/3535   昨日はkurashiruのwebグロース全般を担当しているinternet_ghostがこちらの記事を書きました。 クラシルでのSEO施策についてや、外部の方が気になりそうなポイントについて書かれています。ぜひご覧ください! はじめに 今日は 先日の記事 に引き続き、AndroidアプリのViewPagerをいい感じの動きにしていく際にやったことをご紹介します。 今回はページ切り替え時の動きをいい感じにしていきたいと思います。 ※ 注意: 今回の記事を読んで試しに実装する場合は一旦下まで読んでから実装してください。上の方のコード使わなくていいかもなので。 ViewPagerのページ切り替えをいい感じに 先日の記事 で作成したサンプルアプリに少し機能を追加します。 ページを切り替えるボタン「←」「→」の設置と★のタップではじめに戻るようにします。 ページを切り替える処理を実装 ViewPager の setCurrentItem を使います。 star.setOnClickListener { viewPager.currentItem = 0 } これでページが切り替わるようになりました。 もう少しゆっくりページを切り替えたい ページが切り替わるようになりましたが、ちょっと切り替わるスピード早いですよね?ズビュンって感じ。 なので、少しゆっくり切り替わるようにします。 ViewPager を継承して CustomViewPager を作成します。 class CustomViewPager @JvmOverloads constructor ( context: Context, attrs: AttributeSet? = null ) : ViewPager(context, attrs) { companion object { private const val CUSTOM_DURATION: Int = 1000 } init { ViewPager:: class .java.getDeclaredField( "mScroller" ).run { isAccessible = true set ( this @CustomViewPager, CustomScroller(context)) } } private class CustomScroller(context: Context) : Scroller(context, FastOutSlowInInterpolator()) { override fun startScroll(startX: Int , startY: Int , dx: Int , dy: Int , duration: Int ) { super .startScroll(startX, startY, dx, dy, CUSTOM_DURATION) } } } ViewPager で使用される Scroller を自分で作成してセットします。 duration を1000msにしたので、1秒でページが切り替わるようになります。 これでページがゆっくり切り替わるようになりました。 スワイプするときは今までどおりにしたい ページがゆっくり切り替わるようになったと思ったら、今度はスワイプする際におかしな挙動をするようになってしまいました。 ボタンを押した時はゆっくり切り替わってほしいけど、スワイプする時は今までどおりに動いてほしいですよね。 なので、先程作成した CustomViewPager に手を入れて setCurrentItem を呼ぶ時だけゆっくり切り替わるようにします。 class CustomViewPager @JvmOverloads constructor ( context: Context, attrs: AttributeSet? = null ) : ViewPager(context, attrs) { companion object { private const val CUSTOM_DURATION: Int = 1000 } private val interpolator: CustomInterpolator = CustomInterpolator() private val scroller: CustomScroller = CustomScroller(context, interpolator) init { ViewPager:: class .java.getDeclaredField( "mScroller" ).run { isAccessible = true set ( this @CustomViewPager, scroller) } addOnPageChangeListener( object : OnPageChangeListener { override fun onPageScrollStateChanged(state: Int ) { when (state) { SCROLL_STATE_IDLE, SCROLL_STATE_DRAGGING -> { // ページ切り替えが終わったら、また、切り替え中にスワイプした際に元の挙動で切り替わるように interpolator.isCustom = false scroller.isCustom = false } } } override fun onPageScrolled(position: Int , positionOffset: Float , positionOffsetPixels: Int ) = Unit override fun onPageSelected(position: Int ) = Unit }) } override fun setCurrentItem(item: Int ) { interpolator.isCustom = true scroller.isCustom = true super .setCurrentItem(item) } private class CustomScroller(context: Context, interpolator: Interpolator) : Scroller(context, interpolator) { var isCustom: Boolean = false override fun startScroll(startX: Int , startY: Int , dx: Int , dy: Int , duration: Int ) { super .startScroll(startX, startY, dx, dy, if (isCustom) CUSTOM_DURATION else duration) } } private class CustomInterpolator : FastOutSlowInInterpolator() { private val default: Interpolator = Interpolator { input -> (input - 1 ).pow( 5 ) + 1 } // ViewPager内で作成されているInterpolatorをここで再実装 var isCustom: Boolean = false override fun getInterpolation(input: Float ): Float = if (isCustom) super .getInterpolation(input) else default.getInterpolation(input) } } ちょっと長くなってしまいましたが、こんな感じになるよう変更しています。 ゆっくりとデフォルト両方の動きができる Scroller と Interpolator を作成( isCustom が true でゆっくり) ボタンで切り替えを行うタイミングで isCustom を true に 切り替えが終わったら isCustom を false に ゆっくり切り替え中にスワイプした場合にもデフォルトの動きができるように isCustom を false に これで、ボタンで切り替える時はゆっくり、スワイプした時は今までどおりにページが切り替わるようになりました。 この実装でいいのか アプリの挙動だけ見ると、うまいこと動くようになりました。 しかし、今回作成した CustomViewPager はリフレクションで mScroller にアクセスしています。 ViewPager の中身の実装が変わって mScroller がなくなる可能性も0ではないので、できれば違う方法で実現したいところです。 *1 隣のページに移動するだけでいいなら 今度は先程作成した CustomViewPager ではなく ViewPager に戻し、別の実装をしてみます。 ViewPager の fakeDragBegin fakeDragEnd fakeDragBy を用いてページを切り替えます。 これを用いると、隣のページへのスワイプを模した動きをさせることができます。(本記事ではfake dragと呼ぶことにします) private var prevDragPosition = 0 override fun onCreate(savedInstanceState: Bundle?) { /* 省略 */ left.setOnClickListener { if (viewPager.currentItem > 0 ) fakeDrag( false ) } right.setOnClickListener { if (viewPager.currentItem + 1 < viewPager.adapter?.count ?: 0 ) fakeDrag( true ) } } private fun fakeDrag(forward: Boolean ) { if (prevDragPosition == 0 && viewPager.beginFakeDrag()) { ValueAnimator.ofInt( 0 , viewPager.width).apply { duration = 1000L interpolator = FastOutSlowInInterpolator() addListener( object : Animator.AnimatorListener { override fun onAnimationStart(animation: Animator?) = Unit override fun onAnimationEnd(animation: Animator?) { viewPager.endFakeDrag() prevDragPosition = 0 } override fun onAnimationCancel(animation: Animator?) { viewPager.endFakeDrag() prevDragPosition = 0 } override fun onAnimationRepeat(animation: Animator?) = Unit }) addUpdateListener { val dragPosition: Int = it.animatedValue as Int val dragOffset: Float = ((dragPosition - prevDragPosition) * if (forward) - 1 else 1 ).toFloat() prevDragPosition = dragPosition viewPager.fakeDragBy(dragOffset) } }.start() } } このようなことをしています。 beginFakeDrag でfake dragを始める いい感じの duration と interpolator の ValueAnimator を用意 fakeDragBy で少しずつスクロールさせる ViewPager の width 分のスクロールが終わったら endFakeDrag でfake dragを終了 このように CustomViewPager と同様の挙動で隣のページへ切り替えることができました。 おわりに 今回と前回使ったサンプルアプリのソースは こちら です。 先日の記事「 Androidにいい感じの動きをさせていく話(ViewPagerとその他Viewとの連動」 と合わせてみなさまのアプリ上のViewPagerの動き方をよりよいものにしていくための一助になれば幸いです。 明日は検索エンジニアsakuraの「社内SQL勉強会を開催しました」です。お楽しみに! *1 : リフレクションでmScrollerを変更するやり方はぐぐるといっぱい出てくるし、やたら使われてそうなので、そうそう変えられないかなーとは思いますけど
アバター
おはようございます。 delyでkurashiruのwebグロース全般を担当しているinternet_ghostです。 この記事はdely Advent Calendar 2018の12日目の記事です。 Qiita:  https://qiita.com/advent-calendar/2018/dely Adventar:  https://adventar.org/calendars/3535   昨日は サーバーサイドエンジニア兼、新米slackBot整備士のjoe (@joooee0000) さんが tech.dely.jp という記事で弊社の「哲学部」のslack botの運用について書いてくれました。 ちなみに哲学部、割と人気です。slack bot使いこなせると日々のKPI追ったりなど超活用できると思うので、興味のある方はぜひ読んでみてください! さて、はじめに アドベントカレンダーではあまり見たことがないのですが、本日はがっつりSEOの話をします。 刺さる人には刺さるし、刺さらない人には全然刺さらなさそうですが、あんま気にしないで書いていこうと思います。 さて、kurashiruは一般的にはレシピ動画の「アプリ」というイメージが強く、そもそもWebやってるんだね!知らなかった!」と言われることが多いです。 (今も結構言われます) そんな中、「SEOをやっています!」というと結構驚かれることが多いのでせっかくなのでkurashiru webで今までなにをしてきたか?というのを少しだけ紹介します。   kurashiru webの現状   みんな大好きGoogle Analyticsの2018年1月〜201811月末までのデータです。 ざっくりこの1年でトラフィックは 6~7倍まで増えました。やったね!😆😆😆 正直アルゴリズムの影響がでかいんですが、それでも伸びてくれて嬉しいです。 (9月あたりに凹んでいるのは、モロにアルゴリズムの影響です) やった施策 / やっている施策 SEOの施策については割と大胆にドコドコとやってます。 全部説明すると長いので、箇条書きでまとめます! 内部リンクの改善 ・無数にあった、タグとカテゴリの重複ページを正確なページに301リダイレクト ・紐づいているレシピが少ないカテゴリやタグはnoindex ・カテゴリに紐付いているレシピが最適かどうか、とにかく見直す手動でつけ直し ・レシピ同士、カテゴリ同士など横・縦関係の内部リンク設計の整備 ・そもそもカテゴリリニューアルしちゃおうぜ etc... UX・パフォーマンス改善・SPA ・kurashiru webをSPA化しました (※なんでそもそもSPAにしたの?という話はまたどこかで...)   ・検索体験を考えた上でのUX改善 / モバイル最適化への移行 (Search Experience Optimizationとか言われてるらしいですね最近) コンテンツ拡充とそれを支えるオペレーション作りや管理画面の作成 ・動画だけではない、ユーザーが困っている料理に関する悩みやわからないことを解決するためのコンテンツの制作(仕込んでいるので、そのうち出ると思います)   よく聞かれること 外の人と話すとよく聞かれる事についても2つだけ書いておきます。 1.動画のサービスなのにどうやってSEOやるの? 「コンテンツが動画かどうか?」はあまり関係なく、Googleのクローラーは主にテキスト情報を認識した上でランキングの評価しています。 (もちろんそこだけじゃなくて、様々な指標の元ですけど) ※「動画」の認識をさせるために、構造化データの実装などは取り組んでいます。 下の記事に検索結果のスクショを交えて書いているんですが、一時期は動画コンテンツが検索結果によく現れる時期がありました。 Googleもいろんな検索体験に基づいて検索結果をテストしています。   note.mu ただし、レシピコンテンツは差別化がとても難しいです。 材料・手順・ワンポイントそれ以上の答えを特にユーザーが求めていないからですね。 じゃあどうしているの?というと次の質問につながってきます。 2.なんで開発部でSEOやってるの?  てことはコンテンツ以外の部分で、差別化を考えなければならないと考えました。 結局のところ行き着いた答えはすごくシンプルに 「検索エンジンとユーザーの両軸にとって最高のプロダクトを磨き上げること」  だったので、そこにフォーカスするべく、開発部に異動しちゃいました。 (※元々はマーケティング部に所属していました。) SEOはマーケティング戦略や集客チャネルとして語られることが多いのですが、 1.プロダクト開発が軸になるSEO 2.コンテンツマーケティングが軸になるSEO この二つに大きく分類できると考えています。 かつ最近のSEOは、2のコンテンツの話題が多くなってるなと感じています。 ただ、kurashiru webにとっては1の「プロダクト開発」の軸がサービスとしても、SEOをグロースさせるという意味でもかなり大事だという戦略の中で悩んだ結果... マーケティング部としてSEOの事にガミガミ言うパートナー的存在として携わるのではなく、開発部としてプロダクトの全体戦略を考えつつ、SEOをはじめとしたグロース施策を進めていくという体制に舵を切ってみました。   なので、kurashiru webのPM業も兼任しています。 うまく行くかどうかわからないんですが、今の所いい感じです。(多分...)   最後に 今日はkurashiru web、特にSEOの部分にフォーカスを当てて記事を書いてみました。 ・もっと施策の中身や背景の話を聞いてみたい! ・ここもっと詳しく教えて欲しい などがあればいつでも話すので、ぜひともTwitterでお話しかけください。   twitter.com   明日は、Androidエンジニアのkenzoによる「【Android】ViewPagerのページ切り替えをいい感じにする」です! お楽しみに〜〜
アバター
こんにちは。サーバーサイドエンジニア兼、新米slackBot整備士のjoe (@joooee0000) です。 本記事はdely Advent Calendar 2018の11日目の記事です。 Qiita: https://qiita.com/advent-calendar/2018/dely Adventar: https://adventar.org/calendars/3535 前日は、iOSデザインエンジニアの John が という記事でXcodeのDebugging View Hierarchiesの紹介をしました! はじめに 皆さんは、触ったことのない技術をプライベートで触ってみたいけど、結局なにを作ろう。と悩んだ結果座学のみになってしまった経験はありませんか? そんな方がいたら、くだらないslackBotを作って運用をしてみることをおすすめします! 最近、哲学研究会(怪しげ)という社内クラブのslackチャンネルで哲学slackBotの運用をはじめました。 数百種類の哲学名言の中から毎日ランダムで1件名言を呟いてくれるslackBotです。 そのslackBot活動がとても良い勉強になったので、このAdventCalendarではくだらないslackBotを運用するメリットやオススメのslackBotの始め方を紹介します。 また、実際に得たslackBotに関する知見や具体的な実装方法について、次回のdely Advent Calendar 2018の19日目に、 サーバーレス+Go言語で作るインタラクティブな哲学slackBot というタイトルで公開します。 くだらないslackBotを運用するとなにがいいのか (以下、slackBotのことをBotと呼ばせていただきます。) くだらないBotを運用する一番のメリットは、楽しく技術の勉強ができるという点です。 失敗をおそれず色々な技術を試せる 工夫次第でどんな技術でも自由に取り入れられる 得た知見を業務に役立てられる 車輪の再開発/overkillが気にならない フロント側のコードを書かなくても目に見える楽しいアウトプットがある 中でも、 楽しいアウトプットがある ここはモチベーションを維持する上でとても大事です。 特に、ミドルウェアやサーバー周りを触ってみたい場合は、使いたい技術は決まっていても目的(アウトプット)を考えるところに時間がかかってしまうことがあります。Botを作るという目的を決めることで、先に進みやすくなることもいいところです。 また、 工夫次第でどんな技術でも自由に取り入れられる という点もあります。 slackは、slack上で発生したユーザーの様々なアクティビティを、自分で作成したWebAPIサーバー(以降、APIサーバー)に送信することができます。つまり、自分で作成したAPIサーバーに届いたあとはどんな処理をすることも可能です。そして、処理の結果をslackに反映させることができます。自前のAPIサーバーで行う処理を拡張することで、いかようにも自分の好きな技術を取り入れることができます。 その例として、普段はRailsを書いている私ですが、インタラクティブな哲学Botを作ることで下記の技術に触れることができました。 Go言語 (slackBotのメイン処理記述) クローラ(python) (哲学名言の収集) AWS SAM (LambdaやAPI Gatewayを簡単に構築するためのフレームワーク) AWS Lambda (slackBotの本体処理実行) AWS API Gateway (slackのInteractive機能のWebAPI作成) AWS CloudWatch (Lambda定期実行のイベント) AWS IAM Role (AWS SAMを使うためのユーザー権限付与) AWS DynamoDB (哲学名言を保持するためのストレージ) slackAPI (slackとのやりとり) こちらが上記を使ってつくった、実際に私が運用している哲学Botです。 1日に1回哲学者の数百種類の名言の中からランダムでつぶやいてくれます。さらに、ポストされた哲学に対してメンバーが名言を評価できるボタンをおいていて、その結果をDynamoDBに記録しています。 (この日の哲学は高評価) 機能はまだこれだけなのですが、APIサーバーを用意する部分や、DynamoDBからランダムに値を取得して定期的にメッセージをPOSTする部分だけで、色々な技術に触れることができました。 また、今回はAWSのサーバーレスの機能やGo言語を中心に触ってみたかったのでこちらの構成になりましたが、GCPを使っている方だったらGCPを、自前でサーバーを立てて運用してみたい方だったら自前サーバーを、GoではなくNode.jsをやりたい方はNode.jsを、と、柔軟に触ってみたい技術を取り入れることができます。 Bot運用を始めてみる Bot運用を始めるのにおすすめの手順を書いていきます。 1. やりたいことを決める まずはどういうBotを作るか決めます。ここが一番楽しい部分と思う方もいるかもしれません。 一方で、ここがどうしても思いつかない方もいると思います。そんな方は、なんでもいいので単純なBotを作ることから初めてみるのがいいと思います。 例えば、毎日決まった時間に「ばなな」と呟くBotを作るとします。これだけでも、上記でいうところの AWS SAM AWS Lambda AWS CloudWatch AWS IAM Role Go言語 slackAPI に触れることができます。 (Lambdaを定期実行にしてslackAPIを叩いてメッセージをPOSTするように作るとこれらが使えます。) 最初はミニマムではじめても、そこからいくらでも拡張していくことができます。 例えば、これにプラスしてゴリラの画像をクロールしてS3に保存し、決まった時間に画像をPOSTしてくれるようにするだけで「ばなな」をつぶやくだけのBotから、 クローラー AWS S3 を使って開発することができます。 さらに、ゴリラの画像のイケテる度合いを5段階で評価できるボタンをつけて、週末には週間イケてるゴリラランキングを出す拡張をするとしたら上記の技術に加えて、例えば下記のような技術をつかえるようになります。 AWS API Gateway AWS DynamoDB slackのInteractive機能 さらに、インプットされた顔写真に対して一番近いゴリラの画像を返すようにしたら機械学習を使うこともできますし、処理に時間がかかる場合はAWS SQSなどの非同期処理も導入できるかもしれません。(そこまでしたらすごい) このように、ミニマムで作ったところから機能を拡張していくことでいかようにも触れる技術を増やすことができるため、最初に作るものを考え過ぎないようにするといいと思います。 最初は「ばなな」と定期的に呟いておかしな人だと思われても、ここまですれば周りの人々が徐々に興味を持ってくれるかもしれません。 なので、なにも思いつかない人は、なんでもいいのでとにかく始めてみてはいかがでしょうか! (よろしければ「ばなな」のアイデアをお譲りします) 2. slackのアプリを作成する 次に、slackのアプリを登録します。 slackには一般公開のアプリと特定のワークスペースに閉じたアプリを作ることができ、後者であれば手順としても、精神的にも気軽に作成できます。 定期的に「ばなな」と呟くだけのBotであればIncomingWebhooksで十分で、もっと言えばリマインダー機能があれば十分なのですが、今回のような目的の場合拡張性があったほうがよいため、最初からアプリにしておくことをオススメします。 アプリにしておくことで、ボタンをつけたり、slack上で起きた特定のアクションをhookできたり、できることの選択肢がグッと広がります。 アプリを作成する手順は下記です。 1. こちら から CreateNewApp ボタンを押す 2. アプリの名前とアプリを所属させるワークスペースを選択 3. やりたいことに必要な機能の選択 Incoming Webhooks : おなじみの機能ですが、特定のチャンネルに対してURLが発行され、URLにPOSTリクエストを送ることによってチャンネルにメッセージをポストする機能です。メッセージの送信しかできません。 curl -X POST -H 'Content-type: application/json' --data '{"text":"Hello, World!"}' https://hooks.slack.com/services/DUMMY/DUMMY/DUMMYDUMMY slackAPIにもメッセージをチャンネルにPOSTする機能が存在するので、slackAPIを使う場合はこちらの機能はoffで大丈夫です。拡張性を考えると、IncomingWebhookは投稿しかできないのでslackAPIを使うことをおすすめします。 InteractiveComponents : ボタンやプルダウンメニューなどのインタラクティブな機能を使うためのものなので、自分で用意したAPIサーバーのエンドポイントを登録する必要があります。 ボタンを押したタイミングで指定したエンドポイントにPOSTリクエストを飛ばしてくれます。その内容をみて、あらゆる自由な動作を自分のサーバーで行うことができます。 (アプリを作成した時点でAPIサーバーをまだ作ってなかったのでこの時点ではダミーのURLを設定しました。あとから変えられます。) ボタンは こんな感じ の機能です。 また、ボタンやプルダウンの他に、 Action を設定してメッセージに対して特定の動作を紐づけることもできます。 EventSubscriptions : 特定のslack上のアクティビティを検知して、こちらが作成したAPIサーバーにリクエストを送ってくれる仕組みです。なので、InteractiveComponentsと同様に自分で用意したAPIサーバーのエンドポイントを登録する必要があります。 こちらは、アクティビティの結果を指定したAPIサーバーに送信してくれるだけの機能です。アクティビティのPOSTリクエストを受け取ってから、slackにメッセージを送信するなど他のslackのタスクを実行したい場合は slackAPIを使う必要があります。 hookできるslack上のイベントも、様々なものが存在しています。 Permissions : こちらの項目では、作成したアプリで、先ほどから連呼しているslackAPIを叩くための権限管理(Scopeの設定)をします。 様々な動作を許可するScopeが存在しているので、適宜必要になるものを選びます。こちらを適切に選択できていないと、やらせたい動作に紐づくslackAPIを叩いた時にScopeエラーがでます。 1つ以上のScopeを設定すると、Scopeの設定と同じ画面にあるアプリをワークスペースにインストールするための Install App to Workspace ボタンがEnableになります。 (1つ追加してボタンがEnableになれば、Scopeは後から追加することもできるのでとりあえず1つScopeを追加しましょう。) 4. アプリをワークスペースにインストール 3の Permissions の工程でEnableになった Install App to Workspace ボタンを押すと、slackAPIとやりとりする用のOAuthAccessTokenが発行され、晴れてslackAPIが使えるようになります。 5. 機能実装に必要なToken周りの値を確認する 一番重要なのは下記の2つです。 slackAPIを使う場合 OAuth Access Token slackAPIを利用するために使うToken OAuth & Permissionsの項目で参照 InteractiveComponentsやEventSubscriptionsを使う場合 Verification Token slackアプリでインタラクティブなアクションが行われた際、自分のslackがAPIサーバーに飛ばすリクエストにくっついてくる認証Token (これがついているリクエスト以外は弾くように実装する) アプリページのBasicInformationにあるAppCredentialsの項目で参照 6. (EventSubscriptionsを使う場合): APIエンドポイント認証処理 EventSubscriptionsを使う場合、設定したAPIサーバーのエンドポイントに対し、slack側が正当性の検証を行います。 こちら に記述してあるやり方で所定の形式のJSONがslackからPOSTされるので、それに対して適した内容をレスポンスすることで検証を完了することができます。 7. 完了!! 以上で、アプリの作成はOKです。 slackAPIを使って好きなことができるようになります! 3. 好きな技術を選択し、機能実装を開始する どの技術を使うかは、やりたいことによって変化しますし、無数の選択肢が存在しています。 今一番興味がある技術を選んでBot作成のモチベーションをあげてください。 使いたい技術が明確にある方は、Bot運用を開始する手順の1で好きな技術ベースでどのようなBotを作るかを考えるというのも選択肢の一つだと思います。 まとめ slackBotを運用することで、好きな技術の勉強を楽しく行うことができるようになります。 その最初に、まずは拡張性の高いslackのアプリを作成してみるのはいかがでしょうか! ↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓ 次回予告 明日は、delyのWebディレクター/SEO担当のinter_net_ghostによる「kurashiruが取り組むSEOのはなし」です。お楽しみに! また、dely Advent Calendar 2018の19日目で実際に運用している哲学Botの具体的な実装について紹介します。 dely Advent Calendar 2018 - Qiita
アバター
はじめに 本記事はSRE 2 Advent Calendar 2018の11日目の記事です。 SRE 2 Advent Calendar 2018 - Qiita dely Advent Calendar 2018もやっていますので目を通していただけると嬉しいです。クラシルの秘話がたくさん書かれています。 dely Advent Calendar 2018 - Adventar dely Advent Calendar 2018 - Qiita こんにちは!delyでSREをやっている井上です。 SREのみなさん!インフラコストの最適化してますか? delyはどうかというと、正直まだまだ不十分な状況です。。。 クラシルでまだまだやりたいこと・やるべきことがたくさんあり、コスト最適化の優先順位がなかなか上がりにくいのが現状です。 ちなみについ先日クラシルに待望の献立機能がリリースされました!「毎日のメニューを考えるのが大変」という悩みを抱えるSREのみなさん!是非使ってみてください! prtimes.jp そんなフェーズのdelyにおいてもインフラコストについて最低限度行っている取り組みがありますので、そちらを紹介しようと思います! AWSの意図しない料金の上昇に気付く仕組み delyでは過去にAWSなどの従量課金のサービスの料金が想定以上に増加したことがあり、その対策としてAWSの意図しない料金の上昇に気付くための決まりを作りました。 決まりは非常に単純です。 月初めに当月分の料金を見積もる 料金を日々Slackに通知するようにする 上昇に気づいたら決められた内容を調査して報告する 報告内容は 料金の内訳(何の作業でかかっている料金か) 報告時点でかかっている料金 かかっている料金がランニングコストなのかイニシャルコストなのか、どちらも含むのか イニシャルコストが合計でいくらかかるか ランニングコストが毎月いくらかかるか としています。 Slackに通知している内容としては、料金の前日比や対象日時点での見積と実績の比率などです。例えば下記のようなものになります。 もちろん上記だけだと、AWSの何の料金が上がっているのか特定することが出来ません。 直前に設定変更を行ったなど自覚があればある程度想定出来るのですが、自覚のないもの(例えば外的要因など)はその都度調査する必要があります。 そのためには上昇した料金に対して、「サービス」毎、「コスト配分タグ」毎、「Usage Type」毎、「API Operation」毎、「リソースID」毎といったようにドリルダウンしていきながら原因を特定できる手段を用意しておく必要があります。 Cost Explorer AWSにはCost Explorerというコストを表示および分析するためのツールがあります。 (以前は割と使いづらかったのですが、最近UIが改善されて使いやすくなりましたね。) https://docs.aws.amazon.com/ja_jp/awsaccountbilling/latest/aboutv2/cost-explorer-what-is.html docs.aws.amazon.com Cost Explorerを使うことで、特定のサービスに対して「Usage Type」毎や「API Operation」毎や「コスト配分タグ」毎にどれだけ料金がかかっているのか内訳を表示することができます。 ただしCost Explorerでは「リソースID」の指定や「リソースID」毎の集計は出来ません。 そのため例えば下記のような特徴をもったサービスにおいては特定を行うことは難しいのです。 Glueなどコスト配分タグ未対応のサービス S3、CloudWatchLogsなどリソースが多くなりがちなサービス よってリソースIDレベルでの詳細な特定を行うためには、Cost Explorer以外の手段を検討する必要があります。 リソースおよびタグ付きの請求明細レポート AWSには「リソースおよびタグ付きの請求明細レポート」を出力する機能があります。 「リソースおよびタグ付きの請求明細レポート」はリソースIDとコスト配分タグを含んだ料金明細です。 この料金明細を利用することでリソースID毎の料金を算出することが可能になります。 docs.aws.amazon.com 設定を行うことで任意のS3バケットに定期的に下記のようなオブジェクト名で出力されるようになります。 XXXXXXXXXXX-aws-billing-detailed-line-items-with-resources-and-tags-YYYY-MM.csv このファイルを直接エディタで開いてリソースID別料金を確認することも理論的には可能ですが、データ量が多すぎるのであまり現実的ではありません。 そのためAthenaを使ってSQLを実行できるようにする必要があります。そのままだとAthenaが認識できないのでGlueのETLジョブ機能を使って前処理を行います。 Glue ETLジョブ 参考に実際に実行しているコードを紹介します。(雑コードですいません) Glueの実行環境を利用しているだけで変換処理自体は純粋なpysparkで行っています。 import sys from awsglue.transforms import * from awsglue.utils import getResolvedOptions from pyspark.context import SparkContext from awsglue.context import GlueContext from pyspark.sql import SQLContext from awsglue.job import Job from awsglue.dynamicframe import DynamicFrame from pyspark.sql.types import * import boto3 import zipfile from datetime import datetime, date, timedelta args = getResolvedOptions(sys.argv, [ 'JOB_NAME' , 'year' , 'month' ]) date_time = datetime.now() - timedelta(hours= 12 ) # 定期実行時には実行されたタイミングの年月のレポートを対象に動作するようにしていて、 # パラメータを変更することで任意の年月で実行することも出来るようにしています。 if args[ 'year' ] == '9999' : year = date_time.strftime( "%Y" ) else : year = args[ 'year' ] if args[ 'month' ] == '99' : month = date_time.strftime( "%m" ) else : month = args[ 'month' ] print ( 'year:' + year) print ( 'month:' + month) sc = SparkContext() glueContext = GlueContext(sc) job = Job(glueContext) job.init(args[ 'JOB_NAME' ], args) s3 = boto3.resource( 's3' ) # 出力先のS3バケット名に置き換える bucket_name = '<リソースおよびタグ付きの請求明細レポートが格納されたS3バケット>' bucket = s3.Bucket(bucket_name) # XXXXXXXXXXXを自身のIDに置き換える csv_name = "XXXXXXXXXXX-aws-billing-detailed-line-items-with-resources-and-tags-%s-%s.csv" % (year, month) zip_name = csv_name + '.zip' bucket.download_file(zip_name, 'billing.zip' ) # zipファイルのままだとpysparkで処理できないので展開してcsvファイルをアップロードし直しています。 zip_file = zipfile.ZipFile( 'billing.zip' ) filename = zip_file.namelist()[ 0 ] zip_file.extract(filename) bucket.upload_file(filename, filename) # glueは使わず純粋なpysparkだけで実行しています。 # RecordTypeがLineItemのものだけを抽出してparquet型で出力しています。 sqlContext =SQLContext(sc) filename = "s3a://%s/%s" % (bucket_name, csv_name) df = sqlContext.read.format( "com.databricks.spark.csv" ).option( "header" , "true" ).option( "inferSchema" , "true" ).load(filename) df.printSchema() df2 = df.filter(df.RecordType == 'LineItem' ).withColumn( 'InvoiceID' , df.InvoiceID.cast(StringType())) df2.printSchema() dyf1 = DynamicFrame.fromDF(df2, glueContext, 'dyf1' ) prefix = "reports_parquet/year=%s/month=%s" % (year, month) s3client = boto3.client( 's3' ) # 前回実行時のparquetファイルを削除しています。 def delete_all_keys (bucket, prefix, dryrun= False ): contents_count = 0 next_token = '' while True : if next_token == '' : response = s3client.list_objects_v2(Bucket=bucket, Prefix=prefix) else : response = s3client.list_objects_v2(Bucket=bucket, Prefix=prefix, ContinuationToken=next_token) if 'Contents' in response: contents = response[ 'Contents' ] contents_count = contents_count + len (contents) for content in contents: if not dryrun: print ( "Deleting: s3://" + bucket + "/" + content[ 'Key' ]) s3client.delete_object(Bucket=bucket, Key=content[ 'Key' ]) else : print ( "DryRun: s3://" + bucket + "/" + content[ 'Key' ]) if 'NextContinuationToken' in response: next_token = response[ 'NextContinuationToken' ] else : break print (contents_count) delete_all_keys(bucket_name, prefix) glueContext.write_dynamic_frame.from_options( frame = dyf1, connection_type = "s3" , connection_options = { "path" : "s3://%s/reports_parquet/year=%s/month=%s" % (bucket_name, year, month)}, format = "parquet" ) job.commit() リソースIDレベルでの料金の算出 SQLが実行できれば、念願のリソースIDレベルでの料金の算出が出来るようになります。 delyでは可視化ツールにRedashを利用しているのですが、例えば下記のようにS3バケット別の料金も見れるようになっています。 料金上昇の原因もすぐに出来て安心ですね。 Redashによる可視化 SQLが実行できるようになればこっちのものですね! あとは可視化ツールで煮るなり焼くなり好きにしましょう。 せっかくなのでdelyでの具体例をいくつか紹介します。モザイクばかりですいません! 下記は特定の月のダッシュボードです。一つ一つはCost Explorerでも表示可能ですが、やっぱりダッシュボードで一括して見れると便利ですね。 月とサービスの料金一覧をピボットテーブルで表示しています。 下記ではS3の「API Operation」別の料金を表示しています。ピボットテーブルなので更に「Usage Type」毎にドリルダウンすることも出来てアドホックに分析することが可能になっています。 さいごに この記事を書いてる途中でAWSのドキュメントに下記の文章を見つけました。 以下のレポートは利用できなくなります。 代わりに「AWS Cost and Usage Report」を使用することを強くお勧めします。 ・・・ 調べてみると2018年11月15日から「AWS Cost and Usage Report」の機能を使えばparquet型で出力してくれて、かつ設定用のCloudFormationのテンプレートを出力してくれるようになったようです。 https://docs.aws.amazon.com/awsaccountbilling/latest/aboutv2/setting-up-athena.html docs.aws.amazon.com 試しに設定してみましたが、とても簡単にSQLを実行できるようになりました。GlueのETLジョブを定期的に実行する必要もないので断然こちらがおすすめです! 過去に遡って出力することは出来ないようなので、まだ設定していない方はとりあえず出力設定だけはやっておいた方が良さそうです。 簡単にはなりますがdelyでのコスト周りの決まりについてご紹介しました。 AWSなど従量課金のサービスのコストは見積もりが難しいですが、コントロール出来るように現状を常に把握しておけると良いですね。 ということで、最後にCost Explorerに個人的に欲しい機能を記載して終わろうと思います。 Cost Explorerに個人的に欲しい機能 「テーブル」、「線グラフ」、「棒グラフ」以外のビジュアライズ group byの複数指定 ダッシュボードの作成 以上になります。ありがとうございました!
アバター
こんにちは。delyデザインエンジニアのJohn( @johnny__kei )です。 本記事はdely Advent Calendar 2018の10日目の記事です。 Qiita: https://qiita.com/advent-calendar/2018/dely Adventar: https://adventar.org/calendars/3535 前日は、プロダクトデザイナーのkassyが「 ユーザーの声に振り回されないデザインの改善プロセス 」という記事を書きました。いいプロダクトを作るには、ユーザーの声を鵜呑みにするのではなく、きちんと判断する必要がありますよね。 はじめに みなさんは、iOSアプリ開発をするときに、Xcodeの Debugging View Hierarchies を使用していますか? Debugging View Hierarchies を使用すると、アプリが現在の状態で停止され、Viewの階層や、プロパティを確認できます。 出典: Apple Specialized Debugging Workflows AutoLayoutが効いていないのを調査するときに、使用したりする方もいらっしゃると思います。 Debugging View Hierarchies はUIKitのUINavigationBarなどのクラスにも適用され、どのようなView階層か見ることができます。 アプリ独自のUIパーツを作成するときに、できるだけ、View構造や、メソッド、プロパティなどを、UIKitにそろえると、使いやすくなると思います。 そこで 前半は、UIKitのいくつかのクラスのView階層について書きます。 後半は、前半をふまえて、サンプルの実装について書きます。 UIKitのView階層 - UINavigationBar UINavigationBarのsubviewには_UIBarBackground(非公開クラス)があります。 下の画像を見るとわかるように、UIVisualEffectViewやshadowImageが設定されるimageViewなどがあることがわかります。 UINavigationControllerのnavigationBarでは、StatusBarまでnavigationBarが伸びているように見えるのは、この_UIBarBackgroundがはみ出しているからです。 自分で、UINavigationBarをViewControllerのviewにaddSubviewする場合は、適応されないので、UINavigationControllerの方で、そういった実装がされると推測できます。 imageViewも、Viewから 高さ0.5 の分だけ下に はみ出ています 。これを発見したとき、ビビりました。 - UIPageControl 横スクロールでページングがあるときに、よく使用されたりします。 UIPageControlはdotのサイズやdot間のマージンは変更できません。 dot自体は、単なるUIViewであることがわかります。 また、dotは7ptで、dot間のマージンは9ptということがわかりました。 結構シンプルな作りになっています。 また、UIPageControlは、InterfaceBuilderとCodeでの初期状態に違いがあったのも、新しい発見でした。 --- currentPageIndicatorTintColor pageIndicatorTintColor InterfaceBuilder UIColor.white UIColor.white.withAlphaComponent(0.2) Code nil nil サンプル実装 - BottomBar SafeAreaの登場で、下部に、Viewを配置したいときに、どういう風に実装したらいいか、悩む場合がありますよね。 自分なりに、こうしたらいいんじゃないかという実装を書いていきます。 static let viewHeight: CGFloat = 49.0 のように本来表示したい高さを定義します。 そして、実際にViewControllerのviewにのせるのはこんな感じに制約をつけます。 NSLayoutConstraint.activate([ bottomBar.leadingAnchor.constraint(equalTo : view.leadingAnchor ), bottomBar.trailingAnchor.constraint(equalTo : view.trailingAnchor ), bottomBar.topAnchor.constraint(equalTo : view.safeAreaLayoutGuide.bottomAnchor , constant : - BottomBar.viewHeight), bottomBar.bottomAnchor.constraint(equalTo : view.bottomAnchor ) ]) こうすることで、safeAreaのbottomがある場合は、contentの高さと、safeArea分伸びた状態で表示することができます。 このBottomBarの上に、Viewをのせる場合は、contentViewにのせます。 contentViewは高さは以下のように固定されているので、ボタンはcontentViewに対して、制約をつけることで、いい感じに表示することができるようになります。 contentView.heightAnchor.constraint(equalToConstant : BottomBar.viewHeight ) さらに、前述した、UINavigationBarのshadowImageを表示するimageViewのように、border部分は、はみ出して作ってあります。 これも、contentViewにのせるようにしていて、UINavigationBarのようにしてあります。 contentView.addSubview(topBorder) topBorder.heightAnchor.constraint(equalToConstant : 0.5 ) topBorder.bottomAnchor.constraint(equalTo : contentView.topAnchor ) - PageControl 前述したように、UIPageControlはdotのサイズやdot間のマージンは定義されていないので、変更することができません。 そこで、UIPageControlとほぼ同じ、プロパティやメソッドを持ち、dotのサイズやdot間のマージンを設定できる、PageControlを実装してみます。 UIControlは、UIViewのサブクラスなので、普通にViewをのせていくことで大丈夫です。 さらに、UIPageControlのような実装にするには、タップしてPageが変化したときに、 UIControl.Event.valueChanged イベントを発火する必要があります。 UIPageControl の説明にも書いてあります。 その実装は以下のようになっています。 override func endTracking (_ touch : UITouch ?, with event : UIEvent ?) { guard let touchPoint = touch?.location( in : self ), bounds.contains(touchPoint) else { return } // touchPointが、currentPageのdotの左右どちらかでなどのロジックで判断し、変更があれば発火する sendActions( for : .valueChanged) } dotのサイズやマージンを定義することで、サイズの計算も簡単になります。 // UIPageControlに合わせる。 var dotSize : CGFloat = 7.0 var dotMargin : CGFloat = 9.0 // pageCountに応じた、size計算 func size (forNumberOfPages pageCount : Int ) -> CGSize { let height = ( 15.0 * 2 ) + dotSize guard pageCount > 0 else { // UIPageControlがこんな感じの値 return CGSize(width : dotSize , height : height ) } let width = dotSize * CGFloat(pageCount) + dotMargin * CGFloat(pageCount - 1 ) return CGSize(width : width , height : height ) } numberOfPages, currentPageなどのプロパティの実装に関しても、結構シンプルになっているので、ぜひサンプルの実装をみてもらえばと思います。 UIPageControlに実装されていることは、全て実装しています。(たぶん) 実装のサンプルは置いておきます。 GitHub Sample Code まとめ いかがでしたでしょうか? Debugging View Hierarchies を使用すると、AppleのUIKit内部の実装が見れておもしろいですよね。 UI部分の階層構造しかみれませんが、そこから滲み出る、ロジック部分も想像すると、なお面白いと思います。 また、View階層をデザイナーに見せることで、内部構造を理解してもらえるので、次から、そこを考えて、デザインを作ってくれるようになるかもしれませんね。 明日は、サーバーサイドエンジニアのjoeによる「好きな技術を使って作る!くだらないslackBot運用のすヽめ」です。お楽しみに!  
アバター
こんにちは!dely でフロントエンドエンジニアをしている @all__user です。 この記事は dely Advent Calendar 2018 の8日目の記事です。 Qiita: https://qiita.com/advent-calendar/2018/dely Adventar: https://adventar.org/calendars/3535 昨日は、iOSエンジニアの ほりぐち( @takaoh717 ) が「iOS版クラシルの開発からリリースまでの流れ」というタイトルで投稿しました。 tech.dely.jp iOS 版 kurashiru の開発体制の遍歴がよく分かるような内容となっていますので、ぜひチェックしてみてください! はじめに ここ一年間で Web 版 kurashiru のフロントエンドは Rails から Vue の SPA へと少しづつ置き換えられてきました。 今回はその中でも jQuery への依存を外す際に行ったことにフォーカスを当ててまとめてみたいと思います。 目次 はじめに 目次 リプレース or 少しづつ置き換え polyfill を入れる Sprockets 👉 Webpacker gem 👉 npm CoffeeScript 👉 TypeScript jQuery 👉 VanillaJS $() 👉 Array.from(document.querySelectorAll()) .each() 👉 .forEach() .width() 👉 .getBoundingClientRect().width .slideUp() 👉 ? 各工程で動作確認をする 挙動を変えないようにする 動作検証はビジュアルリグレッションテストで BackstopJS まとめ さいごに リプレース or 少しづつ置き換え 現在 jQuery を使った Web サイトを運用していて SPA への移行を考えている方の中には、リプレースするか少しづつ置き換えていくかで悩んでいる方もいるかもしれません。 最終的に kurashiru では少しづつ置き換える方法を取りました。 リプレースという選択肢もありましたが、割けるリソースの数、 SEO への影響などもろもろを考慮すると、エイヤっ!で移行するにはリスクが大きすぎると判断したためです。 稼働中の Web サイトを SPA へ書き換えるという作業は、いってみれば走っている車のタイアを交換するようなものです。人が住んでいるマンションを、人が住んでいる状態のまま建て替えるようなものと言ってもいいかもしれません。 jQuery を残したまま Vue に置き換えていくこともできますが、jQuery を使用している部分が無くなるまで jQuery と Vue が併存することになります。バンドルサイズはできるだけ小さく抑えたかったので、jQuery への依存を先に外すことにしました。 polyfill を入れる Array.from や Array.prototype.findIndex などのメソッドは古いブラウザでサポートしていない場合があります。 どのブラウザでもこれらのメソッドを安全に呼び出せるように polyfill を導入しました。 これまではマルチブラウザ対応を jQuery がやってくれていましたが、依存を外していくにあたり別の方法でサポートする必要があります。 kurashiru では polyfill.io という CDN を使用して、User-Agent ごとに最適な polyfill を読み込むようにしています。 github.com Sprockets 👉 Webpacker まず最初に Sprockets のエントリーポイントをすべて Webpacker に移しました。 github.com Webpacker はデフォルトで CoffeeScript に対応しているので、対象となる CoffeeScript ファイルを import したファイルを用意して Webpacker のエントリーポイントに移すだけです。 gem 👉 npm gem の jquery-rails を削除し npm の jquery に置き換えます。 www.npmjs.com CoffeeScript 👉 TypeScript decafjs を使って CoffeeScript を JavaScript へ自動変換します。 www.npmjs.com と、これで動いてくれれば良いのですが、まあまあ動かないところがあります。 ソースコードを読みながら地道に修正しつつ TypeScript に書き換えていきました。 decafjs はあくまでも TypeScript 版の下書きを作ってくれるツールくらいの感覚で使用しました。 jQuery 👉 VanillaJS VanillaJS とは特定のフレームワークを使わずに DOM の標準 API のみを使って書くことを、バニラアイスのようなプレーンな状態ということに例えてよく使われている表現です。 この工程では特に、ここまでの過程で TypeScript に書き換えてきたことが効いてきます。 TypeScript で型をしっかり縛ることで typo や型エラーなどのケアレスミスで消耗すること無く、安心してリファクタリングを進められます。 $() 👉 Array.from(document.querySelectorAll()) jQuery のメソッドは基本的にセレクタにマッチした要素全てに対して操作を行いますが、標準 API では配列のループを回して一つ一つの要素に対して操作を行う必要があります。 .querySelectorAll() の戻り値は Array ではなく NodeList なので、 Array.from() を使って Array に変換しておきます。 .each() 👉 .forEach() .each() のコールバックに渡る引数は index, element の順ですが、 .forEach() の場合は逆の element, index の順となります。 .width() 👉 .getBoundingClientRect().width .getBoundingClientRect() は実行コストが少し気になりますが、 .width() や .height() を再現する際にとても便利です。 .slideUp() 👉 ? .slideUp() のように単純に標準 API へ置き換えることができないメソッドは、個別に機能を作って対応しました。 すでに Vue のアニメーションの仕組みにのっとって定義されたモーション用のクラス( .fade-enter , .fade-enter-active , .fade-enter-to みたいなやつ)があったので、それを使い、いい感じのタイミングで要素の classList に対してクラスの付け外しを行うことで再現しました。 各工程で動作確認をする これまでの各工程の一つ一つはそれほど難しくないかもしれませんが、全ての工程を一気にやってしまうと、ちょっとした不具合があった際に、どこまでロールバックすれば良いかが分からなくなってしまいます。 できるだけ各工程の間に動作検証をいれるようにして作業を進めました。 挙動を変えないようにする 上記のように書き換えを行っていると、どうしても途中で挙動を変えたくなる箇所が出てきます。 あーこうした方がいいのにな、よしついでに直しちゃおう、っていうことが必ず出てきます。 でも、これをやってしまうと、何か不具合が起きた際に、書き換えにミスがあったのか、挙動を変えたことが原因なのかが分からなくなってしまいます。 気になった部分はコメントなどに残しておき、動作を確認できてから手を付けたほうが、着実に作業を進められます。 動作検証はビジュアルリグレッションテストで 動作確認を各工程で行う、と一口に言っても何をどこまで行えば良いのか、というのは非常に難しい問題です。 kurashiru ではビジュアルリグレッションテストという方法を使って動作検証を行いました。 ビジュアルリグレッションテストとは、コードに変更を加える前後でスクリーンショットを取り、その画像を比較することで動作検証を行うというテスト手法です。 これを各工程ごとに実施することで、書き換え前後で挙動が変わっていないことを確認しながら作業を進められます。 BackstopJS kurashiru では BackstopJS というツールを使用してビジュアルリグレッションテストを行っています。 github.com CI/CD などに組み込みやすいように Docker イメージが提供されていたり、コンフィグ類も過不足なく柔軟に設定できるためとてもオススメです。 意図的に 1px padding を変更した例 差分が強調表示されるので分かりやすい 一見まったく同じ見た目でも、きちんと差分を見つけ出すことができます。 まとめ jQuery の依存を外すまでの工程を順を追ってご紹介してきました。 polyfill の導入 Sprockets から Webpacker への移行 gem から npm への移行 CoffeeScript から TypeScript への移行 jQuery から VanillaJS への移行 細かい変換を繰り返しながら、各工程で動作確認をしていくことが重要です。 と、ここまで書いてきましたが、実際にはかけられる工数との兼ね合い、移行後のコードがその後どう使われるのかによっても違うので、ある程度エイヤっ!で書き換えることも正直たくさんありました。 そのあたりはよしなにやっていきましょう。 さいごに SPA 化にあたっては、紆余曲折、チーム内でもさまざまな議論が行われました。 他にも Webpacker つらい、、、などご紹介したい話はたくさんありますが、それはまた別の機会にご紹介できればと思います! 明日はプロダクトデザイナーの @kassy が「 ユーザーの声に振り回されないデザインの改善プロセス 」というタイトルで投稿します。 こちらもぜひご覧ください!
アバター
この記事はdely Advent Calendar 2018の7日目の投稿です。 昨日は、Androidエンジニアのkenzoが「 Androidにいい感じの動きをさせていく話(ViewPagerとその他Viewとの連動) 」というタイトルで投稿しました。 Android開発を行っている方はぜひチェックしてみてください。 Qiita: https://qiita.com/advent-calendar/2018/dely Adventar: https://adventar.org/calendars/3535 1. はじめに こんにちは、delyでkurashiruのiOSエンジニアをしているほりぐち( @takaoh717 )です。 今回は、delyが運営しているレシピ動画サービス「kurashiru(クラシル)」のiOSアプリがどのようなフローを経てストアに公開されているかをざっくりとご紹介したいと思います。 この内容が、同じようなチーム規模の会社の方やこれからリリースフローなどを確立していくフェーズの方々などの参考になれば幸いです。   2. dely開発部のチーム体制 クラシルは現在アプリダウンロード数が両OSで1400万を超える規模のサービスですが、iOSチームは現在2人体制で開発を行っています。 この人数だと当然リソースが潤沢と言える状況ではないため、何をどのように行うかがとても重要です。 また、delyの開発チームにとって最も重要なことはユーザに本質的な価値を提供するサービスを作ることです。 そのためにはスピード感をもって開発を進めることや施策の最適な優先順位決め、プロダクトの品質を保つための仕組みづくりなどの点に特に考慮する必要があると思っています。   3. スクラム開発 クラシルの開発チームは半年ほど前から1週間を1スプリントとしたスクラム開発を行っています。 施策立案からリリースまでの全体の流れ スプリント計画会 まず、スプリントの始まりには計画会を開いています。 ここで、今デザインフェーズにあるものと実装フェーズにある施策をそれぞれ確認し、そのスプリントで必要な要件定義・デザイン・実装のタスクの洗い出しと担当の割り振り、工数見積もりを行います。   デザインフェーズ UIデザイン、プロトタイピング実装、インタビューなどのサイクルを回します。 このあたりの話についての詳細は PM奥原の こちら のスライドをご覧いただくと分かりやすいと思いますが、ここで重視しているのはきちんとユーザーに使われるものをリリースするための仕組みです。   実装フェーズ  デザインフェーズが完了すると、プロトタイピングを行ったエンジニアやデザイナーが開発仕様書を作成します。 弊社ではドキュメントの共有にQiita:Teamを活用しているため、仕様書もQiita:Teamを使って書きます。また、Sketchで作成されたUIもZeplinで共有されます。 実装フェーズの実装者は「開発仕様書」「Zeplin上のUIデザイン」「プロトタイプ」を確認しながら、アプリの実装を進めます。 デザインフェーズで実装されたプロトタイプは基本的にはリリースするアプリには含まない方針で作られているものなので、ここではプロトタイプのコードは使用せずに新しくコードを書いていきます。 (プロトタイピング段階でプロダクトコードレベルで作り込まれたUIコンポーネントなどはそのまま使用することがあります。)    4. iOSチームの開発スタイル 1スプリントの流れ iOSチームの1スプリントの動き 水:スプリント計画会で今スプリントのタスクが決定 木〜火:実装 水〜金:PRレビュー → 修正 → デバッグ 金:Appleの審査に提出 月:リリース iOSチームは1スプリント(1週間)をざっくりとこのような形で動いています。 スプリントの開始が水曜日になっているのは、iOSの審査のタイミングを考慮してこの形が理想だろうと考えた結果です。 また、ここまできっちりとスケジュールを決めている理由は、実装後のデバッグや修正の時間をきちんと確保し、少ないリソースでもプロダクトの品質を確保できるような仕組みづくりが必要だったからです。 スケジュールを固定にしていなかった頃は、少し無理な実装スケジュールでも直前に割り込みで対応したりなどして、リリース前の検証が疎かになったり審査提出予定日のギリギリまでエンジニアが頑張るなどという問題が起きていました。 Gitのブランチ運用 /master AppStoreで配信しているアプリと同じ状態 /develop 次のリリースで配信されるもの /feature 何らかの新しい機能を実装をするためのブランチ(developから派生) /improvement 新機能ではないがユーザが見て分かる改善を行うためのブランチ(developから派生) /fix バグ・不具合の修正を行うためのブランチ(developから派生) /refactor リファクタリングを行うためのブランチ(developから派生) /other CI/CDやライブラリなどのツールに関する修正やアップデートを行うためのブランチ(developから派生) /release/v.◯.◯.◯ スプリントが終わった段階で次のアップデートでリリースされるバージョン /hotfix releaseブランチを作成してから不具合などの緊急度の高い修正必要になった場合に修正を行うためのブランチ(releaseから派生) Gitのブランチは現在上記のような運用を行っています。基本的な形はGit flowに近いと思います。 開発作業用のブランチを5種類に分けているのは、ブランチの名前だけでどういう変更が含まれたものかはっきりさせるために分けています。 また、この名称と同じタグをプルリクに設定してそこからCHANGELOGに追記されるようにしているため、その部分とも連動しています。 流れ masterブランチ → developブランチ派生 developブランチ → 実装内容に応じて、開発ブランチ(feature or improvement or fix or refactorブランチ)派生 開発ブランチで作業、PR作成、レビュー 開発ブランチ → developにマージ releaseの要件が定まったら(主にスプリントの最終日)、develop → releaseブランチ派生 releaseブランチで緊急度の高いバグがあった場合に releaseブランチ → hotfixブランチ派生 hotfixブランチで修正、PR作成、レビュー hotfix → release、hotfix → developにそれぞれマージ releaseブランチからアーカイブビルドを行い、AppStoreConnectにアップロード リリース後に release → master、release → develop にそれぞれマージ この運用方式にしてから半年以上が経過していますが、今は特に問題なく運用ができています。 以前は様々なタイミングで緊急の修正を行った際などのフローが不明確だったことなどがあり、何度かコミュニケーションが発生したりコンフリクトの修正が必要になったりと無駄な時間が発生してしまうことがありました。 デバッグ releaseブランチを作成したら、社内のメンバーにデバッグをしてもらうためにDeployGateでアプリの配布を行います。 DeployGateでの配布にはSlack、fastlane、Bitriseなどを活用して作業を自動化してあります。 ここでは次にリリースをするバージョンの変更箇所を中心に確認します。また、バグや不具合などの問題以外でも少し気になったことなどもよく報告されることがあります。 審査提出 → リリース デバッグとhotfixの修正が全て完了したら、審査に提出します。 審査通過後は、プロモーションコードをSlackで配布し、社内のメンバーにも最終確認を行ってもらいます。 ここで問題が見つからなければそのままリリースを行います。 リリース後 リリースした後は、まずはログの確認やデバッグ時に見つからなかったクラッシュ、バグの確認を行います。 さらに、必要に応じてiOSエンジニアもSQLを書いてデータ分析を行い、分析結果をもとにさらなる改善をしていきます。 5. おわりに 以上がクラシルのiOS開発からリリースまでの大まかな流れです。 この記事で紹介した仕組みは全てこの1年以内に確立したものです。弊社の開発チームはまだまだ歴史も浅く、メンバーも少ない少数精鋭チームですが、プロダクトを第一に考えて柔軟に仕組み作りを行っています。 もし機会があれば、もう少し深掘りした説明もどこかでしようかと思います。 また、弊社では、プロダクト志向でクラシルの開発ができるiOSエンジニアを募集しています。 少しでも興味があったり、記事に対してご意見などある方がいらっしゃればぜひお声がけください! www.wantedly.com 明日はフロントエンドエンジニアの @all_user が「 jQueryへの依存を外す方法 」というタイトルで投稿します。こちらもぜひご覧ください!
アバター
こんにちは。delyでAndroidのエンジニアをしているkenzoです。 この記事はdely Advent Calendar 2018の6日目の記事です。 Qiita:  https://qiita.com/advent-calendar/2018/dely Adventar:  https://adventar.org/calendars/3535   昨日は検索エンジニアのsakuraが 「クラシルの検索をよくするために1年間取り組んだこと」 を書きました。 普段検索に携わる方はもちろん、それ以外の方にとっても面白い内容となっていますので、ぜひご覧ください。 はじめに 弊社のプロダクト開発はデザインフェーズと実装フェーズの2つのフェーズに分かれています。(詳しくはこちら↓) speakerdeck.com 後半の実装フェーズでは、前半のデザインフェーズで出来上がったデザインを元に画面・機能を作成していくことになります。 デザインフェーズでは、かなりいい感じに動くプロトタイプを作成したり、それを用いてユーザーテストを行ったりと、より良いものをユーザーに提供できるように弊社のデザイナーやデザインエンジニアが頑張ってくれています。 で、下のようなデザインが出来上がっているわけです。(これはデザインの指示ではなくて実際に実装した画面ですが) え、めちゃ動く。。。 (はじめてデザインを見たときのきもち) 初めて見ると一瞬たじろぐかもしれませんが、動きも含めてユーザーに提供する価値なので、難しそうだからといって実装しないわけにはいきません。 今回はこのようないい感じに動くデザインを実装に落とし込んでいく際にやったことの一例を紹介します。 ViewPagerとその他Viewとの連動 まずはこちらをご覧ください。 このように ViewPager を用いたアプリの場合、現在開いているページによって ViewPager 外に表示される内容が変わることがあると思います。(画面下部や右上の★) もちろんこれでOKな場合も多いとは思いますが、もう少し細かい動きにこだわったUIを作成したい場合もあるかと思います。 たとえばこんな感じです。 今回はこれの実装方法をご紹介します。 用いるのは OnPageChangeListener の onPageScrolled です。 val viewPager: ViewPager = findViewById<ViewPager>(R.id.view_pager) viewPager.addOnPageChangeListener( object : ViewPager.OnPageChangeListener { override fun onPageScrollStateChanged(state: Int ) { } override fun onPageScrolled(position: Int , positionOffset: Float , positionOffsetPixels: Int ) { } override fun onPageSelected(position: Int ) { } }) onPageScrolled は ViewPager のスクロール中に呼び出され、その際に引数として下記の値が渡されます。 ( positionOffsetPixels は今回は使いません) position : 表示されている最初のページのindex positionOffset : position で指し示されるページからのオフセットの割合を0以上1未満の値で positionOffsetPixels : position`で指し示されるページからのオフセットのpixel値 つまり、どのタイミングでどんな値が返るかというと、、 position : 0 positionOffset : 0.0 position : 0 positionOffset : 0.31944445 position : 0 positionOffset : 0.6259259 position : 1 positionOffset : 0.0 という感じです。 以上のような値がスクロールしている間に何度も呼ばれる onPageScrolled で渡されます。 この値を使ってその他Viewのtranslationやalpha、scaleをセットします。 上のサンプルの「いいいいい」のViewの例だと、 position が0の時は positionOffset の増加に従って表示位置が上がる position が1の時は positionOffset の増加に従って表示位置が下がる position が2(上記以外)の時は常に表示位置が下のまま なので、下記のように transilationY をセットします。 override fun onPageScrolled(position: Int , positionOffset: Float , positionOffsetPixels: Int ) { view.translationY = when (position) { 0 -> view.height - view.height * positionOffset 1 -> view.height * positionOffset else -> view.height.toFloat() } } // viewは「いいいいい」のviewです このような動きになります。 もう1つ、上のサンプルの「★」のViewの例だと、 position が0の時(下記以外)は不透明度が0のまま position が1の時は positionOffset の増加に従って不透明度が上がる(だんだん見えるようになる) position が2の時は不透明度が1のまま(ページが増えるようなら positionOffset の増加に従って不透明度を下げる) なので、下記のように alpha をセットします。 override fun onPageScrolled(position: Int , positionOffset: Float , positionOffsetPixels: Int ) { star.alpha = when (position) { 1 -> positionOffset 2 -> 1f // 今回はpositionの最大が2なのでこれでよいですが、ページが増える場合は1f - positionOffsetとします else -> 0f } } // starは「★」のviewです このような動きになります。 これで、ViewPagerをスクロールさせた時に他のViewと連動させることができました。 おわりに Androidでも他のフロントエンドでも、動きのある画面を作るのはとっつきにくかったり難しかったりと、実装に若干のハードルはあると思います。 ですが、やっぱり自身がユーザーとしてサービスに触れる際に、いい感じに動いてくれると気持ちよかったり、わかりやすかったりと、良い体験ができていると感じます。 使ってくれる人がより良い体験をできるように、ものづくりをする人間としてはそういうとこ頑張らないとなーと思うところです。   ちなみに上でデザインの例としてあげた 「クラシルかんたん献立機能」 が今回リリースされました! 使ってみてもらえるととても嬉しいです。 アプリはこちら: Android iOS   明日はiOSエンジニアの堀口の「iOS版クラシルの開発からリリースまでの流れ」です。お楽しみに!
アバター