はじめまして、dely開発部の funzin です。普段はクラシルのiOSアプリ開発を担当しています。 この記事は「dely #1 Advent Calendar 2020」の4日目の記事です。 adventar.org adventar.org 昨日はMeilCliさんの 【C#】null許容値型のnonnull判定どれが早いかクイズ という記事でした。 パフォーマンス計測もしていてとてもよくまとまっているので気になる方はぜひ見てみてください。 さっそく本題ですが、この記事ではクラシルのiOSアプリ開発にRenovateを導入したことについてお話しします。 Renovateとは Renovate とはパッケージマネージャー(e.g, Bundler, npm)で管理しているライブラリのアップデート作業を自動化してくれるサービスです。似たようなサービスでは2019年にGitHubにjoinした Dependabot があります。 なぜ導入したか クラシルのiOSアプリ開発では、BundlerやCocoaPodsで管理しているライブラリのアップデートを手動で行っていました。必要なタイミングにならないとライブラリのアップデートが行われないため、定期的にライブラリをアップデートすることがありませんでした。 そのため、いざバージョンを上げる時に大幅な修正対応が発生し開発コストが増加する原因となっていたため、日常的にアップデートする仕組みにしておきたかったのがRenovateを導入した大きな理由です。 なぜDependabotではなくRenovateを採用したのか Dependabotに比べてiOS開発で利用するパッケージマネージャー(e.g. SwiftPackageManager, CocoaPods)のサポートが多かったことが導入の決め手でした。 CocoaPodsと同様にiOSでよく使われているCarthageは残念ながら対応していません。 導入手順 Renovateは以下の手順で導入が完了します。 GitHub Apps 経由でRenovateをRepositoryに追加 Repository配下に renovate.json を作成 Document を参考にしながら renovate.json を修正 RenovateがPRを生成 サンプル 実際にどのようなPRが生成されるかを確認できるようにサンプルのRepositoryを用意しました。 こちらの ios-renovate-example でCocoapodsやBundlerでどのようなPRが作成されるか確認できます。 プロジェクトの構成は新規作成したプロジェクトに pod install を実行したのみなので最小限の構成となっています。 Configファイル https://github.com/funzin/ios-renovate-example からconfigファイルを抜粋したものです。 Podfile platform :ios , ' 9.0 ' target ' ios-renovate-example ' do use_frameworks! pod ' RxSwift ' , ' ~> 5.0.0 ' pod ' RxCocoa ' , ' ~> 5.0.0 ' pod ' Nuke ' , ' ~> 8.4.1 ' pod ' SwiftLint ' , ' ~> 0.40.0 ' end renovate.json { " packageRules ": [ { " groupName ": " RxSwift ", " managers ": [ " cocoapods " ] , " packageNames ": [ " RxCocoa ", " RxRelay ", " RxSwift " ] } , { " groupName ": " SwiftLint ", " managers ": [ " cocoapods " ] , " packageNames ": [ " SwiftLint " ] , " enabled ": false } ] } 1つのPRでまとめて更新してほしいものを packageNames でまとめています。 また、バージョンを更新したくない場合、 "enabled": false を設定することでPRが生成されなくなります(e.g. SwiftLint) Gemfile source 'https://rubygems.org' gem 'cocoapods', '1.9.3' Renovateが生成したPR 実際に生成されたPRの一覧は こちら です。 このように Podfile や Gemfile に定義したライブラリのバージョンが更新されている場合、PRが生成されていることが確認できます。 実際に運用してみて 今まではライブラリをアップデートしたい場合、能動的にライブラリの情報を取りに行くことがほとんどでしたが、RenovateがPRを生成してくれることで受動的にライブラリの更新をしやすくなりました。 クラシルの開発フローでは、Renovateが生成したPRにライブラリのリリースノートを貼って、チームメンバーにレビューリクエストを投げるような仕組みにしています。また、1人のメンバーがライブラリ周りの管理をするのではなく、毎週アップデート担当を設けて、リリースノートの添付、レビューリクエスト、マージまでの役割を担うことで、アップデートのフローが属人化しないようにも努めています。 余談 クラシルアプリに導入当初はBundlerとCocoaPodsで管理しているライブラリをRenovateの自動アップデート対象としていました。しかし、並行して XcodeGen 対応も同時に行っていたためprojectファイルを .gitignore に追加したタイミングでCocoaPodsの自動アップデートが動かなくなってしまいました。これはprojectファイルがRepository配下に存在しないため、Renovate側で実行ができなくなったためです。 これらの対策として、CocoaPodsは自前で自動アップデートの仕組みを作ることでRenovateからは除外し、現在ではBundlerのみをRenovateでアップデートするようにしています。 まとめ Renovateの導入をすることで、ライブラリの自動アップデートまわりを多く任せることができました。自前でライブラリの自動アップデート環境を用意するのは大変だと思うのでこの機会に是非導入してみてはいかがでしょうか? 明日はyasuoさんの「エンジニアがゼロから始めるプロダクトマネジメント」です。 また、dely ではエンジニアを絶賛募集中です! ご興味あればこちらのリンクからお気軽にエントリーください! join-us.dely.jp さらに TechTalk というイベントも行っているので、dely について詳しく知りたい方は是非参加してみてください! bethesun.connpass.com
どうもC#erの @MeilCli です。仕事ではAndroidエンジニアをしていますがC#erなのでアドベントカレンダーではC#について書きます 今回参加しているアドベントカレンダーはこちらです。3日目の記事になります adventar.org あと、同様なカレンダーがもう一つあります adventar.org 問: どれが早いか int ? n = 0 ; if (n.HasValue) {} // ① if (n is int ) {} // ② if (n is int and int ) {} // ③ if (n is not null ) {} // ④ ※ Roslyn master(25 Nov 2020)時点 正解はこの記事の中盤に書いています n.HasValueとはなんぞや C#erではない人向けに解説すると、C#のnull許容型は2種類(null許容参照型・null許容値型)が存在します null許容参照型のほうはC# 8.0でできた概念で、参照型の変数・値がnullになりえるものをnull許容参照型、nullになりえないものをnull非許容参照型とすることで近年のモダンな言語のnullセーフティーを取り入れようとするものです null許容値型のほうはだいぶ昔からあり *1 、通常は何らかの値が存在する構造体においてNullable構造体を介することで疑似的にnullという状態を表現できるようにするという機能です そして本題の n.HasValue についてですが、Nullable構造体が値を持っているかを返すプロパティとして HasValue があるというわけです n is int and intとはなんぞや n is int and int という一見奇妙な式は C# 9.0で機能強化されたパターンマッチング によるものです 強化内容としては主にパターンマッチング中に and , or , not が使えるようになるといった感じです たとえば変数nが0から10の間というのを素直に表現すると 0 <= n && n <= 10 ですが、機能強化されたパターンマッチングで表現すると n is >= 0 and <= 10 となります *2 パターンマッチングには型パターンがあり n is int and > 0 で int? に値があり *3 それが0以上という表現ができるのですが、無意味に int を判定させることもできるので n is int and int という書き方ができるわけです 答: どれが早いか 正解は①, ③, ④です。②の n is int による判定が遅いという感じです ①と④だと考えた人は惜しかったですね。以前、C# 7.3の頃に 同様な記事 を書いて n is int は遅いよ~と話していたのでMeilCliの個人ブログの熱烈な読者の方 *4 は同様に n is int and int も遅いと考えたことでしょう(たぶん) 初見で①, ③, ④と答えれた人はすごいですね。C#のエキスパートもしくはMeilCliのTwitterのフォロワーですね int? n = 0; if (n is int) {} if (n is int and int) {} 後者のほうが早そうということを発見 — C#ワカラナイ (@penguin_sharp) November 30, 2020 確: どれが早いか さて、ほんとに②だけが遅いのか 手っ取り早く BenchmarkDotNet を使って計測してみましょう namespace BenchmarkCode { [SimpleJob] [MeanColumn, MinColumn, MaxColumn] [MemoryDiagnoser] public class NullableInt { public IEnumerable< int ?> Source() { yield return 1 ; } [Benchmark] [ArgumentsSource(nameof(Source))] public bool HasValue( int ? n) { return n.HasValue; } [Benchmark] [ArgumentsSource(nameof(Source))] public bool IsInt( int ? n) { return n is int ; } [Benchmark] [ArgumentsSource(nameof(Source))] public bool IsIntAndInt( int ? n) { return n is int and int ; } [Benchmark] [ArgumentsSource(nameof(Source))] public bool IsNotNull( int ? n) { return n is not null ; } } } 計測コードはこちらです。コードの簡略化のため直接returnしています。 SharpLab で軽く確認した限りだとif文の中で記述した場合と大差がない状態になっていました そして結果がこう *5 BenchmarkDotNet=v0.12.1, OS=Windows 10.0.19042 Intel Core i9-10900K CPU 3.70GHz, 1 CPU, 16 logical and 8 physical cores .NET Core SDK=5.0.100 [Host] : .NET Core 5.0.0 (CoreCLR 5.0.20.51904, CoreFX 5.0.20.51904), X64 RyuJIT DefaultJob : .NET Core 5.0.0 (CoreCLR 5.0.20.51904, CoreFX 5.0.20.51904), X64 RyuJIT Method n Mean Error StdDev Min Max Gen 0 Gen 1 Gen 2 Allocated HasValue 1 0.6882 ns 0.0093 ns 0.0087 ns 0.6732 ns 0.7032 ns - - - - IsInt 1 31.5686 ns 0.1760 ns 0.1646 ns 31.2655 ns 31.8268 ns 0.0023 - - 24 B IsIntAndInt 1 0.6989 ns 0.0074 ns 0.0069 ns 0.6891 ns 0.7116 ns - - - - IsNotNull 1 0.6731 ns 0.0076 ns 0.0071 ns 0.6634 ns 0.6852 ns - - - - 明らかに n is int の場合が遅いですね 解: どれが早いか さて、なぜこのような差がついたかというとベンチマーク結果で明らかなのですが、 n is int の場合にボックス化が発生しているためです bool HasValue( int ? n) => n.HasValue; bool IsInt( int ? n) => n is int ; bool IsIntAndInt( int ? n) => n is int and int ; bool IsNotNull( int ? n) => n is not null ; SharpLab で上記のコードをデコンパイルするとIsInt以外のケースは ldarga.s で引数のアドレスを取得して call でNullable構造体のHasValueの値を返すようになっています .method assembly hidebysig static bool '<< Main >$> g__IsIntAndInt |0_2' ( valuetype [ System .Private.CoreLib ] System .Nullable ` 1 < int32 > n ) cil managed { // Method begins at RVA 0x2056 // Code size 8 (0x8) .maxstack 8 IL_0000 : ldarga .s n IL_0002 : call instance bool valuetype [ System .Private.CoreLib ] System .Nullable ` 1 < int32 >:: get_HasValue () IL_0007 : ret } // end of method '<Program>$'::'<<Main>$>g__IsIntAndInt|0_2' こんな感じ .method assembly hidebysig static bool '<< Main >$> g__IsInt |0_1' ( valuetype [ System .Private.CoreLib ] System .Nullable ` 1 < int32 > n ) cil managed { // Method begins at RVA 0x205f // Code size 15 (0xf) .maxstack 8 IL_0000 : ldarg . 0 IL_0001 : box valuetype [ System .Private.CoreLib ] System .Nullable ` 1 < int32 > IL_0006 : isinst [ System .Private.CoreLib ] System .Int32 IL_000b : ldnull IL_000c : cgt .un IL_000e : ret } // end of method '<Program>$'::'<<Main>$>g__IsInt|0_1' しかし、IsIntのケースでは box でボックス化をしていることがわかります ボックス化をするコストがかかることによってほかのケースより遅いというわけです なぜこのようなことが起きるのか C#コンパイラーであるRoslynのソースを読めば答えが見つかると思いますが、手元でいろいろなケースのCIL *6 を確かめてるとnull許容値型の value is T のケースのみ最適化のようなことがされず常にボックス化されるコードが吐き出されており、C# 7.0以降で追加されたパターンマッチングの機能の領域に入ると最適化されたコードが吐き出されてる印象がありました そのためなのか一見 n is int より無駄が多そうな n is int and int のほうが早いみたいな不可解な現象がおきてるようです ちなみに この記事のネタはアドベントカレンダーの記事を書いてて偶然発見したものをアドベントカレンダーの記事化したものです。本題のほうは16日に投稿すると思います(たぶん) 昨日の「dely #1 Advent Calendar 2020」はGENさんの「 木も見て森も見るための Athena(Presto) 集計術 」でした 明日はfunzinさんの「RenovateをiOSアプリ開発に導入してみた」です。お楽しみに! join-us.dely.jp bethesun.connpass.com *1 : たぶんC# 2.0ですかね *2 : 数値範囲に関しては個人的にはパターンマッチングを使わない表現のほうが数直線上でとらえることができるので好きです *3 : not nullでも表現できますね *4 : 熱烈な読者の方が存在するのかは定かではない *5 : Hyper-V上で計測してるので、i9-10900K 10C20Tではなくi9-10900K 8C16Tです *6 : Common Intermediate Language、共通中間言語、一部からはMSILとも呼ばれる。さっき出てたC#じゃないコードのこと
こんにちは! 今年、dely株式会社に新卒入社した開発本部のGENです。 この記事は「dely #2 Advent Calendar 2020」の3日目の記事です。 「dely #1 Advent Calendar 2020」はこちら↓ 昨日はisidaさんの「Swiftで1+1が何故2になるのか調べてみた」という記事でした。 swiftの加算演算子をアセンブリまで深掘りしている面白い記事です! https://tech.trilltrill.jp/entry/2020/12/02/105825 今回は今流行りの〇〇で湯婆婆を実装してみるの Athena(Presto) × Redash 版です。 ↓こちらに記事がまとまっています。 コード コードの解説 湯婆婆から渡される契約書にサインする処理は Redash のパラメータで実現しています。 var_orig_name AS ( SELECT ' {{ 契約書だよ。そこに名前を書きな。}} ' AS orig_name ) 名前を奪う処理は、元の名前からランダムに一文字取ってくることで実現しています。 var_name_given_by_yubaba AS ( SELECT SUBSTR ( ( SELECT orig_name FROM var_orig_name) , CAST ( FLOOR (RAND() * LENGTH (( SELECT orig_name FROM var_orig_name))) + 1 AS INTEGER ) , 1 ) AS name_given_by_yubaba ) まず、ランダムに 0 ~ 1 の小数を返す RAND() 関数と元の名前の文字数を返す LENGTH(元の名前) 関数をかけ、0 ~ length(文字数)の小数を生成します。それを床関数 FLOOR() で切り捨てすることで何番目の文字を取ってくるかを決定しています。Prestoの添字は1から始まるため、最後に1足してあげる処理が必要になります。 何番目の文字を取ってくるかが決まったら、次は実際に取ってくる処理です。 SUBSTR() 関数に元の名前、先ほどの何番目の文字を取ってくるかの数値、何文字取ってくるか(今回、湯婆婆は一文字残してすべて奪ってしまうので 1)を入れてあげます。 以上の処理で、名前を奪う(= 元の名前から一文字だけ抽出する)処理が完成します。 おまけ Redash は HTML を出力に適用することができるので、サインしたときの名前と名前を奪われた後の新しい名前をそれぞれ青と赤にしました。 実行結果 さいごに 明日はen.さんの「マジで助かった、新卒1年目デザイナーの教科書的noteや便利なサービス8選」です。 就活生の役にもたちそうな記事になるので、お楽しみに! また、dely ではエンジニアを絶賛募集中です! ご興味あればこちらのリンクからお気軽にエントリーください! https://join-us.dely.jp/ 社内のメンバーがテーマ毎に話すイベントもやってます!
TRILL開発部の石田です。 この記事は「dely #2 Advent Calendar 2020」2日目の記事です。 dely #1 Advent Calendar 2020 - Adventar dely #2 Advent Calendar 2020 - Adventar 昨日はsacoさんの記事「 ノンデザイナーでも大丈夫!見やすいプレゼン資料をつくる6つの手順 」でした。 デザイナーの視点から、分かりやすいプレゼンの作り方を順序立てて解説しているので是非ご覧ください。 さて、大学生のとき「1+1=2の証明」を授業で習ったのですが、小学生のとき当たり前のように教えられた自然数の足し算が大学の数学で証明され、数学の奥深さに触れた気がして今でも記憶に残っています。 そんなことを思い出して、普段書いているSwift内ではどうやって 1 + 1 が 2 であることを計算しているのか調べてみました。 Xcode上で、1 + 1 の加算演算子 + からCmd+Ctlで定義にジャンプしてみます。 定義は以下のようになっています。 public protocol AdditiveArithmetic : Equatable { public static func + (lhs : Int , rhs : Int ) -> Int } 加算演算子 + は Equatable に準拠した AdditiveArithmetic というプロトコルの中で定義されています。 しかしこれではインターフェースが分かっても実装が分かりません。 実装を確認するため、GitHubに公開されている Swiftのソースコード を見にいきます。 上記の加算演算子コードは Integers.swift というファイルにありました。しかしこちらもインターフェースのみで実装がありません。 どうやら実際の実装はGitHub上では見ることができず、gybファイルからビルド時に生成されるようです。 gybは Generate Your Boilerplate の略で、Pythonで記述するテンプレートシステムです。 該当のgybファイルは IntegerTypes.swift.gyb にあります。 このままでは実装が見られないので、 ガイド に従ってSwiftのソースコードをビルドします。 全部ビルドしなくても gyb.py を使って IntegerTypes.swift.gyb だけをSwiftファイルに変換することもできます。 ビルドすると IntegerTypes.swift というファイルが生成されます。加算演算子 + の実装を見てみます。 @_transparent public static func + (lhs : Int , rhs : Int ) -> Int { var lhs = lhs lhs += rhs return lhs } 加算演算子 + は内部的に加算代入演算子 += を使っているようです。 加算代入演算子 += の実装を見てみます。 @_transparent public static func += (lhs : inout Int, rhs : Int ) { let (result, overflow) = Builtin.sadd_with_overflow_Int64(lhs._value, rhs._value, true ._value) Builtin.condfail_message(overflow, StaticString( "arithmetic overflow" ).unsafeRawPointer) lhs = Int(result) } それらしいコードが出てきました。 Builtin.sadd_with_overflow_Int64() という関数が実際に加算をしているようです。 この関数を使って実際に加算ができるか試してみます。 import Swift let a : Int = 1 let b : Int = 1 let c : Builtin.Int1 = Builtin.trunc_Int8_Int1(Int8( 0 )._value) let (result, overflow) = Builtin.sadd_with_overflow_Int64(a._value, b._value, c) print(Int(result)) 実行するために多少面倒な定義をしています。 Builtin を使うため -parse-stdlib オプションを付けて実行します。 $ swift -parse-stdlib addition.swift # 2 ちゃんと 2 が出力されました。 加算演算子 + が内部的に Builtin.sadd_with_overflow_Int64() という関数を使っていることが確認できました。 しかし Builtin モジュールは組み込み関数にアクセスするものなので、これが内部で何を行っているのかが分かりません。 簡単なSwiftコードを作成し、それがLLVMの中間表現でどう書かれているかを見てみます。 LLVMはコンパイル基盤で、中間表現を経由しながら最適化を行い、最終的に機械語が生成されます。 1 + 1 だと分かりづらいので値を変えます。 let a = 1234 let b = 5678 let c = a + b このコードを中間表現であるLLVM IRに変換します。 $ swiftc -emit-ir addition.swift LLVM IRへの変換結果(抜粋)は以下のようになります。 ... store i64 1234 , i64 * getelementptr inbounds ( %TSi , %TSi * @ "$s8addition1aSivp" , i32 0 , i32 0 ), align 8 store i64 5678 , i64 * getelementptr inbounds ( %TSi , %TSi * @ "$s8addition1bSivp" , i32 0 , i32 0 ), align 8 %3 = load i64 , i64 * getelementptr inbounds ( %TSi , %TSi * @ "$s8addition1aSivp" , i32 0 , i32 0 ), align 8 %4 = load i64 , i64 * getelementptr inbounds ( %TSi , %TSi * @ "$s8addition1bSivp" , i32 0 , i32 0 ), align 8 %5 = call { i64 , i1 } @llvm.sadd.with.overflow.i64 ( i64 %3 , i64 %4 ) ... なんとなくですが、1234と5678をstoreし、loadし、加算する流れが分かります。 この @llvm.sadd.with.overflow.i64 という命令がSwiftの Builtin.sadd_with_overflow_Int64() に相当するようです。 @llvm.sadd.with.overflow.i64 が加算していることは分かったのですが、実際にはどのように実行されているのでしょうか。 今度はSwiftをbitcodeに変換し、そこからアセンブリを出力します。 $ swiftc -emit-bc addition.swift > addition.bc $ llc addition.bc llc コマンドは brew install llvm でLLVMをインストールすることで使えるようになります。 アセンブリの抜粋は以下のようになります。 ... movq $ 1234 , _ $ s8addition1aSivp (% rip ) ## imm = 0x4D2 movq $ 5678 , _ $ s8addition1bSivp (% rip ) ## imm = 0x162E movl $ 1234 , % eax ## imm = 0x4D2 addq $ 5678 , % rax ## imm = 0x162E ... addq という命令が実行され、加算されていることが分かります。 これがプロセッサの加算器で処理されるようです。 まとめ Swiftで1+1が何故2になるのか調べました。 1+1=2というプリミティブなコードではありますが、普段iOSアプリを開発しているときには触れることの少ないSwiftのソースコードやLLVM IR、アセンブリの中身を垣間見ることができ、楽しい経験ができました。 明日はGENさんの記事「Athena(Presto) × Redash で湯婆婆を実装してみる」です!お楽しみに! delyでは全方面でエンジニアを積極採用中です! 興味のある方は是非お声がけください! join-us.dely.jp TechTalkという社内のメンバーがテーマ毎に話すイベントもありますのでこちらも是非! bethesun.connpass.com
こんにちは! 今年、dely株式会社に新卒入社した開発本部のGENです。 この記事は「dely #1 Advent Calendar 2020」の2日目の記事です。 「dely #2 Advent Calendar 2020」はこちら↓ 昨日はmochizukiさんの「Ruby 3.0へ向けて、型周りをさわってみた」という記事でした。 記事で使用しているレシピのチョイスも最高なのでみてみてください! 今回は Athena(Presto) でアクティブユーザ(DAU,WAU,MAU)の推移を長期間でみるための集計術を紹介したいと思います。 アクティブユーザ集計における木も見て森も見るとは 「木を見て森を見ず」と言うことわざの意味を eigobu.jp で見てみると「細かい部分にこだわりすぎて、大きく全体や本質をつかまないこと」でした。アクティブユーザ集計(DAU,WAU,MAU)における「木を見て森を見ず」とはどういった状態かというと、「短期的な数値の増減を意識しすぎて、長期的な数値の増減の変化を考えられていない状態」です。ここから「木も見て森も見る」を考えると「短期的な数値の増減も意識しつつ、長期的な数値の増減の変化を考える状態」ということになります。 短期間での集計方法 例えば、以下のようなデータ(添付したものはデータの一部を抜粋したもの)が入っているテーブルがあります。 launchテーブル このときに10日前から昨日までの各日付ごとに DAU,WAU,MAU を集計するとなると、window 関数を用いる手法が思いつきますが、DISTINCT と ORDER BY は同時に用いることができないためエラーが出てしまいます。それを加味した上でクエリを書くと以下のようになります。 これを1年間といった長期間で集計を行おうとすると、クエリの実行がタイムアウトになってしまいます。このようになる原因は JOIN するデータ量が多くなりすぎて処理に時間がかかってしまうことが考えられます。 長期間の集計で使用した手法 集計する手順は以下のようになります。 user_id ごとに launched_date を格納した array を用意 各ユーザごとに CARDINALITY を用いて対象の期間に launch しているかを求める launch したユーザ数を集計 これをクエリに書いてみると以下のようになります。 launched_date ごとに user_id を array に格納するパターンも試しましたが、恐らく array に格納できるリソースに限界があり、リソースエラーが発生してしまいました。 さいごに 明日はMeilCliさんの「【C#】null許容値型のnonnull判定どれが早いかクイズ」です。 お楽しみに!! また、dely ではエンジニアを絶賛募集中です! ご興味あればこちらのリンクからお気軽にエントリーください! https://join-us.dely.jp/ さらに TechTalk というイベントも行っているので、dely について詳しく知りたい方は是非参加してみてください!
はじめに こんにちは! delyサーバーサイドエンジニアの望月 ( @0000_pg )です クラシルのアプリを中心にサーバーサイドを担当しています 今年もdelyのアドベントカレンダーが始まりました 🎉 adventar.org adventar.org 今年は開発部の人数も増えてきたので カレンダーを1と2にわけて行うことになりました 去年は2日目だったので 今年はトップバッターをやることにしました💪 本日公開された dely #2 Advent Calendar 2020 のほうの記事は デザイナーのsakoさんの ノンデザイナーでも大丈夫!見やすいプレゼン資料をつくる6つの手順 です! note.com これをみれば、誰でもイケてる資料がつくれるようになっています😎✨ とても勉強になりました! さて、 dely #1 Advent Calendar 2020 1日目の記事は Ruby 3.0へ向けて、型周りをさわってみた ことを書きたいと思います Ruby 3.0 Ruby 3.0.0 Preview 1 に関しては現時点で触れるようになっています Ruby 3.0 はいまから触るRBSや、並列処理のRactorなど 多数の新機能が追加される予定です 型周りの機能は、すでにgemとして配布されているので 必ずしも3.0を使う必要はありません 準備 レシピ決め 一番重要なことは、今日作るレシピを決めることです クリスマスが近づいてくるので クリスマスっぽいレシピにしましょう🎄 いちごで作る サンタクロース🎅 www.kurashiru.com \ めちゃかわ💖 / \ かわいいの暴力です💖 / これにしましょう!!🎅🎄 コード レシピが決まったので、買い物に行きつつ コードをつくっていきます Gemfile source ' https://rubygems.org ' gem ' rbs ' gem ' typeprof ' gem ' steep ' (※あえてgemとして記載してあります) recipe.rb class Recipe attr_reader :title def initialize ( title :, ingredients :, instructions :) @title = title @ingredients = ingredients @instructions = instructions end def ingredients Ingredient .summary( @ingredients ) end def instructions Instruction .summary( @instructions ) end end ingredient.rb class Ingredient attr_reader :name , :quantity , :unit def initialize ( name :, quantity :, unit :) @name = name @quantity = quantity @unit = unit end class << self def summary (ingredients) ingredients_text = " 材料 \n" ingredients.each do | ingredient | ingredients_text << <<- INGREDIENTS_TEXT #{ ingredient.name } #{ ingredient.quantity }#{ ingredient.unit } INGREDIENTS_TEXT end ingredients_text end end end instruction.rb class Instruction attr_reader :text def initialize ( text :) @text = text end class << self def summary (instructions) instructions_text = " 手順 \n" instructions.each do | instruction | instructions_text << <<- INSTRUCTIONS_TEXT #{ instruction.text } INSTRUCTIONS_TEXT end instructions_text end end end app.rb require_relative ' recipe ' require_relative ' ingredient ' require_relative ' instruction ' title = ' いちごで作る サンタクロース ' ingredients = [ Ingredient .new( name : ' いちご ' , quantity : 2 , unit : ' 個 ' ), Ingredient .new( name : ' ホイップクリーム ' , quantity : 10 , unit : ' g ' ), Ingredient .new( name : ' チョコレートペン (黒) ' , quantity : 1 , unit : ' 本 ' ) ] instructions = [ Instruction .new( text : ' チョコレートペンは湯煎にかけて溶かしておきます。 ホイップクリームは絞り袋に入れておきます。 ' ), Instruction .new( text : ' いちごはヘタを切り落とします。 ' ), Instruction .new( text : ' ヘタの部分から2/3のところを切ります。 ' ), Instruction .new( text : ' ヘタの部分を下にして切り口にホイップクリームを絞り、挟みます。上にホイップクリームを直径5mm程絞り、帽子をつくります。 ' ), Instruction .new( text : ' チョコレートペンで顔とボタンを描いて完成です。 ' ) ] recipe = Recipe .new( title : title, ingredients : ingredients, instructions : instructions) puts recipe.title puts recipe.ingredients puts recipe.instructions $ bundle exec ruby app.rb いちごで作る サンタクロース 材料 いちご 2個 ホイップクリーム 10g チョコレートペン (黒) 1本 手順 チョコレートペンは湯煎にかけて溶かしておきます。 ホイップクリームは絞り袋に入れておきます。 いちごはヘタを切り落とします。 ヘタの部分から2/3のところを切ります。 ヘタの部分を下にして切り口にホイップクリームを絞り、挟みます。上にホイップクリームを直径5mm程絞り、帽子をつくります。 チョコレートペンで顔とボタンを描いて完成です。 本題 我々はいちごでサンタさんをつくりながら 型と向き合っていかないといけません🎅🎄 rbs でも雛形はつくれるのですが github.com $ rbs prototype rb recipe.rb 今回は typeprof をつかっていきます typeprof github.com 雛形をつくります (色々とオプションはありますが割愛します) $ typeprof lib/recipe.rb -o sig/recipe.rbs $ typeprof lib/ingredient.rb -o sig/ingredient.rbs $ typeprof lib/recipe.rb -o sig/recipe.rbs sig/recipe.rbs # Classes class Recipe @ingredients : untyped @instructions : untyped attr_reader title : untyped def initialize : ( title : untyped, ingredients : untyped, instructions : untyped) -> untyped def ingredients : -> untyped def instructions : -> untyped end sig/ingredient.rbs # Classes class Ingredient attr_reader name : untyped attr_reader quantity : untyped attr_reader unit : untyped def initialize : ( name : untyped, quantity : untyped, unit : untyped) -> untyped def self . summary : (untyped) -> String end sig/instruction.rbs # Classes class Instruction attr_reader text : untyped def initialize : ( text : untyped) -> untyped def self . summary : (untyped) -> String end 余談ですが、 rbi と rbs について itoさんのブログに記載がありました koic.hatenablog.com steep 今回は steep をつかって型チェックをしていきます github.com 準備 Steepfile をつくります $ steep init # target :lib do # signature "sig" # # check "lib" # Directory name # check "Gemfile" # File name # check "app/models/**/*.rb" # Glob # # ignore "lib/templates/*.rb" # # # library "pathname", "set" # Standard libraries # # library "strong_json" # Gems # end # target :spec do # signature "sig", "sig-private" # # check "spec" # # # library "pathname", "set" # Standard libraries # # library "rspec" # end 今回はこうしました target :lib do check " lib " signature " sig " end 型定義 先程の rbs に型を定義していきます sig/recipe.rbs # Classes class Recipe @ingredients : Array [ Ingredient ] @instructions : Array [ Instruction ] attr_reader title : String def initialize: ( title : String , ingredients : Array [ Ingredient ], instructions : Array [ Instruction ]) -> void def ingredients: -> String def instructions: -> String end sig/ingredient.rbs # Classes class Ingredient attr_reader name : String attr_reader quantity : Integer attr_reader unit : String def initialize: ( name : String , quantity : Integer , unit : String ) -> void def self .summary: ( Array [ Ingredient ]) -> String end sig/instruction.rbs # Classes class Instruction attr_reader text : String def initialize: ( text : String ) -> void def self .summary: ( Array [ Instruction ]) -> String end sig/app.rbs # Classes class Recipe attr_reader title : String def initialize : ( title : String , ingredients : Array [ Ingredient ], instructions : Array [ Instruction ]) -> void def ingredients : -> String def instructions : -> String end class Ingredient attr_reader name : String attr_reader quantity : Integer attr_reader unit : String def initialize : ( name : String , quantity : Integer , unit : String ) -> void def self . summary : ( Array [ Ingredient ]) -> String end class Instruction attr_reader text : String def initialize : ( text : String ) -> String def self . summary : ( Array [ Instruction ]) -> String end 型チェック $ bundle exec steep check (色々とオプションはありますが割愛します) 失敗してみる title の型を Integer にし、 recipe.instructions の戻り値を Integer にしてみます sig/recipe.rbs # Classes class Recipe @ingredients: Array[Ingredient] @instructions: Array[Instruction] + attr_reader title: Integer def initialize: (title: String, ingredients: Array[Ingredient], instructions: Array[Instruction]) -> void + def instructions: -> Integer end lib/recipe.rb:5:4: IncompatibleAssignment: lhs_type=::Integer, rhs_type=::String (@title = title) lib/recipe.rb:14:2: MethodBodyTypeMismatch: method=instructions, expected=::Integer, actual=::String (def instructions) 最後に 明日の dely #1 Advent Calendar 2020 は GENさんの 木も見て森も見るための Athena(Presto) 集計術 です! お楽しみに! delyではRailsエンジニアを募集しています! サーバーサイドのカジュアル面談は自分が担当しています! 少しでも興味があれば、お気軽にお話ししましょう〜 join-us.dely.jp また、定期的に開発組織についてイベントを行っています! こちらもカジュアルに話を聞きにきてもらえればと思います〜 bethesun.connpass.com
はじめまして。TRILL開発部PdMの米田です。 主にTRILLアプリ開発のマネジメントを担当しています。 TRILLというプロダクトの開発について、非技術者の視点であれこれご紹介できればと思います。 PdMと一口に言っても様々定義がある中で、今回は「TRILLのPdMって何をしてるの?」という話をしてみようと思います。 大まかに、何をしているの? サービス全体のKPI目標達成に向け、施策を考える 各所のステークホルダーとあれこれすり合わせる 考えた施策たちの優先度を判断する エンジニアとコミュニケーションをとり、開発タスクに落とす 施策の効果を検証する ↑これらのスケジュールを管理する 基本的にはこのサイクルをぐるぐる回すのが仕事です。 その過程においてトラブルが発生すればその対応を行ったり、チームの決まりごとを作ったりといった細々したものは発生しますが、基本的には施策を実行してサービスを開発視点で改善していく上で舵を切ることがメインの業務です。 施策を考える 施策には大きく2種類あると考えています。 ひとつは、プロダクトの「負」を解消し体験を良くするもの。 例えばアプリにおける各挙動の速度であったり、クラッシュを減らしたりといったものです。 ユーザーが アプリを開き 記事に出会い 記事を読み 別の記事に出会い また記事を読み 気に入った記事をお気に入りにストックし 知人等に記事やアプリを薦める といった一連の行動をいかにストレスなく行えるかを考え、日々コツコツ「負」を解消していっています。 そしてもうひとつが、数字を積み上げるための改善施策です。 事業として定めるKPIに対し、どこに大きな課題があり、その課題をどう改善していくかを開発視点で考えていきます。 ここは開発内だけでなく定例の場や日頃のやり取りの中でマーケチームに相談するようにしています。 施策を推し進める 日々出てくる課題に対しての打ち手(施策)が出たら、それらの優先度を判断してどこから手を付けるかを意思決定します。 この判断を誤ると、事業に対する成果やコストに影響が及ぶので、PdMにとって施策の優先度判断というのは非常に重要な業務です。 また、施策の優先度判断をする上で、他部署他職種の担当者などその施策に関わるステークホルダーとの調整が発生する場合があります。 TRILLにおいては、ここの調整を行うのもPdMの仕事です。 これらを踏まえ、サービス全体を俯瞰して何を優先すればよいかを考慮する必要があるため、開発以外の状況もある程度常にキャッチアップし、適切な判断を下す必要があります。(勉強不足を痛感する日々です) 開発する 施策の方針がある程度固まったら、エンジニアと話し合い、タスクに落としていきます。 便宜上ここで初めてエンジニアが登場していますが、もちろん前段階の施策の優先度を判断するタイミングでエンジニアに意見を求めたりということも頻繁にあります。(自分が非技術者ということもあり、判断に誤りがないようエンジニアとは非常に密なコミュニケーションをとっています) ちなみにですが、TRILLのアプリ開発は2週で1スプリントのスクラムを採用しています。 少数での開発のため、以前は特にフレームワークに則らずよしなに開発を進めていたのですが、タスクが可視化されず管理がうまくいかなかったり、それによってスケジュール調整がうまくいかなかったりという問題がありました。 これらを解消すべく、エンジニアからの提案によってスクラムのフレームワークに則って開発を進めることにしました。 現在は比較的シンプルな開発フローが実現できています。 リリース・効果検証 スプリントバックログに積んだタスクは、開発を終えると新しいバージョンに載り、リリースされます。 TRILLではリリース作業自体はエンジニアが行いますが、リリースの責任はPdMが持つため、リリース内容は申請前のタイミングで必ず目を通します。 また「負」の解消にせよ積み上げの施策にせよ、開発したものは必ずリリース後にその効果を検証して省みる必要があります。 事前に定めた良し悪しの判断軸と照らし合わせて、次のアクションを検討していきます。ここまでが施策のワンセットです。 さいごに こうして整理してみるとPdMとしてめちゃくちゃ特別ななにかをしているわけではありませんが、プロダクト開発の方向性を示し、ひとつひとつの判断に責任を持つという点でとても意味のあるポジションだという自覚を持っています。 そしてエンジニアをはじめとしたチームメンバーと肩を組み、スピード感をもった開発ができています。 もしTRILLの開発にご興味をお持ちいただけた方は、下記よりご連絡ください。ぜひ一度お話ししましょう! 積極募集中 www.wantedly.com www.wantedly.com www.wantedly.com www.wantedly.com
TRILL開発部の石田です。 TRILLでは、Swagger Codegenで生成したAPIクライアントライブラリを使ってサーバと通信しています。 このライブラリはGitHubで管理しており、Carthage経由で利用しています。 Xcode11からSwift Package Manager (以下SwiftPM) がサポートされたということで、上記ライブラリをSwiftPMに対応させてみました。 Swagger Codegen製APIクライアントライブラリ Swaggerは、REST APIを記述するための仕様であり、その仕様からクライアントのライブラリや、サーバのスタブを自動生成するツールがSwagger Codegenです。 TRILLのクライアントアプリでは、Swagger Codegenで生成されたAPIクライアントライブラリを使っています。 iOSのクライアントライブラリは、内部でRxSwiftとAlamofireを使っており、そのためそれらライブラリと依存関係にあります。 SwiftPM対応 SwiftPM対応は、 Package.swift がルートディレクトリに存在し、GitHubなどのリモートリポジトリ経由でライブラリが参照できれば完了です。 Package.swift は以下のコマンドを実行することで生成されます。 $ cd MyPackage $ swift package init 生成された Package.swift を必要に応じて編集します。 上述の通りRxSwiftとAlamofireと依存関係にあるので、 dependencies の部分に記載します。 また path の部分も必要に応じて編集します。 // swift-tools-version:5.2 import PackageDescription let package = Package( name : "API" , platforms : [ .iOS(.v11) ] , products : [ .library(name: "API", targets: ["API"] ) ], dependencies : [ .package(url: "https://github.com/ReactiveX/RxSwift.git", from: "5.1.1"), .package(url: "https://github.com/Alamofire/Alamofire.git", from: "4.9.1") ] , targets : [ .target( name: "API", path: "Source/Path", dependencies: [ "RxSwift", "Alamofire" ] ) ], swiftLanguageVersions : [.v5] ) Package.swift の編集が完了したら、ビルドをします。 $ swift build Package.resolved が生成されると思います。 これらファイルをまとめてGitHubなどにアップロードします。 Xcodeからの利用 Xcodeのメニューから、File → Swift Package → Add Package Dependency... から上記のライブラリを追加します。 プライベートリポジトリの場合は認証を必要としますが、GitHubのアカウント情報を入力すればダウンロードができます。 まとめ Carthageで管理している社内ライブラリをSwiftPM対応しました。 Xcode公式のパッケージ管理ツールなので、信頼感がありますし、ソースコードもXcodeから確認することができるので便利に利用することができます。 しかし、Carthageのように事前のビルドがないため、クリーンビルドには時間がかかってしまいます。 そのため、最終的にはSwiftPM移行を諦め、現在はCarthageでの管理を行っています。 こちらに関しては、Xcode 12/Swift 5.3で対応したBinary Frameworkに期待したいと思います。
TRILL開発部の石田です。 delyでは様々な情報をSlackに流して共有しているのですが、今回はTRILLで行っているBigQueryのクエリ結果のSlack投稿について紹介します。 背景 delyでは、透明性を大事にする取り組みとして、経営指標をオープンにSlackに流しています。 参考: dely会社紹介資料 / クラシルに関わるエンジニア・デザイナー募集 / dely - Speaker Deck 経営指標に限らず、アプリのパフォーマンス結果(クラッシュ率や速度など)を開発者だけでなくビジネスチームも含めて確認しています。 課題 TRILLではGoogleAnalyticsやFirebaseを使ってログを取得しています。 取得したログの結果は、GoogleAnalyticsとFirebaseの各管理画面から確認することができます。 しかし、欲しい情報を確認するためには管理画面を深く辿らなければならないことがあります。 また、もっと細かい粒度で分析するために、rawデータを使いたいときもあります。 そこで、GoogleAnalyticsやFirebaseのrawデータを加工してSlackに投稿することで、簡単に欲しい情報を確認できるようにしました。 やったこと GoogleAnalyticsやFirebaseをBigQueryに連携し、BigQueryにrawデータを流し、BigQueryのクエリ結果をSlackに投稿するようにしました。 BigQueryのクエリ結果はGoogleスプレッドシートに書き込み、欲しい情報を溜めていくようにしています。 全体像 全体の構成は下図のようになります。 まず、Google Apps Script (以下GAS) からBigQueryにクエリを投げ、その結果をスプレッドシートに書き込みます。 次にスプレッドシートからデータを取得し、Slackに投稿します。 スプレッドシートにはデータが溜まっているので、先週比、先月比など所望の差分データを取り出すことができます。 BigQueryのクエリ結果をスプレッドシートに書き込む まず、GASからBigQueryにアクセスできるようにする必要があります。 メニューの [リソース] → [Googleの拡張サービス] を選択し、BigQueryを有効にします。 以下のコードは、BigQueryのクエリ結果をスプレッドシートに書き込むサンプルとなります。 スプレッドシートのセルA1に、 COUNT(*) の結果が書き込まれます。 function runQuery() { var projectId = 'GCPのプロジェクトID' ; var sql = '\ #standardSQL \ n \ SELECT COUNT(*) \ FROM "BigQueryのテーブル名"'; var resource = { query: sql } ; var queryResults = BigQuery.Jobs.query(resource, projectId); var jobId = queryResults.getJobReference().getJobId(); while (!queryResults.getJobComplete()) { queryResults = BigQuery.Jobs.getQueryResults(projectId, jobId); Utilities.sleep(1000); } var spreadsheetId = 'スプレッドシートのID' ; var spreadsheet = SpreadsheetApp.openById(spreadsheetId); var sheet = spreadsheet.getSheetByName( 'シート名' ); sheet.getRange( 'A1' ).setValue(queryResults.rows [ 0 ] .f [ 0 ] .v); } GASから実行するとスプレッドシートとBigQueryのアクセス許可ダイアログが表示され、許可すると対象のスプレッドシートにクエリ結果が書き込まれます。 スプレッドシートの値をSlackに投稿する 以下のコードは、スプレッドシートの値をSlackに投稿するサンプルとなります。 実行するとセルA1に書き込んだ値をSlackに投稿します。 function post() { var spreadsheetId = 'スプレッドシートのID' ; var spreadsheet = SpreadsheetApp.openById(spreadsheetId); var sheet = spreadsheet.getSheetByName( 'シート名' ); var result = sheet.getRange( 'A1' ).getValue(); var data = { 'text' : result } ; var options = { 'method' : 'post' , 'contentType' : 'application/json' , 'payload' : JSON.stringify(data) } ; var webhookUrl = 'SlackのWebhook URL' ; UrlFetchApp.fetch(webhookUrl, options); } 実際には、日毎にデータを集計しており、先週比、先月比でデータがどう変化したかを投稿しています。 まとめ Google Apps Scriptを使ってBigQueryのクエリ結果をSlackに投稿する方法について紹介しました。 欲しい情報をrawデータから加工し、毎日Slackへ自動的に投稿することで、誰でも簡単に情報を取得することが出来ます。 delyでは全方面でエンジニアを積極採用中です。 興味のある方は是非お声がけください。
どうも、Android担当の永井です。 TRILLでは、OSSのライセンス表記をHTMLに張り付けてWebViewに流し込むような運用をしていたけど、ライブラリ追加削除するたびにいちいち変更がめんどう! とういうことで、Google謹製の com.google.gms:oss-licenses を導入してライセンス表記の編集作業とおさらばしました! 詳しい手順はこちら developers.google.com developers.google.com 実作業はかんたん。 依存関係追加してActivityを呼び出すだけ。 あとは勝手にライセンス情報を取得してリスト表示してくれます。 ・依存関係の追加 ルートレベルのbuild.gradleにoss-licensesプラグインを追加。 buildscript { repositories { google() } dependencies { classpath 'com.google.android.gms:oss-licenses-plugin:0.10.2' } ・アプリレベルのbuild.gradleでプラグインを適用 apply plugin: 'com.google.gms.oss.licenses.plugin' これで準備OK。 ビルドするとpomから依存するライブラリのライセンス情報を取得して一覧化してくれます。 あとは適当なところで画面を呼び出すだけ。 setActivityTitleでActionBarに表示するタイトルを変更できます。 @OnClick(R.id.activity_information_title_license_tv) void onClickLicense() { startActivity(new Intent(this, OssLicensesMenuActivity.class)); OssLicensesMenuActivity.setActivityTitle(getString(R.string.activity_setting_license_title)); } ・画面のカスタマイズ OssLicensesMenuActivity使うのであればできることはだいぶ少なくタイトル設定とテーマ変更くらいしかできなさそう。 マニフェストにテーマ指定して、 <activity android:name="com.google.android.gms.oss.licenses.OssLicensesActivity" android:theme="@style/LicenseTheme" /> <activity android:name="com.google.android.gms.oss.licenses.OssLicensesMenuActivity" android:theme="@style/LicenseTheme" /> テーマで指定すればOK。 ActionBarいらないならこれで。 自分はこれにしました。 <style name="LicenseTheme" parent="Theme.AppCompat.Light"> <item name="windowActionBar">false</item> <item name="windowNoTitle">true</item> <item name="android:statusBarColor">@color/gray_medium_light</item> <item name="android:textSize">@dimen/font_size_tiny</item> </style> つかうならたぶんこんな感じである程度デザイン揃えられそう。 <style name="LicenseTheme" parent="Theme.AppCompat.Light"> <item name="windowActionBar">true</item> <item name="windowNoTitle">false</item> <item name="actionBarStyle">@style/LicenseTheme.ActionBar</item> <item name="android:actionBarStyle">@style/LicenseTheme.ActionBar</item> <item name="android:statusBarColor">@color/gray_medium_light</item> <item name="android:textSize">@dimen/font_size_tiny</item> </style> <style name="LicenseTheme.ActionBar" parent="Widget.AppCompat.Light.ActionBar"> <item name="background">@color/white</item> <item name="android:background">@color/white</item> <item name="titleTextStyle">@style/LicenseTheme.ActionBar.TextStyle</item> <item name="android:titleTextStyle">@style/LicenseTheme.ActionBar.TextStyle</item> </style> <style name="LicenseTheme.ActionBar.TextStyle" parent="TextAppearance.AppCompat.Widget.ActionBar.Title"> <item name="android:textSize">@dimen/font_size_tiny</item> </style> </resources> もっと細かくデザイン合わせた画面作りたければ、 ライセンス情報自体は、app/build/generated/third_party_licenses/res/raw ディレクトリに、third_party_licenses、third_party_license_metadataとして出力されているので、これを読み取って表示すればいろいろできそうです!
TRILL開発部の石田です。 TRILLでは、ver.3.5.0でiOS14で新しく登場したWidgetに対応しました。 もともとToday Extensionには対応していたのですが、Widget Extensionは新しい機能ということでデザインや実装を見直しました。 Widget自体はWidgetKitフレームワークとSwiftUI用のウィジェットAPIを使って実装していくのですが、以下ではWidgetの実装で悩みやすい部分についてサンプル実装を紹介したいと思います。 サイズごとに別のViewを実装する WidgetにはSmall、Medium、Largeの3種類のサイズがあります。 デフォルトでは、EntryViewで定義したViewがそれぞれのサイズに伸縮され描画されますが、 WidgetFamily を使って場合分けすることでサイズごとに別のViewを設定することができます。 例えば以下のようになります。 struct SampleWidgetEntryView : View { @Environment (\.widgetFamily) var family : WidgetFamily var entry : Provider.Entry @ViewBuilder var body : some View { switch family { case .systemSmall : Text ( "Small" ) case .systemMedium : Text ( "Medium" ) case .systemLarge : Text ( "Large" ) default : Text ( "NotAvailable" ) } } } 特定のサイズのみ対応する 上述の通り、Widgetには3種類のサイズがあります。 その中でもLargeには対応せず、SmallとMediumのみに対応したい場合があると思います。 そのときは、 supportedFamilies(_:) )を使うことで、対応するサイズを定義することができます。 @main struct SampleWidget : Widget { var body : some WidgetConfiguration { StaticConfiguration(kind : "widget" , provider : Provider ()) { entry in SampleWidgetEntryView(entry : entry ) } .configurationDisplayName( "My Widget" ) .description( "This is an example widget." ) .supportedFamilies([.systemSmall, .systemMedium]) } } 同じサイズで別のViewを設定する Widgetは同じサイズでも別のViewを設定することができます。 例えば天気アプリのWidgetを考えたとき、Smallサイズでも「現在の天気」と「雨雲レーダー」の2種類を実装したいという場合です。 その場合、 WidgetBundle を使い、Widgetを複数定義することで実装することができます。 @main struct SampleWidgets : WidgetBundle { @WidgetBundleBuilder var body : some Widget { WeatherWidget() RainRadarWidget() } } 定期実行する ニュースアプリなどのWidgetでは、定期的にサーバにアクセスし、最新の情報をWidgetで提示したいと思います。 その場合、Timelineの TimelineReloadPolicy に所望の時間を設定することで実現できます。 下記は15分ごとに実行するサンプルとなります。 func getTimeline ( in context : Context , completion : @escaping (Timeline < Entry > ) -> ()) { let currentDate = Date() let refreshDate = Calendar.current.date(byAdding : .minute, value : 15 , to : currentDate ) ! ... let timeline = Timeline(entries : [entry] , policy : .after(refreshDate)) completion(timeline) } Deep Linkを設定する Widgetはデフォルトではタップするとアプリを開くだけです。 特定の画面に遷移させるには widgetURL(_:) )を設定することで実現できます。 Smallサイズではタップ領域は1つだけですが、Mediumサイズ、Largeサイズには複数のタップ領域を設けることができます。 その場合、 Link を使うことで複数の導線を設定できます。 例えば下記のようになります。 var body : some View { VStack { Link(destination : hogeDeepLink ) { Text( "Hoge" ) } Link(destination : fugaDeepLink ) { Text( "Fuga" ) } }.widgetURL(piyoDeeplink) } まとめ Widgetの実装において、「これってどうやるんだ?」という機能のサンプル実装を紹介しました。 Widgetはサイズが固定されており、機能的な制限もありますが、既存の機能とは異なる新しい価値をユーザに提供できるのではないかと思います。 まだ登場したばかり対応しているアプリは少ないですが、今後どのようなWidgetが出てくるかウォッチしながら、さらなるブラッシュアップをしていきたいと考えています。 TRILLではiOS限らずAndroid、サーバなどでエンジニアを積極採用中です。 Widgetなど新しい機能にも積極的に取り組めますので、ご興味がありましたら採用ページよりご連絡ください!
クラシルを開発してるAndroidエンジニアの MeilCli です。前回の クラシルAndroidプロジェクトの開発者体験の向上を頑張ってます! を投稿してから進捗があったので報告します 前回予告した内容は以下の感じでですが、設定した目標通りに行動できないのがエンジニアです。ご了承ください 長年放置し続けてきたlintの対応、不要になったリソースの削除 使用しているOSSなどのライセンス表記のための集計ツールを MeilCli/Librarian に置き換える ライブラリーアップデートの自動検出・通知 モジュールのbuild.gradleの共通な宣言部分をbuildSrcへまとめる 長年放置され続けてきたlint問題 不要なリソースの削除 クラシルの開発は多種多様な機能を作っては検証し、価値がなければ消すの繰り返しをしてきました。そのため開発者が消し忘れているリソースがたくさんあり、そのリソースによってビルドに時間がかかっているのではと考え大掃除することにしました 大掃除の様子 大掃除自体はIntelliJ IDEAやAndroid Studioに搭載されているInspect Code機能(ツールバーのAnalyze=>Inspect Codeで使えます)を使えば機械的に使用していないコードなどをあぶり出してくれるので比較的楽に行うことができます ※Inspect Codeでの機械的なチェックだと検知ミスやリフレクションなどのすり抜けがありえるのでプロジェクトの全文検索などを駆使して人力で消すかどうかの最終確認をすることをお勧めします 結果としてはビルド時間は大して変わりませんでした(残念) ただ、lintのwarningを結構な数減らすことができたのとダウンロード時のapk容量を2MBほど減らすことができたのでやって正解だったかなと思います 今後の計画 不要なリソースを削除したあとではAndroid lintのみで約400件ほどのWarningがある状態となりました(IntelliJ IDEAのInspect Codeベースだとそれを遥かに超えるWarningが貯まっています) それらのWarningを今すぐに撲滅するのは現実的ではないため、計画を立てて減らしていき最終的に撲滅するということにしました lint戦略 画像はAndroidチームに共有したlint戦略です。概要としては以下の感じです ファーストステップ 2Q終わり(つまり9月末)までを想定 ルール調整や重要度の高いものへの対応 モニタリング環境整備 セカンドステップの準備(PullRequestでのwarningのインラインコメント) セカンドステップ 撲滅作業が苦にならない程度に減るまでを想定 Warningを増やさない・気づいたら消す・気が向いても消す ファイナルステップ Warningをひたすら消す 計画は立てたのであとは実行するのみです。モニタリングは週1ペースでWarningの総数や推移が分かればいいかなと考えています Android lintとDetektに関してはGitHub ActionでWarningの計測と統計・推移画像を生成する MeilCli/android-lint-statistics と MeilCli/detekt-statistics を作成&導入をし、現在は週1で試験実行しています 他にもIntelliJ IDEAのInspect CodeをCI上で実行したいところなので JetBrains/inspection-plugin を使っていこうと考えています *1 Librarianの導入 クラシルで使用しているライブラリーのライセンス表記を自動生成するツールとして自作の MeilCli/Librarian を導入しました 今までも自動生成ツールを使用していたのですがいくつかの問題を抱えていました マルチモジュールプロジェクトによる複雑なConfigurationによるライブラリーの誤検知 新たなライブラリーを導入したなどにConfigファイルを手作業による調整 Maven Artifactごとにライセンス表記が生成されていたことによって膨大なライセンス表記 HTMLを生成していたため2MBにもおよぶファイルをアプリに組み込んでいた これらの問題が表面化し始めた頃、ちょうどMeilCliが他のライブラリーを作るためにライブラリーのライセンス集計ライブラリーを作っていたためそれを導入する運びになりました *2 実際に導入して数週間経ちましたが検知精度としては期待値通りになっています。すでにLibrarianを導入したものがストア配信されているので気になる方はぜひクラシルをインストールしてマイページの設定からライセンス表記画面を見てください また、 個人ブログでLibrarianの解説記事 を書いているので気になる方はぜひ見てください ライブラリーアップデートの自動検知・通知(+ ついでにbuild.gradle整理) さて、みなさん dependabot という便利なbotをご存知でしょうか?リポジトリー内のライブラリー依存宣言部分をスキャンしてアップデートがあったらバージョン宣言を置き換えたPullRequestを自動で作ってくれるbotです (npmやnuget方面の方は)セキュリティー関連の警告やPullRequestが自動で作成されるのを見たことがある人がいるかもしれませんが、dependabotがGitHubに統合された結果、リポジトリーにdependabotの設定ファイルを追加すればセキュリティー関連のPullRequest以外も自動で作成されるようになりました *3 PrivateリポジトリーでもGitHubへのデータ提供を許可していればdependabotを運用することができるのでこれを使わない手はないですよね?ということでクラシルAndroidにもdependabotを導入しました # .github/dependabot.yml version : 2 updates : - package-ecosystem : "gradle" directory : "/" schedule : interval : "weekly" assignees : - "MeilCli" labels : - "dependencies" commit-message : prefix : "[improvement] " open-pull-requests-limit : 15 今の所、クラシルではこのような設定ファイルで運用していこうと考えていて、運用を始めて数週間経ちましたが毎週何個かアップデートがあるという状態になっています *4 。この設定ファイルで毎週画像のようなPullRequestが作成されています dependabotが作成してくれたPullRequest また、Gradleプロジェクトでdependabotを導入するには多少手心加えるとより良いマルチモジュールプロジェクト運用ができるのですが、ここで解説すると長くなるためマルチモジュールプロジェクトで導入したい方は 以前に個人ブログで書いたdependabot導入記事 を参照ください あと、ついでに行ったbuild.gradleの整理ですが、具体的に言うとbuild.gradleで記述する内容をbuildSrc配下のGradle Pluginとして記述するようにしました。この記事に書くとコードで埋め尽くされてしまいますので、同様な記述をしているLibrarianを見ていただければと思います。↓コードリンク Librarian/buildSrc/src/main/kotlin/net/meilcli/librarian/gradle/plugins at master · MeilCli/Librarian · GitHub その他 前回紹介したPullRequestのマイルストーンチェックのGitHub ActionのWorkflowですが、マイルストーン張替えのときにもコメントされてしまう件はAPIの情報を参照することによって回避するようにしました actions/.github/workflows/check-has-milestone.yml at master · MeilCli/actions · GitHub 今後について 細かい改善はいろいろとありますが、皆さんにご紹介するほどの内容になると Android lintのインラインコメント対応 inspection-pluginの続報 ライブラリーアップデート時の差分検知 といったところを次回予告としておきます https://join-us.dely.jp/ join-us.dely.jp 新しい採用ページできたらしいです *1 : Gradle v6に対応できていませんでしたがそこはコントリビュートしておきました、最新版を使えばおそらく皆さんのAndroidプロジェクトでも動作するはずです *2 : ライブラリーを作るためにライブラリーを作る、GitHub Actionを作るためにGitHub Actionを作る、それがMeilCliです *3 : 個人的にはいつの間に統合されたんだという感じです *4 : dependabot任せにできないPullRequestは手作業でアップデート作業したり、場合によっては何らかの都合によってPendingするといったこともやっています
こんにちは、クラシル Android のプロダクトマネージャーをしている tummy です。 先月から立て続けに Android チームで行っている取り組みについて紹介させていただいてますが、今回も Android チーム全員で毎朝アプリを触る時間、 通称ぽちぽち会 について紹介させていただきます。 クラシル Android アプリの開発上の課題 Pixel シリーズが日本に来てから、道端を歩いていても以前に比べると Android ユーザーを見かけるようになりました(筆者の観測内では)。しかし、日本ではまだまだ iOS ユーザーのほうが比率としては高く、dely 社内もその傾向にあります。 弊社にはプロダクトレビューという毎週定期的に時間を取ってアプリをいじる文化が存在しますが、 iOS チームに比べて人が触る機会が少なかったり 、iOS で良かった施策を Android でも行う際などにこのデザインは Android 向けのものなのか?といった 定性的な評価をする機会が少ない 状態にありました。 クラシル Android チームの開発体制の課題 また、弊社の Android チームの体制にもいくつか課題がありました。 Android チーム以外がリリース前の機能に対してフィードバックできる機会が、 専用のチャンネルでアナウンスされたタイミングからリリースされるタイミングまでの間しかない Android を普段使いしている人がクラシルの Android のデバッグ版を触る機会があまりなく、 Android ユーザー視点でのプロダクトへの指摘がやりにくい 以上のような課題を、Android チームでそのアプリをいじり倒して確認することや気になったことを逐一共有し合う時間を設けることで払拭できないかと考えました。 これをきっかけに、ぽちぽち会が誕生します。 sandbox アプリとぽちぽち会の誕生 sandbox アプリについて Google Play から落とすことができるアプリと共存でき、常に開発ブランチの最新バージョンが反映され、API エンドポイント等も基本本番に向いているアプリです。日次でビルドされます。 アプリを触る人の手間を最小限にするために、一度 DeployGate 経由でアプリをインストールすれば、 適宜日次で勝手にアップデートが降ってきてアプリを触ることができる状態になります。 API サーバーに修正が反映されておらず確認できない、ということも考えられますが、 なるべくいつも通りの環境でアプリを触れる状態にして置くことを意識 しています。 普段と状況が違っているというのはユーザーとしてアプリを触っているという気持ちから離れてしまう原因になりがちだからです。 ぽちぽち会について 朝に 1 時間とって Android チームで集まってアプリを触る時間を毎日設けました。専用のタグがついているプルリクがリストアップされているので、そのリストを 1 つ 1 つ確認します。 プルリクを 1 回は sandbox ブランチに対してマージして 確認し、OK が出るまでマージしてはいけない というルールにしています。 Android 的におかしいデザイン(たとえばマージンが狭くて気になる、もう少しフォントサイズが大きいほうが良いんじゃないかなど)や要件的に他の実現方法のほうが良いのでは?といった議論がここでなされます。 文言のやりとり 実際リリースしたもの 上記の例では、コーチマークに表示する文言が微妙に気になる、ということで少し調整したりしました。実際にここで決まった文言が最新アプリで表示されるようになっています。 文言のやりとり 実際リリースしたもの また別の例では、ダイアログの消えるタイミング・消すことができるタイミングについて議論し、なるべくユーザーが不快に思わないような挙動に寄せる決定をしリリースに至ったこともあります。 3 ヶ月ほどやってみて チームメンバーに聞いてみました 作業しているエンジニアが考慮しきれなかった箇所を他の人が発見してくれたり、作業として未完成な段階でもUXや仕様上のフィードバックを貰えるので作業の手戻りを抑制するという点でよかった 毎朝 30〜60 分ほど時間を費やしてるので時間を浪費しすぎてないか?という不安はある。ユーザーさんに早く機能を届ける量という視点と正しく機能を届ける質という視点での葛藤 毎朝コミュニケーションを取ることで、実装の方向性をチーム内で合意を取りつつ収束させることができ、温度感のすり合わせができたのでよかった 確かに、ぽちぽち会が終わったらもうお昼になっているという日もあったりするので、タイムマネジメントは考えていく必要があるなぁと思いました。 まだまだ改善の余地はあると思うので、引き続きやっていきます 💪 最後に dely ではエンジニアを全方面で絶賛募集中です。 興味がある方は是非お声がけください! speakerdeck.com note.com
はじめに こんにちは。 機械学習エンジニアの辻です。 さて本日は、「国内初?マルチリービングでランキングを勝手に自動改善!」ということで、 マルチリービング という手法と、その手法を使ったランキングの自動最適化の方法についてご紹介したいと思います。なお、今回の取り組みは、筑波大学・図書館情報メディア系・准教授の加藤誠先生 *1 に大変ご助力賜りました。この場を借りてお礼申し上げます。 目次 はじめに 目次 アローの不可能性定理 クラシルのランキングについて ランキングの課題 では、どうしたらいいか? マルチリービングって何なん? ランキング評価エコシステム 難しかった点、課題について まとめ アローの不可能性定理 さて、マルチリービングのご紹介に入る前に、みなさん アローの不可能性定理 *2 なるものをご存知でしょうか?ちょっとWikipediaで調べてみたところによると、「投票者に3つ以上の独立した選択肢が存在する場合、如何なる選好投票制度であっても、個々人の選好順位を共同体全体の(完備かつ推移的な)順位に変換する際に、特定の評価基準(定義域の非限定性、非独裁性、パレート効率性、無関係な選択肢からの独立性)を同時に満たすことは出来ない。」とありました。 ちょっと何言ってるかわからないですね。 要するに、この定理は次の5つの「公正さ」の基準を、 常に 、 同時に 、 複数の人に 対して満たすようなランキングなんて作れないよ、という主張です。 人々の選好の順序は自由だ 満場一致 独裁者はいない 他の選択肢から影響を受けない 堂々巡りの矛盾にならない(a>b , b>cなら必ずa>c) つまり、いつの世もみんなの心が一つなることは絶対にないというわけです、悲しいかな。 クラシルのランキングについて 前置きはこれくらいにして、クラシルには様々なランキング機能 *3 があります。 クラシルのランキング機能(ある日のランキング) ランキングの課題 クラシルのランキングは、現在利用していただいている多くのユーザさんの声によって作成されています。 しかしながら、先ほどのアローの定理が主張するように、 ユーザさん全員に喜んでもらえるただ一つのランキング を作成することは無理なので、 この例の場合は、申し訳ないことにサバが嫌いな人にとって、今日のランキングは最低!ということになってしまうのです。(美味しいのにね。) では、どうしたらいいか? 先ほどのアローの定理を再び思い出してください。ここで主張されているのは、列挙した5つの「公正さ」の基準を 常に同時に複数人 では満たせないということでした。 でも逆に言えば、 常に→適宜 同時→変化する 複数人→少人数 このような条件なら、完璧ではないにせよ、少しはユーザさんに喜んでもらえるランキングに近づけるんじゃないか? それが今回の取り組みのモチベーションになります。 マルチリービングって何なん? マルチリービング(Multileaving)というのは、インターリービング(Interleaving)の複数版のことです。 それでは、インターリービングが何かというと、それはA/Bテストを手っ取り早く行う手法の一つです。 A/Bテストとはシステムの機能やデザインの良し悪しを判定するための取り組みのことで、 仮説検定やベイズ最適化を用いて評価するのが一般的です。(FirebaseのA/B Testing機能などを使うと便利ですね。) ただ、こういった方法だと、サンプル数を十分に確保できるまで評価できなかったり、評価方法が複雑になり良し悪しの判断が難しいという状況が起こったりします。 そうなると手っ取り早くAとBのどっちがいいか知りたいというニーズを満たすことができません。 そこで、このマルチリービング(インターリービングも同じ)という手法は、非常に高感度なランキング評価手法であって、 結果判定が可能になるまでの時間やサンプル数が比較的少なくても、AとBのどっちがいいか判断したいというニーズを満たすことができるという、 そんな非常に画期的な手法になります。 *4 *5 さて、詳しく知りたい方は、脚注4の加藤先生のQiitaをご参考頂きたいと思うのですが、 この手法をめちゃくちゃ大雑把に説明すると、 いくつか(たとえば4つ)の条件でランキングを作る いい感じにランキングをごちゃ混ぜにするアルゴリズム を使って、ごちゃ混ぜランキングを作る(初期は1/4ずつ均等に配分するなど) ユーザさんが、このごちゃ混ぜランキングをクリックした結果(暗黙フィードバック)を集計する 3で得られた結果から、最初に作った4つのランキングのうち一番強いランキングを評価して、その強いランキング(好まれた)の混ぜる割合をちょっと増やす 混ぜる割合を変えた状態で2に戻り、再びごちゃ混ぜランキングを作る (以降、繰り返し) このとき、この いい感じにごちゃ混ぜにするアルゴリズム の種類にはこのようなものがあります。 Balanced interleaving(均衡交互配置) (Joachims 2002a, Joachims 2003) Team draft interleaving(チームドラフト交互配置) (Radlinski et al. 2008) Probabilistic interleaving(確率的交互配置) (Hofmann et al. 2011) Optimized interleaving(最適化交互配置) (Radlinski and Craswell 2013) そして、今回使用したのは、これらをさらにいい感じにした版のPairwise Preference Multileaving(略してPPM)になります。 *6 こちらは加藤先生ご自身がPythonモジュールとしてオープンソースとして公開されているのでご指導いただきつつ利用させて頂きました。 *7 以下、加藤先生のモジュールを使用したPPMのデモ実装になります。 少しだけ詳しくみていきましょう。 まずは、ランキング対象となるダミーレシピデータとそのデータに合わせたスキーマクラスを用意します。 レシピ動画の素性を持つVideoクラス、ランキングを生成するRankerクラス、そしてユーザ情報を保持するUserクラスです。 Rankerクラスのrank関数では、与えられたランキングの条件ごとの割合とRankerの重みを線形結合しています。 class Video ( object ): def __init__ (self, id , genre_ids, features): self.id = id self.genre_ids = genre_ids # レシピのジャンル self.features = features # 素性(ランキングの条件) def __hash__ (self): return self.id class Ranker ( object ): def __init__ (self, keys, w): self.keys = keys # 素性(ランキングの条件) self.w = w # 重み def rank (self, documents): result = sorted ( documents, key= lambda x: np.array([x.features[k] for k in self.keys]) @ self.w.T, reverse= True ) return result class User ( object ): def __init__ (self, id , favorite_feature_id): self.favorite_feature_id = favorite_feature_id self.id = id def click (self, videos, click_cnt= 3 ): # ユーザはfavorite_feature_idが高いものをクリックする return to_id( sorted (videos, key= lambda x: -x.features[self.favorite_feature_id])[:click_cnt]) videos = [] # ダミーレシピデータの作成 def create_videos (): for i in range ( 10000 ): video = Video( id = i + 1 , genre_ids = random.sample(genre_ids, 2 ), features = {fid: random.random() for fid in feature_ids} ) videos.append(video) def gen_rand (size): m = np.random.uniform(low=- 1.0 , high= 1.0 , size=size) m /= (np.linalg.norm(m)) return m つぎに、generate_fluctuated_weight関数として、重みをちょっとだけ変更する(これを摂動と呼んでいます。)関数を定義します。 つまり、初期の重みは単位ベクトルに保存していますが、これを微変化させることで、混合するランキングの割合を調整しています。 # 閉区間[0,1]に収まるように制限する def generate_fluctuated_weight (original_weight): unit_vec = gen_rand( len (original_weight)) diff_vec = unit_vec * delta # 元ベクトルに加算、閉区間[0,1]からはみ出す場合は、はみ出る分を他に付け替え tmp = original_weight + diff_vec tmp2 = sorted ([(i, _) for i, _ in enumerate (tmp)], key= lambda x: x[ 1 ]) for i, v in tmp2: if v < 0.0 : tmp[i] = 0.0 tmp[-(i+ 1 )] += v tmp /= np.linalg.norm(tmp) # 正規化 diff_vec = tmp - original_weight unit_vec = diff_vec / delta return (original_weight + diff_vec, unit_vec) get_superior_rankers関数は、ユーザさんのクリック状況から、一番強かったランキングを判定する関数です。 def get_superior_rankers (il_result): prefs = defaultdict( int ) for res in il_result: for r in res: prefs[r] += 1 winner_indexes = [] for i in range ( 1 , ranker_size): wins = prefs[(i, 0 )] - prefs[( 0 , i)] if wins > 0 : winner_indexes.append(i) return winner_indexes そして、このupdate_ranking関数が実際にランキングを生成する関数になります。 現在の重みを摂動させて混合ランキングを作成します。 作成後に、摂動した結果の重みも更新します。 def update_ranking (user, genre_id): # メイン重みを取得 res = get_parameter() w = np.array([_[ 1 ] for _ in res]) target_keys_count = len (feature_ids) # unit_vecs = np.array([np.zeros(target_keys_count)] + [generate_fluctuated_weight(w) for i in range(ranker_size - 1)]) weight_vecs = [w] + [generate_fluctuated_weight(w)[ 0 ] for i in range (ranker_size - 1 )] rankers = [Ranker(feature_ids, _w) for _w in weight_vecs] top_videos = get_top_videos_per_genre(genre_id) rankings = [to_id(ranker.rank(top_videos)[:ranking_count_per_genre]) for ranker in rankers] ppm = PairwisePreference(rankings) mixed_ranking = ppm.interleave() # ランキング保存 # 摂動後の重み更新 こちらのevaluate関数がランキング結果を評価する関数になります。 ユーザさんのクリックした結果を受けてランキング優劣を評価し、重みを更新します。 def evaluate (user, genre_id, click_video_ids): # 保存済み摂動重み群をもとに、競わせたランキングを復元 res = get_ranking() weight_vecs = [] for i in range (ranker_size): ranker_id = i + 1 w = np.array([_[- 1 ] for _ in res if _[ 0 ] == ranker_id]) weight_vecs.append(w) main_w = weight_vecs[ 0 ] # print("main weight: %s" % str(main_w)) rankers = [Ranker(feature_ids, _w) for _w in weight_vecs] top_videos = get_top_videos_per_genre(conn, genre_id) rankings = [to_id(ranker.rank(top_videos)[:ranking_count_per_genre]) for ranker in rankers] origin_rankings = get_origin_ranking() mixed_ranking = [_[ 1 ] for _ in origin_rankings] # 復元ランキング達とクリック結果からマルチリービングで優劣判断 # rankingオブジェクトも復旧する必要がある ranking = PairwisePreferenceRanking(rankings, contents=mixed_ranking) # クリック対象IDを混合ランキングのインデックスに変換 click_indexes = [mixed_ranking.index(vid) for vid in click_video_ids] result = PairwisePreference.evaluate(ranking, click_indexes) # 勝者ランカーを決定 winner_indexes = get_superior_rankers([result]) main_w = weight_vecs[ 0 ] # 重み更新(勝者ランキングに用いた単位ベクトルの平均値を加算) if winner_indexes: diff_vecs = np.array([w - main_w for w in weight_vecs]) unit_vecs = diff_vecs / delta main_w += alpha * np.average(unit_vecs[winner_indexes], axis= 0 ) # メイン重み更新 return main_w ここまで定義した関数を、後は順番に呼び出すだけという形になります。 if __name__ == "__main__" : update_ranking(user, genre_id) clicked_video_ids = get_click_data(user, genre_id) #クリック結果を取得 evaluate(user, genre_id, clicked_video_ids) こちらのデモ実装におけるシミュレーションでは、 素性数(ランキングの条件)は4つ 生成するランキングのダミーアイテム数は50に固定 ダミーアイテムは、4つの素性値をランダム生成したものを事前に大量生成 という条件に基づいてランカー数をnに決定しています。 また、1回のイテレーションで、ユーザさんは提示されたランキング(50アイテム)の中から、 1つめの条件が大きいものを順に3つクリックしたと仮定しました。 それを50イテレーション(=1セット)繰り返した後、第一素性の重みを記録したうえで、さらに20セット繰り返して、最終的に1個目の素性重みの平均を取得し、 それをランカー数:nの場合の「最終第一条件の重み」として評価しました。 その結果、このシミュレーションでは、このようにランカー数:2~20に変化させて行ったところ、次のような結果が得られ、 十分な収束を確認することができました。 ランキング評価エコシステム それでは、このマルチリービングをどのようにクラシルのランキングに反映したかについてご紹介したいと思います。 この仕組み全体を ランキング評価エコシステム と呼ぶことにします。 導入したクラシルの機能は、特定の検索キーワードに紐づいて表示されるテーマ別ランキング *8 という機能になります。 マルチリービングを用いたランキングを自動改善する流れは、このような流れになります。 こちらのテーマ(殿堂入り、時短、子供が喜ぶなど)一つ一つについて、複数のランキング生成条件(視聴数、新しい順など)に基づくランキングをいくつか生成します。 ユーザさんを行動に基づいていくつかのグループ(クラスタ)に分類します。 各クラスタごとに1で生成したランキングからごちゃ混ぜランキングを生成します。 クラスタごとにそれぞれごちゃ混ぜランキングを表示します。 1週間経って、ユーザさんがクリックした結果を集計します。 集計結果から、ユーザさんがこの1週間でもっとも好んだランキング生成条件を評価します。 ユーザさんが好んだランキングを考慮して、ごちゃ混ぜにする割合をちょっと変化させます。 変化した割合が反映された状態で、再びごちゃ混ぜランキングを生成します。 つまり、ランキングを複数作ってABテストを行い、ABテストの結果を反映したランキングを毎週自動で作ることで、 ほんのちょっとずつランキングを最適化していこうという作戦になります。 なお、この機能実装については、こちらの先行研究を参考に行いました。 *9 先行研究でのアルゴリズム こちらの研究では、オンラインでの評価が対象となっていますが、これをそのままクラシルに適応してしまうと処理コストが高すぎるし、 食コンテンツというのは、ニュースコンテンツなどとは異なり即時性をそこまで求められないことから、 今回は1週間に1回更新するバッチ処理として導入することにしました。 また、先行研究のアルゴリズムはTDM(Team draft multileaving)が採用されていますので、PPMを用いている部分も異なっていると言えます。 それでは、ランキング自動改善エコシステムをより具体的にご紹介したいと思います。 全体概要 ランキング自動改善エコシステムは、以下の5つのSTEPで構成されています。 1. aggregate ステップ Athenaを経由して以下のデータ収集・集計を行う ユーザさんの行動から1週間分(起動当日-7日)の特徴量を収集・集計する 1週間分のクリック数をユーザごとジャンルごとに集計し保存する 2. user clustering ステップ ユーザさんの行動状況から、ユーザさんをk個のクラスタに分類します。 このクラスタごとにランカー(ランキング生成器)を生成していきます。 3. create ranking ステップ aggregate ステップで収集したデータに基づいて、複数のランキングを生成します。 ランキングを生成したのち、現状のランカーパラメータの保存を行います。 ランカーパラメータの状況に基づき混合ランキングを生成します。 4. evaluate ステップ ユーザクラスタごとジャンルごとにランカーの評価を行い更新します。 勝者ランカーのパラメータに対し割合を調整して保存します。 5. load ステップ 生成した混合ランキングをDynamoDBに格納します。 難しかった点、課題について こちらの実装期間については、加藤先生のご指導とモジュールのおかげで、5日ほどでサービスリリースすることができました。 リリースしたばかりなので適合率の評価などは実際まだこれからですが、 導入までの難しかった点としては、マルチリービングがいかに少数のサンプル数で収束効果があるとはいえ、 今回非常にニッチな機能なので、摂動の影響は非常に小さく、それほど大きな効果を得ることはできなそうという点があります。 また、当初ユーザごとにパラメタを調整する設計だったのですが、やはり処理コストが肥大化してしまうので、 今回は、初回導入ということでユーザクラスタ *10 に対するランカーパラメータにすることで処理コストを抑えました。 今後の課題としては、このエコシステムを改善していくことで、ランキングに限らず他機能のA/Bテストにも活用できるようにしていきたいと思っています。 また、ランカー数を増やしたり、摂動するハイパーパラメタをチューニングすることによってさらに最適なランキングを生成し、 可能な限り アローの不可能性定理 に立ち向かっていきたいと考えています。 まとめ いかがでしたでしょうか? 今回は、Pairwise Preference Multileavingという手法を用いてランキングのA/Bテストを行い、その結果を反映してランキングを勝手にどんどん最適化させるという手法について ご紹介させて頂きました。 実際、ユーザさんに満足してもらえるような適合率にしていくためには、まだまだこれから改善が必要になると思っていますが、 アローの不可能性定理に人海戦術で対応していくのは、その名の通り不可能なので、 こういった技術や工夫を用いてクラシルを利用してくださるユーザさんにもっともっと満足してもらえる機能を提供・改善していきたいと考えています。 *1 : https://trios.tsukuba.ac.jp/researcher/0000004282 *2 : https://ja.wikipedia.org/wiki/%E3%82%A2%E3%83%AD%E3%83%BC%E3%81%AE%E4%B8%8D%E5%8F%AF%E8%83%BD%E6%80%A7%E5%AE%9A%E7%90%86 *3 : ごめんなさい、有料機能です。 *4 : 詳しくは加藤先生のQiitaを参照してください → https://qiita.com/mpkato/items/99bd55cc17387844fd62 *5 : こちらのGunosyさんの記事でも紹介されてます。 → A/Bテストよりすごい?はじめてのインターリービング - Gunosyデータ分析ブログ *6 : https://qiita.com/mpkato/items/cc7fc36d74268af130d5 *7 : https://github.com/mpkato/interleaving *8 : すみません、やっぱり課金対象機能です。 *9 : https://dl.acm.org/doi/pdf/10.1145/2835776.2835804 *10 : クラスタリングについては以前投稿した記事をご参考ください。→ クラシル・パーソナライゼーションの歩み - クラシル開発ブログ
こんにちは。androidエンジニアと兼任でスクラムマスターをしているkenzoです。 スクラムマスターの業務において、メンバーや自分へのリマインド、バックログ整理の自動化、タスク状況の可視化などをGAS(Google Apps Script)を使って実施しています。 また、それ以外にもプロジェクトの進行管理やいろいろなことをGASでやってきました。( 自分の健康管理とかも ) GASを使うとちょっとしたスクリプトを書くだけで簡単にG Suiteのサービス(ドキュメント、スプレッドシート、カレンダー等)と連携することができます。 HTTP通信もできるので、APIを通じて他のサービスと連携させることも可能です。 そのため、うまく使えばかなりの業務を効率化させることができます。 簡単なことをやらせているうちは良いのですが、次第にいろんなことをさせたくなり、スクリプトは複雑化していきます。 永遠に自分だけで管理するのならそのままでも良いでしょう。 しかし、会社で業務としてやる以上は役割が変わったり退職したりと他の人がそのbotの面倒を見るようになることが考えられます。 その後任の方に辛い思いをさせないよう、きちんと整理しておく必要があります。 本記事では筆者が初めてGASを触ってから今まで作ってきたbotの管理方法の変遷について書いています。 コードの中身の話はありません。 GASのコードエディタ直書き + トリガーで定期実行 初めはスプレッドシートからGASのコードエディタを開き、そこにスプレッドシートの内容の操作やSlack等の外部サービスと連携するコードを直に書いていました。 そして、その処理を定期実行させるために作成した関数をトリガーに設定して運用していました。 おそらく誰しも初めはここから始めたことと思います。 Chromeの拡張機能を使いGitHubでコードを管理 運用を続け、コードが増えてくるとGitHub等で管理したい気持ちが強くなってきます。 初めのうちはGASのコードエディタに書いたコードをローカルのファイルにコピペし、それをGitHubに上げていました。 なにか良い方法はないかと思い、見つけたのがChromeの拡張機能「Google Apps Script GitHub アシスタント」でした。 これを使うとコードエディタ上にGitHubでのコード管理に使うボタンが表示されるようになり、その場でpushやpullをすることができるようになります。 当時はこれでずいぶん楽になったのを覚えています。 claspを使ったローカル開発 コード管理は楽になりましたが、当時は依然としてGASのコードエディタ上でコードを書いていました。 また、コードをコピペしてきてIDE上で変更してそれをまたコードエディタに戻したりというきつい運用をしていたこともありました。 その頃知ったのがGoogleが作ったCLIツール「clasp」でした。 このツールを使うとコードエディタのコードをコマンドでローカルに持ってきて開発することができます。 そして、ローカルで開発したコードは別のコマンドでコードエディタにアップロードして動かすことができます。(他にもできることはあります) これを使うことで泥臭くやっていたローカル開発を手軽に行えるようになりました。 そして、いつからかTypeScriptに対応していたので、ローカルではTypeScriptで書けるようになりました。 トリガーをコードとスプレッドシートで管理 これまで多くのトリガーを作成して運用してきましたが、トリガーには若干扱いづらい部分もいくつかありました。 トリガーが作成したユーザーに紐付くため他のメンバーから見られない 定期的に走るトリガーは○時〜○時の間としか指定できず、その間のいつ発火するかわからない どのトリガーがいつ発火するのものなのか、コンソールで一つずつ開いてみなければわからない GASにはトリガーを作る関数もあるのでそれを使うことにしました。 スプレッドシートにこのように実行したい関数と曜日、時間を指定します。 (イメージです) 毎日0時を過ぎたくらいにこのスプレッドシートを元にトリガーを作成する処理を走らせることで、上記の問題も下記のように解決できました。 ちなみにこの処理のトリガーだけはコンソールで管理していました。 *1 スプレッドシート(+コードも)の権限さえ付与すれば誰でもトリガーを管理できる 指定した曜日、時間にトリガーを発火させられる スプレッドシートを見ればどのトリガーいつ発火するのかひと目で分かる Slackに送るメッセージをスプレッドシートで管理 リマインドすべきものが増えてくると、中には毎度同じメッセージを送っているものもあることに気が付きました。 そこで、上記のトリガーと同様にスプレッドシートで送りたいメッセージを送信タイミングを指定して管理することにしました。 (イメージです) SlackのユーザーやグループのIDを用意しておけば、メンション付きのメッセージを送ることもできます。 こちらも毎朝早い時間にその日のメッセージを送るトリガーを作成して *2 指定した時間に実行しています。 ちなみにこれはslackのremindでもそこそこ近いことができます。 スクリプト毎にclaspの設定を持たせて管理 スクラムに関するBotが多くの機能を持つようになってきて下記の理由のためにスクリプトエディタを分ける必要が出てきました。 1日にトリガーの最大設定数20を超える処理を実行する場合がある シートに設置したオブジェクトのクリックで処理の実行をさせるためそのシートのスクリプトエディタ上に処理を書く必要がある それぞれが異なる役割を持っている(リマインド機能、タスクの可視化機能など、、) スクリプトを分けてもスクラムに関するBotとして共通の処理もあるため、GitHub上では1つのリポジトリで扱い、ディレクトリ毎にclaspの設定ファイルを置いてアップロード先を分けて運用する形にしました。 各スクリプト共通で使う処理の括り出し 上記でスクリプト毎にアップロードする単位を分けましたが、Slackへの投稿をする機能のようにそれぞれのスクリプトで同じ機能を使う箇所があります。 分けた当初はその機能をファイルとして切り出してはいたものの、それを各ディレクトリにコピペして置いていました。 そのため、処理に変更を加えたければそれぞれのディレクトリ以下にあるファイル同じファイルに同様の変更を加える必要がある状態でした。 こちらへの対応としては、共通処理を書いたファイルをディレクトリに括り出し、アップロードの直前でそれらを移動して対象のファイルと一緒にするかたちをとりました。 |--common | |--slack.ts | |--github.ts |--script1 | |--.clasp.json | |--src | | |--script1.ts |--script2 | |--.clasp.json | |--src | | |--script2.ts |--script1アップロード用(スクリプトで作成) | |--.clasp.json | |--common | | |--slack.ts | | |--github.ts | |--script1 | | |--src | | | |--script1.ts package.jsonにファイル移動・アップロードをするスクリプトを作成し、それを使って運用しています。 (webpackでまとめてからアップロードすることもできそうでしたが、こちらの方法に比べて学習・作業コストが高そうだったため一旦見送っています。いずれ時間のあるときに試してみようと思います。) まとめ 以上が今日に至るまでに実施してきたGASで作ったBotの管理の仕方でした。 どこまで管理してどのような運用をすべきかはBotを使う環境や扱う方の職種等によって様々かと思います。 こちらはあくまで一例ですが、Botの管理や運用の仕方で困っている方の一助になれば幸いです。 delyはエンジニア大募集中です。クラシルのエンジニアとざっくばらんに話をするお茶会・ランチ会なども実施していますので、興味のある方は是非ご応募ください! *1 : V8ランタイムで実行する際にバグっぽい挙動があったので注意してください(コンソールで作成したトリガーで実行する関数でさらにトリガーを作成するとそのトリガーが無効になる?ような挙動でした。あんまりやらないとは思いますが。) *2 : トリガー毎にメッセージを渡すことができなかったので、別シートに送信予定のメッセージを置いています。
こんにちは、そしてはじめまして、今年2月にAndroidエンジニアとして入社したばかりの MeilCli です。先日プロジェクトマネージャーの tummy がユーザー視点での クラシルAndroidアプリの改善 を紹介しましたが、今回は自分が入社してから改善されてきた開発者視点でのクラシルAndroidアプリについてご紹介できればと思います Detektの導入 クラシルAndroidアプリの開発にはKotlinコードを静的解析するツールとして今まで ktlint が使用されてきました。類似ツールとして detekt というものがあり、このdetektにはktlintの解析ルールが収録されているため、解析ルールベースで考えると上位互換にあたります。そのため、ktlintからdetektへ移行することにしました detektにはGradle Pluginが用意されているためAndroidプロジェクトへの導入はスムーズにできますが、クラシルAndroidのプロジェクトは現在マルチモジュールプロジェクトへの移行をしている最中なのでモジュールが複数存在しています。ktlintを運用しているときはマルチモジュールに対応した設定をしていなかったため、アプリケーションモジュールとなる app モジュールのみにktlintを実行している形になっていたのでdetektへの移行と同時にマルチモジュールに対応した設定を行うことにしました 各モジュールへの設定をいちいち書くのは面倒であり管理コストが増えてしまうので、今回は buildSrc にGradle Pluginを作成し、各モジュールの build.gradle で作成したGradle Pluginを適用するだけでよい形にしました まずプロジェクトのルート直下に buildSrc/build.gradle.kts を作成し、buildSrcのための設定を記載します plugins { `kotlin-dsl` } repositories { jcenter() google() maven( "https://plugins.gradle.org/m2/" ) } dependencies { implementation( "io.gitlab.arturbosch.detekt:detekt-gradle-plugin:1.6.0" ) } そうしたら buildSrc がモジュールのように識別されるのでDetekt用のGradle Pluginのソースコードを追加していきます buildSrc/src/main/kotlin/com/kurashiru/gradle/plugins/DetektConfigPlugin.kt: package com.kurashiru.gradle.plugins import io.gitlab.arturbosch.detekt.extensions.DetektExtension import org.gradle.api.Plugin import org.gradle.api.Project import org.gradle.kotlin.dsl.dependencies import java.io.File class DetektConfigPlugin : Plugin<Project> { override fun apply(project: Project) { project.extensions.findByType(DetektExtension :: class .java)?.apply { toolVersion = "1.6.0" buildUponDefaultConfig = true parallel = true config = project.files( " ${ project.rootProject.rootDir } /detekt.yml" ) reports.apply { xml.enabled = true val name = project.projectDir.toRelativeString(project.rootProject.rootDir).replace( "/" , "_" ) xml.destination = File( " ${ project.rootProject.rootDir } /reports/detekt/ ${ name } .xml" ) } } project.dependencies { add( "detektPlugins" , "io.gitlab.arturbosch.detekt:detekt-formatting:1.6.0" ) } } } buildSrc/src/main/resources/META-INF/gradle-plugins/DetektConfigPlugin.properties: implementation-class=com.kurashiru.gradle.plugins.DetektConfigPlugin このGradle Pluginではルート直下の detekt.yml をconfigとして指定し、CI用にルート直下の reports/detekt フォルダーにレポートを書き出すようにしています あとは通常と同じようにdetektのGradle Pluginの依存関係をルート直下の build.gradle に書いて、detektを適用するモジュールでは apply plugin: 'io.gitlab.arturbosch.detekt' apply plugin: 'DetektConfigPlugin' この2行を追加すれば良いという形になりました detektは ホームページ でたくさんのRuleSetsが紹介されるほど細かいところまで静的解析し、問題を検知・報告してくれるツールです。便利な反面、RuleSetsの設定をチューニングしなければ不要な警告を出してしまいます。そのため今後はクラシルAndroidアプリ開発に適したRuleSetsのチューニングをしていこうと考えています、あと最新版のdetektにバージョンアップしないとですね笑 GitHub Actionsの導入 クラシルAndroidアプリの開発では以前からAmazonの CodeBuild を使用してきました。CodeBuildではPullRequestでのビルドチェックやストアリリース時のリリースビルドを行っていましたが、それ以外でもCIで自動化してみたいよねということで、OSSでは無料でありプライベートリポジトリーでも無料枠の多い GitHub Actions を部分的にですが導入していくことになりました クラシルAndroidでは様々なGitHub Actionsのワークフローを運用していますが、そのうちの何点かご紹介します PullRequestのタイトルチェック クラシルAndroidでは社内的なリリースノート作成のためにPullRequestのタイトルに対して一定の規則を設けています。具体的には [improvement] タイトル のようにタグをprefixとして付けるだけですが、開発者がこの規則通りにPullRequestを作成できているかを人の手でチェックするのは大変なため、GitHub Actionsで自動化しました name : プルリクのタイトルチェック on : pull_request : types : [ opened, edited, reopened ] jobs : check : runs-on : ubuntu-latest steps : - uses : octokit/request-action@v2.x # 条件式は長いので省略しています if : startsWith(github.event.pull_request.title, '[improvement]' ) == false with : route : POST /repos/:repository/pulls/:pull_number/reviews repository : ${{ github.repository }} pull_number : ${{ github.event.pull_request.number }} event : "COMMENT" body : "PRのタイトルは`[タグ] .+`という形式にしてください" env : GITHUB_TOKEN : ${{ secrets.GITHUB_TOKEN }} このワークフローでは octokit/request-action を使用しています。これを使うことによってワークフローのステップにてGitHubのREST APIを楽に呼び出すことができるので、「こういう場合にこういうコメントを付ける」といったワークフローを作るときに便利です PullRequestのマイルストーンチェック タイトルと同じように、マイルストーンも社内的なリリースノート作成のために設定をルール化しています name : プルリクのマイルストーンチェック on : issues : types : [ demilestoned ] pull_request : types : [ opened ] jobs : check : runs-on : ubuntu-latest steps : - uses : octokit/request-action@v2.x name : Check PR Demilestoned if : github.event_name == 'issues' && github.event.action == 'demilestoned' && github.event.issue.pull_request != null with : route : POST /repos/:repository/pulls/:pull_number/reviews repository : ${{ github.repository }} pull_number : ${{ github.event.issue.number }} event : "COMMENT" body : "PRには必ずマイルストーンを付けてください" env : GITHUB_TOKEN : ${{ secrets.GITHUB_TOKEN }} - uses : octokit/request-action@v2.x name : Check PR Opened if : github.event_name == 'pull_request' && github.event.action == 'opened' && github.event.pull_request.milestone == null with : route : POST /repos/:repository/pulls/:pull_number/reviews repository : ${{ github.repository }} pull_number : ${{ github.event.pull_request.number }} event : "COMMENT" body : "PRには必ずマイルストーンを付けてください" env : GITHUB_TOKEN : ${{ secrets.GITHUB_TOKEN }} 先にワークフローファイルをお見せするとこのような形で運用しています。 octokit/request-action を使ってコメントで通知するのはタイトルチェックと同じですね さて、このワークフローではトリガーとなるイベントに issues の demilestoned を使用しています。デフォルトブランチに直Pushする開発体系ならばこういったワークフローの作成を難なくできるかと思いますが、クラシルAndroidプロジェクトではPullRequestを介してデフォルトブランチにpushする形式を取っています このワークフローを開発していたときのGitHub Actionsの挙動では、PullRequestで変更されたワークフローファイルはPullRequest上のCheckでは pull_request のイベントしかトリガーを引いてくれないという仕様になっている様子でした。そのため最初このワークフローを作成するときに issues の demilestoned に対する動作を確認できなくてつまづきました。「マイルストーンが外された」や「マイルストーンが付いていない状態」をトリガーにするワークフローを作成したいときは是非参考にしていただければと思います PullRequestに限らずIssueについても動作するサンプルを MeilCli/actions に置いていますのでそちらもよければどうぞ ちなみにですが、GitHub上でマイルストーンを貼り替える作業をすると demilestoned のイベントが発火するらしく、このワークフローではそのままコメントを付けてしまうのでdelayをかけてAPIから情報を取ってくるなどの改善点があります。そのうち直そうと考えていますm(__)m Detekt さて、最初に紹介したDetektですが、PullRequestで自動でチェックしてくれなければ意味がありません。そこでGitHub ActionsでDetektを実行し、 Danger を使ってDetektの結果をコメントで通知するようにしました クラシルAndroidではCodeBuildのほうでもDangerを運用しているためすでに Gemfile や Dangerfile が存在していました。GitHub Actionsではそれらのファイルと共用するのは避けたかったため、 .github 配下にそれらのファイルを配置することにしました。Gemの解決にBundlerを使用している都合上、そのような配置をすると依存関係を解決できなくなるという状態に陥ったため、過去に自分が勉強がてらに作成した MeilCli/danger-action を Gemfile の位置にとらわれないように改修して使用しています .github/Gemfile: # frozen_string_literal: true source " https://rubygems.org " git_source( :github ) {|repo_name| " https://github.com/ #{ repo_name }" } # gem "rails" gem ' danger ' gem ' danger-checkstyle_format ' , ' ~> 0.0.1 ' .github/Dangerfile: github.dismiss_out_of_range_messages checkstyle_format.base_path = Dir .pwd report_files = Dir .glob( " reports/detekt/** " ) for report_file in report_files do checkstyle_format.report report_file end .github/workflows/ci.yml: name : CI on : [ pull_request ] jobs : detekt : runs-on : ubuntu-latest env : GITHUB_USER : "github-bot" GITHUB_TOKEN : ${{ secrets.GITHUB_TOKEN }} if : github.event.action == 'opened' || github.event.action == 'synchronize' || github.event.action == 'ready_for_review' steps : - uses : actions/checkout@v2 - uses : actions/setup-ruby@v1 with : ruby-version : '2.6' - uses : actions/setup-java@v1 with : java-version : 1.8 - uses : actions/cache@v1 name : Cache Gradle with : path : ~/.gradle/caches key : ${{ runner.os }}-gradle-${{ hashFiles('buildSrc/build.gradle.kts') }}-${{ hashFiles('buildSrc/src/**') }} restore-keys : | ${{ runner.os }}-gradle-${{ hashFiles('buildSrc/build.gradle.kts') }}- ${{ runner.os }}-gradle- - uses : actions/cache@v1 name : Cache Bundler with : path : vendor/bundle key : ${{ runner.os }}-gems-${{ hashFiles('.github/Gemfile') }} restore-keys : | ${{ runner.os }}-gems- - name : Grant permission run : chmod +x gradlew - name : Run Detekt run : ./gradlew detekt - uses : MeilCli/danger-action@v4 name : Run Danger with : plugins_file : '.github/Gemfile' install_path : 'vendor/bundle' danger_file : '.github/Dangerfile' danger_id : 'danger-detekt' env : DANGER_GITHUB_API_TOKEN : ${{ secrets.GITHUB_TOKEN }} さて、GitHub ActionsやDangerを運用されている方々にとってはあまり目新しいワークフローではないかもしれませんが、Detektを導入するにいたって、開発者がPullRequestに変更を加えてから静的解析のコードレビューがされるまでの時間を減らす点でこだわることにしました 具体的には actions/cache を使って依存関係やタスクの実行結果などをキャッシュすることによって、キャッシュが存在する場合にキャッシュを利用することで早く動作させるようにしました GitHub ActionsのCIワークフローの実行一覧 結果としては遅ければ5分程度、早ければ2分程度、平均3~4分で実行が完了するようになったためキャッシュを利用してワークフローを高速化する試みは成功だったかなと思います (参考数値としてktlintの頃はいろいろな処理を待機していたために約8分ほどかかっていました) 今後について 開発者体験という視点で言えばクラシルAndroidプロジェクトはたくさんの課題があります。その中で直近でやろうと考えてるものをご紹介し、これを次回予告にします 長年放置し続けてきたlintの対応、不要になったリソースの削除 使用しているOSSなどのライセンス表記のための集計ツールを MeilCli/Librarian に置き換える これによってライブラリーやライセンス通知の集計精度が向上する見込みです ライブラリーアップデートの自動検出・通知 モジュールの build.gradle の共通な宣言部分を buildSrc へまとめる note.com 最後にdelyではエンジニアを大募集しています。ランチ会などを定期的に開催してるので、中の人と話してみたい!という風に興味がある方は是非お声がけください
こんにちは、クラシル Android のプロダクトマネージャーをしている tummy です。dely では今年 4 月から本格的にチームが立ち上がり、Android アプリの改善に取り組み始めました。スタートから 2 ヶ月弱経ち、徐々にアプリ内も変化してきたのでスクショを交えつつ取り組みについてご紹介できればと思います。 まずはデザインのお話から。 検索バーが変わりました ホーム上部にある検索バーですが、パッと見ここから検索できるように見えないのではないかという仮説のもと、人気キーワードを定期的に変える形で表示してみたり、検索ボタンを明示的に表示するようにするなど様々な UI を試しました。 結果、検索バーを固定したパターンでわずかなタップ率の向上が見られたため現状はこのパターンになっています 。あまり数字は変わりませんでしたが、こういった地道な改善も行っていっています。 お気に入り検索結果画面からフォルダ追加できるようになりました 今までお気に入りの検索結果画面からフォルダに追加することはできませんでした。しかし、ユーザーさんからのご要望が多かったため機能を実装することにしました。少しずつですが使われている数が増えているので嬉しいです。ぜひご活用ください! 改善ブログに使い方も載っているので良ければこちらも御覧ください 💁♀️ note.com レシピ詳細の材料部分にアイコンがつきました 以前からここはタップできる領域で、材料名で検索できるようになっていて使ってくれているユーザーさんも多いことがわかりました。そこで、上2つの材料名に虫眼鏡のアイコンをつけることで 「ここはタップできて、タップすると検索に飛べる」という認知を持ってもらえるのではないか と考えました。結果、認知を持ってもらえたのか、以前より材料検索を使う方が増えてくるようになりました。 通知の設定が変わりました 今までは通知をオフにするとすべての通知が届かないようになってしまっていました。 しかし最近のアップデートで、以下の項目ごとに通知のオン・オフを設定できるようになりました。 お得な情報(キャンペーンなど) チラシ 質問への返信 公式アカウントからのメッセージ おすすめレシピ その他(以上の項目に属さないものすべて) そのため、チラシの通知はいらないけどおすすめレシピの通知はほしい!といった場合にも対応できるようになります。 内容変わって、技術的なお話もさせてください。 minSdkVersion をアップデートしました minSdkVersion を 16 から 19 に引き上げました。 クラシルでは一度もあげたことがなかったので、ついに!といった感じ です。今回はあまりコードを削除できた部分はありませんでしたが、今後徐々にあげていって古い OS 向けに書いているコードも消せるといいなぁと思います。 minSdkVersion を上げるリリースをした際のリリースノート 改善がんばってます! 分析したデータ、ユーザーインタビューやアンケートの結果など、たくさんの情報を活用して改善を進めています。(インタビューやアンケートにご協力いただいたユーザーの皆さまありがとうございます!) 今後も検索機能を中心に改善を進めていくほか、プッシュ通知の改善にも着手していく予定です。また内容についてはブログ等でお知らせできればと思います。 最後に、dely ではエンジニアを全方面で絶賛募集中です。 興味がある方は是非お声がけください! speakerdeck.com note.com
はじめまして。開発部の sakura818uuu です。 CS(カスタマーサクセス)チームのサポートエンジニアを始めて3ヶ月が 経過しました。 3ヶ月が経ったので振り返ってみようと思います。 主な業務内容 求められるスキル 1.データ分析(SQL) 2.プロダクトの仕様把握 3.情報共有 他にもあったら良さそうなスキル キャリアとして さいごに 主な業務内容 主な業務内容はCSチームの技術的サポートや施策の計画や管理をしています。 また、CS業務のドキュメントの拡充や整理を行いました。 やってること(一部) ・お問合せの技術的サポート ・CSに関する施策の計画・実行管理 ・CS業務の言語化 お問合せの技術的サポートはブログも書かせていただきました。 tech.dely.jp 求められるスキル 3ヶ月働いてみて実際に感じた求められスキルを3つ記載します。 データ分析(SQL) 、プロダクトの仕様把握 、情報共有です。 1.データ分析(SQL) データ分析は言わずもがな必要になります。 SQLはCSエンジニアになる前もデータサイエンスチームに所属していたため書いていました。 ただ、データサイエンスチームでは「全体の何%が」と森を見ることが多かったのですが、CSは逆で個別のデータログなど木を見ることが多いです。 どちらも経験したからこそデータの仕様についての理解が一段と深まったように思います。 2.プロダクトの仕様把握 ユーザーさんから多種多様なお問合せをいただくため、プロダクトの仕様把握は最重要です。 運がいいことに私にはかなり合っている仕事でした。なぜなら入社してから2年間テスター(のようなこと)をしていたから仕様把握が既に細かなところまで出来ていたからです。 新機能やバージョンアップに伴う仕様把握も重要です。 この3ヶ月はリリースのサイクルが非常に速かったため、開発状況から常に目を離さないようにしていました。 新機能でわからないところや不明点があると時折エンジニアに確認したりして仕様認識の齟齬を起こさないよう気をつけました。 3.情報共有 情報の共有はどんな仕事でも必ず必要ですが、特にCSではこまめな情報共有が必要となります。 ・主にユーザーさんから届いたバグや不具合の情報を開発者に伝える ・新機能や不具合修正などの開発状況をCSチームに伝える この2つの情報共有は欠かせないです。 また、全体的なお問合せの傾向を把握できるよう お問合せ内容をカテゴリ別に分類し月別にどんなお問合せが何件届いているのかを可視化するようにしました。 情報共有をまめに行うことのできる人がCSエンジニアには向いているんだな、と思いました。 他にもあったら良さそうなスキル 他にもあったら良さそうだと感じたものです。 今はテクニカルライティングを書籍やネットを通して学んでいます。 ・テクニカルライティング ・原因を突き止めるために問題の切り分けを上手くできること ・少ない情報からあらゆることを想像できること キャリアとして 3ヶ月経ってみてキャリアとして考え方が変わりました。 CSエンジニアの前は検索エンジニアをやっていて、「これからもずっと検索に携わっていきたいな。いくんだろうな」と思いきっていたからです。 CSエンジニアになってみて、CSエンジニアとして歩んでいくのもいいなと思いました。 さいごに CSエンジニアになって3ヶ月経ち大分慣れてきました。 まだ土台が出来た状態にすぎないので、これからCSエンジニアとしてどんなことができるか模索していきたいと思います。 delyではエンジニアを絶賛募集中です。 興味があるかたは是非お声がけください!
こんにちは、開発部の高橋です。 最近弊社のRailsプロジェクトで active_hash というgemが使われ始めました。 個人的にも結構重宝しているgemでとても便利なのですが、一方で特性を理解せずに使うとハマりやすいgemでもあると思っています。 今回は、ActiveHashのクラシルでの事例と自分の過去の知見に基づくハマりポイントなどを書いていきます。 目次 目次 ActiveHashとは クラシルでの事例 導入経緯 使われ方 ハマりポイント インスタンス変数がクラスインスタンス変数相当 データがロードされるタイミング(特にActiveYaml, ActiveJsonの場合) 対応策 最後に ActiveHashとは データをRDBではなくHashやYAMLで定義し、それをActiveRecordライクに利用できるGem。 コード上にデータを持つためmigrationやseedなどの考慮が不要で、テーブルを持つほどでもない・ほとんど変更のないようなちょっとした静的データを保持する際にとても便利です。 詳しい使い方はREADMEを見てほしいですが、基本的には以下のようにActiveHashに用意されているクラスを継承し、データを設定して使います。 class Foo < ActiveHash :: Base self .data = [ { id : 1 , name : ' a ' }, { id : 2 , name : ' b ' }, { id : 3 , name : ' c ' }, ] end 上記のように data 属性にデータを追加していく方法以外にも、json,yamlなどのファイルにデータを設定してを読み込ませることもできます。 クラシルでの事例 導入経緯 以前もブログにて紹介された「サーバーサイド&SRE改善MTG」での議題として自分が発案し、導入に至りました。 ちなみに、この改善MTGは1月で1周年を迎えたようです 🎉 tech.dely.jp もともとクラシルのプロジェクトにはactive_hashが使われるようなユースケースが存在しており、すでにレポジトリ内にYAMLなどで定義された静的データが散在してました。 それらがバラバラに管理され、使われ方も統一されていなかったところを一元化したいというところが主な導入意図です。 また、今後何かしらのマスターデータを管理していく際にも便利なので、今のうちに入れておきたいということで入れました。 使われ方 データはyamlで管理し、 app/models 配下にActiveRecord継承なモデルと一緒に置くというおそらくはスタンダードな使い方で利用してます。 ActiveYamlRecord という親クラスをつくり、各クラスで継承していきます。 # app/models/active_yaml_record.rb class ActiveYamlRecord < ActiveYaml :: Base set_root_path Rails .root.join( ' config ' , ' masters ' ) end # app/models/foo.rb class Foo < ActiveYamlRecord end - id : 1 name : 'aaa' - id : 2 name : 'bbb' # ... 2020年2月時点ではまだ3モデル程ですが、これから要所要所でガンガン使っていきたいと思っています。 ハマりポイント ActiveHash導入に関して、自分の過去の経験からハマりやすい(ハマった)ポイントをいくつかご紹介しようと思います。 インスタンス変数がクラスインスタンス変数相当 ActiveRecordでは取得されるごとに生成されるオブジェクトは異なりますが、ActiveHashでは常に同じオブジェクトが返ります。 これはデータが読み込まれる際に各レコードのインスタンスが作成され、クラスインスタンス変数に保持され使い回されるからです。 以下はActiveRecordとの挙動の差です。 class Foo < ActiveHash :: Base self .data = [ { id : 1 , name : ' a ' }, { id : 2 , name : ' b ' } ] end require " active_record " ActiveRecord :: Base .establish_connection( adapter : " sqlite3 " , database : " :memory: " ) ActiveRecord :: Schema .define do create_table :bars , force : true do | t | end end class Bar < ActiveRecord :: Base end Bar .insert_all([{ id : 1 }, { id : 2 }]) b1 = Bar .first b2 = Bar .first p b1 #=> #<Bar id: 1> p b2 #=> #<Bar id: 1> p b1.equal? b2 #=> false f1 = Foo .first f2 = Foo .first p f1 #=> #<Foo:0x00007ff31791e738 @attributes={:id=>1, :name=>"a"}> p f2 #=> #<Foo:0x00007ff31791e738 @attributes={:id=>1, :name=>"a"}> p f1.equal? f2 #=> true このような挙動になるため、ActiveRecordと同じ気分でインスタンスの状態を変更するとハマるので注意が必要です。 class Foo < ActiveHash :: Base self .data = [ { id : 1 , name : ' a ' }, { id : 2 , name : ' b ' }, ] attr_accessor :foo end f1 = Foo .first f1.foo = ' bar ' p f1.foo #=> "bar" f2 = Foo .first p f2.foo #=> "bar" # nilではなくf1で代入した値が返る データがロードされるタイミング(特にActiveYaml, ActiveJsonの場合) これはActiveYamlやActiveJsonなど、データをファイルで持ってる場合に特に気をつけたいことです。 これらのクラスのデータロードのデフォルトの挙動は、 取得メソッドが実行されたタイミング になります。 具体的には以下のメソッドが実行された際にファイルのパースが実行され各クラスにデータが格納されます。 find find_by_id all where method_missing github.com class Foo < ActiveYaml :: Base set_root_path File .expand_path( __dir__ , ' ./ ' ) end # この時点ではまだYAMLは未ロード Foo .find(params[ :id ]) # ここで初めてYAMLが読み込まれる 小さいデータであれば問題はないようにも思えますが、大きなデータの場合は初回リクエストだけロードに時間がかかるようになってしまいます。 また、スレッドセーフな作りになってるわけではなさそうなので、複数スレッドで実行される際にも注意が必要です。 以下のように、意図しない挙動になってしまう可能性があります。 class Foo < ActiveYaml :: Base set_root_path File .expand_path( __dir__ , ' ./ ' ) end t1 = Thread .new do p Foo .first p ' t1 done ' end t2 = Thread .new do p Foo .first p ' t2 done ' end t1.join t2.join $ ruby sample.rb nil " t2 done " #<Foo:0x00007fe224c622e0 @attributes={:id=>1, :name=>"name1"}> " t1 done " これはおそらく以下のような状況になってるものと思われます。 t1 ActiveFile::Base.first が呼び出される データ読み込みフラグ(data_loaded)のチェック .reload が呼び出される data_loaded フラグがtrueになる YAMLのロード処理が走る t2 ActiveFile::Base.first が呼び出される データ読み込みフラグ(data_loaded)のチェック データ読み込み済み(data_loaded=true)と判定される ActiveHash::Base.first が呼び出される t1 のロードが終わっておらずデータが空なので [].first が処理として実行される nilが返る t1 YAMLのロード完了 ActiveHash::Base.first が呼び出される レコードが返る アプリケーションサーバーなどでマルチスレッドな環境を利用している場合は、意図しない挙動が引き起こされやすい状況と言えそうです。 対応策 この対応策として思いつくものとしては、クラスロード時にデータを読み込んでしまうことです。 class Foo < ActiveYaml :: Base self .reload end 上記のようにクラス定義で .reload を呼び出せばクラスが読み込まれる起動時などにデータを読み込ませることができるため、メソッドの初回呼び出し時でもロードが行われません。 あるいは、 config/initializers 配下で呼び出す方法もあるかと思います。 # https://github.com/zilkey/active_hash#defining-data の応用 # config/initializers/data.rb Rails .application.config.to_prepare do Country .reload end ただしどちらも起動が遅くなるというデメリットがあるため、各々のアプリケーションに適用できるかどうかは要確認です。 いずれにせよ、利用前にある程度意識しておく必要はあるかと思います。 最後に delyではRailsエンジニアを絶賛大募集中です。 大量のトラフィックをさばきたい 食の課題を解決したい ユーザーファーストな開発がしたい といったことに少しでも興味があるかたは、是非お声がけください! www.wantedly.com
こんにちは! サーバーサイドエンジニアの望月です! 先日開催された「Rails Girls Tokyo 13th」に、 delyはスポンサーとして協賛してきました! また今回は、私とサーバーサイドエンジニアの安尾が コーチとして参加者のサポートもさせていただきました。 今日はその様子をかんたんにご紹介したいと思います。 Rails Girlsとは? railsgirls.com Rails Girlsとはプログラミングに興味のある女性が コーチである現役のエンジニアのサポートのもと、 全2日間でWebアプリケーションを1から作ってみるイベントです。 13回目の開催となる今回はプログラミング初挑戦の方や、 デザイナーの方、会社のプロダクトがRailsを使用している人事の方など、 合計で25人のガールズが参加してくださいました。 Rails Girlsのスポンサーになりました&LTをしました! delyでは運営しているレシピ動画サービス「クラシル」の web・アプリでサーバーサイドとして Ruby、そしてフレームワークとしてRailsを使用しています。 www.kurashiru.com RailsはRubyのフレームワークであり、 Rubyの開発者のMatz(まつもとゆきひろ) さんいわく enjoy programming がコンセプトになっているため 可読性が高く、わかりやすいです。 またRailsは DRYやCoCなどRails Wayや、豊富なgemなど 高速に開発が進めることができる、という特徴から delyでもクラシルの開発にRailsを採択しています。 今回クラシルを運営しているdelyとしても Ruby、Railsコミュニティに対してなにか貢献したいと思い、 GitHubさんやSansanさん、SmartHRさんなどとともに 今回スポンサーとして協賛させていただきました。 当日はスポンサー枠でサーバーサイドエンジニア 安尾による 会社紹介LTもさせていただき、 “delyの魅力を 「4P」でお伝えします!” というテーマのもと、 delyやクラシルについてご紹介しました。 企業の魅力因子とも言われる「4P」とは ・Philosophy(理念・目的) ・Profession(仕事・事業) ・People(人材・風土) ・Privilege(特権・待遇) のこと。 delyがこれら4Pに対して、 どのような取り組みをしているかをお話させていただきました。 参加者のみなさんに、 「あなたにとって一番大切なPはなんですか?」 という質問をして、 挙手をしてもらったところ、それぞれのPにわかれる結果に。 若干ではありましたが、Philosophy(理念・目的) が多かった印象を受けました。 また、 「クラシルを知っている方〜」という質問にも多くの手が挙がりました! 🙌 LT後にもたくさんの方から、 「4Pって始めて知りました!」「LT良かったです!」 と、 声をかけてもらって嬉しかったです!! クラシルでは、アプリ、WebともにRailsで開発をおこなっています dely株式会社 様 夜の1時に作った資料とのことです!(お疲れ様です!) #railsgirlstokyo pic.twitter.com/ImVnjkEU7k — Rails Girls Tokyo (@railsgirlstokyo) 2020年2月15日 (笑いもあり、みなさん積極的に参加してくれて感謝です🙂) コーチとしても参加しました 今回はスポンサーとしての協賛だけでなく、 プログラミングに興味のある参加者の方のサポート役として サーバーサイドエンジニアの安尾と、コーチもしてきました。 プログラミング自体が初挑戦の方もいたので、 数時間で多くの知識を学ぶのは、なかなかハードではありましたが 皆さん真剣に取り組んでいました。 コーチとしてもいかにわかりやすく伝えられるかを考え 質問などにも丁寧に納得のいくまで回答するように心がけました。 当日の様子 ザッハトルテチーム ザッハトルテチームのみなさん、㊗️初deployおめでとうございます! #railsgirlstokyo pic.twitter.com/CWwSY9nlVi — Rails Girls Tokyo (@railsgirlstokyo) 2020年2月15日 マカロンチーム マカロンチームのみなさん、㊗️初deployおめでとうございます! #railsgirlstokyo pic.twitter.com/TMvz01fO49 — Rails Girls Tokyo (@railsgirlstokyo) 2020年2月15日 無事デプロイができました〜〜🎉🎉 参加者の方のなかにはこんな嬉しいツイートをしてくださっている方たちも。 楽しかった #railsgirlstokyo キャンセル待ち繰上げ参加させてもらえて、本当にありがとうございました。おかげで楽しく勉強続けられそうです。 写真はスポンサーさんからのノベルティの数々。✨ #プログラミング楽しい #プログラミング学習 pic.twitter.com/Cj6G3Oq1BA — neutral@楽しいプログラミング (@neutral75584112) 2020年2月15日 皆さん優しくて、ご飯美味しくて(選ぶ楽しみもあり)、OSSってありがてぇ🙏となって、コミュニティ大事だなってなった😇スタッフの皆様ありがとうございました!!! #railsgirlstokyo pic.twitter.com/P5A7pgCdOm — takinari (@takinarisa) 2020年2月15日 無事デプロイできたので打ち上げてます〜💎🎉GithubもHerokuもアカウントできてウレシイ〜 #railsgirls #railsgirlstokyo pic.twitter.com/78mYoJeWj7 — みーる (@meal_tw) 2020年2月15日 今回参加して/コーチをしての感想 安尾: 私が担当させていただいたガールズさんで、 普段はQA担当としてエンジニアと一緒に業務をしている方からは 「普段エンジニアと話していて理解できなかったことが今日一日で色々と分かるようになって嬉しかった!」 というフィードバックを頂くことができました。 プログラミングを学びたいという気持ちに 少し応えられたかなととても嬉しかったです。 今後もまたコーチとして参加できればと思っています! 望月 : マカロンチームのコーチをしましたが、 もう既にプログラミングを学習している方や 普段はデザイナーとしてフロントエンドのコーディングをしている方だったので、 皆さんの理解がはやく、特に問題が起きることもなくデプロイすることができました。 プログラミングは楽しいので、これからも続けていってくれたらなと思います。 最後に 今回私たち2人ともコーチとして初めての参加でしたが、 プログラミング初心者の方にどうしたらわかりやすく伝えることができるか、 どうやったらプログラミングを楽しんでもらえるかを 改めて考える良いきっかけになりました! これからもdelyではRuby、Railsコミュニティへの貢献をしていき Railsを活用してより良いプロダクトを世の中に届けていきたいと思います。 "80億人に1日3回の幸せを届ける" というミッションのもと 一緒に挑戦してくれるRailsエンジニアを募集しています! www.wantedly.com