ScalaでHTTPクライアントのライブラリは何を使えばいいのかと思ってAkkaを調べる
scalaのhttpクライアント
今までPlayしかやったことないからWSしか知らなくて、Playじゃないやつでhttpクライアント的なライブラリって何を使えばいいんだろうってのがスタート。Comparing Scala's HTTP client libraries - implicitdefとか見てdispatchってのがいいのかなーと思って同僚に質問してみたらakka-httpがよさそうって話になったので、何者なのかよく分からないので理解をしたいと思って調べた。
そもそもAkkaとは?
What is AkkaのGoogle翻訳
What is Akka? — Akka Documentationの冒頭を適当に日本語訳。
- 弾力性のある弾性分散リアルタイムトランザクション処理
我々は正しい、分散、同時、フォールトトレラントでスケーラブルなアプリケーションを作成することはあまりにもハードであると信じています。我々は間違ったツールと抽象化の間違ったレベルを使用しているので、それはですほとんどの時間。アッカは、それを変更するためにここにあります。アクターモデルを用いて、我々は、抽象化のレベルを高め、より多くの詳細については、反応性マニフェストを、スケーラブルな弾力性と応答性の-参照アプリケーションを構築するためのより良いプラットフォームを提供します。フォールトトレランスのために我々は通信業界は、自己治癒すると停止することはありませんシステムアプリケーションを構築するために大きな成功を収めて使用されている "それがクラッシュしましょう」モデルを採用しています。アクターは、透明分布と真にスケーラブルでフォールトトレラントなアプリケーションのための基礎のための抽象化を提供します。
うーん、なんとなく分かったような分からないような・・・
Akka とは
でググる
www.slideshare.net
はぁー、、これはとっても分かりやすい。そもそもアクターモデルという原理があってそれを実装してるのがAkkaということかな。 今日はここまで
Xcode上でImplicit declaration of function 'notify_cancel' is invalid in C99 でハマった
Pureeをcocoapodsで導入しようとしてたら
Pureeが依存してるYapDatabaseというやつがCocoaLumberjackに依存してて、CocoaLumberjackのところでタイトルのエラーが出た。
Implicit declaration of function 'notify_cancel' is invalid in C99
notify_cancelメソッドをクリックすると、osのコードっぽいところ/usr/include/notify.hにいきつく。
Implicit declaration of function '' is invalid in C99
というやつでググるとコンパイラのフラグでどうのこうのとか、osのコードっぽいところまできたので、xcode7.3.1とかxcode8とかswiftのバージョンとかを疑ってしまって、CocoaLumberjackをビルド対象から外せないかとか、other_c_flagsとかで無視できるってissueとかにつられて、盛大にはまってしまった。
敵はここにいた・・
そもそもCocoaLumberjack単体でビルドできるのかとか試してたけどなんの問題もなくてどうしようかなーと考えてて、ふと、エラーが出る冒頭で、#include <notify.h>
としてるんだけど、そこをクリックすると、なんとFoxのSDKのNotify.hに飛んだ:joy:
解決策
Podfileに追記
+ # HEADER_SEARCH_PATHSからfoxSdkを削除する + def remove_fox_sdk_from_header_search_path(pod) + workDir = Dir.pwd + xcconfigFilename = "#{workDir}/Pods/Target\ Support\ Files/#{pod}/#{pod}.xcconfig" + xcconfig = File.read(xcconfigFilename) + newXcconfig = xcconfig.gsub('"${PODS_ROOT}/Headers/Public/foxSdk"', '') + File.open(xcconfigFilename, "w") { |file| file << newXcconfig } + end + + ['CocoaLumberjack', 'YapDatabase'].each do |proj| + remove_fox_sdk_from_header_search_path proj + end
cf: http://dev.classmethod.jp/smartphone/iphone/cocoapods-inherited/
役割を明確化すると働きやすいなと感じた
今まで
入社して開発メンバーとして仕事をしてきて、いつのまにかチームリーダー的な役割になった。 スケジュール間に合うかベロシティを計測したり、やることがなくならないようにタスクを検討したり、新卒メンバーの受け入れをやったり、ミーティングの調整・ファシリテーションしたり、外部との窓口やユーザーの問い合わせに対応したり、1日中プルリクレビューしたり、その他のこぼれ落ちていく雑用を拾ったりしてた。
その結果、いろいろ抱え込みすぎて辛くなった。
役割の明確化
このままだと辛いと言ったらすんなり部署を変えてもらった。 チーム運営みたいなところから離れて今は開発のための開発をする部署に異動になった。
今まで嫌々拾っていた雑用やこぼれ球も開発を支援するという役割になったことで、ポジティブに向き合えるようになった。 雑用も一つのタスクとして記録に残していこうとか、どうやったらこの雑用を自動化できるかとか、部全体として他のチームでも同じようなタスクを巻き取れないかとかそういうことを考えながら取り組めているので楽しい。あと、今までやりたいと思っていたインフラまわりの開発が多いので学ぶことが多くて楽しい。
自分は、リーダーとしてみんなを引っ張っていくというより、メンバーが働きやすくなるようにサポートするという方が性にあってる気がするなーと思った。
今後
インフラもアプリもサーバーもフロントもチーム運営もできるスーパー便利屋になっていきたい。
elasticsearchのindicesとindex
indiceとindexってどう違うか分からなくてなぜかindiceはindexの親概念かなと勘違いしてしまっていて、かなり混乱してたけど、indice 意味 で、検索したらそもそも同義語だった…
kibanaで見る場合はアスタリスクとか使ってどうせ複数のindexをまとめて見ることが多いのでindicesって表現されてるのかも。
fluentdのDockerfileの書き方がちょっと変わってた
ubuntuが16.04になってた
これが一番大きな原因。 ubuntu 16.04が2016/4にリリースされてて、今Dockerhubのubuntuのlatestは16.04になってる。ちなみに、コードネームは「Xenial Xerus(ジーニアル ジリス)」で『おもてなしのアラゲジリス』
https://hub.docker.com/_/ubuntu/
install.shが変更
- ubuntu 16.04 xenial用のものが出てる
- httpsになってる
http://docs.fluentd.org/articles/install-by-deb#step-1--install-from-apt-repository
- http://toolbelt.treasuredata.com/sh/install-ubuntu-trusty-td-agent2.sh + https://toolbelt.treasuredata.com/sh/install-ubuntu-xenial-td-agent2.sh
libssl0.9.8じゃダメ libssl1.0.0
こういうエラーが出る。
E: Package 'libssl0.9.8' has no installation candidate
- apt-get libssl0.9.8 + apt-get libssl1.0.0
sudoのinstallが必要
You will be prompted for your password by sudo. 100 563 100 563 0 0 1497 0 --:--:-- --:--:-- --:--:-- 1501 sh: 5: sudo: not found sh: 8: sudo: not found The command '/bin/sh -c curl -L http://toolbelt.treasuredata.com/sh/install-ubuntu-trusty-td-agent2.sh | sh && mkdir -p /var/log/td-agent' returned a non-zero code: 127 make: *** [build] Error 127
insatall.shでsudoやってるけど、ubuntu 16.04には最初からsudoが入ってないらしくそこで怒られる。なので、apt-getでsudoもインストール。
+ apt-get sudo
余談
W: --force-yes is deprecated, use one of the options starting with --allow instead.
apt-getの際のオプションの--force-yesがdepricatedになったらしく、--alow-xxx
を使えとwarningが出る。細かく指定してねってことらしい。今回は無視した。
--force-yes Force yes; this is a dangerous option that will cause apt to continue without prompting if it is doing something potentially harmful. It should not be used except in very special situations. Using force-yes can potentially destroy your system! Configuration Item: APT::Get::force-yes. This is deprecated and replaced by --allow-downgrades, --allow-remove-essential, --allow-change-held-packages in 1.1.
airbrake-logbackを非同期化することができなかった
背景
Logbackとは
- Logback は巷で大人気の log4j プロジェクトの後継プロジェクト
- java製のデファクトっぽいロギングライブラリ
Airbrakeとは
- 商用のエラートラッキングシステム
Errbitとは
- AirbrakeのOSSクローン
airbrake-logbackとは
- logback はロギングイベントを出力する仕事を、アペンダーと呼ばれるコンポーネントに任せています
- つまり、LogbackのAirbrakeのためのアペンダー -> Logback Appender for Airbrake
非同期にしたいということはどういうことか
- logbackのappenderをAsyncAppenderにしたいということ
- 例: AsyncAppenderの設定(logback-examples/src/main/java/chapters/appenders/conc/logback-async.xml)
<configuration> <appender name="FILE" class="ch.qos.logback.core.FileAppender"> <file>myapp.log</file> <encoder> <pattern>%logger{35} - %msg%n</pattern> </encoder> </appender> <appender name="ASYNC" class="ch.qos.logback.classic.AsyncAppender"> <appender-ref ref="FILE" /> </appender> <root level="DEBUG"> <appender-ref ref="ASYNC" /> </root> </configuration>
結果
airbrakeのappender設定
<appender name="AIRBRAKE" class="net.anthavio.airbrake.AirbrakeLogbackAppender"> <apiKey>298c68320df62a7c2a7736ff6cca9b4e</apiKey> <env>production - ${config.resource}</env> <notify>ALL</notify> <url>https://errbit/notifier_api/v2/notices</url> <filter class="ch.qos.logback.core.filter.EvaluatorFilter"> <evaluator> <expression> event.getMessage().contains("Logger configuration in conf files is deprecated and has no effect.") </expression> </evaluator> <onMatch>DENY</onMatch> </filter> <filter class="ch.qos.logback.classic.filter.ThresholdFilter"> <level>WARN</level> </filter> </appender>
asyncなappenderの設定
もともと設定されてるAIRBRAKE
のappenderをasyncで囲うだけ。
<appender name="ASYNC_AIRBRAKE" class="ch.qos.logback.classic.AsyncAppender"> <appender-ref ref="AIRBRAKE"/> </appender>
試した結果
root=info | root=warn | threshold filter | どうなったか |
---|---|---|---|
AIRBRAKE | ASYNC_AIRBRAKE | WARN | warnが2回飛ぶ 🙅 |
ASYNC_AIRBRAKE | - | WARN | 何も飛ばない 🙅 |
- | ASYNC_AIRBRAKE | WARN | 何も飛ばない 🙅 |
AIRBRAKE, ASYNC_AIRBRAKE | - | WARN | warnが2回飛ぶ 🙅 |
- | ASYNC_AIRBRAKE | - | 何も飛ばない 🙅 |
まとめ
- できなかった
- そんなに困ってる訳ではないのであきらめた
deis上にhubotをデプロイする
hubot作成
- https://hubot.github.com/docs/に従って、
yo hubot
でhubot作成 - adapterはslack
- Procfileは削除して、Dockerfileを Docker + Yeoman + Hubot + Slack で簡単bot作成 参考にして書く
FROM node:latest RUN mkdir /bot && cd /bot ADD . /bot WORKDIR /bot EXPOSE 11000 CMD ["bin/hubot", "--adapter", "slack"]
deisでアプリケーション作成
- deisのclientをインストール。homebrewだとver1だったので、shで入れ直す。
$ deis register <controller>
$ deis create
Slack側の準備
- Integrationでhubot作成
- API Tokenを見る
- Slack に Hubotを導入する際にはまったところ(Heroku経由) - Qiita を参考に
.env
ファイルに環境変数追加していく。
HUBOT_SLACK_TOKEN=xxxxxxxx HUBOT_SLACK_TEAM=hoge HUBOT_SLACK_BOTNAME=fuga HUBOT_SLACK_CHANNELMODE=whitelist HUBOT_SLACK_CHANNELS=piyo
deisにdeploy
$ deis keys:add
<- 公開鍵登録$ git push deis master
$ deis config:push
ping
HUBOT_SLACK_CHANNELS
でHUBOT_SLACK_BOTNAME
をinviteしてping pong
Future.sequenceは1つずつ順番に実行される訳じゃない
sequenceっていう名前からどうしても順序を意識してしまって順番に(直列に)実行されるものだと思っていた。 でもそうじゃなかった。
Future.applyの場合
ためしに、こんな処理を書いてみる。1から10までを1s待って出力するだけのやつ。
def process: Future[Seq[Int]] = Future.sequence { for { i <- 1 to 10 } yield { Future { println(s"start -> ${i.toString} ${System.currentTimeMillis}") Thread.sleep(1000) println(s"end -> ${i.toString} ${System.currentTimeMillis}") i } } }
これを実行すると、出力はこうなる。同時に4つの処理が走るみたいだ。並列で同時に実行されているので全ての処理が終わるまでに3秒ぐらいで終わっている。ただ、最終的な結果は、Vector(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)
となって順番通りになっている。
scala> Await.result(process, Duration.Inf) start -> 1 1459695509918 start -> 3 1459695509919 start -> 2 1459695509919 start -> 4 1459695509919 end -> 3 1459695510923 end -> 4 1459695510923 end -> 1 1459695510923 start -> 6 1459695510923 start -> 7 1459695510923 end -> 2 1459695510923 start -> 5 1459695510923 start -> 8 1459695510923 end -> 8 1459695511929 end -> 7 1459695511929 start -> 9 1459695511929 start -> 10 1459695511929 end -> 5 1459695511929 end -> 6 1459695511929 end -> 9 1459695512933 end -> 10 1459695512933 res24: Seq[Int] = Vector(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)
Future.successfulの場合
こんな風にFuture.successfulで書き直して
def process: Future[Seq[Int]] = Future.sequence { for { i <- 1 to 10 } yield { Future.successful { println(s"start -> ${i.toString} ${System.currentTimeMillis}") Thread.sleep(1000) println(s"end -> ${i.toString} ${System.currentTimeMillis}") i } } }
実行すると、順番に実行される。実行時間も10秒ぐらいかかってる。結果もVector(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)
で同じになってる。
scala> Await.result(process, Duration.Inf) start -> 1 1459696443179 end -> 1 1459696444180 start -> 2 1459696444180 end -> 2 1459696445180 start -> 3 1459696445180 end -> 3 1459696446180 start -> 4 1459696446181 end -> 4 1459696447181 start -> 5 1459696447182 end -> 5 1459696448186 start -> 6 1459696448186 end -> 6 1459696449187 start -> 7 1459696449187 end -> 7 1459696450188 start -> 8 1459696450188 end -> 8 1459696451188 start -> 9 1459696451188 end -> 9 1459696452188 start -> 10 1459696452188 end -> 10 1459696453191 res25: Seq[Int] = Vector(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)
Future.successful
で同じことをした場合は、順番に行われる。これは、Future.apply
とFuture.successful
の処理をとるときに、名前渡しかそうじゃないかが関係ある。(となんとなく思う)
scala/Future.scala at 2.11.x · scala/scala · GitHub
def apply[T](body: =>T)(implicit @deprecatedName('execctx) executor: ExecutionContext): Future[T] = impl.Future(body) def successful[T](result: T): Future[T] = Promise.successful(result).future
MySQLで全く関係のないテーブルをJOINする
FROMでカンマで並べるだけ
SELECT * FROM A, B;
例えばゲームみたいなやつで、usersテーブルと、levelsテーブルがあって、usersとlevelsには何の関係もないけど、user一人一人に全レベルを結合したいみたいなとき。
SELECT users.name, levels.level FROM users, levels;
ってやれば、
name | level |
---|---|
太郎 | Level1 |
太郎 | Level2 |
太郎 | Level3 |
花子 | Level1 |
花子 | Level2 |
花子 | Level3 |
CROSS JOIN
ググると、CROSS JOIN
ってのも出てきたけどMySQLでは違うみたい。30歳にもなってこんなことも知らなかったです。。こういうのをデカルト積っていうらしい。
MySQL :: MySQL 5.6 リファレンスマニュアル :: 13.2.9.2 JOIN 構文
MySQL では、JOIN、CROSS JOIN、および INNER JOIN は構文上同等です (互いに置き換えることができます)。標準 SQL では、それらは同等ではありません。INNER JOIN は ON 句とともに使用され、CROSS JOIN はそれ以外のときに使用されます。
パイプしたときにlessコマンドでエスケープシーケンスの色表示を有効にする
例えば、$ docker logs hoge
みたいな感じでログを見たいけど長くて画面におさまらないから、パイプしてlessに渡したいけどそうすると、色が出ないのでなんかないかなと思ったらあった。
-R
をつければOK
$ docker logs hoge | less -R
golangでコマンドラインツールを作る #9 Compositeパターン
その8の続き
PBXGroup
Xcodeは実際のファイル構造じゃなくて好きな構造で表示できる。例えば実際のファイルは全部一つのフォルダに入ってたとしても、階層構造の表示できる。それを表現してるsectionがPBXGroup。で、PBXGroupはchildrenを持ってて、そこにはPBXGroupかPBXFileReferenceを持ってるって訳。
ん、確かこのパターンどこかで見たことあるぞ・・あっ!これ進研ゼミで習ったやつだ!!
ってことで、Compositeパターンを思い出したのでやってみた。
Compositeパターン
ディレクトリ構造とか再帰的に辿る的なやつのときに使うやつ。 これ→11. Composite パターン | TECHSCORE(テックスコア)
今回は、GroupっていうtypeとFileReferenceっていうtypeを同一視するために、interfaceを宣言する。
interface名悩むなぁ。Xcodeだと、グループとかファイルの情報ってFile Inspectorっていうペインで見れる。 FileInspectorっていう名前か、Fileか、それか参考サイトみたいにDirectoryEntryにするか、うーむ。 よし、GruopEntryにしよう。
GroupEntryはグループかどうかを教えてくれるIsGroup()と、その子供達を返すChildren()、そのEntryの情報を返してくれるDescription()あたりがあればよさそう。
とりあえずそんな流れでいこう。
// GroupEntry describes xcode project's entry // it is file or group type GroupEntry interface { IsGroup() bool Children(p Pbxproj) []GroupEntry Description() string }
Goの場合、interfaceを定義したらあとは同一視したいtype、今回でいうと、Group
とFileReference
でこのメソッドを実装してあげれば2つとも全然違うtypeなのに、GroupEntityとして処理できる。すばらしい。
実装の仕方は例えばIsGroupのやつはこんな感じ。レシーパーとしてそのstructを指定する。
func (g Group) IsGroup() bool { return true } func (f FileReference) IsGroup() bool { return false }
ここで、ポインタをつけた方がいいのかつかなくてもいいのかがよく分からなかった・・ 紹介されてる記事だとよくポインタついてたりして、その辺ちゃんと理解しないといけなそう。(そのときがきたら考える)
再帰処理をどうする?
で、再帰処理をどう書くか悩んでて、GoはOSSだったこと思い出した! filepath.Walkと同じことやればいいやと思って実装パクることにした。 いやー、OSSってすばらしい。普段、iOS書いてるからすごく素晴らしく感じる。
filepath.Walkの処理はここに書いてある。
go/path.go at master · golang/go · GitHub
今回のやつの場合、丁寧にerror返すみたいなところまでやってないからその辺削ってこんな感じで実装した。 表示するときに階層構造表したいから、levelも渡すようにした。
// WalkFunc is the type of the function called for each fileReference or // group visited by Walk. type WalkFunc func(entry GroupEntry, level int) // walk recursively descends group entry func (p Pbxproj) walk(entry GroupEntry, level int, walkFn WalkFunc) { walkFn(entry, level) for _, c := range entry.Children(p) { p.walk(c, level+1, walkFn) } } // Walk walks the xcode project tree rooted at root // calling walkFn for each group or file in the tree, including root func (p Pbxproj) Walk(walkFn WalkFunc) { for _, g := range p.groups { if g.isRoot() { rootLevel := 0 p.walk(g, rootLevel, walkFn) } } }
で、コマンドで利用する側は、こんな感じで好き勝手できる。
proj.Walk(func(entry pbxproj.GroupEntry, level int) { if level == 0 { return } for i := 1; i < level; i++ { fmt.Print(" ") } if entry.IsGroup() { fmt.Println("+ " + entry.Description()) } else { fmt.Println(" " + entry.Description()) } })
結果は、こんな感じ。それっぽい。今回は、グループの場合は、+表示してるけど、ほんとは色とか変えたり絵文字とか表示したりしたらXcodeでみてるやつっぽくなるかも?swiftだったら鳥の絵文字表示するとかw
+ Sample AppDelegate.swift ViewController.swift Images.xcassets + Supporting Files Info.plist + SampleTests SampleTests.swift + Supporting Files Info.plist + Products Sample.app SampleTests.xctest
ということである程度完成した。
ここまでの感想
- ポインタがよくわかんなくなってきた。つけてもつけなくても動いてるっぽいのなぜ
- 標準パッケージのソース見てたら意外と長い名前つけてる。forの変数は1文字にしないとと思ってたけどそんなことなさそう
- 調べ物してるときにforの中でselect使ってる構文があったけどなんだろうか、あれは。
- パッケージ名と変数名がかぶってたときってどうするんだ?
- classがないっていうの、ポリシーというか設計者の思想が分かればすっと理解できるかな?
- Enumないの?とふと思ってさらっと見てみたけど構文としてはなさそう
golangでコマンドラインツールを作る #8 ディレクトリを分けてリファクタする
その7の続き
パッケージの分けかた
ファイル分けて処理を関数色々書いたけど、同じ名前にするとduplicateになる。 goの場合はpackageという概念があって、その中では同じ定義になるようだ。
ということで、別パッケージに分けようとしたら、そんなパッケージ見つからないよと言われた。
やったのは、普通にpbxproj
っていうディレクトリ作って、その下にpbxproj.go
を作って、package pbxproj
って先頭にいれておく。
それで、他のファイルで、import "pbxproj"
ってやった。
軽くググってみると外部に公開しないようなpackageはinternalってやつがGo1.4から導入されてmainのGoのpackageについては適用されるみたいな記事を見つけた。
golang - go1.4で追加されたinternal packageについて - Qiita
ただ、サブパッケージとか色々ググってたり、他のプロジェクト見てたら自分が間違ってることに気づいた。importするときに、"github.com/mpon/xgodeproj/pbxproj" みたいに github.comから始めないといけないようだ。 じゃあ、さっきのinternalってやつはなんなんだろうか。まぁ今は深追いはやめておこう。
structにメソッドを所属させる
packageは分けれたけど、インスタンス作成して、そいつに対してメソッドを呼び出していくみたいのやりたい。 structにメソッドを生やせるみたいのあったのを思い出した。これが参考になりそう。2年前だけど。
Go の interface 設計 - Block Rockin’ Codes
そして、structにコンストラクタもたせる方法。
golang - Goで見かける構造体パターン - Qiita
staticなfunctionとか作りたくなったりするけど、今はとりあえずutil.goっての作ってそこにfuncを定義しておいた。
これで、結構コマンドライン側のファイルからproject.pbxprojをパースするあたりを切り出せた。 pbxprojに、project.pbxprojのパスを渡すと、jsonとかパースしてそのインスタンスがデータを 持っておいてくれるみたいな感じ。golangではインスタンスっていっちゃいけないのかも。
// CmdShow for print sections func CmdShow(c *cli.Context) { // find project.pbxproj path proj, found := findProjectPath() if !found { fmt.Println("Not found project.pbxproj file.") return } // get flags section := c.String("section") isSectionNotSet := section == "" // parse pbxproj pbxproj := pbxproj.NewPbxproj(proj) 略
ここまでの感想
golangでコマンドラインツールを作る #7 ある拡張子のファイルを検索する
その6の続き
project.pbxprojを探す
今はコマンドの引数でファイルを指定してる。でも、本当はproject.pbxprojって、xcodeprojの下にあるって決まってる。 なので、何も指定しなくても、コマンド実行ディレクトリ配下を再帰的に辿ってそれでパースして欲しい。 なので、golangでファイルを検索するやり方を調べる
再帰的にファイルを探す
早速よさそうなの見つけた。filepath.Walkでいけそう。
Go言語で指定したディレクトリ以下のディレクトリおよびファイルの一覧を取得する - taknb2nchのメモ
ルートパス指定するのに、カレントディレクトリどうやってとるんだろう。path.Base()
でいいのかな?
Getwd関数の方がそれっぽいかな。
とりあえず、こんな感じで隠しフォルダ以外を再帰的に探すようにできた。汚いけど。
cur, err := os.Getwd() if err != nil { panic(err) } err = filepath.Walk(cur, func(path string, info os.FileInfo, err error) error { if info.IsDir() { if strings.HasPrefix(info.Name(), ".") { return filepath.SkipDir } return nil } ext := filepath.Ext(path) if ext == ".pbxproj" { rel, err := filepath.Rel(cur, path) if err != nil { panic(err) } proj = rel } return nil }) if proj == "" { fmt.Println("Not found project.pbxproj file.") return }
あとは、Flagでpbxprojを指定できるようにしたい。
ここまでの感想
- ファイル操作系は色々揃っててすごいやりやすい気がした
golangでコマンドラインツールを作る #6 interface{}をstructにマッピングする
その5の続き
PBXFileReference
isaが特定できるようになったので、どれか一つのisaのjsonをパースしてみる。 例えば、なんのファイルがあるかのisaはPBXFileReferenceでこれは必ずこういう形をしてる。(※進めていくうちに気づいたけどしてなかった。)
{ "path" : "AppDelegate.swift", "isa" : "PBXFileReference", "lastKnownFileType" : "sourcecode.swift", "sourceTree" : "<group>" }
これなら型にできそう。
golangの型
golangにはクラスという概念がないっぽい。その代わりに構造体を使って型を作れるって感じで合ってるのかな? よく分からないけどそんな雰囲気と仮置きしておいてとりあえず先に進む。
構造体を定義する。
type FileReference struct { path string lastKnownFileType string sourceTree string }
構造体を初期化するやり方は色々あるようだ。これが参考になる。[Go] 構造体の初期化方法まとめ - Qiita
で、初期化しようと思ってるんだけど、ループで回してるバリューがinterface{}
なのでキャスト、どうすればいいかわからない。
ので、調べてみる。
interface{}のキャスト
interface{}
をある型として扱うっていうやつなんかうまくいってたから、適当にv.(string)
とかやってたけど
これがまさにそうだったらしい。ここにやり方まとまってる。Golang: interface{}, type assertions and type switches
全部で3パターンやり方がある。
俺が今までやってたのは危険なキャストの仕方を使ってたようだ。間違えると、panicを起こす。
panic: interface conversion: interface is string, not map[string]interface {}
安全なキャストは、第2返却値をもらうようにする。キャストがうまくいかなかったかが分かる。
もう一つはswitchで考えうる限りの型チェックもできる。TPOによって使えばよさそう。
var a interface{} = "string" s := a.(string) // キャストできたかチェックできない s, ok := a.(string) // okにtrue、falseが帰ってきてキャストできなければチェックできる switch v := a.(type) { case string: fmt.Println(v) case int32, int64: fmt.Println(v) default: fmt.Println("unknown") }
swiftのas?に似てるかもしれない。
let a: AnyObject = "string" if let s = a as? String { println(s) } else if let i = a as? Int { println(i) }
で、いざやってみたらやっぱりPBXFileReferenceは決まった型じゃないことが分かった。 mapのkeyがないとpanicになるようだ。 これも、さっきのキャストみたいに、第2返り値で安全にチェックできるようだ。 ということで新しい型を定義
type FileReference struct { name string path string lastKnownFileType string includeInIndex string explicitFileType string sourceTree string }
そして、mapでとるときに安全にとる。安全にとりつつなければデフォルト値とかってmapでできるのかな? なんかちゃっと調べたけどなさそう。なので、これも自分で書く。 ジェネリクスみたいのできないのかな。
func lookupStr(m map[string]interface{}, k string) string { if v, found := m[k]; found { if s, ok := v.(string); ok { return s } } return "" }
そして、ようやくそれっぽいのができた。
コマンドラインのフラグで分ける
で、今のままだとセクションを指定してその情報を見るみたいなことができない。 こうやりたい。
$ xgodeproj show project.pbxproj --section PBXFileReference
どうやら、これはcodegangsta/cliの仕組みなのかgolangの仕組みか分からないけど、 Flagというやつでできるっぽい。 やり方は簡単。commands.goのFlagsに以下のように追加する。
var Commands = []cli.Command{ { Name: "show", Usage: "Prints section names or each section information", Action: command.CmdShow, Flags: []cli.Flag{ cli.StringFlag{ Name: "section", Value: "", Usage: "section name for pbxproj", }, }, }, }
使う側では、こんな感じで取得できる。
section := c.String("section")
この辺りを組み合わせて結果的にこうなった。それっぽくなってきたぞ。
$ xgodeproj show project.pbxproj --section PBXFileReference SampleTests.xctest Base.lproj/LaunchScreen.xib Images.xcassets Base.lproj/Main.storyboard AppDelegate.swift Sample.app Info.plist Info.plist SampleTests.swift ViewController.swift
ここまでの感想
- interfaceに対して実装する、継承できないっていうのswiftのPOPに似てるかもと思った。
- キャストのやり方分かった。
- 安全にやるやり方は、たいてい、
xxx, ok := process()
みたいな感じってことが分かってきた - switchが便利だ。if-elseの代わりになる
- structをnewするやり方がいっぱいあるのは逆にやめてほしい
golangでコマンドラインツールを作る #5 配列をまとめる
その4の続き
セクション名をまとめて表示する
READMEにこうなってほしいコマンドを先に書いた。 とりあえずどんなセクションがあるかは見たいかなと思ったので、
xgodeproj show project.pbxproj
ってやったらセクション名一覧が表示されるといいかもと思ったのでそれをやってみる。
goで配列を作って、重複を許さないようにして、適当にsortして表示するみたいなのやりたい。 このサイトよさそう。
配列の定義の仕方
配列の定義の仕方から調べないとだけど、固定長のやつのことを配列っていって、可変長のやつをスライスっていうらしい。 スライスを使っといた方がいいってことで、こう宣言する。
a := []string{}
で、あとは、これにisaのvalueを代入してく。append(スライス, 値)
って書いてこいつが新しい配列を返してくる。
そして、それを自分に代入する。これはなんか直感的じゃない気がするな。
a = append(a, v)
最後に重複を取り除く。 これは、 golang-setってライブラリで実現できるようだ。
s := set.NewSetFromSlice(a)
しかし、[]string
が入らない。[]interface{}
じゃないとダメなようだ。うーむ。
スライスから重複を取り除く
ということで重複を取り除く別の方法探す。パフォーマンスとか全く考えずにすでに配列に存在してたら、 追加しないでいいかと思って、containsみたいな関数ないかなと思って探してもないっぽい。 ありそうなのに標準で用意しないっていうのは何かポリシーでもあるんだろうか。
go - Contains method for a slice - Stack Overflow
しょうがないので、contains関数作る。
func contains(s []string, e string) bool { for _, a := range s { if a == e { return true } } return false }
そして、愚直にisaのキーの値を重複なしで突っ込む。
for k, v := range mm.(map[string]interface{}) { if k == "isa" && !contains(ss, v.(string)) { ss = append(ss, v.(string)) } }
ソートもしておきたいのでどうやるのかなーと見ると、これは標準のパッケージがあるようだ。 これでOK。破壊的メソッドしかないっぽいのが気になる。
sort.Strings(s)
そして、重複抜きでそれっぽいのできた。
$ xgodeproj show project.pbxproj PBXBuildFile PBXContainerItemProxy PBXFileReference PBXFrameworksBuildPhase PBXGroup PBXNativeTarget PBXProject PBXResourcesBuildPhase PBXSourcesBuildPhase PBXTargetDependency PBXVariantGroup XCBuildConfiguration XCConfigurationList
ここまでの感想
- GoTourでスライスってなんか難しい気がしてたけど、可変長の配列ってだけ覚えてればよさそう?
- golang-setがないって言われて気軽のgo getすればいいっての、だんだん慣れてきた。
interface{}
っていうのが何でも入る可能性のある型みたいに使われてるってことが理解できてきた。- containsみたいなどう見ても使いそうなやつが用意されてないのなんでだろう?
- 結構破壊的なメソッドが多いというかそれしか用意されてない感じな気がする
- scalaのvalみたいな再代入禁止な宣言ができないの違和感感じた
- スライスをfilter関数とかmap関数とかやりたいけどないのもそういうポリシーなのかな?