Danger「Unused import」僕「ウザ!ww」
CIでktlintCheckを実行してからその結果をDangerでコメントさせていたのですが、報告するだけじゃなくてお前が直してくれという気持ちになったのでCIに修正させようという少年少女の物語である。
背景
Dangerでlintの結果をコメントさせると、unusaed import
やneedless blank line(s)
を自動でレビューしてくれて非常に便利なのですが、
- 結局ローカルで走らせる
ktlint Format
は地味に時間がかかって嬉しくない - 大きいPRだとDangerからのコメントが大量に来て、レビュワーからのレビューが
hidden conversation
扱いされることがあってウザい
という嬉しくないポイントをしばしば感じていました。
これどうにかならんかとまずググってみるとpre-commitでlintを実行するものが出てきたのですが、チームの面々のgit commitの実行方法が異なっていて各々設定が必須なのがうむぅなのと、ktlintFormat
で時間がかかって嬉しくないのは同じでもっとなんとかならんのかと。そこでCIでやればまじで完全にktlintFormatを意識する必要がなくなって最強無敵、天衣無縫のアイドルテロリスト*1ではと思いました。
どうやったか
before
./gradlew ktlintCheck bundle exec danger
after
git reset --hard origin/${BITRISE_GIT_BRANCH} ./gradlew ktlintFormat # ktlintFormatで変更がなければDangerを実行、あれば変更をcommit if [ $(git status --porcelain | grep .kt$ | wc -l) -eq 0 ] ; then ./gradlew ktlintCheck bundle exec danger else git config --local user.email "マシンアカウントのメアド" git commit *.kt -m "ktlintFormat" git push origin HEAD:${BITRISE_GIT_BRANCH} fi
です。
だいたい書いてある通りなのですが、いくつか説明したいと思います。
まず、git reset --hard origin/${BITRISE_GIT_BRANCH}
ですが、これをしないと自ブランチをPRを向けた先にマージしたマージコミットが一緒にpushされてしまうことがあるので最初にpush時と同じ状態に戻しています。
あとは、なんか忘れましたけど一行にいっぱい書くんじゃないよ!
みたいなktlintFormat
じゃ直らないやつについてはコメントしたいので、lintで差分が出なかったら普通にDangerを走らせてます。
適当なpush -> CIでktlintFormat
を実行して差分をpush -> そのpushに対してCIでktlintFormat
をかけるが差分が出ないのでDangerが実行
って感じです。
で、どうなん
なんというか、感じてた嬉しくなさは解決できたけど意外と微妙でした。
まずよかったのは、手元で全くlintを意識する必要がなくなった所に尽きます。
で、よくなかったのが、
の二点です。
一つ目に関しては、lintのコミットがリモートリポジトリに入ったらresetなりcherry-pickなりしてローカルリポジトリにもそのコミットを取り込めば問題なくなるんですが、それだと結局lintを意識することになって本末がコケる。で、脳死でlint意識しないでやってるとpush時にAndroidStudioさんから「リモートの方に知らんコミットあったからマージするかい?マージするやで!」って言われてマージコミットが入るんですよね。
二つ目は個人開発とかならモーマンタイなのですが、僕のインターン先ではCIが走るとQA用のビルドが作られてそれが社内でテストできる環境にデプロイされるようになっているので、それが二つ作られてしまうんですよね。しかもlintかけただけなので挙動は全く同じビルドが違ったバージョンで二つ作られる。ぐぬぬぬ。
結論
完璧なktlintFormatなどといったものは存在しない。完璧な絶望が存在しないようにね。
ナナシスを始めませんか?
AndroidアプリのCIをGitHub Actionsに移行した話他
個人で作っているAndroidアプリのCIをBitriseからGitHub Actionsに乗り換えた話などをしていきます。
GitHub Actionsで使ってるyamlはこんな感じです。
name: Android CI on: [pull_request] jobs: build: runs-on: ubuntu-latest steps: - uses: actions/checkout@v1 - name: set up JDK 1.8 uses: actions/setup-java@v1 with: java-version: 1.8 - name: Setup ruby uses: actions/setup-ruby@v1 with: ruby-version: '2.6' architecture: 'x64' - name: Install danger run: | gem install bundler bundle install - name: Build with Gradle run: ./gradlew assembleDebug - name: Check lint if: always() run: ./gradlew ktlintCheck - name: Run test if: always() run: ./gradlew testDebugUnitTest - name: Run Danger if: always() env: DANGER_GITHUB_API_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: bundle exec danger
単純に諸々の準備をした後でアプリのビルド、lint、ユニットテストを行い、lintとテスト結果をDangerでコメントさせています。
if: always()
を使うと前のステップで失敗していても当該ステップを実行できます。
ちなみにDangerFileはこんな感じです*1。
warn("このPRはWIPです") if github.pr_title.include? "[WIP]" ktlint_report_dir = "**/ktlintMainCheck.xml" Dir[ktlint_report_dir].each do |file_name| checkstyle_format.base_path = Dir.pwd checkstyle_format.report file_name end ktlint_test_report_dir = "**/ktlintTestCheck.xml" Dir[ktlint_report_dir].each do |file_name| checkstyle_format.base_path = Dir.pwd checkstyle_format.report file_name end junit_output_dir = "**/test-results/**/*.xml" Dir[junit_output_dir].each do |file_name| junit.parse(file_name) junit.show_skipped_tests = true junit.report end
マルチモジュールなのでテスト結果やlintの結果が複数出てくるのでえいやしています*2。
マルチモジュールでDangerをいい感じにするのはインターンでお世話になったしせうがエレガントに解決してる記事があるのでこちらも参照されたし。
閑話休題。yamlのシンタックスについては僕が何か説明するより公式(GitHub Actionsのワークフロー構文 - GitHub ヘルプ)が整っているのでそちらをご覧いただければと思います。
jobsをいい感じに使えてなかったりうむぅな箇所もありますが、こんな感じでGitHub Actionsライフを送っております。
Bitriseから移行した理由やいいとこなど
新しいから試してみたかったというのも大きな理由ではありますが、テストを走らせるようになってからBitriseの無料枠で使える10分ではビルドが終わらなくなってしまっていたのが最大の理由です。
Circle CIとGitHub Actionsは無料で使うには月ごとのビルド時間に制限があるのですが、Circle CIは1000分でGitHub Actionsは2000分(パブリックリポジトリなら無制限)なのでなんかお得感があります*3。
また、並列で行えるジョブについてもCircle CIが無料だと1つのみなのに対してGitHub Actionsは料金プランに関わらず(っぽい)20までと、お金を払いたくない人にとってはGitHub Actionsは結構いいのではと感じています *4。
*1:参考 Kotlin+DangerをGithub Actionsで動かす - Qiita
*2:参考 Danger on Android :: iamjonfry — iamjonfry
*3:追記: Circle CIの方が早いらしいです。参考 GitHub ActionでAndroidのCIを設定してみた - phicdy devlog
*4:僕Circle CI使ったことないのでアレなんですけどね
出会って即注入!〜Koinのsingleを一瞬でインスタンス生成する裏技に専門家も驚きを隠せない!?〜
男には泣いていい瞬間が3つある
ひとつ目は 生まれたとき
ふたつ目は 大切な人を失ったとき
みっつ目はTokyo 7th シスターズ
EPISODE.1.0-001、Episode.KARAKURI、EPISODE.3.0-014、EPISODE.4.0 AXiS 12話、13話を見たとき、Koinでsingle { Hoge() }
したクラスをlazyではなくApplicationのonCreate()
でインスタンス生成したくてstartKoin
の次の行でgetKoin().get<Hoge>()
したらアプリが起動した直後にクラッシュしたときである。*1
解決
single { Hoge() }
をsingle(createOnStart = true){ Hoge() }
にするとHogeのインスタンスがstartKoin
で生成されます。*2
〜めでたしめでたし〜
おまけ
なんでこんなことしたかったのか
FluxのStoreやActionCreatorをKoinでsingleにしてるのですが、その中でStoreがinit()
の中でDispatcherに自身を登録しています。
KoinのsingleはApplicationに直にインスタンスを持っているのとは少し勝手が違い、一度生成されたインスタンスを保持してくれるだけです。その肝心の生成の処理はby injectを書いたところでby lazyで行われるので、何も指定しなければ「初めてインスタンスを使おうとした時にインスタンスが作られ、そのインスタンスが保持される」がKoinのsingleです。
そんな状態だと、StoreがKoinのlazy(by inject)で初期化される前に飛んできたActionの情報を保存することができないので、できればApplication起動時にStoreのinit()
を呼んでおきたかったのです。
どうやらmoduleもmodule(createOnStart = true) { /*いろいろ*/ }
できるそうですが試してないので詳しいことはわかりません。
*1:ナナシスのエピソード、沁みてホロリときたり微笑んで終わるようなものが多くて、思ってたよりガチ泣きエピソードが少なかった。
*2:https://insert-koin.io/docs/2.0/documentation/reference/index.html#_create_instances_at_start
Nimでオブジェクト指向する
先日Nimでオブジェクト指向っぽいコードを書いたのですが、まとまってる記事などが見当たらなかったので軽いまとめです。
内容の高度さで言えば
class Greeting { fun printHello(s: String) { println("Hello, $s") } } fun main() { val greeting = Greeting() greeting.printHello("World") // > Hello, World }
くらいのことしか言わないので知ってるよって人は読まないで大丈夫です。
※編集後追記:公式以上のことは書いてないのでもう公式を読めばいいんじゃないかな*1
クラス(っぽいもの)を作る
Nimにはクラスの概念がないのですが、構造体を利用してクラスっぽいものを作ることができます。
日本語だと下2つの記事がまとまってると感じたので是非見てみてください。
公式がよければ公式ではNim Tutorial (Part Ⅱ)にて説明されています。
公式では説明されているのですが、記事でやたら継承されているRootObj
とはなんぞやということですが、Nimでは何も継承していないobjectはfinal
として扱われるので、そのobjectを継承した別のobjectを作りたい場合はRootObj
を継承しようね、というものであり、それ以上のことは何もないようです。
メソッド(っぽいもの)を作る
「え、お前クラスの説明してないじゃん」と言われたらごめんなさいですけど、クラスについては公式、または英語がキツいと感じても上で示した2つの記事を読めばだいたい大丈夫だと思うので、ここからはメソッドについて書きたいと思います。
むしろクラスの作り方についてはまとまってるものがあったのですが、メソッドについてまとめられたものが見当たらなかったので書きたいことはここからです。
上で示したQiitaや公式にもある通り、Nimではクラスはなく、あくまで構造体をクラスっぽく扱えるのでクラスのメンバ関数はありません。*2
その代わり、関数proc sum(a: int, b: int) : int = a + b
をsum(1, 2)
または1.sum(2)
のように書くことができます。
ちょっと例が微妙ですが説明すると、関数を呼び出す際に、その関数を第一引数の型のメソッドのように扱えるので、これを利用してメソッドっぽいものが作れます。
Nim公式によると、クラスにメソッドを追加できなくすることによって
- メソッドがどのクラスに所属しているかいちいち気にする必要がなくなる
- 上の
1.sum(2)
のように既存のクラスや外部のクラスにもメソッドを追加したっぽくできるようになる
の2点のメリットがあるとのことです。2点目はKotlinの拡張関数みたいで確かになかなか素敵に使えました。
関数の宣言
Nimで関数の宣言に使うキーワードにはproc
とmethod
、func
の三種類があります。
まず、method
は引数にobjectを取らなければならない制約があるので、これは言葉の通りメソッドとして使うことが前提とされているような気がします。
staticメソッドが欲しかったらproc
で宣言する必要がありそうです。
次に、proc
は引数の型がコンパイル時に決定されるのに対し、method
キーワードを使うとそれらが動的に決定されます。
ものすごくざっくりと言うとポリモフィズムを適用したかったらmethod
を使おうね、ということだと僕は解釈しました。
また、どうやらmethod
ではproc
で効く最適化が一部効かなくなるようです。
func
は大体の部分がproc
と同じですが、副作用のない関数であることを保証するキーワードになっています。
Nimのややマイナーな言語仕様(funcとnot nil) - Qiita
まとめ
ここまでの内容で、冒頭で出したようなやり方でHello, Worldを出すことができるかと思います。
type Greeting = ref object # base methodにしないとWarningが出て気持ち悪かったので{.base.} method printHello(greeting: Greeting, s: string) {.base.} = echo "Hell, " & s # Nimではmainメソッドはないのでトップレベルに書いた処理が実行される when isMainModule: let greeting = Greeting() greeting.printHello("World")
できた。
あとはお好みでpragma
を。下の記事が詳しいです。これはあまり公式に書いてなくてつらい、、*3
ちなみに僕がNimでオブジェクト指向してみたのはなんとなく作ったライフゲームですので、参考になるかはわかりませんがもしよければ。
何もわからないマンのweb系ベンチャー就活振り返り
20卒で某web系のベンチャー企業でソフトウェアエンジニアとして働けるであろうという目処が少し前についたので振り返りです。
就活やプログラミングを中心に2018年1月以降くらいのことを時系列的に書いていこうと思います。
僕自身が周りに同じような職種や業界を志望している人がおらず、全然わからないまま就活を進めていて不安だったので、そのような人に「ほーん、こんな感じでやってた人がいたのね」って思ってもらえたら最高です。
スペック
2018年1月(B2)時点での僕のスペックです。
- 非情報系
- B2の4月からロボットサークルに所属
- 大学に入ってからプログラミングを始めるも
ぐらいのものでした
こんな感じの1年とちょっと
まず時系列で振り返って、その後でやって良かったものなど振り返ろうと思います。 もっとざっくりした経歴だけ見たいかたは僕のWantedlyでも。
Android開発を始める(2月)
冬コミで内容が一ミリもわからないのにイキって買ったてくぶ*1の人たちのTwitterから、何やらDroidKaigiという催しがあったのを知りました。
その写真がめちゃめちゃかっこよくて「かっこいい!来年はいくぞ!」となってAndroidアプリをきちんとやろうと始めました(謎動機)*2
半分くらい読んでいた入門書を最初から読み直すなどした記憶があります。
自作のAndroidアプリを作り始める(5月)
僕自身がウイスキーが好きということで、ウイスキーの味をメモできるアプリ"Whiskeynote"の開発を始めました。*3
当初は何もわからなかったので設計とかでものすごく苦労した記憶があります。
今後この子には就職活動を通してお世話になるのですが、当時はプログラミングを仕事にするかどうかすら決めていませんでした。
インターンに応募、参加する(7~9月)
エンジニアとして就活をするかどうかの決め手にしたいと思い、7月あたりから未熟ながらもエンジニアインターンに応募をし始めました。
また、それに伴ってサポーターズを使い始めました。
後でも書きますが、ここをもっと早くから動いておけばよかったなと感じています。
最終的にSansanの1dayインターンシップとfreeeの2週間のインターンシップに参加しました。両方ともAndroidアプリ開発でした。
ここで初めてweb業界を志望している学生や、実際に働いている社会人の方々に出会いました。
freeeでのインターンを通して、エンジニアとして働きたいと思うようになった気がします。
本選考が始まる(10月末〜)(雑なくくり)
夏インターン以降はコーディングが割と日常にも入り込んできており、コードと過ごす時間がそれ以前と比べるとかなり増えたと思います。
学校の都合で京都に住んでいたので就活はスタートダッシュを決めるつもりなどはなく、まあ春休みに東京に戻ってからガッとやればいいやのテンションでした。
とは言ってもSkype面接が可能な企業を中心に少しづつ選考を受け始めました。
10月に参加したサポーターズの1on1でお話させてもらった企業が多めだった気がします。
そこで出会った企業さんにお話を聞きにいきそのまま受けることにした企業もいくつかあり、結局は11月中に選考を受け始めた企業が多かったです。
なんやかんやで色々選考は進み、最終的に6社受けることになるうち、2018年内に5社は選考を受けている状態と割と進んでいました。 会社のことを知るためにピクシブとクックパッドのインターンを受けるなどもしました。
pixivの冬インターンに行ってきた回 - あおさのお味噌汁
クックパッドの春インターン「サービス開発コース」に参加してきました - あおさのお味噌汁
1月は京都の方でテストなどもあり選考は進めず、2, 3月に残っている面接を受け今に至ります。
全体を通して
やってよかったこと
社の選考を受けたりする中で、アプリやブログ(これ)など技術の経験に対するアウトプットの質を評価していただくことが多かったので、未熟ながらもやってきて良かったなあと感じます。
自作アプリ作り
これはマジ。
僕と同じ境遇の人にあったら自分の作品を作ることを全力でお勧めします。(とは言っても作りたいから作る以外だとしんどい気もする)
就活のアピールになることもありますが、継続的にコードに触れるきっかけにもなりますし、情報収集をしていく動機にもなります。あと楽しい。Twitter
就活の情報源が全くない状態だったので、Twiterからの情報がものすごく多かったです。
いくつかの社のインターンや選考はTwitterきっかけで受けました。情報とかを抜きにしても刺激的だし、web系目指してる人が周りにいない僕にとって素晴らしいコミュニティでした。Twitter就活的なものもいくつかありました。あと楽しい。サポーターズ
これを始めたのがおそらく就活を通して最大の好手。大体全部のきっかけでした。*4
1on1は絶対に行った方がいい。(個人の感想)
もっとやっとけばよかったこと
全体的にもっと早く動いとけば選択肢も増えたのになあと感じます。
あと、インターンは会社を知るには説明会なんかよりよっぽど強いので積極的に受けとけばもっと本選考の時期楽だったかなあとかも思います。
早めに動いておくと、得られる情報の総量も早さも違ったのではないかと思います。
終わりに
長くダラダラな記事でしたが読んでいただきありがとうございました。
就活関連に限定したことを書きましたが、全体を通してだいぶ最高の1年だった感があります。
就職先の本決定もまだですし、それもしっかりやって今年も最高の一年にしていくぞ。
AtCoderでNimのコンパイルが通らなかった回
先日参加したAtCoderのABC121で、手元ではコンパイルが通ったコードがCE(コンパイルエラー)だったのでそのお話です。
手元でコンパイル通ったコードがAtCoderではコンパイルエラー出て鬱になった
— あおさ (@aosa4054) March 9, 2019
理由はよくわからないままなんか解決したので、次引っかからないように一応書いておきますといった感じです。
環境
手元: Nim 0.19.4
AtCoder: Nim 0.13.0
本題
コードはこちらの問題に提出したものです。 atcoder.jp
CEのコード(手元では通った)
import sequtils, strutils let root = readLine(stdin).split.map(parseInt) black = readLine(stdin).split.map(parseInt) echo (root[0] - black[0]) * (root[1] - black[1])
コンパイラ大権現様のお叱り
Main.nim(7, 27) Error: type mismatch: got (empty, int) but expected one of: system.*(x: T, y: T) system.*(x: int16, y: int16) system.*(x: set[T], y: set[T]) system.*(x: float32, y: float32) system.*(x: int64, y: int64) system.*(x: float, y: float) system.*(x: int, y: int) system.*(x: int32, y: int32) system.*(x: int8, y: int8)
つまり(root[0] - black[0])
がemptyで(root[1] - black[1])
がintで型があってないから無理ですと。なぜだ。
(root[0] - black[0])はどう見てもintだろ!ヴォイ!
引き算の部分の型推論がおかしいので、ワンクッション!wwってノリで引き算部分を別の変数に入れてみたら通りました。
ACのコード
import sequtils, strutils let root = readLine(stdin).split.map(parseInt) black = readLine(stdin).split.map(parseInt) h = root[0] - black[0] w = root[1] - black[1] echo h * w
なぜだ。
結論
AtCoderさんNimのバージョン上げてくれないかな
— あおさ (@aosa4054) March 9, 2019
いかがでしたか?
根本の原因は結局よくわかりませんでした。
しかし、Nim 0.19.4でコンパイル通るコードがAtCoder(Nim 0.13.0)で「型がおかしいで!」って怒られたら計算を分解してみたら良いかも、というのは間違いなさそうです!