hiragram no blog

iOSとか

転職してakippaに入社しました

ボートレース戸田の様子

前回: クックパッドを退職しnana musicに入社しました - hiragram no blog

こんにちはー。

2024年1月末をもって前職を退職し、2月からakippa株式会社で働いています。3ヶ月の試用期間を経て5月以降も雇用が継続される見込みとなったので、この記事を書いています。

謝辞

前職には2022年2月から2024年1月までのちょうど2年間在籍しました。自分と家族の状況変化と、仕事で負う役割や会社そのものの変化がたまたま近い時期に起こって、自分と家族が健やかに暮らすことを優先するためには環境を変える必要があったという感じです。

在籍中お世話になった同僚の皆様、ありがとうございました。一部の方には、急な告知のあとすぐ居なくなるような形になってしまったことをお詫びします。ご心配をおかけしてすみません。

今はakippaで働いています

一部の方にはまたかよと思われそうですが、現職でも1人目の正社員iOSエンジニアとしてXcodeのアップデートから採用活動までいろいろとやっています。

本社が大阪にあるので、年に何回か行く機会があります(すでに2回行った)。大阪の地理のことはなんにも分からなくて、初めて上京したときのワクワクに似た感覚を覚えます。

転職先を選ぶにあたって、リファラルに頼るかどうかは迷いました(実際にお声をかけてくださる方もいてありがとうございました)が、転職活動をしていた2023年12月当時はフィジカルもメンタルもやられ気味でポキポキしており、 物理的にも所属的にも誰も自分のことを知らない場所へ行きたい気分でした。で実際そういう環境に移って、程よいプレッシャーと、仕事によって信頼をゼロから積んでいく感じに心地よさを感じながら働いています。

いま思うこと

スタートアップの船に乗るには自分はまだ未熟だったと感じます。人や物事を見極める力もなかったし、自分が頑張っていっぱい働けばいい感じの成果が出ると思ってた節があったと思います。そのへんは自分を鍛えてまた何かに挑戦できたらいいなー。

最近久しぶりの方とお会いするシーンが多くて、楽し〜となっています。勉強会などにも出ていきたいと思ってるので見かけたら声かけてやってください。 今後ともよろしくお願いいたします。

クックパッドを退職しnana musicに入社しました

ボートレース戸田の様子

こんにちはー。

2022年1月末をもってクックパッド株式会社を退職し、2月から株式会社nana musicで働いています。 6ヶ月の試用期間を経て8月以降も雇用が継続される見込みとなったので、この記事を書いています(書いてるのは6月末)。

みんな入社していきなり新しい会社を褒めちぎる転職エントリかけるの不思議だなーといつも思っています。

謝辞

クックパッドには2018年8月から2022年1月の3年半在籍しました。感染拡大第6波の最中の退職だったため、多くの方に直接ご挨拶できなかったのが悔やまれます。在籍中お世話になった皆様、ありがとうございました。

横浜移転による片道120分超の通勤時間と週2日のオフィス勤務の組み合わせに適応できなかったのが直接の退職理由です(退職時点での話。今はどうなってるか知らない)。

今はnana社で働いています

冒頭にも書いたとおり、2月から株式会社nana music(以下nana社)で働いています。歌や楽器の演奏を投稿したり他者の投稿に対して重ね録りでコラボレーションしたりできる音楽アプリnanaを開発運営している会社です。

nana-music.com

入社当時nana社にはiOSアプリ開発の深い経験があるメンバーがおらず、他プラットフォームのエンジニアがiOSアプリの開発を兼務している状況でした。で、いろいろ課題があっていろいろいい感じにしてくれーという話を頂いて、他にもいろいろあって入社に至りました。

iOSアプリチームのリーダー(他にメンバーはいない)として入りましたが、今はそれに加えてnana開発全体のリーダーの役職も頂いていて、POと開発者の間に入って各プラットフォームのタスク管理をしたり、チーム全体のワークフローを改善したりみたいなこともやっています。

いま思うこと

4年前に転職してクックパッドに入ったときから、クックパッドで最後のレベル上げをして、その次は大きなチャレンジをしようと思ってました。年齢も20代後半の更に後半になったし、結婚もしたし、自分の趣味やこれまでのキャリアと親和性があるようなオファーを頂いたし、じゃあヨイショという感じです。

