TECH PLAY

株式会社モバイルファクトリー

株式会社モバイルファクトリー の技術ブログ

222

こんにちは。エンジニアの id:kfly8 です。 少し祝うには遅いですが、 技術アドベントカレンダー2021 無事完走しました🎉 ありがたいことに、ホットエントリーした記事もあり、編集担当としてはホッとしています(ホットエントリーだけに) tech.mobilefactory.jp tech.mobilefactory.jp tech.mobilefactory.jp tech.mobilefactory.jp 技術アドベントカレンダーの運用で感じた問題 6年ほど技術アドベントカレンダーを運用してきて、編集担当として大きく2つ問題を感じていました。 記事が多すぎ、埋もれる 毎日の記事公開は、工数負担が大きい 記事が多すぎ、埋もれる 弊社が技術アドベントカレンダーをはじめた2015年は、 Qiitaさんの「企業・学校・団体」カテゴリでいえば、記事数は1,100強 でした。 2020年になると記事数は4,100強 と、約4倍にぎわっています。1エンジニアとしては、お祭り感があり執筆の良いキッカケと思っています。特に初めて技術ブログを書くのには勇気だったり勢いも必要だと思うので、こういうイベントは背中を押すのに良い仕掛けだと思っています。 ですが、企業の編集担当として、12月は競争がはげしく、技術ブログで会社を知ってもらうタイミングとしては不向きと感じます。技術ブログの執筆の動機は様々ですが、社外からの反響は執筆の励みになります。また反響があると技術ブログの意義をステークホルダーに説明しやすくもなります。そういった好循環を作ることが難しい時期と正直感じています。また、読者側の目線で「12月は記事流量が多すぎ、良記事でも読み飛ばしたり、積ん読する、そして、結局、読まないこともある」といった話も聞きます。もったいないオバケがでてきます。 編集担当として、記事が多く、埋もれる問題にどう向き合うか、考える必要がありました。 毎日の記事公開は、工数負担が大きい 弊社では、月に1,2本のペースで、技術ブログを公開しています。技術アドベントカレンダーの時期だけ、25日連続で記事を公開します。運動が通勤だけの人が急にフルマラソンするような異常なペースアップで、執筆者にも編集者にも正直無理が生じます。 特に技術ブログに慣れていないメンバーが執筆する時、コンテキストのわからない相手に「課題」「背景」「解決策」をわかるように書く仕事は苦労します。慣れないうちの苦い経験は後々に引きずりがちなので、丁寧にフォローする余裕はほしいところです。 肩の力を抜いて技術アドベントカレンダーを書く方針にした これだけ問題点を挙げるとやらなければいいとも思いますが、記事に共感いただけたり、良い執筆のチャンスにもなってもいるので、今回はこの問題に抗ってみました。 記事が埋もれることはコントローラブルな問題ではないので、もし読み応えのある記事になりそうなネタなら、12月以外に書くよう誘導しました。 負担を減らすことに注力しました。肩の力を抜いて、記事をコンパクトにするように方針を建て直しました。これまでの技術アドベントカレンダーは、毎日ケーキで胃もたれしていました。一粒のチョコで良く、ちょっとしたtipsを書く方針です。難しく考えすぎず、祭りを気軽に楽しむプランです。 この方針は、技術アドベントカレンダーの原点回帰です。 2008年12月1日のPerlアドベントカレンダーの記事 は理想例です。定数の叩き込みの説明を6行だけでしています。これで十分です。 余談ですが、 日本の技術アドベントカレンダーの歴史 を読むと、気軽に書けたことが日本に広まったと理由の一つとありました。tokuhiromさんの次の言葉、刺さりました。 5分でさくっとかけるような tips でいいのです。そういう tips の方が意外と有用だったりもします。やってみると、自分では Perl のことに詳しいつもりでも、知らないことが多かったりするものです。 結果はどうだったか まず、工数負担は、格段に減りました。技術ブログをみんなで書く場を設け、15分で執筆、即レビュー、即公開予約という人もいました。社内ドキュメントを一部抜粋し公開する省力例もありました。例年と比べどうだったか聞くと、9割負担は減り、1割は昨年参加していないのでわからないという回答でした。 閲覧状況に関しては、下図の通り、PVや訪問数は昨対比で大差ない結果でした。ページ滞在時間は17%弱下がりました。記事をコンパクトにしたので、想定内だと思います。直帰率、離脱率の数値が悪くなっていますが、次回、導線を調整できればと思います。 この辺の良し悪しは目的次第のところだと思いますが、試してみて良かったと思います。 まとめ 技術アドベントカレンダーの運用で、編集担当として記事が多すぎ埋もれたり、工数負担が大きいといった問題を感じていました。そこで原点回帰し、より気軽にちょっとしたtipsを書く方針にし、結果、工数負担は下がるなど、試してみて良かった結果と感じています。企業の技術アドベントカレンダーで、もし同じような課題感を感じている企業がいれば、肩の力を抜いて技術アドベントカレンダーを運用してみることをオススメします! モバイルファクトリーでは、エンジニアのカジュアル面談を実施しています。今回の記事を読んで、詳しい運用が気になったり、弊社に少しでも興味を持っていただけなら、ぜひお気軽にご連絡ください。 カジュアル面談のお申し込み あわせて、こちらの採用サイトもご覧ください。 recruit.mobilefactory.jp
アバター
こんにちは、ブロックチェーンチームのエンジニア id:charines です。 この記事ではNFTにおける「ロイヤルティ」と呼ばれる機能について紹介します。 ロイヤルティとは? クリエイターがアート作品などをNFT化して販売された後、そのNFTが購入者によってマーケットプレイスに再度出品されることがあります。 しかし従来は再販売でいくら高値がついてもクリエイターが新たに収益を得ることはできませんでした。 そこで OpenSea や Rarible などの一部のマーケットプレイスでは、クリエイターの需要に応えて独自にロイヤルティの仕組みを導入し、再販売時にクリエイターに取引額の一部が還元されるロイヤルティの機能を実装しています。 しかしこれらの機能はそれぞれのマーケットプレイス独自の機能であるため、NFTが異なるマーケットプレイスで再販売された際は依然としてクリエイターは還元を受けることができませんでした。そこで、ロイヤルティの仕組みを共通化するための仕様が EIP-2981: NFT Royalty Standard として提案されました。 EIP-2981に対応したマーケットプレイスはまだ少ないですが、2021年7月にステータスがFinalへと移行したこともあって、今後徐々に増えていくことが期待できます。 ブロックチェーンチームでの対応 現在ブロックチェーンチームでは、NFTの導入を支援するためのサービスであるユニマSaaSを開発しています。 ユニマSaaSで作成可能なNFTではロイヤルティの還元率を設定できるようになっており、EIP-2981に対応したマーケットプレイスでの再販売では、還元率に基づいたロイヤルティを受け取ることが可能です。 さらにRarible独自のロイヤルティの仕組みにも対応し、ユニマSaaSで発行したNFTがRaribleで再販売された際にも同様の還元率でロイヤルティを受けることができます。 ちなみにOpenSeaの還元の仕組みはオンチェーンではないため、ユニマSaaSでの設定に関係なくデプロイ後にOpenSeaから直接設定可能です。 設定例 ユニマSaaSでコントラクト作成時にロイヤルティを設定すると、 ユニマ ではこのように 二次流通でのクリエイターへの還元率 という項目が表示されます。 ユニマでの販売自体は一次販売なので影響はありません。 ユニマでの還元率の表記 またこのNFTをRaribleで表示すると ○% royalties という表記を確認することができます。 Raribleでの還元率の表記 まとめ NFTが再販売された際にクリエイターが還元を受けられるロイヤルティの仕組みと、その標準であるEIP-2981について紹介しました。 EIP-2981は比較的新しい標準なので、まだ対応しているマーケットプレイスは多くないですが、予めEIP-2981に対応しているコントラクトをデプロイしておくことで、将来的に対応マーケットプレイスが増えたときに再販売時のロイヤルティを受けられるかもしれません。
アバター
こんにちは、21 卒エンジニアの id:d-kimuson です。 モバイルファクトリーでは、最近のプロダクトではフロントエンドに TypeScript を採用していますが、僕がアサインされているプロダクトは歴史が長く JavaScript で書かれていて、今回 TypeScript へのリプレースを行いました。 既存プロダクトの TS リプレースではしっかり型付けすることは難しいので、型チェックオプションを緩くしてリプレースすることが多いと思います。しかし、既存コードからリプレース後のコードまで全て型安全性が担保できなくなってしまうので、後からの strict 化は非常に大変になってしまいます。 今回のリプレースでは、型チェックオプションは緩くしない代わりに @ts-nocheck や @ts-expect-error を使用することで、段階的に型安全性を高めやすい形でリプレースを行いました。 このエントリでは TypeScript へのリプレース方針やプロセス等について説明します。 TypeScript の利点 JavaScript から TypeScript へ置き換えることで以下のような利点があります。 (1) 安全性の向上 静的解析がない状態では、null チェック漏れがないか等を開発者の意識だけで担保することになります。 テストで担保できますが、網羅的にチェックして全体の安全性を底上げできるのは TypeScript の良いところです。 置換え中にも型チェックや型を利用した ESLint ルールのおかげで、いくつも null チェックが漏れているコードや実行時エラーになる誤ったコードが見つかりました。 (2) 開発体験の向上 エディタが型を理解することで、正確な補完や hover 時に変数の中身が分かる等の強力なエディタのサポートを受けることができます。 例えば、VSCode では、TypeScript の Language Server が動いているので文字を入力すると同時に型チェックが動きます。実際に動かすよりも間違ったコードを書いた時のフィードバックループが圧倒的に早く、開発スピードがあがります。 また、型はドキュメントとして機能し、外部パッケージを含むモジュールの使い方を教えてくれます。 他人のコードを使う機会が多いほど開発体験に影響します。 (3) ソースコードの品質が高まる JavaScript は潜在的に秩序のないコードをになりがちです。秩序のないコードでも正しい挙動さえしていればその当時は問題ないかもしれませんが、後から意図が理解しにくい・バグを生みやすい等の問題を生みます。 TypeScript は導入するだけで JavaScript に一定の秩序をもたらします。 TypeScript ではソースコードを静的に解析して型をつけるので、静的に解析することが難しいコード(≒秩序のないコード)は型エラーで怒られることになります。よってそもそも秩序の無いコードを書くこと自体が難しくなります。 あるいは、型を上書きするような手段を使うことで秩序の無いコードを書くこと自体はできますが、そこに型情報があれば秩序のないコードを読み解くヒントになるはずです。 TypeScript リプレースによってこれらの利点を教授するため、リプレースを行うことにしました。 TypeScript へのリプレースの方針 今回のリプレースでの課題と、どうやって解決したのかについて説明します。 問題 ― 巨大なコードのリプレースと向き合う 上記のような利点があるので TypeScript へリプレースしたかったのですが、プロダクトの歴史が長いこともあって「既存の JavaScript のコードベースが巨大である」という問題がありました。 TypeScript では、静的に検知できる問題が最大化していることが理想です。静的に検知できる問題が多ければ多いほど、開発時のフィードバックループが速く周るので開発スピードがあがりますし、安全性も向上します。 したがって、理想的な導入後の状態は以下のような状態です。 全てのソースコードに型がついている 硬い型チェックオプションによって型の信用度が高い strict オプションが有効 (暗黙の this 禁止, 厳格な null チェック, ...etc) any 禁止 (eslint の no-explict-any ルールで禁止できます) ts-ignore や as 等の型安全性を損なう構文の使用が必要最低限になっている しかしながら、既存の JavaScript コードベースが大きいため全てのファイルに厳格な型をつけることは現実的なリソースでは難しい状態でした。 型を書く工数もそうですが 元のコードが間違っている (null チェックをしていない等) 元のコードが静的解析に向かない(手続き的な書き換えが多いコード等) 等のケースも多くあり、これらのケースでは型情報だけではなく実装に修正を入れるリファクタリングも必要になります。型情報だけなら気軽に追加・修正が行えますが、実装に修正が入る場合はそれだけ動作チェックのリソースも必要になるのでなおさら現実的な工数で行うのは難しいです。 避けた解決の指針 ― がんばらない TypeScript この問題への一般的な対策として、いわゆる「がんばらない TypeScript」があります。 参考: TypeScript再入門 ― 「がんばらないTypeScript」で、JavaScriptを“柔らかい”静的型付き言語に - エンジニアHub|Webエンジニアのキャリアを考える! 現実的なリソースで理想的な状態へのリプレースを一気に行うことは難しいので、型チェックのオプションを緩くすることで型チェックが通るようにしようというものです。 例えば 暗黙的な any を禁止する noImplicitAny オプションを外せば引数の型注釈が抜けていてもエラーにならなくなります null チェック強要の strictNullChecks オプションを外すことで null チェックをしていないコードでエラーが出なくなります オプションを緩くするだけでエラーの数は大きく減りますので、いくつか残った型エラーを手動で対応するだけでリプレースできます。 がんばらない TypeScript のつらいところ 型チェックオプションを緩くすることで、低コストでリプレースが行える「がんばらない TypeScript」ですが、リプレース後の状態はリプレース後の TypeScript としては辛いところも多いです。 すべてのファイルで型が信用できなくなってしまう 型チェックの強度は型の信用度に直結します。 緩い型チェックオプションの元では、型が実装と乖離する可能性が高くなります。 // 緩い strictNullChecks が off なら、以下は型エラーにならない const text = [ 'hello' ] .find (() => false ) // 型は string だが値は undefined この例では、 strictNullChecks オプションを無効にしている場合、 変数 text は string 型に解決されますが、見ての通り text には undefined が入ります。 緩い型チェックオプションでは、こういった型と実装が異なるということが起こりやすくなります。 緩い型チェックオプションの元では 新規のコードも含めて こういった型と値の乖離が起こりやすくなります。型を疑いながらコーディングをすることになりますし、型チェック自体も緩いので、本来型チェックで気付けるはずだった単純なミスも実際に動かして気づくことが増えます。 古いソースコードで、こういった乖離が起きてしまうのはある程度仕方ないでしょう。しかし、新規で書くコードでも型が信用できず、型チェックで検知できる問題が少ない状態で TypeScript を書いていくことになってしまうのは避けたいです。 後からの型安全性を高めることが難しい 全体の型チェックを緩くするということは、 リプレース後に追加したコードも含めて 型安全性が低くなるということです。既存の JavaScript コードに合わせて型チェックオプションが緩くされているので、新規で書くコードも同レベルでしか型安全性を担保できません。 もちろん TypeScript を意識して書くことになるので、ある程度は意識的に安全なコードを書くことにはなると思います。しかし、開発者の意識だけで strict と同レベルの型安全なコードを書いていくのは現実的に難しいと思います。 そのまま運用していても型安全性は低いままなので、あとから型安全な状態に行こうすると、再び strict にするためのプロジェクトを計画して実行する必要があります。型チェックオプションを硬くして、プロジェクト全体のソースコードに型エラーが起きるので、それらを直していく作業が必要になります。 strict 化の事例としては以下の事例が参考になります。 https://speakerdeck.com/k2wanko/c5a64443-55b3-4863-aafa-da539a6ef623 リプレース後のコードも修正対象になり、非常に大変です。 解決の指針 ― メリハリのある TypeScript TypeScript には、行やファイル単位で型エラーを無効にできるコメントが用意されています。 @ts-nocheck : そのファイルの型チェックを無効にする @ts-expect-error : 次の行の型エラーを無視する。次の行に型エラーがない場合、エラーになる これらを使うことで「全体の型チェックオプションを緩くする代わりに、型チェックの範囲を狭めて堅い型チェックをかける」ことができます。このやり方でも同じく低いコストで巨大なコードベースを TypeScript へ移行をできます。 具体的には、ファイルの型エラーが少ない場合は @ts-expect-error で対応して、一定より多いファイル場合は @ts-nocheck で型エラーを無視すれば移行が完了します。 この記事では、この指針を型が堅い範囲と緩い範囲がしっかり分かれるので「メリハリのある TypeScript」と呼ぶことにします。 「メリハリのある TypeScript」では、「がんばらない TypeScript」でのつらいポイントが解消されます。 型の信用度にメリハリが付く 型チェックの強度自体は堅いので型チェックを無効にしているファイルとそうでないファイルで明確に型安全性がメリハリがつきます。 目印 型の信用度 静的解析 心持ち @ts-nocheck 低 無 補完が効きやすいだけの JavaScript @ts-expect-error 中 有(型の信用度が低いので一部ではあるが検知できる) 型は間違ってる可能性もあるから疑いつつ使う (静的に検知できる問題も多い) 上記が存在しない 高 有 型を全面的に信用することで恩恵を最大限受けられる @ts-nocheck , @ts-expect-error を目印に、明確に型が信頼出来る範囲か分かるので、型安全性が高いファイルでは型を信頼して TypeScript の恩恵を最大限受けることができます。 逆に、移行時に @ts-nocheck を書いたファイルでは、静的解析が機能しませんが、型自体はついているので多少のエディタサポートは受けながら書くことができます。 型安全性は運用とともに向上していく 型チェックオプションが緩い状態では、型チェックオプションを高めるときに大きなコストが必要になりますが、こちらの指針であれば運用とともに型安全性が向上していきます。 新規で追加したコードは自動的に型安全性なコードになるので、「新規で追加したコードまで型安全性が低くなる」という問題はなくなり、型安全な割合は自動的に増えていきます。 また、既存のコードもファイルや行単位で型エラーを無効にしているだけなので、 @ts-nocheck や @ts-expect-error のコメントを外すことで型チェックの範囲を容易に広げることができます。移行時にすべてリファクタリングするのは難しくても、機能改修等のタイミングで既存のコードを触るときならしっかりとした型付け・リファクタリングも行いやすいでしょう。 strict 化は一括で行う必要があり、コストが大きいですが、脱 @ts-nocheck は運用しながら段階的に行うことができます。 置き換え直後こそ @ts-nocheck の影響で、型チェックで問題を発見できない範囲が広くありますが、運用しながら徐々に型安全な範囲を広げて理想状態に近づけることができます。 これらの理由から、このプロダクトでは「メリハリのある TypeScript」の指針でリプレースを行うことにしました。 既存コードに型を付ける 移行時点では、型エラーが出る箇所はコメントで型エラーを無視することにしましたが、そのまま型エラーを一括無視するとほとんどすべてのファイルで無視することになってしまいます。したがって、ある程度型付け対応をした後に、仕上げとして型エラーが出ているファイルの型チェックを無効にしました。 ここでは、実際に型付けをしていった方法について説明します。 基本的な方針: 型エラーは ts-expect-error で握りつぶす JS の拡張子を .ts に変えただけの状態では、型注釈が抜けている箇所が多くあるので、まずは型注釈を書きます。 そうすると 静的解析に適していない 実装に問題がある (null チェック等が抜けている・実装ミス) といった箇所が型エラーとして残るので、これらの型エラーを解消するのが基本的な流れです。 型エラーの解消方法は (1) as や @ts-expect-error を使って型エラーを握りつぶす (2)元のコードをリファクタリングして静的解析に適した・あるいは問題のないコードにする のいずれかになりますが、このプロダクトでは(1)の方針で、 @ts-expect-error を書くことで型エラーを解消することにしました。 (2)のようにちゃんと問題ないコードにするのが理想的ですが フロントエンドのテストが存在しない現状では気軽に直すことができないこと リファクタリングを伴う型付けは後者に比べて機械的に行えず、時間的に厳しかったこと これらの理由で、現実的なリソースでは難しかったので、リファクタリングは行いませんでした。 また、型エラーと言ってもエラー箇所が原因の問題とは限らず、依存しているモジュールの型がおかしいケースもあります。こういったケースでは、後からモジュール側の型定義を直したときに型エラーが解消されます。 ts-expect-error であれば次の行にエラーがないとエラーになるので、後から型定義が修正されたときに一括で ts-expect-error を削除できます。 型付けの作業の流れ 「型エラーは @ts-expect-error で握りつぶす」という方針で以下のような流れで型付けを行いました。 1. コンポーネントを独自の型付け関数に通す この記事では詳しく触れませんが、このプロダクトでは Vue の 1 系を使っている都合でコンポーネント内ではほとんどの値が any に型付けされてしまいます。また Vue2 系で廃止されているイベント通信を多用している都合もあって独自の型付け関数を定義してコンポーネントでも適切に型が付くようにしています。 まずは、型付け関数を通すことでコンポーネント内のコードにも型を付けていきます。 2. コンポーネントのエラーを消す 型付け関数を通すことで適切に型エラーが出るようになるので、上で書いたように型注釈を書きつつ、エラーを @ts-expect-error で消していきます。 基本的には全て型エラーを握りつぶしますが、HTTP クライアント・エラーをロギングするためのモジュール等の依存モジュールの問題である場合も多かったのでそちらを先に直していきました。 型エラーの出るファイルを一括で対象外にする 残った型エラーの出るファイルは一括で @ts-nocheck コメントを付けました。 これで型チェックを回しても一切エラーがでない状態になり、リプレースを終えることができました。 リプレースの結果 リプレース後に、機能開発等の機会がありましたが、新規で書くコードに関しては型安全性が担保されているので、快適な状態で開発できました。 また、運用しながら徐々に型が信用できる範囲を広げられるようにリプレースを行ったので、今後継続的に型が信用できる範囲を広げていけることが大事だと思っています。 型が信用できるファイルの割合は継続的に計測していて、リプレースから 2 ヶ月で型安全なファイルの割合を 27% → 46% に増やすことができました。コードの自動生成をするようになり、その分が含まれているので、既存コードに型を付けたことで向上した割合は7%程です。現在進行中のフロントエンド環境改善のタスクが完了すると57%まで向上する見込みです。 今後は、チームの TS 習熟度向上の目的も兼ねて既存のソースコードに型付けする作業をペアプロで行う予定ですので、一層型安全な範囲を広げていけることが期待できます。 まとめ このエントリでは、プロダクトの TypeScript へのリプレースについて運用しながら型安全性を高めやすい「メリハリのある TypeScript」という方針について紹介しました。 型チェックを緩くする代わりに、 @ts-nocheck , @ts-expect-error のコメントで型エラーを無視することで 後からのやると大変な strict 化を同時に行うことができる 段階的に安全な範囲を広げやすい という運用しながら型安全性を高めやすい形でリプレースを行うことができました。 今回は TS 移行時の方針として紹介しましたが、既に緩い型チェックオプションで TypeScript を利用している場合でも有効だと思います。 特にチームメンバーが TypeScript に慣れていない場合であれは、移行直後に困りにくいように「がんばらない TypeScript」で移行するのも良いと思います。 ですが、一定時間が経ってある程度 TypeScript が浸透したのであれば、ずっと緩い型チェックのまま進めるのは望ましくありません。型エラー無視のコメントを利用してでも厳格化できると良いのではないでしょうか。厳格化以降は徐々に型安全性が向上しますし、新規で書くコードでは TypeScript の恩恵を最大限享受しながら開発できます。
アバター
この記事は モバイルファクトリー Advent Calendar 2021 の25日目の記事です。 メリークリスマス🎉 エンジニアの id:kfly8 です。 技術ブログの「ネタがない」といったコメントや「この記事の課題がよくわからない」といった記事レビューをすることがあります。技術アドベントカレンダーの時期は、短期間に記事が集中するので、特に困らせているように感じます。 普段から意識する習慣で、楽ができないかと考えると、 「技術ブログが書ける開発をする」 のが良いと思いました。 誤解しないでほしいのが、「技術ブログを書くために開発をしよう」と言いたいわけではないです。あくまで、チーム、事業の目的ありきです。 ただ「技術ブログが書ける開発をする」ことは、普段の開発の質を高めると思っています。 技術ブログが書ける開発とは? モバファクの技術ブログでは、「課題を解決する方法や経験を発信したい」と思っています。課題の大小は気にしていなくて、解決策もエレガントでなくても、最新の技術でなくても全然良いというコンセプトで運用をしています。(継続も大切にしたいので、技術ブログの執筆の敷居はどんどん下げたいです。) 昔、 koba04 さんに言われたことで、よく覚えている言葉があります。 「半年経って、技術ブログに書くことがなかったら、何かがおかしい。」 当時、エンジニア1年目だった私は、ネタに困ったので、印象に残っています。 今思うと当然のことだと思います。なぜなら、普段の開発は、課題解決の連続だと思うからです。普段やっていることを文章にすれば、技術ブログ1本になる。書けないとしたら、普段、課題解決をしていないかもしれない。そんな気づきを得る言葉だと解釈しました。 つまり、 技術ブログが書けるかどうかが、課題を解決する開発になってるかどうかを示すバロメータ になっています。技術ブログが書ける開発は、良い開発ができていると思います。 「技術ブログが書ける開発」のための2つの工夫 けれど、実際問題、技術ブログを書きたかったとしても、書くのに苦労します。 不安、恥ずかしい、忙しいといった技術ブログが書けない理由は一旦置いておき、普段の開発で、次の2つの工夫をすると技術ブログを書きやすくなると思います。そして、開発の質も上がると思います。 1つ改善したいことを設定して、開発する 「仕様から動くモノにする」ことは、プロダクト開発をするエンジニアのメイン業務だと思います。ですが、この開発業務をそのまま技術ブログに書くことは難しいです。なぜなら「仕様から動くモノにする」ことは、プロダクトチームの人だけに通じる業務知識が混じり、そのコンテキストを共有しない人は、理解・共感ができないからです。つまり「記事の課題がよくわからない」となりがちです。 そうならないために、1つ改善したいことを課題設定して、開発すると良いと思います。テストケースを足す、ドキュメントを丁寧に書く、不要なコードを消すなど些細なことでも良いと思います。 「本当に改善になるのか?現状がどうなっているか?どうなったら嬉しいか?解くべき問題は?」と少し立ち止まって、普段の開発の抽象度を上げて考えてみます。 例えば「MyApp::Utilsが開発しづらいから改善しよう」より「巨大になったユーティリティクラスを、役割ごとに分割し、ユーティリティと名乗るのをやめたい」の方が、MyApp::Utilsを知らない人にも通じると思います。また、単一責務の原則など一般的な知識を利用できます。 開発業務の抽象度を上げることで、コンテキストを共有していない相手に伝わりやすくなり、一般的な改善方法を適用しやすくなります。 簡単に言えば、改善の取り組みは、そのまま技術ブログに書きやすいです。 もし技術ブログに書きにくい改善の取り組みがあれば、真に改善とは言えない内容かもしれないです。 改善したいことを1つに限定するのは、シンプルさのためです。一度に複数のことをすると効果がわかりにくくなり、リードタイムも長くなり、結果、変化に追いつきにくくなります。 ふりかえりをする 技術ブログの「ネタがない」と言う人は、大抵ネタを持っています。「ネタを思い出せない」「ネタになると認識していない」ため、そんなことを言うんだと思います。 開発業務で様々な経験をしても記憶が薄れれば、大したことがない、当たり前と感じ、わざわざ他人に共有する動機がなくなります。 これは正直、もったいないと感じます。当たり前に感じてることが、他人に取って当たり前とは限らないと思うからです。また、似た失敗をしやすくなると思うからです。 ふりかえりは、すごく普通の対応です。がちゃんとやると良いと思います。ふりかえりで、経験、行動の意味、価値を言葉にし、嬉しかったこと、ハッとしたこと、しくじったときの苦渋など、感情も味わいます。 言葉にできていないことは、思い出しづらく、認識がしづらいです。 まとめ 開発に関する技術ブログを書くには、開発の課題解決を抽象化する必要がある 改善やふりかえりは、課題解決の抽象化、言語化を助ける もし技術ブログを書けないとしたら、真に課題を解決していないかもしれない 以上です。それでは良いお年を!
アバター
皆さん Jetpack Compose は触っていますか? Jetpack Compose といえば Modifier ですが、Modifier の関数は場所によって使えたり使えなかったりする場合があると思います。 どうなっているのでしょうか? 例えばこの画像のようなものを実装したいとします。 実装したいコンポーザブルの画像 方法はいくつかあると思いますが、今回は Modifier.align(Alignment.Center) を使いたいと思います。 次のコードでは、Box コンポーザブルの中にある Text コンポーザブル内では Modifier.align(Alignment.Center) は期待通り動作します。 @Composable fun HogeComposable() { Surface( modifier = Modifier .fillMaxWidth() .height( 70 .dp) .background(Color.White) ) { // Box コンポーザブルの中では Modifier.align(alignment: Alignment) を解決できる Box { Text( text = "真ん中に表示したいテキスト!" , fontSize = 20 .sp, modifier = Modifier.align(Alignment.Center) ) } } } Box コンポーザブルを利用せず、Text コンポーザブルをそのまま書いた場合は、Text 内の Modifier.align(Alignment.Center) は解決できず、Unresolved reference: align エラーとなります。 @Composable fun HogeComposable() { Surface( modifier = Modifier .fillMaxWidth() .height( 70 .dp) .background(Color.White) ) { // Text コンポーザブルをそのまま書いた場合は Modifier.align(alignment: Alignment) を解決できない Text( text = "真ん中に表示したいテキスト!" , fontSize = 20 .sp, modifier = Modifier.align(Alignment.Center) ) } } なんで Box コンポーザブルの中でないと Modifier.align(alignment: Alignment) が解決できないんだろう? Compose では、カスタム スコープによってこの型の安全性が適用されます。たとえば、matchParentSize は BoxScope でのみ使用できます。 https://developer.android.com/jetpack/compose/layouts/basics?hl=ja#type-safety なるほど、どうやら BoxScope というカスタムスコープがあるらしい? 実際に Box コンポーザブルの実装を見てみます。 @Composable inline fun Box( modifier: Modifier = Modifier, contentAlignment: Alignment = Alignment.TopStart, propagateMinConstraints: Boolean = false , content: @Composable BoxScope.() -> Unit ) { val measurePolicy = rememberBoxMeasurePolicy(contentAlignment, propagateMinConstraints) Layout( content = { BoxScopeInstance.content() }, measurePolicy = measurePolicy, modifier = modifier ) } https://cs.android.com/androidx/platform/frameworks/support/+/androidx-main:compose/foundation/foundation-layout/src/commonMain/kotlin/androidx/compose/foundation/layout/Box.kt;l=64-77?q=BoxScope&ss=androidx%2Fplatform%2Fframeworks%2Fsupport:compose%2F 何やら content: @Composable BoxScope.() -> Unit が怪しそうですね。 BoxScope を追ってみます。 @LayoutScopeMarker @Immutable interface BoxScope { /** * Pull the content element to a specific [Alignment] within the [Box]. This alignment will * have priority over the [Box]'s `alignment` parameter. */ @Stable fun Modifier.align(alignment: Alignment): Modifier /** * Size the element to match the size of the [Box] after all other content elements have * been measured. * * The element using this modifier does not take part in defining the size of the [Box]. * Instead, it matches the size of the [Box] after all other children (not using * matchParentSize() modifier) have been measured to obtain the [Box]'s size. * In contrast, a general-purpose [Modifier.fillMaxSize] modifier, which makes an element * occupy all available space, will take part in defining the size of the [Box]. Consequently, * using it for an element inside a [Box] will make the [Box] itself always fill the * available space. */ @Stable fun Modifier.matchParentSize(): Modifier } https://cs.android.com/androidx/platform/frameworks/support/+/androidx-main:compose/foundation/foundation-layout/src/commonMain/kotlin/androidx/compose/foundation/layout/Box.kt;l=208-235?q=BoxScope&ss=androidx%2Fplatform%2Fframeworks%2Fsupport:compose%2F Modifier.align(alignment: Alignment): Modifier が Kotlin の Extension functions で宣言されていることがわかりました。 つまり、BoxScope の中であれば Modifier.align() を解決することができます。 では、 content: @Composable BoxScope.() -> Unit の BoxScope.() とは? これは Function literals with receiver といい(詳しくはリンク先を見てください)、呼び出しに渡されるレシーバーオブジェクトが暗黙の this になるため、今回では BoxScope が暗黙の this になります。 なのでここで渡されている lambda の中では Modifer.align(alignment: Alignment) を特に気にすることなく呼ぶことができます。 そして最後、BoxScope は interface なので、その実装はどうなっているのかをみます。 Box コンポーザブルの実装で BoxScopeInstance.content() とありますね。content は content: @Composable BoxScope.() -> Unit なので、この場合 this は BoxScopeInstance になります。 つまり、Box コンポーザブル内の Modifier.align(alignment: Alignment) は実際には BoxScopeInstance の実装が使われることになります。 BoxScopeInstance をみます。 https://cs.android.com/androidx/platform/frameworks/support/+/androidx-main:compose/foundation/foundation-layout/src/commonMain/kotlin/androidx/compose/foundation/layout/Box.kt;l=237-258?q=BoxScope&ss=androidx%2Fplatform%2Fframeworks%2Fsupport:compose%2F BoxScope interface に沿って Modifier.matchParentSize() と Modifier.align(alignment: Alignment) の実装が書いてありました。 このようにして特定のスコープのみで使える仕組みを作り上げていたということになります。 この仕組みは Box コンポーザブルに限らず、Column コンポーザブルや Row コンポーザブル等様々あります。 もし「あれ?いつも使ってる Modifer の関数がない!」となったら、スコープが正しいのかを見てみるといいかもしれません。 いや〜面白いですね。
アバター
こんにちは、20卒エンジニアのthe96です。 弊社では、リモート下においても勉強会が日頃から開催されています。 以前、 勉強会で同期のワンライナーのプロからawkを授けられ て以来、ワンライナーで業務を少し改善することの楽しさに目覚めました。 この記事では、そうして生まれた、 gitを使った開発が少し快適になるかもしれないワンライナー を3つ紹介します。 注意 なお、筆者は ripgrep にどっぷりなので、たびたび ripgrep を使うワンライナーが登場します。 ripgrep をインストール するか、適宜 grep に置き換えてください(動作するかは知りません)。 リポジトリ 検証などしたい場合はこちらのリポジトリをご利用ください。 https://github.com/the96/advent-calendar-2021 カレントブランチで変更したファイルのみを対象に検索する カレントブランチ内で触ったファイルのみを対象にgrepしたいときってありませんか? そういうときにはこのワンライナーをお試しあれ。 git diff --relative --name-status --diff-filter=AM main...HEAD | awk '{print $2}' | xargs -r rg --with-filename test このワンライナーは、カレントブランチでdiffのあったファイルのみを対象にgrepします。 コマンド中の main の部分をお使いの環境の親ブランチに変更してご利用ください。 仕組みは結構シンプルで、前半の git diff と awk で差分のあったファイルの名前を抽出して、 ripgrep で検索しています。 差分のあったファイルがないブランチで実行したときのために xargs -r を噛ませているのがミソですね。 他にも、未コミットのファイルを対象にしたい場合は main...HEAD を --cached に変えてあげると良いでしょう。 git checkoutで切り替えたブランチの履歴を表示する みなさんはいろんなタスクを並行してこなしているとき、自分がどのブランチで作業していたのか思い出せなくなることはありませんか? そんな時は、このワンライナーをご活用ください。 git branch -a | sed -e 's/..//' | grep -x -f- --color=never < (git reflog | awk '$3~/checkout/{print $8}' | awk '!colname[$1]++{print $1}' | head -n 10 ) このワンライナーを使うと、このようにリポジトリ内でチェックアウトしたブランチの履歴を表示することができます。 ▼下のスクリーンショットでは使いやすいようにエイリアスを張っています 削除されたブランチは表示されないようになっています。 仕組み 簡単に仕組みを解説します。 git reflog は、gitによるローカルリポジトリの操作履歴などが入っているものです。 チェックアウトの履歴はこのコマンドをもとに取り出しています。 awk でcheckoutのログを探して、headで件数を指定しています。 git reflog そうして取り出した履歴に対して、 git branch -a | sed -e 's/..//' で現存するbranch名のみを抽出して、grepを使ってチェックアウト履歴から削除されたブランチを削除しています。 エイリアスの張り方 gitのエイリアスは ~/.gitconfig に記述することで利用できます。 ここで設定したエイリアスは、 git *** の *** 部分を直接置換するだけなので、外部コマンドを使う場合は一工夫必要です。 まずは適当な場所にこのワンライナーをシェルスクリプトとして置いておきます。 (ここでは ~/git-checkout-log.sh としておきます) #!/usr/bin/env zsh git branch -a | sed -e 's/..//' | grep -x -f- --color=never < (git reflog | awk '$3~/checkout/{print $8}' | awk '!colname[$1]++{print $1}' | head -n 10 ) 次に、 ~/.gitconfig 内で下記のように ! を頭につけてエイリアスを貼ることで利用できます。 [alias] checkout-log = !~/git-checkout-log.sh 参考 2.7 Git の基本 - Git エイリアス 一部のブランチにコミットしないようにする ブランチ保護ができないprivateリポジトリや、親ブランチなど、直接コミットすることを基本的に避けたいブランチってありますよね。 そういったブランチを保護する方法の一つとして、git hookがあります。 #!/usr/bin/env zsh readonly PROTECT_BRANCHES=( 'main' 'release' 'develop' ) current_branch= `git rev-parse --abbrev-ref HEAD` for protect_branch in " ${PROTECT_BRANCHES[@]} " do if [ $current_branch = $protect_branch ]; then echo "WARNING: You tried commiting to protected branch( $protect_branch )!" ; echo " If you really wanna commit to protected branch, you can use '--no-verify' option." exit 1 ; fi done このスクリプトを、対象リポジトリ内に .git/hooks/pre-commit として配置して実行権限を付与すると、対象のブランチにコミットしようとしたときにコミットを止めてくれます。 3行目の readonly PROTECT_BRANCHES=('main' 'release' 'develop') に任意のブランチ名をセットすれば、お好きなブランチを保護することができます。 (もはやワンライナーではないんですが、便利なので紹介したかったんです…) 時には、hotfixなどでどうしても直接コミットしたい時があると思います。 そんなときは no-verify とつけてコミットすると git hook を無視してコミットすることができます。 8.3 Git のカスタマイズ - Git フック 終わりに どれもちょっとしたスクリプトでしたが、みなさんの git 操作が少しでも快適になれば幸いです。
アバター
皆さんのシェルの起動速度はどうですか?シェル起動時に eval "$(hoge init)" を実行するようなツールをたくさん入れていると徐々に遅くなってきてつらいですよね そこで以下のように hoge init の出力をファイルに書き出しておいて、起動時にはそれを source する戦略をとると少しだけシェルの起動を高速化できて少しだけ嬉しいです。 # zshでの例 HOGE_RC_FILE=/path/to/hoge-rc.zsh [[ ! -e " $HOGE_RC_FILE " ]] && hoge init > " $HOGE_RC_FILE " source " $HOGE_RC_FILE " plenv , goenv , nodenv , pyenv を管理している anyenv でのベンチマークを以下に貼っておきます。 # source/zshrc ANYENV_RC_FILE=./anyenv-rc.zsh [[ ! -e " $ANYENV_RC_FILE " ]] && anyenv init - > " $ANYENV_RC_FILE " source " $ANYENV_RC_FILE " # eval/zshrc eval " $( anyenv init - ) " $ hyperfine --shell=zsh --warmup=3 ' source $PWD/eval/zshrc ' ' source $PWD/source/zshrc ' Benchmark 1: source $PWD / eval /zshrc Time ( mean ± σ ) : 1 . 288 s ± 0 . 010 s [ User: 0 . 487 s, System: 0 . 727 s ] Range ( min … max ) : 1 . 278 s … 1 . 314 s 10 runs Benchmark 2: source $PWD / source /zshrc Time ( mean ± σ ) : 452 . 8 ms ± 5 . 7 ms [ User: 189 . 2 ms, System: 240 . 1 ms ] Range ( min … max ) : 446 . 9 ms … 467 . 1 ms 10 runs Summary ' source $PWD/source/zshrc ' ran 2 . 85 ± 0 . 04 times faster than ' source $PWD/eval/zshrc ' この戦略で少し困ることとしては、ツールの更新があるたびに hoge init > $HOGE_RC_FILE 相当のコマンドを良きタイミングで実行する必要があることですね。
アバター
はじめに 今年の夏に在宅環境(筋肉)の整備に成功した うっひょい です. tech.mobilefactory.jp 今年,ワインエキスパートの資格を取得しました. 勉強のために様々なツールを使って進捗管理したことについて書きます. なぜ受験したのか いろいろ理由はあります. お酒が好きでその中でワインが一番好きだから 美味しいワインを選べるようになりたいから 勉強というのを久しくやっていなくて挑戦したかったから 合コンで行った先のチャラい男がワイン飲まないのにモテるためか知らないが必死にワイン好きをアピールしたせいで,自分もワイン好きって言ったときもそういう目で見られて嫌だったから ワインの資格について 皆さんが思い浮かべるワインの資格と言えば「ソムリエ」だと思います.自分も最初はソムリエの資格を取ろうと思いました. 日本でソムリエの資格を取るにはJSA(日本ソムリエ協会)が開催している試験を受ける必要があります. しかし,ソムリエは受験するためには条件があります. sommelier.jp 【一般】 以下の職務を通算3 年以上経験し、第一次試験基準日においても従事している方 【会員】 会員歴が2年以上あり、以下の職務を通算2年以上経験し、第一次試験基準日においても従事しているJ.S.A.正会員および賛助会員所属者 ◆酒類・飲料を提供する飲食サービス ◆酒類・飲料の仕入れ、管理、輸出入、流通、販売、製造、教育機関講師 ◆酒類・飲料を取り扱うコンサルタント業務 飲食業に2年以上従事している必要があります.調べたところシステムエンジニアは飲食業ではありませんでした. しかし,JSAはワインエキスパートという別のワインの資格も出しています. ◆酒類、飲料、食全般の専門的知識、テイスティング能力を有する方 ◆職種、経験は不問 ◆ソムリエ職種に就かれていて、受験に必要な経験年数に満たない方 ワインエキスパートは業種による条件はなく,趣味などでワインを飲む人向けの資格です. 3次試験はありませんが,難易度はソムリエとほとんど同じなので自分が趣味として挑戦するには申し分のない資格だと思いました. ワインエキスパート受験を決め,合格に向けての勉強が始まりました. 1次試験 1次試験は4択の選択問題を解く,知識を問われる問題です. 膨大なページ数の教本 試験に申し込みしばらくすると,JSAから鈍器のような教本が届きます. おそらく自分のMacよりも攻撃力は高いと思われる 教本はA4サイズで700ページ以上あり,1次試験はこの本の全範囲から出題されます. 1ページから全部覚えていたらおそらく1年前から始めても間に合わなかったと思います. 教本で1から勉強するのは非効率的だったので重要なポイントなどをまとめた鈍器のような参考書 *1 を買ってそれで勉強することにしました. 単語帳作りとポモドーロ・テクニック 重要なポイントに絞られたとは言え,それでもたくさん覚えることがあるので効率的に勉強する必要がありました.覚えることをノートにまとめたりするとそれだけで腱鞘炎になりそうだったので,単語帳アプリを使うことにしました. 暗記すべき事柄を自分で入力してクイズ形式で見れる 電車内や寝る前に見られるようにスマホ対応 忘却曲線に基づいて出題する問題を日に日に変えてくれる 作問時,フリック入力は辛いのでPCで作問できてそれをスマホで見れる ↑をすべて満たすのが remindo.co というアプリでした.これで問題を作って繰り返し解くことで覚えることにしました. まずは,本の重要ポイントを自分なりに作問していくところからですが,なにせ膨大にあって,割と単純作業なので,集中力が切れてしまいがちでした. そこで,自分が業務時にやる気でないときに使うポモドーロ・テクニックをやってみました. asana.com 単純作業でかつ1問1問が区切りなのでポモドーロテクニックとの相性はよかったです. 日々のスキマ時間でできたので負担なく1日あたりの作問数を増やせました. 最終的に2719問作りました. 自動作問ツール キーワードを覚えるときは前述の単語帳が便利ですが,表を覚えるときは単語帳で作問することが難しいです. 例えば,各国のスパークリングワインの残糖度による名称一覧などは このように対応表のようになっていて,国によっては別称があったりと複雑です. なので,初めはGoogle Apps Scriptを使って表の一部を穴あきにすることで自動的に問題を生成するスクリプトを作ることにしました. しかし,結合されたセルを含む表をうまく取得する方法がわからなかったり,穴あきの作問自体がGoogle Apps Scriptで実現することがいろいろ難しくてずっとこれ作っていたら試験に間に合わないと思って断念しました. 結局,スプレッドシートで問題作ることをあきらめて表をJSON形式にしてランダムで取り出してGoogle Formで自動生成して解くという実装にしました. *2 Google Formになっているので誰でも見れると思います.暇な人は挑戦してみてください. 問題は毎日変わります. スパークリングワインの残糖量名称 後回しにする暗記項目はツールでタスク管理 地図を見て地方名や地区名を答える問題もあります.remindoは画像も貼れるので以下のような問題を作ることも可能です. ただ,地図の画像を用意したり問題用に答えが出ている部分を加工する手間があるので,単語帳を作ってから作問することにしました. 後からどの問題を作るかを忘れないためにTrelloというタスク管理ツールを使って,タスクとして残しました. trello.com 単語問題の作問を終えた後に↑の「やること」のレーンにあるタスクに従って地図問題の作問に着手しました. 終わったものは隣のレーンに移して...でスムーズに作問できました. 作問後はひたすらに解く ある程度問題を作ったら解いていきます. 記憶が定着する前に完全ランダムで問題を解こうとしてもちんぷんかんぷんなので,初期は同じカテゴリーの問題だけ解くようにしていました. PCで作問した問題をスマホから見られるのでスキマ時間でちょいちょい解いていました. 寝る前 風呂入っている間 移動中 筋トレのインターバル 本番 1次試験はCBT方式の試験です. cbt-s.com 全国にあるテストセンターで好きなタイミングで受験できます. テストセンターにあるパソコンを使ってポチポチ問題を解き,帰る前に結果がわかります. 自分は無事受かることができました. 2次試験 2次試験はテイスティングです.ワインを飲んで選択式でコメントを書く試験です. 2次試験合格までのおおまかな方針 2次試験対策として自分は徹底的に飲み比べました. 以下の基本品種同士で徹底的な飲み比べをして見た目,香り,味わいを覚えました. ただし,同じブドウ品種でも生産国が異なるとテイスティングコメントが全然違うため同じブドウ品種で異なる国の飲み比べもやりました. カベルネ・ソーヴィニヨン フランス アメリカなど ピノ・ノワール フランス アメリカなど シラー フランス オーストラリアなど シャルドネ フランス アメリカなど ソーヴィニヨン・ブラン フランス ニュージーランドなど リースリング フランス ドイツなど ブラインドテイスティングで上記の品種をある程度当てられるようになったら,それ以外の品種を練習しました. それ以外の品種はどの基本品種に近いか比べることで覚えていきました. 飲み比べの方法 自分は「小瓶詰替法」という方法で飲み比べしました. www.wine-jyuken.com tomiwine.com 1つのワインを長期間酸化させず,何日にも分けて練習できる方法です. 購入したワインを開栓後小瓶に詰め替えて付箋を貼って冷蔵庫に貯めていきました. ただ,冷蔵庫が小さくて試験終わるまでは食材を入れられなくなりました... *3 練習用のワイン 最初は模範解答付きの試験対策セットを購入しました.これでテイスティングコメントを勉強しました. 基本品種は何度も飲むので,2回目以降は近所のワインショップで1000〜2000円の同品種同生産国のワインを購入していました. *4 試験に出る可能性がある品種はかなり多いので基本品種以外は結局は1品種ハーフボトル1本〜2本(小瓶に詰め替えて3〜6本)程度の飲み比べで試験を迎えることになりました. 本番 本番は都内のホテルの宴会場で他の受験者と一斉に行いました. 試験のワインは口に含んで吐き出してもいいし,そのまま飲み込んでもいいです. 自分は緊張して香りも味もわからなくなっていたので自分を落ち着かせようとそのまま飲み込んでほろ酔い状態で受験しました. 結果 受験から数日後,JSAのサイトに結果発表が公開されました. 確認してみると,自分の受験番号があって合格してました. ワインエキスパートは2次試験で終わりなのでこれで正式にワインエキスパートとして認定されました. 認定料を払ってしばらくすると認定証とバッジが届きました. まとめ 暗記系の受験にはremindoを使うと,単語帳作りにPC,暗記にはスマホが使える 単語帳作りの単調作業にはポモドーロ・テクニックが効率的 受験申し込みから受験までを一つのプロジェクトとして捉えてタスク管理するのは有効だった 自分の場合はTrello使った 趣味の資格はいいぞ 体系的に学べるので趣味でやってたとは言え基礎中の基礎でも知らないことがいっぱいあった *1 : 参考書でもA4で400ページ以上 *2 : データをJSONにしてそれに合わせてコード書くのも結構工数かかりましたが... *3 : 試験が終わった今はその日食べるものに合わせて小瓶からワインを消費しています. *4 : 試験対策セットを何回も買うお金がない
アバター
ポモドーロテクニックを改めて実践したら結構良い感じだったので知見を共有します。 作業に集中できない、効率よく作業したい、そんな人におすすめです。 そもそもポモドーロテクニックとは 時間管理術の1つ。 集中の時間と短い休憩を繰り返して作業を効率よく進める目的で使われます。 達成しようとするタスクを選ぶ キッチンタイマーで25分を設定する タイマーが鳴るまでタスクに集中する 少し休憩する(5分程度) ステップ2 - 4を4回繰り返したら、少し長めに休憩する(15分 - 30分) ポモドーロ・テクニック - wikipedia 以前やってみたけど 25分時間を計ってもその間にやることは色々増えるし、差し込みはあるし、あまり効果無いな〜と思っていました。 そんなある時 最近めちゃくちゃサウナにハマっていて、よく聞いているサウナのラジオがあるんですがそこでポモドーロテクニックについて話している回がありました。 そもそもなんでサウナのラジオでポモドーロテクニックやねんと思うんですが、この回の ゲスト がサウナミュージシャン(?)の人で、新作でポモドーロタイマーに使えるサウナミュージック(??)をリリースした事での宣伝でした。 この回でポモドーロテクニックの方法について話していたのですが、要約すると 25分集中、5分休憩を繰り返す。途中で大休憩を挟む。 25分でやることは1つに絞る 休憩は自分が休憩できると思う事をする 例えばメール返信で休憩できるならそれで良い 「25分でやることは1つに絞る」が大きな気付きでした。時間計るだけでは効果は薄くて、やることを1つ決めてそれだけを全力でやる、休憩は全力で休む、のがコツかなと思いました。 改めてポモドーロテクニックと自分の場合 ということで最近それを意識すると以前よりうまく出来るようになりました。なので最近やっている方法とコツも合わせて紹介します。 時間 1セット 作業: 25分 休憩: 5分 3セットぐらいしたら大休憩15分 ツール1 macアプリの Just Focus 集中の間はひっそり動いていて、時間になったら全画面で強制的に教えてくれるのが気に入ってます。 ツール2 AppleMusic よく曲を聞きながら作業しているんですが、試しに合計25分の長さになるように曲のプレイリストをいくつか作ってそれをタイマー代わりにしてます。これもプレイリスト流すだけで時間が計れるので便利。 上で紹介したサウナミュージックも使ってます✌ コツ 25分でやることは1つに絞る 例えば「hogeを実装する」「piyoのドキュメントを書く」「fugaを調べる」など 25分やっている間は、他のことをやらない slackの返信とか、Twitterみるとか、 もちろん緊急なことがあれば対応が必要ですが、25分後でも大丈夫そうなことなら後回しにする 自分の場合、slackで通知来たら一瞬チラ見して後回しで大丈夫そうなら深く考えないようにしてます 25分の集中が途切れないようにするのが大事 休憩はちゃんと休憩する 25分経ったけどあとちょっとで終わるから延長…はなるべくやらない 30秒とか1分とか、本当にちょっとで終わるならいいけど、延長がズルズル長引かないようにする 次また25分ちゃんと集中しないといけないので、休憩もちゃんとする メリハリ大事 まとまった時間を確保する これはポモドーロテクニック自体のコツではないですが、ポモドーロテクニックは集中-休憩のセットを何回か繰り返すことが効果を発揮すると思うので、まとまった時間を確保しておくのも大事 mtgとmtgの間30分だけ1セットやる、のはあんまり意味がない 2時間とか3時間とか、mtgが無い時間を用意して、数セット繰り返すとコンスタントに集中の時間を出せるので効果が大きいと思われます
アバター
MySQL 5.7.6 以降では Generated Column が使えます。 テーブル定義に計算式を記述すると計算結果をカラムとして扱えるようになる機能です。 駅メモ!でも最近利用しているGenerated Columnですが、データベース内で増えたGenerated Columnをリストアップしたくなったので方法を調べました。 GENERATION_EXPRESSION に値があるカラムリストを得る MySQL 5.7のリファレンス から例を流用します CREATE TABLE triangle ( sidea DOUBLE, sideb DOUBLE, sidec DOUBLE AS (SQRT(sidea * sidea + sideb * sideb)) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; information_schema.columnsの GENERATION_EXPRESSION にGenerated Columnかどうかの情報が格納されています MySQL :: MySQL 5.7 Reference Manual :: 24.3.5 The INFORMATION_SCHEMA COLUMNS Table For generated columns, displays the expression used to compute column values. Empty for nongenerated columns. Generated Columnなら値があります SELECT table_name, column_name, generation_expression, extra FROM information_schema.columns WHERE table_schema = ' test ' -- データベース名 AND generation_expression != '' mysql> SELECT table_name, column_name, generation_expression, extra FROM information_schema.columns WHERE table_schema = 'test' AND generation_expression != ''; +------------+-------------+---------------------------------------------------+-------------------+ | table_name | column_name | generation_expression | extra | +------------+-------------+---------------------------------------------------+-------------------+ | triangle | sidec | sqrt(((`sidea` * `sidea`) + (`sideb` * `sideb`))) | VIRTUAL GENERATED | +------------+-------------+---------------------------------------------------+-------------------+ 1 row in set (0.00 sec) Generated Columnであるtriangle.sidecのみの結果が得られました! extraにもGenerated Columnの場合は STORED GENERATED または VIRTUAL GENERATED が入りますが、他の値も入りうるのでやはり GENERATION_EXPRESSION を見るのが簡単でしょう
アバター
駅奪取エンジニアの id:dorapon2000 です。駅奪取では11月にゲーム内の地図のリプレースを行いました。地図そのもののスタイルも変わりましたが、地図の表示に使うライブラリも変更しています。今回は、アプリに地図を埋め込むだけであれば、ほんの少しのコードだけで実現できるということを紹介したいと思います。 記事の前半で地図表示の仕組みを簡単に説明して、後半で具体的なコードをお見せします。 地図表示の仕組み 地図を表示するためにはサーバ側とクライアント側の2つの仕組みが必要です。また、サーバとクライアントは地図をタイルという形式で送受信します。 地図タイルサーバ 地図タイルを配信する 自前でホストすることもできるが、いずれかのサービスのタイルAPIを利用すると楽 地図クライアント 地図タイルの受信と表示をする 地図の上にピンや吹き出しを置くこともできる タイルにはベクトルタイルとラスタタイルの2種類があり、地図を座標情報として受け取るか、画像として受け取るかという違いがあります。ちょうど画像の.svgと.pngの関係に近いです。例えば、ラスタタイルで配信されている場合は、地図クライアントもラスタタイルに対応している必要があります。 本記事のサンプルコードでは、タイルAPIを利用し、ラスタタイルで地図を表示します。 地図タイルAPI 地図タイルAPIを提供しているサービスはいくつかあります。 OpenStreetMapのタイルAPI 無料で商用利用可能なものとそうでないものがある MapboxのタイルAPI 登録をすることで、一定まで無料で利用可能 1 地理院タイル Webサイトの一部として地図を表示する場合、利用申請不要 2 サンプルコードではクレジットを入れることで無償利用可能な「OpenStreetMap's Standard tile layer(リンク先テーブルの一番上)」を利用します。 地図クライアントライブラリ 地図クライアントライブラリもいくつかあります。 Leaflet ラスタタイル対応 無償利用可能 3 Mapbox GL JS ベクトルタイル対応 登録してアクセストークンを取得することで、一定まで無料で利用可能 4 MapLibre GL JS ベクトルタイル対応 Mapbox GL JS v1のフォークプロジェクトで、登録不要で無償利用可能 5 サンプルコードでは地図クライアントライブラリとして真っ先に選択肢になるであろうLeafletを利用します。昔から親しまれているライブラリで、個人的に初期学習コストも大きくないと思います。 OpenStreetMap + Leaflet 本題です。地図タイルサーバとして「OpenStreetMapのAPI」を、地図クライアントライブラリとして「Leaflet」を利用します。タイルの形式はラスタです。 https://codepen.io/dorapon2000/pen/xxXqzgM < head > < link rel = "stylesheet" href = "https://unpkg.com/leaflet@1.7.1/dist/leaflet.css" integrity= "sha512-xodZBNTC5n17Xt2atTPuE1HxjVMSvLVW9ocqUKLsCC5CXdbqCmblAshOMAS6/keqq/sMZMZ19scR4PsZChSR7A==" crossorigin = "" /> < script src = "https://unpkg.com/leaflet@1.7.1/dist/leaflet.js" integrity= "sha512-XQoYMqMTK8LvdxXYG3nZ448hOEQiglfqkJs1NOQV44cWnUrBc8PkAOcXy20w0vlaXaVUearIOBhiXZ5V3ynxwA==" crossorigin = "" ></ script > </ head > < body > < div id = "mapid" ></ div > </ body > const mymap = L.map( 'mapid' ).setView( [ 35.6810,139.7670 ] , 14); L.tileLayer( "https://tile.openstreetmap.org/{z}/{x}/{y}.png" , { attribution: "<a href='https://www.openstreetmap.org/copyright' target='_blank'>© OpenStreetMap contributors</a>" } ).addTo(mymap); たったこれだけで地図が表示できてしまいます! htmlでしていることは、LeafletのCSSとJSの読み込みと地図を表示するdivの定義だけです。LeafletのJSを読み込むと、Leafletモジュールが L というグローバル変数として登録されます。javascript側では、 div#mapid に地図を表示しています。 setView の引数の3つの数字は、前から緯度・経度・ズームレベルを表します。 tileLayer でOpenStreetMapのAPIを叩いて地図を取得します。 最後に、LeafletとOpenStreetMapの地図データを利用していることを忘れずクレジットします。 ここへ少しコードを付け加えることで、 ピンを立てたり 、 吹き出しをつけたり 、 ラインを引いたり することができるようになります。 Leaflet公式が提供するサンプルコード も充実しているため、まずは目的のコードに近いサンプルを探すのが良いと思います。 Mapbox pricing ↩ 国土地理院の測量成果の利用手続 | 国土地理院 ↩ BSD 2-Clause License ↩ Pricing | Mapbox GL JS | Mapbox ↩ BSD 3-Clause License ↩
アバター
NestJS は Node.js 向けのウェブフレームワークです。その特徴として Decorator を用いてクラスやメソッドにアノテーションをする仕組みを提供しています。 例えば API のエンドポイントを定義する場合は次のようなコードを実装します。 import { Controller , Get } from '@nestjs/common' ; @Controller ( 'cats' ) export class CatsController { @Get () findAll () : string { return 'This action returns all cats' ; } } 高速な TypeScript のトランスパイラの実装として esbuild や SWC が有名です。今回は SWC の jest binding である @swc/jest を ts-jest の代わりに使用して、NestJS 製のプロジェクトのテストを実行する方法を紹介します。 サンプルコードは以下のリポジトリにあります。 github.com SWC は .swcrc ファイルで設定を管理します。デコレータを有効にしたり、NestJS の DI のためにクラス名を維持する設定をします。 { " jsc ": { " parser ": { " syntax ": " typescript ", " tsx ": false , " decorators ": true } , " target ": " es2017 ", " keepClassNames ": true , " transform ": { " legacyDecorator ": true , " decoratorMetadata ": true } } , " module ": { " type ": " commonjs ", " noInterop ": true } } jest.config.js は @swc/jest の README にある通りに設定します。 const fs = require( 'fs' ) const config = JSON.parse(fs.readFileSync( ` ${__dirname} /.swcrc` , 'utf-8' )) module.exports = { transform: { '^.+ \\ .(t|j)sx?$' : [ '@swc/jest' , { ...config, /* custom configuration in jest */ }] , } , } これで NestJS 製のプロジェクトのテストのトランスパイラに @swc/jest を使用することができるようになりました。 上記のサンプルリポジトリのテストを GitHub Actions 上で実行したときに、 ts-jest だと3.42秒かかっていたのが、 @swc/jest だと0.79秒に短縮されました。
アバター
はじめに CloudFrontからオリジンへのリクエスト時に特定のHTTPヘッダーを含めるには、オリジンリクエストポリシーの設定が必要です。 docs.aws.amazon.com CloudFrontを使用すると一部HTTPヘッダーが書き換えられ、特に User-Agentヘッダー は Amazon CloudFront になってしまい、オリジンサーバーでUser-Agentを利用してデバイスの判定ができなくなります。 ただし、オリジンリクエストポリシーで CloudFront-Is-Mobile-Viewer といったヘッダーをオリジンリクエストに追加することで、オリジンサーバーで CloudFront-Is-Mobile-Viewer ヘッダーの値を使ってデバイスの判定ができるようになります。 @nuxtjs/device といったライブラリでは、このヘッダーに応じてデバイスの判定をすることができ、CloudFrontを導入してUser-Agentが Amazon CloudFront になってしまっても、そのままレスポンシブ対応をすることができます。 github.com そんな便利なオリジンリクエストポリシーのヘッダー追加機能ですが、CloudFront + API Gatewayの構成の構築をしていて、 CloudFront-Is-Mobile-Viewer といったヘッダーの値が意図したものにならないことがありました。 ヘッダーの値が意図したものにならなかった時の状況 ビヘイビアでオリジンをLambda関数にルーティングするAPI Gatewayに設定する。 上記ビヘイビアのオリジンリクエストポリシーで CloudFront-Is-Mobile-Viewer を追加する。 API GatewayからルーティングされるLambda関数はSSRでHTMLを配信している。(モバイルかデスクトップかで表示を分けたい) モバイルでアクセスしても、Lambda関数でAPI Gatewayからのリクエストヘッダーを見ると、 CloudFront-Is-Mobile-Viewer が false になっている。 また、オリジンリクエストポリシーで設定した覚えのないヘッダーがなぜか追加されている。 'cloudfront-forwarded-proto': 'https', 'cloudfront-is-desktop-viewer': 'true', 'cloudfront-is-mobile-viewer': 'false', 'cloudfront-is-smarttv-viewer': 'false', 'cloudfront-is-tablet-viewer': 'false', 'cloudfront-viewer-country': 'JP', 構成図 問題の原因 API GatewayのEndpoint Typeをエッジ最適化(Edge)にしていたため。 Endpoint Typeをリージョン(Regional)にすることで、モバイルからアクセスしたときに CloudFront-Is-Mobile-Viewer の値は true になり、設定した覚えのないヘッダーが送信されることもなくなりました。 API Gatewayのドキュメントによると、 エッジ最適化 API エンドポイント の場合、こちらが設定したCloudFront ディストリビューションとは別にCloudFront ディストリビューションを経由することになるため、そちらのヘッダーが適用されることになってしまったのではないかと考えられます。 リージョン API エンドポイント ではCloudFront ディストリビューションを経由せず、リージョン固有の API Gateway API を直接ターゲットとするため、こちらが設定したCloudFront ディストリビューションのヘッダーが適用されて、意図したヘッダーをLambda関数で取得することができたと考えられます。 CloudFrontを導入する意図がクライアントへの接続時間の改善であれば、CloudFront + API Gateway(リージョンAPIエンドポイント)の構成ではなく、API Gateway(エッジ最適化 API エンドポイント)で十分かもしれません。 CloudFrontからAPI Gatewayへのオリジンリクエストでヘッダーを追加する場合、 エッジ最適化 API エンドポイント で設定していると、ヘッダーの値が意図しない値になったり、意図しないヘッダーが追加されている可能性があるのでご注意ください。
アバター
はじめに 駅メモ!チームでエンジニアをしている id:wgg00sh です. 駅メモ!では2021年11月に「未取得の駅を地図で確認できる機能」をリリースしました. 今回はこの機能を実現するにあたって発生した問題の一例と,その問題をどのように解決したかについて,使用した地図ライブラリである Mapbox GL JS の使い方と合わせて紹介していきます 地図上に全ての駅を表示する 駅メモ!には,9300を超える駅が使用されているので,まずはその全てを表示してみます ↓のような駅情報を持っていて, export const stationList = [ [ 1, '函館' ,140.xxxxxx,41.yyyyyy ] , [ 2, '五稜郭' ,140.xxxxxx,41.yyyyyy ] , ... ] このデータを使って地図上に全ての駅を表示してみます const stationGeoJSON = convertGeoJSON (stationList) // 座標データを GeoJSON に変換する処理 map.addSource( 'station' , { type: 'geojson' , data: stationGeoJSON, } ) map.addLayer( { id: 'station' , type: 'circle' , source: 'station' , paint: { 'circle-color' : '#FF0000' , 'circle-radius' : 6, } } ) // クリックイベントの追加 map.on( 'click' , 'station' , (e) => { new mapboxgl.Popup( { anchor: 'bottom' , } ) .setLngLat(e.lngLat) .setHTML(e.features [ 0 ] .properties.name) .addTo(map); } ) 遠くから見た場合 近くで見た場合 クリック 問題点 クリックして駅の詳しい情報などを見ることを考えると,遠くから見た場合は駅数が多すぎてまともに操作できないです. また,これだけの量を描画するのは,パフォーマンスの観点から見てもよく無さそうです 解決法 そこで,複数の駅が密集している場合は,それらを纏めて別の表示にするクラスタ化を行います Mapbox GL JS では, addSource のオプションに cluster: true をつけることで,クラスタ化を行ってくれます クラスタ化に関するオプションとして, clusterRadius clusterMinPoints clusterMaxZoom がありますが,これらは実際に使用するデータによって良い感じに見えるパラメータを模索するのが良いと思います // 駅データの投入 map.addSource( 'station' , { type: 'geojson' , data: stationGeoJSON, cluster: true , // クラスタ化を行うオプション clusterRadius: 120, // クラスタ化を行う半径 clusterMinPoints: 30, // クラスタ化に必要な最小の要素数 } ) // クラスタ化されていない座標データの描画 map.addLayer( { id: 'station' , type: 'circle' , source: 'station' , filter: [ '!' , [ 'has' , 'point_count' ]] , paint: { 'circle-color' : '#FF0000' , 'circle-radius' : 6, } } ) // クラスタ化されたデータの描画 (円部分) map.addLayer( { id: 'station_cluster' , type: 'circle' , source: 'station' , filter: [ 'has' , 'point_count' ] , // クラスタ化されている要素かのチェック方法 paint: { 'circle-color' : [ 'step' , [ 'get' , 'point_count' ] , // クラスタに含まれる要素数に応じて色を変更 '#FF4000' , 30, '#FF8000' , 50, '#FFB000' , 100, '#FFFF00' , 300, '#FFFFFF' , ] , 'circle-radius' : [ 'step' , [ 'get' , 'point_count' ] , // クラスタに含まれる要素数に応じて円の大きさを変更 24, 30, 36, 50, 48, 100, 60, 300, 72, ] , } } ) // クラスタ化されたデータの描画 (テキスト部分) map.addLayer( { id: 'station_cluster_label' , type: 'symbol' , source: 'station' , filter: [ 'has' , 'point_count' ] , layout: { 'text-field' : '{point_count}' , 'text-size' : [ 'step' , [ 'get' , 'point_count' ] , 14, 30, 18, 50, 24, 100, 30, 300, 36, ] } } ) このようにして,地図上に描画した多数のデータから,目的の場所を探しやすくする機能を実現することができました 参考 Sources | Style Specification | Mapbox GL JS | Mapbox Create and style clusters | Mapbox GL JS | Mapbox
アバター
普段Perlのテストはターミナル上で prove t/hoge.t で実行しています。 これを楽するために、VS Codeの拡張機能 Code Runner - Visual Studio Marketplace を使い、編集中のファイルを開いたまま、ボタン一つで実行できます。 拡張機能 Code Runner をインストールします settings.json を開いて以下の設定を追記します { " code-runner.executorMapByGlob ": { " *.t ": " prove " } , VS Codeウィンドウの右上に ▷ のボタンを押します 開いたテストファイルを実行する
アバター
miro アプリ 弊社は モバワーク を導入していて、チームメンバーも普段はフルリモートワークをしています。 チームでは オンラインホワイトボード として miro を導入していて、例えばこんな事に使ってます。 ボード上でタスクチケットの管理 残りタスクと進捗の可視化 タスクの見積もりと作業可能時間の集計と比較 特に最後はmiro SDKを使ったmiroアプリを開発して実現しています。 そんなmiroアプリを開発できるようにするまでの手順をご紹介します。 1 準備 下のリンクの手順に従って、dev teamとYour appsを作る https://developers.miro.com/docs/getting-started 2 app作成 パッケージがあるので使いましょう https://www.npmjs.com/package/create-miro-app $ yarn create miro-app my-miro-app $ cd my-miro-app $ yarn start 3 公開 ここではひとまずngrokで公開 $ brew install ngrok ngrokの アカウント登録 をして、 Your Authtoken からtokenをコピペ $ ngrok authtoken <取得したtoken> $ ngrok http https://localhost:3001 # yarn startしたときに動いてるポート Forwardingに出てるアドレスをコピペ(必ずhttpsの方) 4 app URL登録 1 準備 でmiroに登録したappに、 3 公開 でコピペしたアドレスをappに登録 dev teamのボードを開いて、tool barを見たときにappが追加されていて動けばok 自分たちのチームでは、タスクカードを選択するだけでそれらの見積もり時間を集計するアプリとして開発して使ってます。 その後 miro SDKはオブジェクトの読み取り、配置、変更、オブジェクトが更新されたらxxxをする、みたいなことが出来るのでかなり自由度が高いです。 また、miro appは単なる静的ページなので開発し終わったらgithub pagesなどで公開するのも可能です。
アバター
CLIツールのパフォーマンス気になりますね 皆さん普段様々なCLIツールをご利用かとは思いますが、そのCLIツールのパフォーマンスが気になったことはありませんか?私はまれによくあります。今回はそういうときに役に立つ hyperfine をご紹介です。 github.com 基本的な使い方は以下のとおりです。 $ hyperfine ' ここに測定したいコマンドを書く ' sleep 0.3 を測定すると以下のような出力が得られます。 $ hyperfine ' sleep 0.3 ' Benchmark 1: sleep 0 . 3 Time ( mean ± σ ) : 308 . 9 ms ± 8 . 2 ms [ User: 0 . 7 ms, System: 1 . 6 ms ] Range ( min … max ) : 302 . 9 ms … 322 . 7 ms 10 runs 他にもキャッシュを捨てるようなコマンドを事前に実行させたり、ウォーミングアップを指定回数行えたりもできます。詳しくはREADMEを読んでみてくださいね。
アバター
ここ半年、競プロをこつこつ頑張っているエンジニアの id:dorapon2000 です。好きなアルゴリズムは累積和です。 解決したい課題 「MySQL 5.7 内で完結できるように、クエリだけでランキングを取得したい」 データベースのデータを使って調査をする際、ランキングを出したいこと、あるいはランキングを基にした処理をしたいことがあります 1 。アプリケーション側で集計・ソートして値を取得することもできますが、1回限りの調査の場合、わざわざコードを書くことが億劫です。 MySQL 8.0ではRANK関数が導入されたため、そちらで簡単にランキングを取得できます(記事末尾に参考で記載)。今回は、RANK関数導入以前のMySQL 5.7 で、クエリから一発でランキングを取得することを目指します。 検証環境 MySQL 5.7.36 scoreというテーブルを作成し、1万レコードのscore(0〜9999のランダム)をインサートしました。このscoreの降順のランキングを取得することを目指します。 mysql> desc score; + ---------+---------+------+-----+---------+----------------+ | Field | Type | Null | Key | Default | Extra | + ---------+---------+------+-----+---------+----------------+ | user_id | int ( 11 ) | NO | PRI | NULL | auto_increment | | score | int ( 11 ) | NO | | NULL | | + ---------+---------+------+-----+---------+----------------+ 2 rows in set ( 0 . 00 sec) mysql> show create table score\G; *************************** 1 . row *************************** Table : score Create Table : CREATE TABLE `score` ( `user_id` int ( 11 ) NOT NULL AUTO_INCREMENT, `score` int ( 11 ) NOT NULL , PRIMARY KEY (`user_id`) ) ENGINE=InnoDB AUTO_INCREMENT= 16394 DEFAULT CHARSET=utf8 COMMENT = ' score ' 1 row in set ( 0 . 00 sec) mysql> select count (*) from score; + ----------+ | count (*) | + ----------+ | 10000 | + ----------+ 1 row in set ( 0 . 01 sec) 方法① SQL標準 MySQLに限らずどのDBでも実行できる、SQL標準でのランキング取得方法です。 SELECT s1.score, s1.user_id, ( SELECT COUNT (s2.score) FROM score AS s2 WHERE s1.score < s2.score) + 1 AS r FROM score AS s1 ORDER BY r LIMIT 10 ; そこまで複雑なクエリではないですが、ポイントはサブクエリの外と内で別々に2つのscoreを呼び出していることです。外側の score AS s1 で選ばれたs1.scoreが score AS s2 の中でどの順位にいるのかをWHERE句で判定します。scoreを使った2重のfor文と考えるとイメージしやすいかもしれません。正確ではないかもしれませんが、時間計算量はO(N 2 )だと考えられます。 実行結果は以下のようになります。 mysql> select s1.score, s1.user_id, (select count (s2.score) from score as s2 where s1.score < s2.score) + 1 as r from score as s1 order by r limit 10 ; + -------+---------+------+ | score | user_id | r | + -------+---------+------+ | 9999 | 2080 | 1 | | 9999 | 9919 | 1 | | 9999 | 9507 | 1 | | 9997 | 6792 | 4 | | 9997 | 7412 | 4 | | 9995 | 532 | 6 | | 9993 | 5835 | 7 | | 9990 | 8757 | 8 | | 9990 | 8890 | 8 | | 9988 | 2596 | 10 | + -------+---------+------+ 10 rows in set ( 17 . 23 sec) mysql> explain select s1.score, s1.user_id, (select count (s2.score) from score as s2 where s1.score < s2.score) + 1 as r from score as s1 order by r limit 10 \G; *************************** 1 . row *************************** id: 1 select_type: PRIMARY table : s1 partitions: NULL type : ALL possible_keys: NULL key: NULL key_len: NULL ref: NULL rows : 10000 filtered: 100 . 00 Extra: Using temporary; Using filesort *************************** 2 . row *************************** id: 2 select_type: DEPENDENT SUBQUERY table : s2 partitions: NULL type : ALL possible_keys: NULL key: NULL key_len: NULL ref: NULL rows : 10000 filtered: 33 . 33 Extra: Using where 2 rows in set , 2 warnings ( 0 . 00 sec) 時間計算量が大きいため、たった1万レコードの集計でもかなり時間がかかってしまうことが難点です。 同スコアの場合は同順位になります。 方法② ユーザー定義変数 SET @r = 0 ; SELECT score, user_id, @r:=@r+ 1 FROM score ORDER BY score DESC LIMIT 10 ; MySQLでは、そのセッションでのみ有効な ユーザー定義変数 というものをクエリの中で使うことができます。ここに順位を一時格納することで、ランキングを実現します。時間計算量はO(N)です。 実行結果は以下のようになります。 mysql> set @r = 0 ; Query OK, 0 rows affected ( 0 . 00 sec) mysql> select score, user_id, @r:=@r+ 1 from score order by score desc limit 10 ; + -------+---------+----------+ | score | user_id | @r:=@r+ 1 | + -------+---------+----------+ | 9999 | 9507 | 1 | | 9999 | 2080 | 2 | | 9999 | 9919 | 3 | | 9997 | 7412 | 4 | | 9997 | 6792 | 5 | | 9995 | 532 | 6 | | 9993 | 5835 | 7 | | 9990 | 8890 | 8 | | 9990 | 8757 | 9 | | 9988 | 2596 | 10 | + -------+---------+----------+ 10 rows in set ( 0 . 01 sec) mysql> explain select score, user_id, @r:=@r+ 1 from score order by score desc limit 10 \G; *************************** 1 . row *************************** id: 1 select_type: SIMPLE table : score partitions: NULL type : ALL possible_keys: NULL key: NULL key_len: NULL ref: NULL rows : 10000 filtered: 100 . 00 Extra: Using filesort 1 row in set , 1 warning ( 0 . 00 sec) 同じscoreの場合、同順位にならない点が意図しないかもしれませんが、時間計算量が小さいため爆速で集計が終わります。 (参考)MySQL 8.0の場合 MySQL :: MySQL 8.0 リファレンスマニュアル :: 12.21.1 Window 関数の説明 mysql> select score, user_id, rank () over w as ' rank ' from score window w as ( order by score desc ) limit 10 ; + -------+---------+------+ | score | user_id | rank | + -------+---------+------+ | 9995 | 5081 | 1 | | 9994 | 3665 | 2 | | 9994 | 7494 | 2 | | 9993 | 558 | 4 | | 9993 | 6981 | 4 | | 9992 | 3584 | 6 | | 9992 | 8848 | 6 | | 9990 | 9185 | 8 | | 9989 | 525 | 9 | | 9989 | 4740 | 9 | + -------+---------+------+ 10 rows in set ( 0 . 00 sec) Redisのsorted setなどランキングに適したデータストアを使う方がより良いと思いますが ↩
アバター
こんにちは、新卒エンジニアの id:d-kimuson です 先日 type-predicates-generator という型定義からユーザー定義型ガード・アサーション関数を自動生成するツールをリリースして 紹介記事 を書いたのですが、感想とかを眺めていたら同じく外部から来た値に安全な型付けをするためのライブラリやツールの情報をいくつも観測しました この辺りのランタイムチェックライブラリの情報ってあまりまとまっていない印象で自分が知らないものもいくつかあったので、調べつつ簡単にまとめられたらなと思ってこのエントリを書きました 外部からやってきた値を型安全にするにはざっくりと 型生成によるアプローチ ランタイムチェック用の独自型を書かせるアプローチ 型情報からランタイムチェック関数を自動生成するアプローチ の 3 つのアプローチがあると思うので、それぞれのアプローチごとに紹介します ① 型定義の生成によるアプローチ 外部から値がやってくる主たるケースは API 通信で、GraphQL や OpenAPI のスキーマから型定義を自動生成することで型安全性を守るアプローチです スキーマと生成ツールの実装が正しいという前提の元ですが、外部からやってくる値に正しさを一定担保した上で型をつけることができます GraphQL Code Generator GitHub - dotansimha/graphql-code-generator: A tool for generating code based on a GraphQL schema and GraphQL operations (query/mutation/subscription), with flexible support for custom plugins. GraphQL のスキーマから型定義を自動生成するツールです 自分は Gatsby で個人の技術ブログを書いていて、そこで使用してます 公式サイト に例が載っているので、見てみるとイメージしやすいと思います openapi-generator GitHub - OpenAPITools/openapi-generator: OpenAPI Generator allows generation of API client libraries (SDK generation), server stubs, documentation and configuration automatically given an OpenAPI Spec (v2, v3) OpenAPI スキーマから型安全に API を呼べる API クライアントを自動生成してくれるツールです // openapi-generator + axios のサンプルコード import axios from "axios" import { PetApiFactory , Configuration , PetStatusEnum } from "./typescript-axios" // 自動生成されたコード const api = axios.create ( { /* config here */ } ) const config = new Configuration ( { /* config here */ } ) export const endpoints = PetApiFactory ( config , `baseURL` , api ) // APIコール endpoints .getPetById ( 0 /* 引数に型が付く */ ) .then (( response ) => { response.data /* Pet に型が付く */ } ) aspida + openapi2aspida GitHub - aspida/aspida: TypeScript friendly HTTP client wrapper for the browser and node.js. aspida という型安全に API コールを行うためのライブラリがあり、 openapi2aspida を使うことでスキーマから aspida のクライアントを自動生成してくれます // aspida のサンプルコード import axios from "axios" import aspida from "@aspida/axios" import api from "./api/$api" // 自動生成されたコード const client = api ( aspida ( axios )) // API コール client.pet ._petId ( 0 /* 引数に型が付く */ ) . get() .then (( response ) => { response.body /* Pet に型が付く */ } ) openapi-generator は TS に限らず様々な言語のクライアントを生成しますが、openapi2aspida は TypeScript 専用でより使いやすい印象です 自分は API を叩くときは aspida を使うことが多いです この辺りのツールは型安全性の担保ももちろんですが、すでに存在するスキーマと同じ型を手動で書かなくて良い点も嬉しいポイントですね ② TS の型ではなくランタイムチェックを行える独自の型宣言を行うアプローチ TypeScript では型定義からランタイムのコードを生成することはできません なので型定義とは別にランタイムチェック用の型を書いてそれでチェックしようというアプローチです ランタイムチェック用の型はライブラリが指定する独自の書き方で宣言する必要があるので、重複管理になりそうに思うかもしれませんが、値から TS の型を取り出すのは難しくないので、ランタイムチェック用の型から TypeScript の型を拾えるようになっています io-ts GitHub - gcanti/io-ts: Runtime type system for IO decoding/encoding 言わずとしれたランタイム型チェックのライブラリの王道です io-ts の指定する形で型定義を書いてランタイムチェックを行えます // io-ts のサンプルコード import * as t from 'io-ts' import { isRight } from 'fp-ts/lib/Either' const UserRuntimeType = t. type( { id: t. number , name: t. string , union: t.union ( [ t. string , t. number ] ), optional: t.union ( [ t. string , t. undefined ] ), nullable: t.union ( [ t. string , t. null ] ), } ) type User /* : { id: number; name: string; union: string | number; nullable: string | null; optional?: string | undefined; } */ = t.TypeOf <typeof UserRuntimeType > const maybeUser: unknown = 'invalid' const user = UserRuntimeType.decode ( maybeUser ) if ( isRight ( user )) { // ランタイムバリデーションが成功したときだけこのブロックを通る user.right /* : User に型が付く */ } isRight とかに関数型っぽさが見え隠れしますね runtypes GitHub - pelotom/runtypes: Runtime validation for static types io-ts と同様に独自の構文でランタイムチェックの型を宣言してチェックします io-ts は fp-ts に依存していて書き方も関数型チックになるので、関数型に寄るのを好まないケースで使われる印象です // runtypes のサンプルコード import { Number , String , Undefined , Null , Record , Union , Static , } from 'runtypes' const UserRuntimeType = Record ( { id: Number , name: String , union: Union ( String , Number ), optional: Union ( String , Undefined ), nullable: Union ( Null , Undefined ), } ) type User = Static <typeof UserRuntimeType > const maybeUser: unknown = 'invalid' const user = UserRuntimeType.check ( maybeUser ) // おかしかったら error を投げる user /* User に型が付く */ io-ts は decode 時に型ガードを行いますが、runtypes ではバリデーションして値がおかしかったら例外を投げるという形のようです superstruct GitHub - ianstormtaylor/superstruct: A simple and composable way to validate data in JavaScript (and TypeScript). これは自分が知らなかったライブラリなのですが、結構人気のあるライブラリらしくスターも 5600 ついていました Introduction - Superstruct ドキュメントもかなり充実していました // superstruct のサンプルコード import { object , number , string , Infer , assert , union , optional , nullable , is , } from 'superstruct' const UserRuntimeType = object ( { id: number (), name: string (), union: union ( [ string (), number () ] ), optional: optional ( string ()), nullable: nullable ( string ()), } ) type User = Infer <typeof UserType > const maybeUser: unknown = 'invalid' if ( is ( maybeUser , UserRuntimeType )) { maybeUser /* User に型が付く */ } assert ( maybeUser , UserRuntimeType ) // バリデーションに失敗したら例外が発生 maybeUser /* User に型が付く */ 型の絞り込みはアサーションと型ガード両方に対応しているようです Utilities - Superstruct Utility types に対応する omit, partial, pick も使えるらしく表現力がかなり豊かそうで好感触でした ランタイムチェックはしたいけど io-ts は合わないって方にはファーストチョイスになりそうです zod GitHub - colinhacks/zod: TypeScript-first schema validation with static type inference 上の2つと同様の Zod が提供するデータ型でスキーマを宣言して、ランタイムチェックを行います // zod のサンプルコード import { z } from "zod" const UserRuntimeType = z. object ( { id: z. number (), name: z. string (), union: z.union ( [ z. string (), z. number () ] ), optional: z.union ( [ z. string (), z. undefined () ] ), nullable: z.union ( [ z. null (), z. string () ] ), } ) type User = z.infer <typeof UserRuntimeType > const maybeUser: unknown = "invalid" const user = UserRuntimeType.parse ( maybeUser ) // 失敗したら例外を投げる user /* User に型が付く */ const result = UserRuntimeType.safeParse ( "invalid" ) if ( result.success ) { maybeUser as User // 型ガードは非対応 (unknown のまま) だがバリデーションはできるので、型キャストは安全 } 型ガードには対応してないようですがバリデーション自体はできるので型キャストは一応安全です また今回の値の型が TypeScript の型通りかをランタイムチェックするという趣旨とは若干ズレるので詳しくは触れませんが、 ajv , yup , joi 等のバリデーションライブラリを使う手段もあります ③ 型定義からランタイムチェックの関数を自動生成するアプローチ ② のアプローチでは TS の型を一時の型にしたい状況にはあまり適しません 使うことができないわけではありませんが、型情報が二重管理になってしまいます 具体的には ① のアプローチで TypeScript の型を自動生成しているケース 学習コスト等の問題で TypeScript の型で定義したいケース 等です 実行時に TS の型から値を作ることはできませんが、事前に型情報からコード生成をすることなら可能なのでコード生成によって対応しようというアプローチです ts-auto-guard GitHub - rhys-vdw/ts-auto-guard: Generate type guard functions from TypeScript interfaces cli が提供されていて $ ts-auto-guard ./path/to/type.ts すると、 type.guard.ts に型ガード関数が生成されてインポートして使うことができるようです // ts-auto-guard のサンプルコード import { isUser } from './type.guard' const maybeUser: unknown = 'invalid' if ( isUser ) { maybeUser /* : User */ } type-predicates-generator GitHub - d-kimuson/type-predicates-generator: generating predicates and assertion function by type definitions. 今回僕が作ったツールです、詳細は 紹介記事 を書いたばかりなのでそちらに譲りますが watch を立てておき、型定義の変更にリアルタイムに追従してランタイムチェック関数を自動生成することができます $ type-predicates-generator -f 'types/**/*/ts' -o predicates.ts -a -w // type-predicates-generator のサンプルコード import { isUser , assertIsUser } from '/path/to/predicates' const maybeUser: unknown = 'invalid' if ( isUser ) { maybeUser /* : User */ } assertIsUser ( maybeUser ) maybeUser /* : User */ typescript-is GitHub - woutervh-/typescript-is typescript-is は少し特殊で ttypescript という TypeScript にデフォルト以外の transform 処理を挟むツールとセットで使うことで、ビルド時にランタイムチェック関数を生成することができます // typescript-is のサンプルコード import { is } from 'typescript-is' type User = { id: number name: string } const maybeUser: unknown = 'invalid' if ( is < User >( maybeUser )) { maybeUser } 本来は is<User>() のような形で User 型に合わせたようなチェック関数を作ることはできませんが、カスタムトランスフォーマーで前の 2 つと同じようなコード生成をビルド時に行うことでトランスパイル後のファイルにランタイムチェックを書き出すことができます 上の if 文は以下のようにトランスパイルされます if ( (0, typescript_is_1.is)(maybeUser, object => { function _number(object) { if ( typeof object !== 'number' ) return {} else return null } function _string(object) { if ( typeof object !== 'string' ) return {} else return null } function _0(object) { if ( typeof object !== 'object' || object === null || Array .isArray(object) ) return {} { if ( 'id' in object) { var error = _number(object [ 'id' ] ) if (error) return error } else return {} } { if ( 'name' in object) { var error = _string(object [ 'name' ] ) if (error) return error } else return {} } return null } return _0(object) } ) ) { maybeUser } 標準ではない ttsc でビルドする必要があること 同じ型に対するチェックも毎回長々と書き出すのでバンドルサイズが増えがち ランタイムチェックの実装がアップデート等で変わったときにソースコードじゃないので気づけない といった成約はあると思いますが、直感的かつ手軽に値の型を守ることができます おまけ: as-safely プリミティブ等の型を気軽にチェックするには、 as-safely というライブラリが手軽です。isString 等のランタイムチェック関数が提供されています import { asSafely , isString } from 'as-safely' const maybeStr: unknown = 'valid' asSafely ( maybeStr , isString ) // チェックに失敗したら例外を投げる maybeStr /* string に型がつく */ カスタムのランタイムチェック関数も使用できるので、② や ③ のライブラリ/ツールと組み合わせてアサーションを手軽に行うこともできます 例えば ts-auto-guard はアサーション関数の自動生成を提供しないようですが、asSafely とセットで使うと手軽にアサーションも行うことができます import { asSafely } from 'as-safely' import { isUser } from './type.guard' const maybeUser: unknown = 'invalid' asSafely ( maybeUser , isUser ) maybeUser /* : User に型が付く */ まとめ 外部からやってきた値に安全に型をつける方法について3つのアプローチに分けて紹介しました! io-ts が一番有名だと思いますが、これに限らず複数の選択肢があるのでプロジェクトにあった形で型を守れると良いのではないでしょうか 個人的には ちゃんとしたスキーマがある箇所 → ① + ③ のあわせ技 それ以外 → ② 関数型っぽさが入っても問題ない → io-ts もっと普通のが良い → superstruct 辺りになりそうかなという印象でした それでは良い型安全ライフを!
アバター
はじめに はじめましての方ははじめまして、ブロックチェーンチームの id:Nanamachi です。夏頃まではエンジニアとして関わっていましたが、故あって現在はプロダクトマネージャとしての道を進んでいます。今日はそんな新米プロダクトマネージャが、膨れ上がるプロダクトバックログ、特にその中で発生するチケットの依存関係を可視化するために、ホワイトボードツールであるmiroを導入した話を書いていきます。 抱えていた問題 現在弊社では、 backlog をタスク管理ツールとして用いています。カスタム項目が使えることやガントチャートが自動的に引けることなどは便利なのですが、タスク間の依存関係が見えにくい問題を抱えていました。 一見ふつうのプロダクトバックログ 例えば上のバックログ。1191のチケットは1137の調査タスクであるため、先に1191を終わらせる必要があります。また、998、999の修正を結合するのが1000であるため、998、999の両方が終わらないと1000に取りかかれません。しかし、この関係性はbacklogだけで直感的に表現できない……そのために、本来先に進めていなければいけないものが終わっていないなど、計画どおりに進行できないことがありました。 実際はこの順番で実装しなければいけない! この問題をホワイトボードアプリケーションのmiroを使って解決していきます。 miroについて詳しく知りたい方は下記の記事などがわかりやすいかと思いますので良ければご覧ください。 jaco.udcp.info プロダクトバックログをmiroで可視化する miro上で整理されたチケットの例 チケットの依存関係と優先順位を可視化するために欲しいものはチケット名、マイルストーン、進行状況、ストーリーポイントなどです。これを空き時間で書いたスクリプトで転記できるようにしました。あとはこれを元にmiro上でチケット間に矢印を引いていきます。 実際に使い始めると、当初の目論見であったチケットの依存関係や優先順位が整理されるのはもちろんですが、それ以上に恩恵がありました。例えば機能ごとのグループやロードマップの管理などプロダクト戦略に関すること、また現在のマイルストーンの進捗状況など、プロダクト・プロジェクトに関する様々なことをひと目で認識できるようになりました。チームが転ばないようにするための見通しを立てやすくなり、非常に便利です。 おわりに チケットをmiroで可視化することでチームの状態が整理されました。チームメンバーがスプリントプランニングをする際にも役に立つため、ぜひやってみてはいかがでしょうか。
アバター