nanaというプロダクトは2012年から運営されていて1000万を超えるユーザーを抱える一方で、親会社だったDMMから2021年に再び独立したばかりで創業期みたいなワチャワチャ(あえて濁します)が超あるみたいな、プロダクトを応援してくれるユーザーコミュニティと毎日文化祭前夜みたいなヒリヒリ感がどっちもあるみたいな、そんな感じで結構稀有な環境だと思っています。そして強いエンジニアを全方位で欲しています。ネットカルチャー、歌ってみた、VOCALOID、二次創作みたいなキーワードにグッとくる方はぜひ一緒にヒリヒリしましょう。Twitterで僕宛にDMください。会社紹介しろでも社長と面談させろでもなんでも設定します。

コロナ時代に入ってからは開発者コミュニティ活動からちょっと距離を置いていましたが、またワイワイするのが恋しくなってきたのでいろいろ参加していきたいと思います。見かけたら声をかけてやってください。今後とも宜しくお願いいたします。

痔瘻の手術をした

3行

  • 2泊3日の入院をして、大腸の内視鏡検査と持病の痔瘻の手術をした
  • リモート期間で助かっている
  • 健康マジ大事

痔瘻とは

"じろう"と読みます。以下のページが大変わかりやすいです。

https://www.iwadare.jp/G-WEB/1-4.html

経過

  • 2018年12月に最初の肛門周囲膿瘍ができる。
    • しこりの痛み、38度の発熱がある。
    • 症状をググりまくったところ肛門周囲膿瘍という病名にたどり着き、近隣の有名っぽい肛門科を受診することにする。
  • クリニックを受診、日帰りで切開術を受ける。仙骨麻酔で膿瘍を切開して膿を流す。
    • 術前の血液検査で炎症の度合いを示す値が異常に高かったらしい。膿んでるし熱出てるし当然っぽい。
    • 切開後は麻酔が抜けるまで3時間ほど病室で安静。傷にはガーゼを当てて自分で運転して帰る。
    • 切開によって瘻管が開通し痔瘻が形成される。2泊の入院を伴う根治手術を勧められる。
      • 「仕事のスケジュール確認してまた連絡します〜」→チキって逃げる
  • 2020年2月末、肛門周囲膿瘍が再発し同クリニックを受診する。
    • 「言ったじゃん、根治手術やりなって」「はい…」
    • その場ではまた切開を行い、その場で入院と手術の日程を決めて帰宅。
  • 2020年3月26日、入院、切開開放術による痔瘻根治手術を受ける。
    • クローン病という病気が原因で痔瘻を起こすことがあるらしく、その検査のための大腸の内視鏡検査もやった。良性ポリープ1つ切除。
    • 痔瘻の手術で、以前切開したときと同じ仙骨麻酔が今回は効かなかったので、背中に刺す別の下半身麻酔でやった。これが原因かわからないけど術中にめっちゃ血圧が下がってすごい気持ち悪かった
    • 入院日午前に内視鏡、午後に痔瘻手術、のこり2日は静養という感じでした。手術前日21時から手術翌日18時まで45時間絶食だったので病院食がめっちゃ美味しく感じた。
    • 28日退院。

退院後

痔瘻の根治のために取り除かないといけない瘻管は直腸から肛門付近にかけて存在していて、そのため肛門のすぐとなりに結構な傷を負っている。細菌が滞留しないようにあえて完全に縫合せず傷を開けてあるらしい。2~3時間おきにガーゼ交換をしている。ガーゼが汚れるのはだいたい傷からの浸出液で、たまに思い出したように血が出る。

傷 治りを早める 食べ物 とかで検索したところビタミンCタンパク質鉄亜鉛とかが大事らしいのでとりあえず西友ネットスーパーで豆乳ブロッコリーみかんほうれん草とかを買ってみた。まだ食べていない。

眠ってしまえば途中で起きることは無いが、眠りに落ちるまでと、起きて痛むのがしんどい。

感想など

健康は大事。アルコールの多量摂取や免疫低下などによって便がゆるくなりがちな人は痔瘻を起こしやすいらしい。私は腹下しがちパーソンなのでナルホドという感じ。食生活などをちゃんと見直してみようと思いました。

傷が埋まって完全に支障がなくなるまで1ヶ月から1ヶ月半くらいかかるらしく、これが普通に通勤してる期間だったらマジで生活が破滅していたなと思いました。布団にうつ伏せで仕事ができるし、トイレもオフィスにいるよりずっと近い。いい機会だと思ってちゃんと直します。

2月の日帰り切開処置と3月の入院2泊3日で窓口負担がトータルで11万くらい。生命保険に入院保障と手術保障がついてたので、雑に確認したところ保険金が50万ちょっと入るらしい(本当か?)。iPadMacBookか迷う。毎回年末調整で保険グエ〜とか言いつつちゃんと払っておいてよかったと思いました。

あと1月に受けた健康診断の結果、肝機能がE(要治療)判定だったので尻なおったら次は肝を直す予定です。対戦よろしくおねがいします。

開発合宿した

仲良しが集まったコミュニティであるところの hiragram.slack.com メンバーで開発合宿をやりました。

メンバーは私, suthio, anoChick, miyachik。場所は湯河原にあるおんやど恵という宿。

出発当日の様子。

f:id:hiragram:20181030131147p:plain

本当は踊り子の中でさっそく酒盛りだ〜❗と思ってたけど寝不足で死ぬと思ったのでカルピスにした。

品川から湯河原まで特急踊り子で1時間くらい。

車窓から。

f:id:hiragram:20181030131224j:plain

12時くらいに着いたら昼ごはん。

お店はここです。

https://tabelog.com/kanagawa/A1410/A141002/14029882/

宿。

私はFirebase+Vue.js+Stripeでクレカ決済付きのwebページみたいなやつを作ってました。

あとはなんかいい感じのご飯を食べたり、

f:id:hiragram:20181030131237j:plain

プロジェクタ借りてマリオパーティしたり、

f:id:hiragram:20181030131303j:plain

wifiが届く足湯で作業したりしました。

施設はすごくきれいだし、温泉も夜通し入れるし、wifi足湯があるし、自由に使える会議室めっちゃ広いし、

という感じです。

作ったものはもうちょっと整えてそのうち公開します。

いつもhiragramを応援してくださる皆様へ

スタートトゥデイテクノロジーズ(旧VASILY)を退職します。今日から有給消化です。
次は決まっていません。8月からiOSらへんの技術者として雇ってもらえる会社を探しています。

最近業務やら個人やらでやったことは以下の通りです

今の所あんまりやりたいと思っていないことは以下の通りです

  • 医療
  • 金融

新しい環境に強く求めることは以下の通りです

  • 有給を使わなくても出社前に猫を病院に連れていけること

出来上がってるチームにあとから入るならめちゃめちゃ凄い人に学ばせてもらえる感じがいいです。スタートアップ博打にも興味はありますという感じです。

もしご興味持っていただける方がおられましたら、お気軽にTwitterまでご連絡ください。

f:id:hiragram:20180702020416j:plain

レギュレーションに則ったエントリを出すのが夢だったのですが、求職エントリになりました。

Vapor + Vapor Cloudでクローラー的バッチ処理

この記事はVASILY Advent Calendar 2017の17日目の記事です。

また、以下の記事の続きです。

hiragram.hatenablog.jp

アプリにJSONを返す部分を安全にする話を前回したので、今度は取引所のAPIを叩いてデータを取得するバッチを紹介する。

データ集めのバッチもVaporの Command の仕組みで動かしている(ここでは詳細は割愛するが、Vapor Cloudはcronもサポートしている)。

今度はバッチ側が取引所APIのクライアントサイドの位置付けになるので、まずは自作フレームワークAbstractionKit を用いて各取引所APIを抽象化し、RxのObservableを経由してデータを取得するクライアント層を作る。

Coincheckの売買価格を取得するAPIをこう定義する。

struct CoincheckEndpoints {
    struct CurrentRate: EndpointDefinition {
        typealias Response = SingleResponse<Element>
        typealias Environment = CoincheckEnvironment

        struct Element: Himotoki.Decodable, SingleResponseElement {
            // 略
        }

        var path: String = "/api/exchange/orders/rate"

        var method: HTTPMethod = .get
        var parameters: [String: Any]

        enum Order {
            case sell
            case buy

            var name: String {
                switch self {
                case .buy:
                    return "buy"
                case .sell:
                    return "sell"
                }
            }
        }

        init(orderType: Order) {
            parameters = [
                "order_type": orderType.name,
                "pair": "btc_jpy",
                "amount": 0.005,
            ]
        }
    }
}

EndpointDefinition はAbstractionKitが提供するプロトコルの一つで、各エンドポイントのリクエストパラメータ、レスポンスの型、HTTPメソッド、パスなどを定義する。

次いで、 EndpointDefinition のインスタンスを受けて実際にリクエストを投げ、結果を取得できるObservableを返す request メソッドを作る。

    static func request<Endpoint: EndpointDefinition>(_ endpoint: Endpoint) -> Single<Endpoint.Response.Result> {
        return Single.create(subscribe: { (observer) -> Disposable in
            DispatchQueue.global(qos: .default).async {
                do {
                    let response: Response
                    let absoluteURLString = Endpoint.environment.url(forPath: endpoint.path).absoluteString
                    let parameters = endpoint.parameters.mapValues({ (value) -> String in
                        String.init(describing: value)
                    })

                    switch endpoint.method {
                    case .get:
                        response = try httpClient.get(absoluteURLString, query: parameters)
                    case .post:
                        response = try httpClient.post(absoluteURLString, query: parameters)
                    case .delete:
                        response = try httpClient.delete(absoluteURLString, query: parameters)
                    case .patch:
                        response = try httpClient.patch(absoluteURLString, query: parameters)
                    }
                    if response.status.statusCode >= 300 {
                        // Error
                        observer(.error(NetworkError.uncategorized(message: response.status.reasonPhrase)))
                        return
                    } else if let bytes = response.body.bytes {
                        let data = Data.init(bytes: bytes)
                        guard let json = try JSONSerialization.jsonObject(with: data, options: []) as? Endpoint.Response.JSON else {
                            observer(.error(NetworkError.uncategorized(message: "Failed to cast JSON type")))
                            return
                        }
                        let result = try Endpoint.Response.init(json: json).result
                        observer(.success(result))
                    } else {
                        observer(.error(NetworkError.uncategorized(message: "Failed to extract bytes")))
                        return
                    }
                } catch let error {
                    observer(.error(error))
                }
            }

            return Disposables.create {
            }
        })

必要になったものから実装しているので、HTTPリクエストの機能をすべて網羅していないが気にしないでほしい。

エンドポイントの定義とリクエストメソッドが揃ったので、以下のように通信を起動する事ができるようになった。

swift let endpoint = CoincheckEndpoints.CurrentRate.init(orderType: .buy) APIClient.request(endpoint).subscribe().disposed(by: bag) ExchangeClient というプロトコルを導入し、取引所APIジェネリックに扱えるようにする。

protocol ExchangeClient {
    func currentAskRate() -> Single<Ask>
    func currentBidRate() -> Single<Bid>
}

クロール処理自体はRxSwift+RxBlockingで管理している。結局同期的にやるのだが、Rxに乗せておけば順番を入れ替えたり並列処理に変えたりというのが極めて簡単にできる。楽。

    public func run(arguments: [String]) throws {
        let clients: [ExchangeClient] = [
            CoincheckClient.init(),
            BitflyerClient.init(),
        ]

        let askRatesObservable = Observable.combineLatest(clients.map { $0.currentAskRate().asObservable().take(1) })
        let bidRatesObservable = Observable.combineLatest(clients.map { $0.currentBidRate().asObservable().take(1) })

        let asks = try askRatesObservable.toBlocking().first() // ここで同期的に通信が完了するのを待っている
        let bids = try bidRatesObservable.toBlocking().first()

        try asks?.forEach {
            try $0.makeQuery(driver).save()
        }

        try bids?.forEach {
            try $0.makeQuery(driver).save()
        }
    }

Vapor Cloudの一番安いDBインスタンスは、直接SQLクライアントアプリ等からつなぐことが出来ず、phpMyAdminが提供されるだけなのがちょっとつらいが、まあ安いので目をつぶろう、という感じです。

簡単にだが、クローラーバッチの紹介を以上とする。

次は23日に

  • SwaggerからSwiftのコードを生成するPython製ジェネレータ

の記事を書くから期待してほしい。

安全サーバーサイドSwift

この記事はVASILY Advent Calendar 2017の9日目の記事です。

目指すのは労働からの引退。

VASILY開発合宿で取り組んだ内容です。

何を作りたいのか

ビットコインの売買価格は取引所によって異なる。そこで、安い取引所で買って、すぐに高い取引所で売ることができれば、ビットコインそのものの価格変動に左右されずに利益を得ることが出来る。これがアービトラージ取引である。

今回は、複数の取引所のAPIを叩いて定期的に売買価格を取得するサーバーサイドアプリケーションと、その情報を表示するiOSアプリケーションを作った。

システム構成

サーバーサイド

  • 言語: Swift
  • Webフレームワーク: Vapor
  • インフラ: Vapor Cloud
  • バッチ処理の一部にRxSwift

iOSアプリ

  • Swift, RxSwift, APIKit

その他

サーバーサイドのAPIとモデル型はSwaggerのドキュメントで管理し、各エンドポイントに対応するコントローラとモデル定義はSwaggerのYAMLから自動生成する仕組みを作った。具体的にはSwaggerのYAMLドキュメントをパースしてテンプレート通りにコードを生成するジェネレータをPythonで作った。テンプレートはAppleのgybで書く。

「Swaggerドキュメントから自動生成できないAPI/モデル定義はそもそも設計が間違っているのではないか」という仮説が自分の中にあり、それを検証するためのこのプロジェクトとも言える。

このコードジェネレータについてはVASILY Advent Calendar 2017の別枠で改めて記事にするのでぜひ購読しておいてほしい。

現状できているもの

f:id:hiragram:20171208200603p:plain:h600

CoincheckとBitflyerの価格を取得して、1BTCを安い方で買って高い方で売ったときの差益を表示している。スクリーンショットは1ヶ月ほど前のものなので、BTCすごいですねという感じ。古いスクリーンショットを出してきたのは、今の価格で表示したら桁が増えた影響でレイアウトがブチ壊れたからである。

アプリ側は特に面白いことはしていないので、この記事ではサーバーサイドアプリケーションについて解説する。

Vaporアプリケーションの開発

Vaporはオフィシャルに提供されたCLIコマンドがあり、Xcodeのプロジェクトファイルの生成やVapor Cloudへのデプロイ、Vapor CloudのDBインスタンスの管理など、開発フローの中で面倒に感じることの多い様々なことをコマンド一発でできるので便利。公式ドキュメントが充実しているので、詳しくは割愛する。

コントローラの自動生成

先述の通り、クライアントアプリに提供する各エンドポイントのコントローラはジェネレータによって自動生成される。

paths:
  /board/ask:
    get:
      tags:
        - Price
      summary: Latest fetched price information for each exchanges
      description: ""
      operationId: getLatestPriceForEachExchanges
      parameters: []
      responses:
        "200":
          description: successful operation
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: "#/components/schemas/Ask"

このようにSwagger側で定義されたコントローラは、以下のように生成される。

// path: /board/ask
final class Board_AskController {
    typealias GetResponse = ArrayResponse<Ask>

    let drop: Droplet

    init(drop: Droplet) {
        self.drop = drop
    }
}

extension Droplet {
    func setupGeneratedRoutes() throws {
        do {
            let controller = Board_AskController.init(drop: self)
            get("/board/ask", handler: controller.get) // (※)
        }
    }
}

ここで生成されるのは、各エンドポイントごとに1つのコントローラクラスと、Dropletと呼ばれるVaporアプリケーションのコアにルーティングを登録するコードである。賢明なSwiftエンジニアの読者は気づいたかもしれないが、このままでは(※)の行でコンパイルエラーになる。ルーターに対して /board/ask へのリクエストを Board_AskControllerget というメソッドに流すように登録しているが、生成されたコードにはそのようなメソッドは無い。

これがこの仕組みの最も気に入っているところで、Swagger上で定義されたモデル型のレスポンスを返すメソッドをデベロッパーが適切に実装しないとサーバーサイドアプリケーションのコンパイルが通らないのである。すなわち、Swaggerで定義されたエンドポイントを適切に実装していないと、デプロイはおろかコンパイルすら通らないので、あとになって実装漏れが発覚するとか、Swaggerと違う型のオブジェクトが返されてアプリ側が困ることは無い。

コンパイルを通すために、Ask 型のオブジェクトを配列で返す get メソッドを実装する。

extension Board_AskController: GetRequestHandler {
    func get(request: Request) throws -> ArrayResponse<Ask> {
        let exchanges = try Exchange.makeQuery().all()

        let prices = try exchanges.map {
            try Ask.makeQuery().filter("exchange_id", $0.id).all().last
            }
            .flatMap { $0 }

        return ArrayResponse.init(element: prices)
    }
}

これで、「DBにある各取引所の最新の買値データを配列にして返す」というエンドポイントが実装された。Ask の配列以外を返すとコンパイルは通らないので、間違った型のデータを返してしまう心配も無くなった。

ここまでまとめ

  • Swagger駆動開発、今のところ良いです
  • サーバーもクライアントもSwaggerを神ドキュメントとして扱う風習ができれば、業務に投入したい仕組みである。

この先

VASILYのアドベントカレンダーを3枠もらっているので、あと2枠で

  • 各取引所のAPIを叩いてデータを集めるバッチをVapor Cloud上に構築する
  • SwaggerからSwiftのコードを生成するPython製ジェネレータ

の話を書く。お楽しみに。