キャディ株式会社のブログ - TECH PLAY

TECH PLAY

キャディ株式会社

キャディ株式会社 の技術ブログ

全240件

たびたび登場失礼します、キャディ HR for Tech の岡野です。 過去にも Tech Blog の中で振れられたこともある 、社内勉強会の「STUDDi」。 エンジニアの知的好奇心を刺激するこのイベントについて一部の情報(3ネタほど)を社外の方にもお伝え出来ればとおもい筆をとりました。 エンジニアの皆さんにとって、日頃の スクラム 開発とブログ執筆の両立が簡単ではないこともあり、たまにはこうして近くでみている僕からの目線でもお伝え出来ればと思っています。 ちなみに上記の記事は2020年8月のものですが、それから1年以上経った今でも形を変えながら脈々と運営されています。 エンジニア全員の持ち回り発表機会が何周かしたことに加えて新しいメンバーのジョインが続いた事をきっかけに、組織としてもまだまだ発展途上であるキャディらしく、この勉強会のあり方について意見があがりました。 Tech組織の中でも多様性が増してきて得意分野が異なるメンバーが増えたことも考えを巡らすよいタイミングだったと思います。 そうした環境のもとで”全員に刺さるテーマ”を探して話すというのがハードルになり始めていたこともあり、検討された結果・・・最近再定義されたのが以下のコンセプトです。(ぱちぱち) まだ一度もスピーカーになっていない方(新しく入られた方)の発表する場として優先する 好きな技術について思う存分語ってOK ちなみに、既に所属していたエンジニアの発表機会についても合わせて検討しています。 前置きが長くなってしまいましたが、レポートを始めていきたいと思います。 Scala つまみぐい キャディではメイン言語として Rust を活用していますが、 Rust に執着しているわけではなく課題を解決できる技術はないか、エンジニアの皆さんは日頃から探求しています。このお話はそんな好奇心を刺激する回です。 ※Rust を選定したときのお話は こちら をご覧ください。 2.13.x系をベースに基本的な特徴について、参加していたエンジニアから気付きの投稿もあがっていました。 発表終盤では、生徒の特定の科目の平均点を計算することをお題にOption型のコンビネーションについて触れられました。 SeqのmapやflatMapを使い、さらにそれらをfor式で書くことで格段に見やすくなる点など学びの声が上がっていました。 普段使っている言語以外の発表は便利な用途を知ることで課題解決の選択肢にもなるためとても好評です。 今後特長を活かすシーンがあれば Scala を採用する事もあるかもしれません。 30分で見るここ6年くらいの単一画像 超解像 Computer Vision 及び Deep Learning の専門性を持たないメンバーに向けた入門編の発表です。 キャディでは、Webアプリケーション開発と研究開発のチームが各々活動しており、こういった勉強会の場などでナレッジの共有がなされる点が魅力の一つになっていると思います。 古典的な方法( ヒューリスティック , 辞書式)と最新の手法(SRCNNの登場~)について解説を交えた発表は入門編という主題通りにわかりやすく理解できました。 ニューラルネットワーク 登場後の発展の歴史においては、 PSNR の大小を競い合う時期を経て、精度以外の観点(見た目, 速度, 実用性)が評価観点に加わってきている事から技術の活用先である「目的」が前提となってくるのだと伝わりました。 論文を読み解く際のワンポイントアド バイス も添えられており、これから 超解像 の分野の勉強を始めるメンバーも出てきそうな予感が・・・! ※ちなみにこの発表のあと、聞いていたエンジニアから「画像コンペにでません?」というお誘いがあるなどコミュニティの広がりにもなっている点が素晴らしいなと思いました。 メモリボ トルネックの概要とその影響について CPUの性能とメモリ性能の関係について、大学院時代のディープな研究内容についておすそ分けの発表です。 理論性能と実効性能についての前提をあわせたあとに、CPUにおけるメモリI/O ボトルネック について深く掘り下げたお話が聞けました。 ここで卓越なのは、わかりやすい比喩表現にWS(わんこそば)をつかっていたこと。 食べる人(CPU)と供給する人(メモリ)の関係で、あくまでWSをベースにお話が進むのでチャット欄がすごい勢いで盛り上がることになります 笑 これを微塵も笑わず発表されているところに尊敬の念を抱かざるを得ない わんこそばをWSって書いてるスライド初めて見たかもしれない いや、わんこそばをここまで語っているスライド初めて (^^; ポーカーフェイスすごい websocket はわんこそばだったか! だめだじわじわくるww スライドのパンチが強い さっきのグラフしか勝たん バランスが大事なんですね つまりみんな早食い 一方で富岳のすごさ、キャッシュヒット率と早さの関係など、きっちりと業務にも役立てられる知識も得られてしまうのがすごい。 「現代はメモリよりCPUがはるかに高速な時代であるが、そのCPU性能を活かすためには(キャッシュヒット率を高める)コードが大切になってくるよ。」というまとめは、パフォーマンスを考える上でも必要不可欠な観点としてエンジニアリング業務にも活かされることでしょう。 ただし、最大の インパク トは残したのはWS(わんこそば)だったかもしれません 笑 おわりに 言葉足らずなところもあったと思いますが、内容が気になった方や「ええやん」と思った方は詳細について こちら でお話する機会がご用意できると嬉しいです。
Tech Blog 初投稿で緊張している、岡野です。(キャディでエンジニア採用を担当しています) 2021年11月11日に開催されたFIndyさん主催のイベント 注目の言語Rust、開発現場でどう使われる?活用企業の現場に聞くVol.2 こちらにキャディのバックエンドエンジニア長田歌菜さん( @ngtkana )が登壇しました。今回は、その内容をまとめましたので当日参加できなかった方にも目を通して頂けると嬉しいです! 注目されている言語の1つとして取り上げられることも多いRust。実際に開発の現場で使う機会は限定的な面もあり、 実際に使っているエンジニア目線でどうなのか ? Rustを業務に取り入れてみたいと考えていらっしゃる方、技術選定の候補として検討されている方の参考になればととても嬉しいです。 [toc] Rustをキャディで採用した背景は? 長田:私自身は、言語選定そのものに関わっていないので、聞いた話や残っている資料ベースでの説明となります。 キャディでは生産管理や原価計算などのプロダクトを内製していくプロジェクトがあり、大きなサービスを展開するのが初めてというフェーズがありました。 それまでは、C++ とか Python など様々な言語を使っていたのですが、堅牢なものを作るにはどのような言語・フレームワーク・技術を使うべきか? 社内でディスカッションを重ねていました。 最終的にはRustを選定したポイントは、 コンパイル時の型や検査に強い こと。 その理由は、数値1つ取っても、 ・長さなのか重さなのか? ・金額であっても「売り」なのか、「買い」なのか? など数値の意味を定義しないと間違ったことが書けてしまうため、計算・検査がしっかりできることが必要でした。 そんな背景から、Rust , Go, Kotlin などが候補に上がるも、 ・キャディのプロダクトによってはスピードが要求されること ・元々キャディに数名Rust使える人がいた ということからRustに決定した背景があるようです(あくまで伝聞です) 例えば、グラム(重さ)とメートル(長さ)が勝手に足し算されてることがあれば、圧倒的に信頼性を失いますからね。 実際にRustを使ってみて特徴を挙げるとすると? 3点あるかなと思っていて、それは 「速い」「安全」「エンジニアに優しい」 です。 「速い」というのは、Rustは 実行時間も速いし、実装時間も速い と思います。 エンジニアによっては、まどろっこしいという先入観もあるかもしれませんが、実装がしやすくなるよう考えられて作られている言語なので、「早い」という特徴がまずありますね。 次に「安全」というのは、 メモリリソースの管理で悩むことがない 点です。 Rustには所有権という概念があり、1つのメモリリソースは1つの変数が持つというものです。 最初はつまづくかもしれませんが、慣れてくるとこの所有権のおかげで実装がしやすくなります。 最後の「エンジニアに優しい」というのは、 コンパイル時に実装の誤りを指摘してくれることが多いから です。 私が Rust を勉強し始めたバージョン1.30くらいからを比較しても、書きやすくバージョンアップしています。 詳しくは Rust公式 や、初心者向けの 入門のまえがき にもありますが、まどろっこしい書き方をしなくて良くなるのがいいです。 フレームワークについてはどうか? キャディが作っているプロダクトはWebサービスがメインです。インフラは共通しているもののチームによってアーキテクチャや使用技術はバラバラです。 私のチームは warp を使っていますが、これの良さはサーバを立てるところがそんなに長くならない点ですね。 設計者は偉いなあと感じるのですが、サーバやFPIやフロントエンドなどがきっちり分かれていて、とても読みやすいと感じました。 どのくらい型の恩恵をうけているか? 直接的なメリットとしては2点。 1つ目は、 ドキュメントが小さくなるところ です。 これは、型が要件を表しているので、関数にわたすものも細かく指定しなければいけません。 ところが Rust だと、 ”こういう意味で実装しました” というのが分かりやすく伝えられるので、コミュニケーションコストが下がる点はメリットですね。 2つ目は、 テストが減ること です。 C++はテンプレートが実体化していないとコンパイルが通る保証がないため、全てのコードポイントを通るように気をつける必要があります。 そのような点で、テスト時に注意する要素が減るのは嬉しいポイントになりますね。 私が以前担当していたプロダクトで原価計算をするときは、とても細かく型をつけていました。 uom というライブラリがありまして、これは物理的なもの(長さ・重さなど)を扱うときに、「長さ」「重さ」を区別できるようになり、掛け算して面接を算出できたりするものです。私たちは、さらにお金の単位も追加して利用していました。 Rust を実際に使ってみてのメリットは? メリットは3つあると思っています。 1つ目は、 環境構築が楽でビルドの再現性が高い 点ですね。 比較対象とする言語にもよりますが、例えば ・コンパイルのプロバージョンが入っている・入っていない ・ライブラリがどのパスに入っている・入っていない などビルドは難しい感じがします。 それと比較すると Rust の難易度は低い方かなと思いますね。 2つ目は、 型が強い ということ。 そして3つ目は、 実行速度が速い ことですね。 オブジェクトを作るとどうしても遅くなったり、抽象化すると抽象化コストがかかるので、コードの読みやすさと実行速度はトレードオフの関係になるのが普通かなと思います。 だけど Rust は”書きやすくて速い”というのを実現できるのがいいですね。 逆にデメリットは? こちらは2点あります。 まず1つ目が、 ビルドに時間がかかる こと。 キャッシュが大きく、ビルドでの待ち時間が長いのはネックですね。 キャディのバックエンドのプロジェクトは、だいたいが Rust を利用しているのですが、様々なプロジェクトを経てビルドを行うと、すぐデータがいっぱいになってしまいます。 なので、泣く泣くプロジェクトを消したり、ローカルから落とすなどで対応しているのですが、もう少しキャッシュが小さくなると嬉しいというのはあります。 2つ目は、まだ 発展途上の言語 ということです。 以前と比較すると書きやすく、機能も増えている事実はあります。 ただ、例えば Rust のエラー処理まわりに関して何のライブラリを使うか?でいうと、 私のチームは anyhow を使っていますが、部分的にちょっとこの辺はまだまだだな・・という点は正直ありますので、そこは我慢して使っているのが実情です。 また乱数を扱うときに、rand というものを使うのですが、数ヶ月前にアップデートがあり、その巻き添えになって大変だったこともありました。 またまだ、発展する余地がある印象です。 人によってデメリットと感じるか、メリットと感じるか個人差があると思いますが、 大きなフレームワークがないのも Rust の特徴 です。 “とりあえずこのフレームワークを使えば何でもできる” というものがないのです。 よって、Rust を使うということはアーキテクチャを自分たちでイチから考えることになるので、これとこれを組み合わせてこうしたプロダクトを作ろうなど、自分たちで構成を考えなければいけません。 これは楽しくもあり、大変なことでもありますよね。 Rust を学習するときの難易度はどうか? 元々 どういう言語ができるエンジニアなのかにもよる と思います。 例えば C++ ができるのであれば、それほど難しくないのではないでしょうか。 逆に Python や JavaScript をメインに扱っている方だと難しいかもしれません。 Rust そのものが特別に難しい言語とは思っていませんが、あらためて所有権という概念は難しいかもしれませんので、その点くらいでしょうか。 ライブラリについては、公式が管理しているものが広範囲で、 crates.io でみると一覧で出てきます。 基本的には ライブラリは1箇所に集まっています。 ドキュメントの生成も、コンパイラと一緒になっているので同じフォーマットでまとまるようになっていますし、ドキュメントを集積する場所はdoc.rsにあり、 crates.io から簡単にジャンプできます。 なので、情報がどこにあるのか?ということで迷わないですし、それは知らなかったということもなく、独学で学習するには向いている言語だと私は思います。 また、 コミュニティの皆さんの面倒見がよかったり もします。 Twitter で質問するといろいろな人が教えてくれたりしますし、rust-jp のslackにも初心者向けチャンネルがありますので、ぜひとも学習してみてください。 採用・育成の観点ではどうか? コンパイルが通りやすいのは、初心者にやさしい点ですよね。レビューする側としても安心できるのかなと。 ただ、Rust はまだマイナー言語なので、元々 Rust がかける人は集まってこないと思います。 なので、これは Rust に限らずですが、書けない言語を書けるようにするために、勉強していく必要があるのはデメリットかもしれません。 Rust そのものが普及すれば、そうしたデメリットも減るでしょう。 日本で Rust を使っている会社一覧 があったりするので、私はそういうのをみて仲間意識を育んだりもしています 笑 Rust 習得の具体的な手順は? Rust-by-Example を一通り読んでいれば、あとはOJTで大丈夫かなと思っています。 そこから興味を持てたら、公式のリファレンスやチュートリアルを見たり、ライブラリは標準のドキュメントを見たり。 いずれにしろ 公式が提供しているコンテンツを読むと勉強が進むし、情報がまとまっているのでチェックしやすい です。 開発に参加するだけであれば、そんなに知識は必要ないかなと思います。 あとは、プログラミングで何をやりたいのか? Tech Blog をご覧になっている皆さんによって、書きたいものや解決したいことは違うと思いますが、それを Rust を使ってやってみるといいのかなと思っています。 新しいメンバーがチームに参加するときの GAP は? 私自身が Rust を使うチームに2週間後に移動となったとき、そこからがんばって勉強しました。 新しいプロジェクトにつくと、このコマンドにはこうしなければいけない、これをインストールしなければ、など環境を整えるのが大変ですよね。 そういう意味で、 Rust のプロジェクトはビルドが簡単なので、オンボーディングは楽 かなと思っていますので、あまり GAP は感じません。 C++ で開発されているところから Rust への切り替えは? C++ でやっていてよかったのは既存のライブラリがあったことですね。 Open CASCADE というライブラリですが、C特有のメモリー管理が難しかったりしました。 Rust に切り替えると、 既存の大きなライブラリを使えなくなるので、自分で作っていくしかない ところは私的にはよかったです。 最後に 今回は、FIndyさん主催のイベントに登壇させていただいた長田歌菜さんの発表内容を記事にしました。 着飾らない等身大のお話ですので、Rust に触れる機会のあるエンジニアにとって有益な情報になっていればとても嬉しいです。 キャディでは、バックエンド、フロントエンドなどのWebアプリを得意とするエンジニアやアルゴリズム、ML、データサイエンスなどの数学の知識を活用したエンジニアリングを得意とされる方にも興味を持っていただきたいなと思っています。 ご興味をお持ちくださった方は、ぜひ こちら からエントリーください!カジュアルにお話できると嬉しいです。 最後までお読みくださり、ありがとうございました。
こんにちは、キャディでソフトウェアエンジニアをしている小倉です。今はフロントエンドを主に触っています。 いきなりまとめ(TL;DR) 本記事は、AgGridのセルの値のReadとWriteについての機構をまとめた記事になります。 記事が長くなってしまったので、触れる内容をまとめた図を先に持ってきました。 やり方が複数あるものについては以下のように考えておくと良いです。 valueSetter,valueGetterは使わなくて良いなら使わないようにして、なるべくfield指定だけですむようにrowDataを設計する。 文字列処理だけで住むときはvalueFormatter, それ以上の処理( CSS で飾り付ける、リッチな機能をつける、etc...)が必要ならcellRenderer プロローグ キャディで作っているTechプロダクトの半数は、人力で回していたオペレーションを代替する立ち位置のものです。どうすればうまくいくのかわからない製造業の課題に対してまずは人力で試して、手応えがあったら、効率化や規模拡大のためにTechプロダクト化する、というよくある流れですね。 さて、このようなプロダクト開発に携わっている方が必ずといって良いほどユーザーから聞くフィードバックがあります。 「 Excel ・ スプレッドシート の方が便利 」 人力で回すオペレーションはUIとDBの部分を 表計算 ソフトで間に合わせていることが多いです。プロダクト化しても良さそうだぞとなるオペレーションはだいたい強力な Excel ・スプシが出来上がっていることでしょう。それと比較されて出てくるフィードバックです。 表計算 ソフトはUIとデザインを一緒くたにして使ってしまうと甚大な アンチパターン を踏んでしまいがちですが、そこをちゃんとすれば、洗練された迅速なオペーレーションを回す手助けをしてくれます。とくに 表計算 UIは大量のデータを扱うのに強かったりします。 表計算 ソフトの アンチパターン を踏まれないようにFEエンジニアがうまくUIを設計すれば強力なツールとなるでしょう。 そういうわけで、私達のチームでは 表計算 コンポーネント をプロダクト内に導入することにしました。 表計算 っぽい コンポーネント を提供するライブラリには WijmoのFlexGrid , Handsortable , cheetahGrid など様々ありますが、私達は AgGrid を使うことにしました。 この記事はなに? AgGridは機能が多く、同じことを実現するにしても様々な書き方ができてしまいます。とくにセルへの入出力まわりはその傾向が顕著です。 公式ページをよく読めばどういうときにどの機能を使うのかのポリシーが書いてあるのですが、開発のたびに参照しに行くのも面倒なので、一覧性を重視したまとめドキュメントを作ることにしました。この記事はそのドキュメントをブログ向けにアレンジしたものです。 用語 Grid:表全体 Row:行 Cell:マス一つ 行は「どのオブジェクトか」、列は「オブジェクトをどう料理するか」 AgGirdでは各行(Row)は1つのオブジェクトと対応します。つまりGrid全体としてはオブジェクトの配列になります。その配列がしばしばrowDataと呼ばれるものです。 各列(Column)は、対応するオブジェクトをどう料理するかに対応します。ここでいう「料理」は大抵の場合は「オブジェクトのこのフィールドの値をtoStringした値を表示」になります。それ以外にも、複数フィールドの合計値を計算して黄色い背景色で表示、などの複雑なパターンにも対応可能です。 つまり、$i$行目$j$列目の場所にあるCellの値は rowData[i] をj列目共通の料理の仕方で得られた値になります。(UI上で列や行の並びを変えられるので厳密な説明ではありませんが、、、) コンポーネント の例 基本的には AgGridReact コンポーネント に行データのArray(rowData)をPropsで与えると、Gridが出来上がります。 AgGridReact のchildrenとして AgGridColumn を追加すると列に関する情報を入れることができます。 AgGridColumn の field 属性にフィールド名を入れておくと i 行目の当該列は rowData[i] のそのフィールドの値を toString した値が表示されます。 ▼素朴なAgGrid コンポーネント import { AgGridColumn, AgGridReact } from "ag-grid-react"; import "ag-grid-community/dist/styles/ag-grid.css"; import "ag-grid-community/dist/styles/ag-theme-alpine.css"; const rowData = [ { firstName: "Taro", lastName: "Tanaka", age: 10 }, { firstName: "Hanako", lastName: "Yamada", age: 25 }, { firstName: "Jiro", lastName: "Suzuki", age: 32 }, ]; export const MyGrid: React.VFC = () => { return ( <div className="ag-theme-alpine" > <AgGridReact rowData={rowData}> <AgGridColumn field="firstName" editable={true} /> <AgGridColumn field="lastName" editable={true} /> <AgGridColumn field="age" /> </AgGridReact> </div> ); }; ▼見え方 表示 上で説明した「料理」は、おおまかに以下の2パートに分かれます。 - オブジェクト(rowData)から値を計算する部分 - 値を表示する部分 オブジェクトから値を計算する部分 オブジェクトから値を計算する部分は以下の2種類のパターンがあります。 ①フィールドを指定する <AgGridColumn field="lastName" /> のようにGridCloumnのfield属性を指定すると、その列のセルの値は、オブジェクトのそのフィールドの値になります。次に紹介するvalueGetterが特に指定されていないときは、フィールド指定だと思って値を取ってこようとします。たとえfield属性が指定されていなかったり、存在しないフィールド名が指定されていたとしても、そのフィールドにアクセスして値を取得しようとするので、undefinedがそのセルの値になります。AgGridは全体的にフィールド指定で済むならばその方がコードがスッキリするようになっていますから、 なるべくフィールド指定で済むようにrowDataの設計をするとよいでしょう。 ②valueGetterを指定する <AgGridColumn valueGetter={myValueGetter} /> のようにGridCloumnのvalueGetter属性に関数を指定すると、その列のセルの値は、その関数が返す値になります。ここでvalueGetter関数には、引数としてその行のオブジェクト(rowDataの要素)が渡されるだけでなく、Grid全体を操作できる API などいろいろおまけがついてきます。そのため実質Grid全体の情報を使ってなんでも計算できることにります。詳細な引数は 公式ドキュメント を参照してください。 どうしても複数フィールドにアクセスしないといけない場合や、複数のセルで同じ値を共通して参照しなければならない場合にvalueGetterを使うとよいでしょう。 値を表示する部分 値を表示する部分は以下の3種類のパターンがあります。 ①toStringして得られる文字列を表示する とくに何も指定がない場合は、その列のセルは、セルの値を Object.toString して得られる文字列が表示されます。 値そのものがテキストや数値で、そのまま表示していいときは、このパターンで実装するとよいでしょう。 ②valueFormatter関数によってフォーマットした文字列を表示する <AgGridColumn valueFormatter={myValueFormatter} /> のようにGridCloumnのvalueFormatterk属性に関数を指定すると、その列のセルは、その関数が返す文字列を表示するようになります。なお、次に説明するcellRendererが指定されている場合はそちらが優先されます。 ValueFormatterr関数に渡される引数の詳細は 公式ドキュメント を参照してください。 値そのものがテキストや数値で、フォーマットし文字列を表示するだけでよい場合にこのパターンで実装するとよいでしょう。 ③cellRendererを指定してJSX/ TSX を描画する <AgGridColumn cellRenderer={myCellRenderer} /> のようにGridCloumnのcellRenderer属性に関数などを指定すると、その列のセルは、それによって計算されるJSXやHTMLを表示するようになります。cellRendererはJSX/HTMLを返す関数、その関数名(string)、レンダラーの コンポーネント 、を指定することができます。 もしReactのFunctionalComponentを渡したい場合はcellRenderer属性ではなくcellRendererFramework属性にFunctionalComponentを渡してあげなければならないことに注意してください。 cellRenderer関数に渡される引数の詳細は 公式ドキュメント を参照してください。 値そのものがテキストや数値で、フォーマットし文字列を表示するだけでよい場合にこのパターンで実装するとよいでしょう。 編集 <AgGridColumn editable={true} /> のようにGridColumnのeditable属性をtrueにするとその列のセルは編集可能になります。 編集は以下の3パートに分かれます - Editorが編集後の値を返す部分 - 編集後の値をパースしてセルの値を更新する部分 - 更新されたセルの値に応じて、オブジェクト(rowData)の値を更新する部分 Editorが編集後の値を返す部分 <AgGridColumn cellEditor="agSelectCellEditor"> のようにGridColumnのcellEditor属性を指定すると、その列のセルを指定されたEditor コンポーネント を通じて編集できるようになります。このEditorはgetValueという メンバ関数 を持っている必要があります。これは編集終了時に呼び出される関数であり、編集後の値を返すことが期待されます。 AgGridは公式に 何種類かのcellEditor を用意していますが、自前でEditorを実装することも可能です。そのときはgetValueメソッドを実装してあげることを忘れないように、、、 もしReactのFunctionalComponentを渡したい場合はcellEditor属性ではなくcellEditorFramework属性にFunctionalComponentを渡してあげなければならないことに注意してください。 また筆者は今回AgGridを使って初めて知ったのですが、FunctionalComponentに メンバ関数 を追加したい場合はuseImerativeHandleフックを使うとよいです。 編集後の値をパースしてセルの値を更新する部分 EditorのgetValueメソッドを通じて得られた値は通常そのままセルの値としてみなされます。しかし、何かしらパースしたい場合は <AgGridColumn valueParser={myValueParser}> のようにGridColumnのvalueParser属性にパース関数を指定することで、パース処理をはさむことができます。valueParser関数に渡される引数の詳細は 公式ドキュメント を参照してください。 更新されたセルの値に応じて、オブジェクト(rowData)の値を更新する部分 更新されたセルの値に応じて、オブジェクト(rowData)の値を更新する部分には2種類のパターンがあります。 ①フィールドを指定する <AgGridColumn field="lastName" /> のようにGridCloumnのfield属性を指定すると、その列のセルの値は、オブジェクトのそのフィールドの値を上書きします。次に紹介するvaluSetterが指定されていないときは、フィールド指定だと思って値を書き込もうとします。 ②valueGetterを指定する <AgGridColumn valueSetter={myValueSetter} /> のようにGridCloumnのvalueSetter属性に関数を指定すると、その列のセルの値(編集後の値)が引数として関数に渡されセルの値が更新する処理が走ります。valueSetterの詳細な引数は 公式ドキュメント を参照してください。 有償プランの機能 AgGridの有償版にはたくさんの便利機能がついています。この記事ではそのうちコピーペーストと補完について触れます。ここで扱う機能は有償ライセンスがなくても試すことは可能です(ただし、 ウォーターマーク が入る)。 どの機能もファイルの先頭に import "ag-grid-enterprise"; を追加しないと有効にならないので注意です。 コピー コピー時の挙動はカラムではなくAgGrid全体に共通のコールバック関数を指定する必要があります。 <AgGridReact processCellForClipboard={myCallBack}/> のようにGridのprocessCellForClipboard属性に関数を指定すると、選択したセルがコピーされたときに、その関数の返り値(string)が クリップボード にコピーされるようになります。複数セル選択でコピーしたときは各セルごとにこの関数が計算した文字列がTSV形式で クリップボード にコピーされます( \t 以外のデリミタを指定することも可能 )。 この関数はどの列も共通なので、どの列から呼び出されたかは関数内で判定しないといけません。以下のようにcolIdで判定するとよいでしょう。 const processCellForClipboard = (params: ProcessCellForExportParams) => { const {column} = params; const colId = column.getColId(); if(colId === 'lastName')return "LastName" + params.value; return params.value; }; ペースト ペースト時の挙動もカラムではなくAgGrid全体に共通のコールバック関数を指定する必要があります。 <AgGridReact processCellFromClipboard={myCallBack}/> のようにGridのprocessCellFromClipboard属性に関数を指定すると、選択したセルにペーストされたときに、 クリップボード の文字列がその関数に渡され、返り値をセルの新しい値として更新します。複数セル選択でペーストしたときは クリップボード の文字列をTSVだと思って、よしなに文字列を分割してくれます。 この関数はどの列も共通なので、どの列から呼び出されたかは関数内で判定しないといけません。以下のようにcolIdで判定するとよいでしょう。 const processCellFromClipboard = (params: ProcessCellForExportParams) => { const {column} = params; const colId = column.getColId(); if(colId === 'lastName')return "LastName" + params.value; return params.value; }; 補完 補完はセルの選択範囲の右下の小さな四角をドラッグしながら選択範囲を広げることで、よしなに値を産めてくれる機能です。言葉で説明するより以下のGIFを見たほうが早いかもしれません。 この機能は <AgGridReact enableRangeSelection={true} enableFillHandle={true}/> のようにenableFillHandleをtrueにすると有効になります。 デフォルトだと、数値の場合は 多項式 補完、そうでない場合は周期的に補完してくれるようです。もしそれ以外の方法で補完したい場合は <AgGridReact fillOperation={myFillOperation}/> のようにGridのfillOperation属性に関数を指定するとよいでしょう。 この関数はどの列も共通なので、どの列から呼び出されたかは関数内で判定しないといけません。以下のようにcolIdで判定するとよいでしょう。 const fillOperation = (params: FillOperationParams) => { const { column, initialValues } = params; const colId = column.getColId(); if (colId === "age") return initialValues.reduce((accum, val) => accum + val, 0); return ""; }; まとめ(再掲) 本記事で触れた内容をまとめた図です。 やり方が複数あるものについては以下のように考えておくと良いです。 - valueSetter,valueGetterは使わなくて良いなら使わないようにして、なるべくfield指定だけですむようにrowDataを設計する。 - 文字列処理だけで住むときはvalueFormatter, それ以上の処理( CSS で飾り付ける、リッチな機能をつける、etc...)が必要ならcellRenderer 最後に AGGridにはほかにもたくさんの機能があります。 表計算 っぽいUIが欲しくなったらぜひ一度使用を検討してい見てください!
はじめに 先日行われた atmaCup #12 にて、「CADDiチーム立ち上げ期MLE・DS積極採用中」チームが 245チーム中 9位 になりました。 惜しくも入賞は逃してしまいましたが、 コンペティション 内でチームとして参加していた中では最も良い成績を残す事ができました。 コンペティション の詳細な内容には触れる事はできないのですが、1週間という短期コンペにチームで参加した経験を振り返り、今後についての考察をまとめて何かの学びになればと思い、本記事を執筆しています。 atmaCup × Sansan戦略 今回は社内から猿田( @srt_taka )と竹原( @myaunraitau )、社外から河合( @vaaaaanquish )を加え、3人での参加しました。 前提として3者とも、 機械学習 エンジニアとしての業務経験、 コンペティション 参加経験を持っていたため、最初の数日はいくつかの特徴量 + LightGBM + 5fold CVで各々が進んでいました。 いくつか特徴量に関する知見を交換しながらも、各々submitする形でした。 中間日辺りから、LightGBMが伸び悩みはじめ、互いの特徴量作りも手数が減ってきたため、河合がtabtransfomer、テー ブルデー タを画像化した上でのCNN、竹原がtransformer等を試しはじめました。 最終日2日前にそれらをマージした上で最終スコアを出し、post processの処理を書き始め最適化を行う事で最終スコアとなりました。 良かったこと 今回短期の コンペティション にチームで参加するにあたって、手数が他チームより多かったと思います。 特に短期のチーム戦では、各々の知見を集約しながらも、「手数を増やす」ことが重要だったと感じます。 今回上位陣は、多くがDeep Neural Networkに関連した手法を用いており、私達もCNN、Transformerを試す事で大きくスコアを向上させる事ができました。 仕事や家庭もある中、短期間の コンペティション では、多くの論文を サーベイ したり、新しいライブラリをガツガツ導入するといった事は単独では難しい部分も多くあります。 その点において、今回チームメンバーの知見を集約し、様々なツール、手法に取り組めた事が、互いの知見向上の意味でも非常に良い体験でした。 利用したツール 具体的な手製の特徴量については触れられませんが、特徴量生成、特徴量選択、 モデリング においては、LightGBMやsklearn、PyTorchに加えて以下のツールを利用しました。 https://github.com/Ynakatsuka/kaggle_utils KaggleMasterである Yuki Nakatsuka( @ynktk1 )さんが公開しているkaggle_utilsです。 前処理で有用な2次特徴量変換が多く実装されており、過去のatmaCupでもKaggle Grand Masterである @takuoko さんがソリューションで利用していました。 以下のように、pandas.DataFrameを入力とし、簡易にCategoryEmbeddingやTargetEncoding、Aggregationを実装できます。 # pip install https://github.com/Ynakatsuka/kaggle_utils from kaggle_utils.kaggle_utils.features.groupby import GroupbyTransformer from kaggle_utils.kaggle_utils.features.groupby import DiffGroupbyTransformer param_dict = [ {'key': ['id'], 'var': box_feat_for_agg, 'agg': ['mean', 'max', 'min', 'std', 'nunique', 'rank']}, ... ] gt = GroupbyTransformer(param_dict = param_dict) gt.fit(df) df = gt.transform(df) gt = DiffGroupbyTransformer(param_dict = param_dict) gt.fit(df) df = gt.transform(df) ... https://github.com/pfnet-research/xfeat 特徴量が増えてくると、Aggregateの実行時間がかかるため、PFNさんが公開しているxfeatを利用しました。 こちらもkaggle_utils同様に、TargetEncodingやAggregationを簡易に実装できるものですが、いくつかのFeatureSelectionの アルゴリズム が実装されているだけでなく、入力をcudfにする事で高速に前処理を行う事ができます。 事前に作った1次特徴量をcudfに変換し、AggregationやFeatureSelectionを行いました。 import cudf from xfeat import aggregation from xfeat.pipeline import Pipeline from xfeat.selector import (ConstantFeatureEliminator, DuplicatedFeatureEliminator, SpearmanCorrelationEliminator) cdf = cudf.DataFrame.from_pandas(df) cdf, agg_cols = aggregation(cdf, group_key=key, group_values=cols, agg_methods=['mean', 'max', 'min', 'sum', 'std', 'rank']) selector = Pipeline([ DuplicatedFeatureEliminator(), ConstantFeatureEliminator(), SpearmanCorrelationEliminator(threshold=0.9), ]) cdf = selector.fit_transform(cdf[cols]) GPU に乗せる事で、半日程掛かっていた前処理が15分前後で終わるようになり、 コンペティション 中の作業効率促進に繋がりました。 https://github.com/awslabs/autogluon 竹原がLightGBM単体でsubmitを繰り返し様々な特徴量の実験を行っている間に、河合が AWS Labsが公開しているAutoGlounを用いてstacking、bagging、Out of Folds(oof)の生成を実行していました。 from autogluon.tabular import TabularPredictor # 利用するmodelの指定 models = ['GBM', 'CAT', 'XGB', 'NN', 'LR', 'KNN'] predictor = TabularPredictor( label=label, eval_metric='f1_macro', # 今回のコンペティションに合わせて problem_type='multiclass', groups=groups # group fold ) predictor = predictor.fit( train_df, # validationを自前で設定する場合は tuning_data=val_df time_limit=time_limit, # N sec以内にfitが終わるようモデル選択が行われる (default=0) presets='best_quality', num_bag_folds=num_bag_folds, num_bag_sets=num_bag_sets, num_stack_levels=num_stack_levels, hyperparameters={x:{} for x in models} ) # oofの取得 oof = predictor.get_oof_pred_proba(train_data=train_df) # predの取得 pred = predictor.predict_proba(df) # 各modelファイルはfit毎に別ディレクトリにあり # predictor自体のsave/loadはpickleで行える import pickle with open('./predictor.pkl', 'wb') as f: pickle.dump(predictor, f) 今回の コンペティション では、周囲のデータのoofを用いた特徴量が順位に大きく響きました。 AutoGlounは、modelごとのoof抽出などの コンペティション に必須な機能を備えているだけでなく、 GPU を自動検知してLightGBM等を GPU ベースで処理したり、ハイパパラーメタチューニングや、timeoutを指定して「指定時間内に終わるようにstacking、emsembleモデルを作る」ことを自動で行ってくれるため、特徴量の追加、削除などの側面でかなり PDCA を回しやすかったです。 今回は自前で特徴量加工を行いましたが、自動での特徴量加工も実装されているようです。 また、LightGBMやCatBoost、XGBoost、NeuralNetなど、実際によく使われる アルゴリズム を利用する実装になっているため、AutoMLとしての信頼度も高かったと言えます。 https://github.com/optuna/optuna コンペティション 終盤では、Optunaによる複合ルールベースのpost process最適化を実施しました。 今回の コンペティション では、あるclass Xの処理を行うかどうかで大きく順位が変動したのですが、河合がtestとtarin間のXの検出量の差の最小化問題として捉えたpost processを実装し、大きく順位を上昇させました。 最小化問題が目の前にある時、Optunaが非常に使い勝手が良く、これが最後のTop10争いに効きました。 次はこうしたい 良い点が多かった反面、最終的に入賞できなかったのが非常に残念な所です。 短期の 機械学習 コンペティション をチームで戦う上での反省点として、以下のように振り返りました。 誰かがリーダーシップを取り役割を割り振る 利用するパイプラインを決めておく データのやり取り等のフォーマットを固めておく 今回各々が簡易コードで特徴量加工部などを共有するのみで終始参加していたため、お互いの進捗が見えづらいだけでなく、前半同じような特徴量生成を全員が書くことに時間を使ってしまいました。 個々がデータをよく見る時間としては有用だったかもしれませんが、全員でディスカッションしていればすぐ出たもので、実装も1つで済んだ所です。 3人のうち1人を挑戦的なモデルチューニングに割り当てる事で、入賞が狙えた位置にいると思うと、実際悔しい限りです。 また、短い コンペティション かつ、互いの開発のバックグラウンドが分かっていない状況だったため、パイプラインツールの統一やデータのやり取りも雑多に行っており、途中特徴量データの受け渡しに失敗して時間をロスしたりしていました。 時間が限られた コンペティション では、事前にチームを組むメンバーで共同開発の練習をしておくと良さそうです。 全体振り返り チーム名の通り、現在CADDiでは 機械学習 ・データサイエンスのチームを立ち上げようとしている所です。 既に数人メンバーが社内で活躍していますが、まだまだプロダクトを推し進めてくれるメンバー探しをしています。 そこで、業界内でのプレゼンスを高め実績を残す意味でも「絶対優勝するぞ!」と応募しました。 atmaCup #12は、一週間という非常に短い期間かつオンラインで開かれました。 タスクもテー ブルデー タを扱うもので、長期間の コンペティション に比べ、立ち上げ期の我々にとっては非常に参加しやすいものでした。 実際参加してみて、運営からのベースライン、 チュートリアル などのヒントや、guruguru *1 上でのディスカッションの盛り上がりが、一週間で行われるものとは思えない程濃密で質が高く、非常に楽しかったです。 チームメンバーが互いに信頼する意味合いでも、同一のタスクをオンラインかつ短期間という制約のある状態で解く事自体に、価値があったと感じています。 各々仕事の隙間を縫っての参加となりましたが、特徴量や手法に関するディスカッションは普段の業務以上に行う事ができました。 この経験を活かし、次回はより良い結果を残したい所です。 おわりに 今回はSansan様のホストする コンペティション で、名刺に関連した内容でした。 CADDiでは、図面を多く扱うプロダクトを作っていますが、今回の コンペティション の課題に似たテー ブルデー タ、画像認識の課題が山積みとなっています。 是非、新しい 機械学習 ・データサイエンスチームへジョインして、より強い力で一緒に前に進んでくれるメンバーを募集しています。 We are hiring! *1 : atmaCupがホストされているWebサイト
こんにちは。Platform チームの飯迫 ( @minato128 )です。 CADDi ではこれまで Hosted Redash(app.redash.io) を利用していたのですが、残念ながら 2021/11/30 に End of Life になるので、10 月末に Self-Hosted Redash 環境を構築して移行しました。今回はそのときやったことを紹介します。 移行の流れ 新しい Redash 環境を v10 で構築する 公式の移行ツールを利用してデータ移行する 監視を追加する 新しい Redash 環境を v10 で構築する まず、 移行ツールは移行先として v10 を前提としている ので、新しい環境は v10 である必要があります。ちなみに、 v10 は 10/2 にリリースされた現時点の最新版 です。 v10 であればどんな方法で構築しても問題ないのですが、今回は社内用 GKE Cluster に入れることにしました。監視もリソースも集約でき、Helm Chart を使うことで初期導入コストも低そうだったからです。 移行対象 GKE Cluster は、このような構成となっており、 基幹システム(社内システム)のほとんどをマルチテナントで運用している ArgoCD で GitOps している Secret は External Secrets + GCP Secret Manager で管理している Datadog で監視、ログを参照できるようにしている 次のように進めていきました。 (1) Terraform に必要な GCP リソースを追加してデプロイ GKE に新しい Node pool の追加 基幹システムに出来る限り影響を与えないようにしたかったので、いったん Node pool ごと分離 移行後のリソース状況をみながら見直す想定 CloudSQL( PostgreSQL ) の追加 DB の管理はできるだけしたくなかったため バックアップもおまかせ (2) Redash Helm Chart の ArgoCD Application Manifest を追加してデプロイ 現在の Chart version は 2.3.1 で Redash version は v8 となっています。ほしいのは v10 だったため、まず v10 に対応する PR を作成しました。(Redash ほど有名なツールの Chart が最新バージョンに対応してないとは思っておらず、これは想定外の作業でした) https://github.com/getredash/contrib-helm-chart/pull/102 追加したファイルの一覧はこれらで、 ├── applications │   ├── redash-assets │      └── overlays │      └── prod │      ├── kustomization.yaml │      ├── redash-postgres-secret.yaml │      └── redash-secret.yaml ├── argocd │   ├── overlays │      ├── prod │         ├── pj-name │            ├── helm-redash.yaml 以下が helm-redash.yaml のイメージです。(実際のものではなくいろいろ端折っています) apiVersion : argoproj.io/v1alpha1 kind : Application metadata : name : redash namespace : argocd finalizers : - resources-finalizer.argocd.argoproj.io spec : project : project-name source : repoURL : "https://minato128.com/contrib-helm-chart/" chart : redash targetRevision : 2.4.1 helm : valueFiles : - values.yaml values : |- # env -- Redash global envrionment variables - applied to both server and worker containers. env : PYTHONUNBUFFERED : 0 REDASH_RATELIMIT_ENABLED : "false" REDASH_MAIL_SERVER : "smtp.sendgrid.net" REDASH_MAIL_PORT : "587" REDASH_MAIL_USE_TLS : "true" REDASH_MAIL_USERNAME : "apikey" REDASH_MAIL_DEFAULT_SENDER : "redash@caddi.jp" ## Redash application configuration redash : # -- REQUIRED `REDASH_SECRET_KEY` value. Secret key used for data encryption. Stored as a Secret value. # helm template 通らないので入れているが、実際はexistingSecretが採用される secretKey : "DUMMY" # -- `REDASH_STATSD_HOST` value. # @default -- 127.0.0.1 statsdHost : "datadog-statsd-service.datadog" # -- `REDASH_GOOGLE_CLIENT_ID` value. googleClientId : "XXXXXXXXXX" # -- REQUIRED `REDASH_COOKIE_SECRET` value. Stored as a Secret value. # helm template 通らないので入れているが、実際はexistingSecretが採用される cookieSecret : "DUMMY" # redash.existingSecret -- Name of existing secret to use instead of either the values above ## This secret must contain keys matching the items marked "Stored as a Secret value" above. existingSecret : "redash-secret" # we dont use ingress ingress : # ingress.enabled -- Enable ingress controller resource enabled : false # CloudSQL # externalPostgreSQLSecret -- Read external PostgreSQL configuration from a secret. This should point at a secret file with a single key which specifyies the connection string. externalPostgreSQLSecret : name : redash-postgres-secret key : connectionString ## we dont use this postgresql postgresql : # postgresql.enabled -- Whether to deploy a PostgreSQL server to satisfy the applications database requirements. To use an external PostgreSQL set this to false and configure the externalPostgreSQL parameter. enabled : false # helm template 通らないので入れているが、実際はexternalPostgreSQLSecretが採用される postgresqlPassword : "DUMMY" ## Configuration values for the redis dependency. This Redis instance is used by default for caching and temporary storage [ref](https://github.com/kubernetes/charts/blob/master/stable/redis/README.md) redis : # redis.enabled -- Whether to deploy a Redis server to satisfy the applications database requirements. To use an external Redis set this to false and configure the externalRedis parameter. enabled : true destination : server : "https://kubernetes.default.svc" namespace : redash syncPolicy : syncOptions : - CreateNamespace= true v10 対応の PR はまだマージできていないので、fork した Repository の Package を参照している v10 の スキーマ に対応した DB で運用していれば、あとで Chart を入れ替えても問題ない 移行のブロック要因にはならない 移行データが多いと API 制限に当たってしまうので REDASH_RATELIMIT_ENABLED を false にする Secret は existingSecret と externalPostgreSQLSecret で指定する redash-assets という名前で、ArgoCD Application を追加して External Secrets をデプロイ REQUIRED な設定には、実際には使わなくても空でない値を入れておかないと helm template コマンドが通らない Redash は Statsd に対応しているが、現在の Chart では REDASH_STATSD_HOST に status.hostIP を指定できないので、Proxy Service を作って指定している 公式の移行ツールを利用してデータ移行する 概要は @ariarijp さんのスライドがわかりやすいです。 https://speakerdeck.com/ariarijp/you-should-know-about-hosted-redash-eol-and-redash-migrate 基本的には 公式ドキュメント に書いてある通りにやっただけです。 redash-toolbelt をインストールする redash-migrate init 移行に必要な設定が meta.json として生成される redash-migrate --help の結果のコマンドを上から順に実行していく データに依存関係があるため 移行の from/to 情報が meta.json に蓄積される こちらが実行後の meta.json の一部抜粋で、データタイプごとに移行元と移行先の id が状態管理されていることがわかります。 " queries ": { " 233300 ": 1 , " 233440 ": 2 , " 234065 ": 3 , } 以下がポイントです。 redash-migrate は最新バージョンを使う 初期バージョンだと group の重複など致命的なバグがある 現時点でも細かいバグは残っている https://github.com/getredash/redash-toolbelt/issues セキュリティ上の都合で、Data Source の Secret は移行されない Redash UI 上で再設定が必要 冪等性があり何度でも実行可能 リソースの id 単位で移行するため、移行済みリソースを origin 側で変更しても再同期はできない Origin 側も Destination 側も Redash 本体の Web API でアクセスする 接続周りに気を使う必要がない 移行量が多い場合は、 Destination 側の API Limit を変えておく 環境変数 REDASH_RATELIMIT_ENABLED 監視を追加する モニタリングのためにこれらを追加しました。 Datadog Logs の Pipeline 追加 一部 Parse しづらいログがあるので手動で Parser を書いてレベル(status)を正しく Remap する Datadog Monitor の追加 GKE や CloudSQL は既存の監視を利用 Terraform で管理しており、Module を参照 Error log 数 Synthetics(外形監視) https ://[REDASH_HOST]/ ping を監視 Queue 監視 前提として、Redash v10 は RQ(Redis Queue) を利用しており、job(message)を処理できているか監視する必要がある 個人的に Queue は Message Age で監視する派ですが、既存 metrics にはないので、message がたまり続けてないか観測することにした redash.rq.jobs.created が enqueue 時、 redash.rq.jobs.started が処理開始時に 記録されている ので、 created - started が n (正の整数)より大きい状態が一定期間続くと worker が message をさばけていないことになる n は traffic や worker count に依るが 0 に近いほうがよい Datadog Dashboard の追加 俯瞰して状況把握できるように まとめとポイント 新しい環境は v10 で作る 移行ツールが v10 しかサポートしていないため Redash Helm Chart は現時点では Redash v8 データ移行は、公式の移行ツールで簡単にできる 移行データに依存関係があるので、help コマンドの順番通りに実行していく セキュリティ上の都合で、Data Source の Secret は移行されないので UI 上で再設定が必要 冪等性があり何度でも実行可能 Origin 側も Destination 側も Redash 本体の Web API でアクセスするので、接続周りに気を使う必要がない 移行量が多い場合は、 Destination 側の API Limit を変えておく
Summary This post is my hobby and has nothing to do with work. I have wanted Extensible Records (a library in Haskell ) for a long time. The time has finally come. The language features we need to implement it are there in C++ 20! Therefore, this post will show you how to emulate row polymorphism in C++ 20. The latest, complete code can be found in this repository . Row Polymorphism Row polymorphism is a kind of polymorphism that allows one to write programs that are polymorphic on record field types (also known as rows, hence row polymorphism). Here is a TypeScript example: type foo = { first: string, last: string }; const o = { first: "Foo", last: "Oof", age: 30 }; const p = { first: "Bar", last: "Rab", age: 45 }; const q = { first: "Baz", last: "Zab", gender: "m" }; const main = <T extends foo>(o: T) => (p: T) => o.first + o.last main(o) (p); // type checks main(o) (q); // type error Mitama.Data.Extensible.Record In TypeScript, it is implemented as a language feature, but in C++ 20, there is no such feature, so we need to emulate it somehow. The reference is the famous Haskell library extensible which emulates the same feature. In the end I succeeded in making a library which allows the following syntax. import Mitama.Data.Extensible.Record; #include <iostream> #include <format> using namespace mitama::literals; using namespace std::literals; void print(mitama::has<"name"_, "age"_> auto person) { std::cout << std::format("name = {}, age = {}\n", person["name"_], person["age"_]); } int main() { using mitama::as; // declare record type using Person = mitama::record < mitama::named<"name"_, std::string> , mitama::named<"age"_, int> >; // make record Person john = Person{ "name"_v = "John"s, "age"_v = 42, }; // access to rows john["name"_]; // "John" john["age"_]; // 42 print(john); // OK auto tom = mitama::empty += as<"name"_>("Tom"s) ; print(tom); // ERROR: constraints not satisfied } Guide-level explanation mitama::named The syntax "name" and "age" is a UDL (User-Defined Literal). These literals create a type mitama::static_string which is a structual type (non-type template enabled class). Thus, mitama::named<"age"_, int> is a wrapped type of int named with mitama::static_string . You can construct mitama::named<_, T> by applying operator%(static_string, T) . mitama::named age = "age"_v = 42; mitama::named has some interfaces like std::optional . mitama::named age = "age"_v = 42; age.value(); // 42 mitama::named name = "name"_v = "Mitama"s; name->length(); // calls std::string::length and returns 6 mitama::record mitama::record<Rows... > is constrained to only take mitama::named in Rows... and strings in mitama::named must be distinct. When initializing a particular record type, the order of the initializers is free, because the Row strings are distinct. using Person = mitama::record < mitama::named<"name"_, std::string> , mitama::named<"age"_, int> >; // OK Person john = Person { "name"_v = "John"s, "age"_v = 42, }; // Also OK Person tom = Person { "age"_v = 42, "name"_v = "Tom"s, }; We can make mitama::record with CTAD (Class template argument deduction). In this case, Rows... of the record is inferred in the order of the initializers, so different record types are inferred depending on the order of the initializers. // john: record< named<"name"_, std::string>, named<"age"_, int> > auto john = mitama::record { "name"_v = "John"s, "age"_v = 42, }; // tom: record< named<"age"_, int>, named<"name"_, std::string> > auto tom = mitama::record { "age"_v = 42, "name"_v = "Tom"s, }; Use mitama::shrink to convert between records that have the same rows but in a different order. // decltype(john) _ = tom; // ERROR decltype(john) _ = mitama::shrink(tom); // OK In fact, a = shrink(b); converts b: B to a: A where A ⊆ B . using Person = mitama::record < mitama::named<"name"_, std::string> , mitama::named<"age"_, int> >; auto tom = mitama::record { "name"_v = "Tom"s, "age"_v = 42, "gender"_v = "m", // an extra row }; Person tom2 = mitama::shrink(tom); // OK Reference-level explanation How to specify string literals as a non-type template parameter Basic idea In order to specify string literals as a non-type template parameter, we first create a structural type class that holds const CharT [N] . CharT is a structural, and an array of a structural type is also structural. And the class such that all base classes and non-static data members are public and non-mutable and the types of all bases classes and non-static data members are structural types or (possibly multi-dimensional) array thereof is structural. Thus, fixed_string below is a structural type. template<std::size_t N, class CharT> struct fixed_string { static constexpr std::size_t size = N; using char_type = CharT; constexpr fixed_string(CharT const (&s)[N]) : fixed_string(s, std::make_index_sequence<N>{}) {} template<std::size_t ...Indices> constexpr fixed_string(CharT const (&s)[N], std::index_sequence<Indices...>) : s{ s[Indices]... } {} CharT const s[N]; }; We can use fixed_string as a non-type template parameter, and CTAD will automatically infer CharT and N from string literals. template <fixed_string S> struct static_string { using char_type = typename decltype(S)::char_type; static constexpr auto size = decltype(S)::size; }; int main() { using ss1 = static_string<"test">; static_assert(std::same_as<char, typename ss1::char_type>); using ss2 = static_string<u"test">; static_assert(std::same_as<char16_t, typename ss2::char_type>); using ss3 = static_string<U"test">; static_assert(std::same_as<char32_t, typename ss3::char_type>); } Complete idea In order to be able to handle std::string_view at compile time, static_string should have static constexpr std::string_view . template<fixed_string S> struct static_string { using char_type = typename decltype(S)::char_type; static constexpr std::basic_string_view<char_type> const value = { S.s, decltype(S)::size }; }; Furthermore, creating a static_string with UDL gives an appearance like "name" . The operator "" () allows us to pass string literals directly to the template parameter, so that we can construct a static_string using the fixed_string deduced by CATD. namespace mitama:: inline literals:: inline static_string_literals{ template <fixed_string S> inline constexpr auto operator ""_() noexcept { return static_string<S>{}; } } How to access to rows in a record mitama::named has a protected member operator[] for record. template <static_string Tag, class T> class named { public: static constexpr std::string_view str = decltype(Tag)::value; // ... protected: template <auto S> requires (static_string<S>::value == str) constexpr delctype(auto) operator[](static_string<S>) const noexcept { return storage::deref(); } }; Rows... in mitama::record<Rows... > should all be mitama::named . mitama::record inherits Rows... and make operator[] visible by using declaration. template <named_any ...Rows> class record : protected Rows... { public: // ... using Rows::operator[]...; }; This will enable to access rows through operator[] by overload resolution. How to check the equivalence of rows in two records In C++ 20, - lots of features can be used at compile time, - lots of classes and functions can be used at compile time, - template syntax for generic lambdas is available and - consteval is available. However, due to lot of compiler bugs, some lack of implementation and some Core Issues, it has become difficult to deliver accurate and elegant code to you. So I leave you with the challenge of the modern metaprogramming techniques in C++ 20. The code, with workarounds everywhere, can be found here . Appendix A: development environment Visual Studio 2022 Version 17.0.0 Preview 5.0 References ISO/IEC 14882:2020 Programming Languages -- C++ extensible
CTO自ら コードゴルフ のキャディをやってみた こんにちは。キャディCTOの小橋です。キャディは特注部品の調達支援などを行っている製造業スタートアップです。8月に約80億円のシリーズBの資金調達を受けて 今後のキャディについての記事 を書かせていただきました。   そんなキャディですが社名の「キャディ」にはどんな意味が込められているか、皆さんご存知ですか? 実は、社名の「キャディ」という名前は、製造業に向き合う皆さまにとっての「プロゴルフにおけるキャディ的な存在」となり、メーカー様や加工会社様の事業に伴走し、ともに改善や成長を続けるという意味が込められています。(でも、他の意味もあります)   そして、ソフトウェアエンジニアにとってのゴルフといえば、 そう、 コードゴルフ ですね!  ということで、ソフトウェアエンジニアの皆さんにとってのキャディとなるべく、私自ら コードゴルフ のキャディをやってみることにしました。   コードゴルフ とは…? コードゴルフ はコンピュータプログラミング・コンテストの一種。参加者は与えられた アルゴリズム を、可能な限りもっとも短い ソースコード で記述することを競う。バイナリサイズではなく、 ソースコード の文字数がスコアとなる。「 Perl golf」など、トーナメントで使用される プログラミング言語 の名前がつけられることもある。ショートコーディング、コードパズル等とも呼ばれる。 ( WikiPedia より抜粋)   競技プログラミング や コードゴルフ のご経験がない方もいらっしゃると思います。でも、大丈夫です!  私がキャディとして、皆さんの コードゴルフ に伴走します!   ゴルファー同士がオンラインでコードの文字数を競い合う形ではありませんが、クリア基準となる文字数を設けることで、一人用 コードゴルフ 的なことができるものをこの記事には組み込んであります。(そういう意味で本来の コードゴルフ とはちょっと異なることをご容赦ください)   なお、本記事ではpaiza.io API を利用してコードを実行しています。 paiza.io paiza.ioの各言語のバージョンや制約について   コードゴルフ の解き方は、文字数を競うという点を除けば 競技プログラミング とだいたい同じです。標準入力にテストケースが与えられるので、標準出力に処理結果を出力するコードを実装します。   例えば、python3での入出力を行うだけのコードはこんな感じになります。 hoge = input() answer = hoge # なにか処理 print(answer) もし、こういったコードの実装に慣れていなければ「 競技プログラミング 標準入出力 使いたい言語名」などで検索すると良いでしょう。   ただ、 コードゴルフ では文字数をできるだけ少なくする必要がありますから、可読性は無視して、使う変数の数を減らしたり、 シンタックス シュガーをフル活用したりなど、短い書き方を追求していくことになります。   最初の問題はクリア基準も緩めにしてありますから心配ありません。(後半は結構難しいかも) それでは、各ホールを回っていきましょう! 1番ホール「板金」 まずは簡単な問題から コードゴルフ をはじめましょう。「板金」に関する問題です。     「板金」とは薄い金属の板のことで、板金加工は箱型やプレート型の部品を作るのに使われるごく一般的な金属加工です。上記画像で私が持っているのはキャディで製作したRustマークの板金加工品です(笑)。キャディは今でこそ切削や製罐など様々な金属加工品を対応していますが、2017年の創業当初は板金加工だけでした。そういう意味でキャディの原点とも言えますね。 問題 縦Hセンチ横Wセンチの長方形の形をした板金があります。この板金から正方形の板金を1つ切り出します。切り出される板金の面積が最大になるように切り出すとき、その面積は何平方センチメートルになるか求めて、それを出力してください。 制約 \\ 0 \le H, W \le 10^5 \\ 入力形式 H W 入力例 10 5 出力例 25 クリア基準 180文字   回答 : 言語: c cpp objective-c java kotlin scala swift csharp go haskell erlang perl python python3 ruby php bash r javascript coffeescript vb cobol fsharp d clojure elixir rust scheme commonlisp nadesiko typescript ※paiza.ioの各言語のバージョンや制約について 回答する <ヒント> 長方形ないし正方形から正方形を切り出すには、短い方の辺に合わせて切り出せば良いですね。   ところで、「キャディ」という社名に込めた意味は、冒頭で少し触れた通り、製造業に向き合う皆さまにとっての「プロゴルフにおけるキャディ的な存在」という意味があることはお伝えした通りです。 しかし、それだけではなく、 実はもう一つの意味があったりします。   「キャディ」に込めたもう一つの意味は… 1番ホールをクリアした皆さんだけにお伝えしましょう。   ギブアップする(コードゴルフを解かなくても続きが表示されます) 社名「キャディ」に込められたもう一つの意味とは…?   キャディをアルファベット表記すると CADDi になりますが、これは CAD Di rect の略であったりもします。設計ツールである CAD からダイレクトに製作の発注ができること。これはキャディが目指す理想の一つであり、創業期にも 3D CAD データを使った自動見積の仮説検証を行っていたりしました。 また、そういった理想を目指すために現在も図面データの情報抽出に関する研究開発を進めていたりもします。   CAD Direct という社名の由来に込められた理想は、 「物理とデジタルの橋渡し」が高度に実現された状態 と言えるのではないでしょうか。残念ながら、ありとあらゆる部品を CAD から即座に発注できるような仕組みはまだ少し時間がかかりそうですが、そういったものを実現していくために必要な物理とデジタルの連携を、キャディでは少しずつ、けれど確実に進めています。 製造業で物理とデジタルの橋渡しをするために 製造業を設計、調達、製造、販売といった形でジャンルを分けた場合、設計は 3D CAD や CAE 、製造はロボット化をはじめとするファクトリオートメーション、販売については 機械学習 や統計解析を使った マーケティング の高度化など、様々な DX や イノベーション が行われてきたと思いますが、調達の分野においてはまだまだ改善の余地があると考えています。   製造業は物理的なものを扱う業種です。製造業の調達の分野こそ、設計情報という最初はデジタルデータとなっている情報を、実際に実物に変換するところまでを 一気通貫 で見れる、まさに物理とデジタルの橋渡しが必要な領域だと考えています。   キャディにおいても、調達領域への支援として特注金属加工部品の受発注を進める上で、以下のようなシステムを内製開発することで、物理とデジタルの橋渡しを進めています。   KLEIN: サプライチェーン 情報 QUIPU: 原価計算 情報 HERODOTUS: 設計図面情報 パートナーポータル: 加工パートナーとの連携システム   それぞれ詳しくは こちらの記事 も是非ご確認ください。   それ以外にも、キャディでは関東関西に検査拠点兼物流拠点を設けていますが、そこでは バーコードリーダ ーを使ってリアルタイムに部品の検査ステータス等を把握するなど、様々な技術を現場に導入して検証を繰り返しています。   将来的にも様々なテーマがあります。製品の在庫管理の高度化や、検査プロセスにおける寸法計測やキズ検知の自動化など、あらゆる受発注の工程にあたって圧倒的な品質や速度をお届けできるように挑み続けます。   私は、リアルのモノを扱っている事業会社のCTOとして、物理とデジタルの世界の橋渡しをすることを通じ、今までは想像もできなかった未来を実現していきたいと思っています。そのために人と技術が協力して事業を推進していきたいですね。   それでは、そろそろゴルフに戻りましょう。次の問題は少し難しいと思う人もいるかもしれません。 2番ホール「シャフト」   次の問題は「シャフト」に関する問題です。シャフトとは金属の棒状の部品のことで、上記画像で私の手のひらに浮いているのもシャフトの一種ですね。   シャフトは、主に回転機構を持つ様々な機械に使われている部品で、ある程度は量産されている汎用品もありますが、キャディで請け負っている特注品のシャフトは、産業機械の動力部分にあたる場合もあり高い加工精度が求められることもあります。 問題 2つのシャフトはジョイントを使って連結することができます。長さXのシャフトと長さYのシャフトを連結すると長さX+Yの連結シャフトになります。   強度の関係で3本以上のシャフトを連結することはできません。   シャフトがN本あります。i番目のシャフトの長さはA_iです。   K個のジョイントがあります。これらを全て使って、N本のシャフトの内いくつかを連結すると、連結シャフトと連結していないシャフトが合わせてN-K個できます。このN-K個の全ての組み合わせの中で、N-K個のうち最も長さが短いものが、他の組み合わせの場合よりも最も長くなる場合の長さを求めてください。 制約 \\ 0 入力形式 N K A_1 A_2 ... A_N 入力例 20 10 5 5 6 6 7 7 8 8 9 9 10 10 11 11 12 12 13 13 14 14 出力例 19 クリア基準 300文字   回答 : 言語: c cpp objective-c java kotlin scala swift csharp go haskell erlang perl python python3 ruby php bash r javascript coffeescript vb cobol fsharp d clojure elixir rust scheme commonlisp nadesiko typescript ※paiza.ioの各言語のバージョンや制約について 回答する <ヒント> 2K=Nの場合はどうなりますか?   この問題は、 競技プログラミング や コードゴルフ に慣れていない方だと少し苦労するかもしれません。   ギブアップする(コードゴルフを解かなくても続きが表示されます) 2番ホールクリアおめでとう! だが、残念だったな!   私は悪のEM(Engineering Manager)の平岩だ!  残念だったな、この記事は私が乗っ取った!   2番ホールをクリアしたみんなには、EMの平岩に延々とキャディへの入社を口説かれる、 カジュアル面談と言いながらうんざりするほど採用色たっぷりのカジュアル面談 をする権利をやろう!   平岩がなぜキャディに入社したか にはじまり、日々のキャディでの業務、キャディ本社(蔵前)まわりのランチ事情、代表加藤の 黒歴史 、平岩が個人的に最近ハマっていることまで、聞いてもいないどうでもいいことを延々と数十分聞かせてやるのだ!     どうだ嬉しいか? 嬉しいだろう! ガハハハハハハハハハハハハ……   <ナレーター> なんてことだ!! この記事は面接の時は圧強めで怖いと噂(実話)の悪のEM平岩によって乗っ取られてしまった! 今こそ本当のキャディパワー(謎)を集め「モノづくり産業のポテンシャルを解放し」悪のEM平岩を打倒しなければ!! モノづくり産業のポテンシャルを解放する 3番ホール「CAD Direct」 ありがとうございます! 皆さんのキャディパワー(謎)により、モノづくり産業のポテンシャルが解放されかけていますが、まだ足りません。     そして、伝説の「CAD Direct」の封印が解かれようとしています!!   ソフトウェアエンジニアの皆さん! 今こそ最後の問題を解いて「CAD Direct」の封印を解き、 モノづくり産業のポテンシャルを解放するのです! 問題 C,A,Dの3種類の文字から構成される長さNの文字列Sが与えられます。文字列Sに対して 以下の操作を可能な限り繰り返します。   操作:文字列Sから文字C,文字A,文字Dをちょうど1つずつ削除する。   1回操作をするたびにX点獲得できます。また、取り出したC,A,Dがこの順に連続していたらCAD DirectボーナスでさらにY点獲得できます。   獲得できる点数が最大になるよう操作を行ったときに、獲得できる点数を出力してください。 制約 \\ 0 入力形式 N x y S 入力例 48 850 33 CDDDDDACCCCCDAAAAACACCADCADCADADCCADACADDCADCADD 出力例 13963 クリア基準 文字数制限なし、解けたらOK!(もはや コードゴルフ じゃない)   回答 : 言語: c cpp objective-c java kotlin scala swift csharp go haskell erlang perl python python3 ruby php bash r javascript coffeescript vb cobol fsharp d clojure elixir rust scheme commonlisp nadesiko typescript ※paiza.ioの各言語のバージョンや制約について 回答する <ヒント> ヒントはありません!   ギブアップする(コードゴルフを解かなくても続きが表示されます) 伝説の「CAD Direct」の封印が解かれた!   皆さんありがとうございます。皆さんのお力により「CAD Direct」の封印が解かれ、キャディの平和は守られました! エンジニア募集中! キャディでは各種エンジニア絶賛募集中です! こちらのサイトで求人をご確認いただけます! @paiza転職へ 謝辞 問題作成協力:Ken Ogura クリエイティブ作成協力:Minami Yamada powered by paiza.io 実行中 .hidden { display: none; } pre.input_form { margin-left: 30px; padding: 10px; background-color: #eee; } pre.error { margin-top: 15px; padding: 10px; color: red; background-color: #f0f0f0; font-size: 12px; } .code { padding: 10px; margin: 10px; background-color: #eef; } .indicator { position: fixed; top: 50%; left: 50%; transform: translate(-50%, -50%); background-color: blue; color: white; padding: 10px; animation: flash 1s linear infinite; } @keyframes flash { 0% { opacity: 1; } 50% { opacity: 0; } 100% { opacity: 1; } } button.giveup { background-color: red; font-size: 18px; } button.release { background-color: yellow; color: blue; font-size: 24px; } a.info { display: block; font-size: 12px; margin: 20px 0; } .katex { font-size: 1em; } (function() { var apiKey = "oem:caddi:ceawnzx3s8xdmnev"; var buttonDisabled = false; function showIndicator(text) { buttonDisabled = true; if (text) { $("#indicator").text(text); } $("#indicator").removeClass("hidden"); } function hideIndicator() { $("#indicator").addClass("hidden"); buttonDisabled = false; } function executeTests(code, language, originalInputs, originalOutputs, allClear, showRuntimeError) { var testNum = originalInputs.length; var inputs = originalInputs.concat(); var outputs = originalOutputs.concat(); function onError(req, status, error) { alert("エラーが発生しました" + " status:" + status + " error:" + error); hideIndicator(); } function watchRunner(id, onComplete) { setTimeout(function() { $.ajax({ cache: false, type: "GET", url: "https://api.paiza.io/runners/get_status", data: { id: id, api_key: apiKey }, success: function(data) { if (data && data.status !== "completed") { watchRunner(id, onComplete); } else if (data && data.status === "completed") { onComplete(); } else { onError(null, "N/A", "watch runner failed"); } }, error: onError }); }, 500); } function getRunnerResult(id, resultHandler) { $.ajax({ cache: false, type: "GET", url: "https://api.paiza.io/runners/get_details", data: { id: id, api_key: apiKey }, success: function(data) { resultHandler(data); }, error: onError }); } function test(input, resultHandler) { $.ajax({ cache: false, type: "POST", url: "https://api.paiza.io/runners/create", data: { source_code: code, language: language, input: input, api_key: apiKey }, success: function(data) { if (data && data.id) { watchRunner(data.id, function() { getRunnerResult(data.id, resultHandler); }); } else { onError(null, "N/A", "create runner failed"); } }, error: onError }); } function executeTest(input, output) { showIndicator("テストケース " + (testNum - inputs.length) + " 実行中"); test(input, function(data) { if (data && data.stdout && data.stdout.trim() === output) { if (inputs.length === 0) { hideIndicator(); allClear(); } else { executeTest(inputs.shift(), outputs.shift()); } } else { hideIndicator(); var err = ""; if (data.build_stderr) { err += "[build stderr]\n" + data.build_stderr; } if (data.stderr) { if (err !== "") { err += "\n\n"; } err += "[stderr]\n" + data.stderr; } if (err === "") { alert("回答コードが正しくありません"); } else { alert("実行中にエラーが発生しました"); showRuntimeError(err); } } }); } showIndicator("コード実行準備中"); executeTest(inputs.shift(), outputs.shift()); } function startRunner(textAreaId, selectId, errorId, inputs, outputs, allClear) { var code = $("#" + textAreaId).val(); var language = $("#" + selectId).val(); var error = $("#" + errorId); if (code.trim() === "") { alert("回答コードを入力してください"); } else { error.text(""); error.addClass("hidden"); executeTests(code, language, inputs, outputs, allClear, function(errMes) { error.text(errMes); error.removeClass("hidden"); }); } } function watchAnswer(lengthId, answerId) { var f = function(event){ var len = $(event.target).val().length; $("#" + lengthId).text((len > 0) ? "(" + len + "文字)" : ""); }; $("#" + answerId).keyup(f); $("#" + answerId).blur(f); } function onload() { watchAnswer("length_1", "answer_1"); $("#button_1").click(function() { if (buttonDisabled) { return; } var threshold = +$("#threshold_1").text().replace(/[^0-9]/g, ""); startRunner("answer_1", "language_1", "error_1", testCase1In, testCase1Out, function() { var score = $("#answer_1").val().length; if (score
はじめに 今までの歩み ドメインモデル構築フェーズ 必要なデータの取扱及び蓄積フェーズ KLEIN QUIPU HERODOTUS パートナーポータル 個人の力を組織の力に 今後への期待と投資 物理とデジタルの橋渡し データを活用するフェーズ プラットフォームチームの立ち上げ 事業のグローバル展開 新規事業の立ち上げ こんな人ウェルカム! もしご興味ありましたら、是非ご連絡下さい はじめに こんにちは。CTOの小橋です。先程、シリーズBの資金調達を発表させていただきました。ビジネスモデルや事業自体に関しては コーポレートサイト をご覧頂ければと思いますが、今日は拡大する事業を支えるテクノロジーを生み出している組織のお話を出来ればと思います。 急拡大していくとともに、組織的課題も技術的課題も急増してきておりまして、折角の機会なので、これまでの歩みと今後への投資を、技術者としてご紹介出来ればと思います。 今までの歩み ドメインモデル構築フェーズ 2017年末の創業期は社員合計3名。会社経営経験無し。金属加工実経験無し。正直分からない事が多すぎて大変でした。事業的にも知識不足でしたが、ソフトウェアの領域に関しても業界的な流通ファイルフォーマットすら分かっていない状況でスタートしました。当然ですが、適切なOSSのライブラリ選定も出来ず手戻りも発生したり、学びの多い日々でした。 取り扱っているものが特注金属加工品だったので、注文毎に完全オーダーメイドの設計や材料の指定があり、多品種少量の難しさを肌で感じていました。当然ですが知識不足や考慮漏れで不良を起こしてしまったり、納期遅延で慌てて再制作に挑むなど、お客様にご迷惑を掛けてしまった日々の反省を、今でも痛烈に覚えています。 もちろん事業を軌道に乗せるフェーズではありましたが、同時に持続する事業を作る上で製造業のドメインをソフトウェアで表現できるように試行錯誤するフェーズでもあり、全て破壊する覚悟で開発を進め、殆どの時間をデータ設計で溶かしていました。塗装の色もシステム的には「色」という項目をRGBで表現すれば良いかなと高を括っていたら、業界的にはマンセル値という別の表現があったり、そもそも色指定がデジタルな文字列でもなく、物理的な見本が郵送される場面もあったりで、ひたすらドメインモデルの更新やリファクタリングを繰り返していました。 業界に寄り添いつつも、この情報の嵐を構造的に表現していくのがキャディのエンジニアリングでドメイン駆動設計(DDD)を活用している起点でもあります。お作法的な部分もありますが、リアルの世界とソフトウェアの世界を往復する上の心構えとして大切にしてます。ソフトウェアの技術者としてその目標に向けて適切な技術やフレームワークを活用し、継続的にドメインの進化にキャッチアップできる仕組み作りを目指しています。 必要なデータの取扱及び蓄積フェーズ 現在キャディは受発注を進める上で大きく3種類のデータを扱っており、それぞれが別システムとして存在します。具体的な画面のキャプチャや概要を紹介できればと思いますが、名前の由来に関しては面談やイベントなどで聞いて下さい! KLEIN: サプライチェーン情報 QUIPU: 原価計算情報 HERODOTUS: 設計図面情報 パートナーポータル: 加工パートナーとの連携システム データ自体は System of Record 的にデータモデルの境界でシステムや開発チームが分かれているものの、システム間参照できるようにキー情報を通じてデータが繋がっている事が重要なポイントです。データを最大限活用するのには管理会計から設計図まで一気通貫で情報が連動している仕組みが不可欠で、そのためにキャディのビジネスモデルの根幹を支える内製開発を進めています。 KLEIN サプライチェーンを全て構築するので、グラフ構造の物流を表現し、それを最終的にキャディの検査拠点で検査して発送するところまで見届けるシステムです。技術的にも複雑なデータ構造を操作するevent sourcingライブラリを開発したり、疎結合な外部システム連携のためにRabbitMQを活用したり、相当モダンな設計が求められているシステムです。 QUIPU 属人化しやすい見積もりを仕組みで担保するための原価計算エンジンです。設計図は作りたいものを表現した、いわば「お絵描き」で、求められている結果を製造するための工程を逆算して価格に変換する、ドメイン知識が山盛りのシステムです。重量や体積等複数の単位を扱うため、Rustの型システムで単位の整合性を担保しています。 HERODOTUS 設計図面を取り扱うシステムで、スキャンされた図面のバージョン管理含む、情報整合性担保の役割も果たしています。製造業に特化した内製画像解析アルゴリズムを活用して、画像データから受発注に必要な情報抽出もできるように機能開発を続けています。 後でも触れる通り、これまで蓄積された/今後も更に蓄積され続けるデータを基に、コンピュータビジョンから深層学習など幅広い技術を活用して、顧客や加工パートナー、そして社内メンバーがこれまで想像もつかなかった機能を複数実現するフェーズに入っていきます。 パートナーポータル 社内で利用しているKLEIN+QUIPU+HERODOTUSのシステムに溜まっているデータを構造型データとしてそのまま加工パートナーに連携するシステムです。文字数のわりに情報量が少なく、自由自在にエクセルとPDFが添付されるメールと違って、共通フォーマットでの情報のやり取りに挑戦しています。社長から事務スタッフまで幅広い方々に利用されるため、ビジュアルのデザインとUXにも重点を置いて作っています。 個人の力を組織の力に 創業からの4年弱で、エンジニア組織も1人から40人規模まで成長してきました。10人くらいまでは個人の力に依存し、全員が単一障害点になっているリスクを抱えつつ、事業を作り上げてきました。当然ですが、人数的にもチーム間の連携強化やタスクの分解と割り振りが重要になり、チーム会議のファシリテーションや交通整備も必要になってきました。お互いの依存関係も増えO(N 2 )でコミュニケーションが増えるのが目に見えていたので、情報集約とプロダクトの方針を担うプロダクトマネージャーの採用を通じてカオスを何とか整理。エンジニアとして個人の力も大切ですが、チームメンバーが全員最大限活躍できるような組織的フレームワークの重要性を実感したのが社員10人くらいのときでした。 20人を超えると、意図的に個人の力を組織の力に繋げる事がより重要に。システムの複雑化と同時に仲間がどんどん入ってきてある程度オンボードも型化しないと効率が悪く感じるようになってきました。昔は「開発ドキュメント読んでSlackフォローして」でしたが、今では新入社員用の「Welcome Document」もでき上がっています。 多様なバックグラウンドの方々がいる中で、組織自体の開発及びそこに属する人材開発も積極的に取り組むフェーズに入り、エンジニアマネジメントというロールも設けて、技術を事業で活用しつつ、それ自体を支える仕組みづくりにも力を入れるようになったのが20人を超えた一年半前です。まだ出来ていない事や後手後手な部分もたくさんあり、これからも更に変わっていくとは思いますが、それはその時のお楽しみという事でまずは事業を伸ばすことに専念していこうと思います。 今後への期待と投資 物理とデジタルの橋渡し これだけ社内で貴重なデータが溜まっている一方、製造業は当然物理的なものを扱っているため決してデジタルの世界で全てが完結しません。パートナー加工会社はデジタル設計情報を実物に変換していますし、我々の検査拠点では制作物を開梱してノギスやハイトゲージで確認していますし、受発注の全ては最終的に実世界の実態とデジタル予定情報の突き合わせに収束します。今後はより物理とデジタルの世界の往復に挑戦し、データを片方の世界に滞留させない事が operational excellence とも言えるかと思います。 それに向けた要素技術としてはバーコードやRFID含むたくさんの技術がありますが、これらを現場に落とし込んで日々運用できている状態まで持っていくのがリアルビジネスの難しいところで、キャディではソフトウェア技術を活用して自社の現場オペレーションにのせるところまで裁量を持てるからこそ、最終的な事業価値まで見れるからこそ、やりがいがあります。 キャディも検査拠点兼物流拠点を関東関西に設けてリアルタイムに部品の検査ステータス等を把握してオペレーション全体の最適化するために、常に新しい技術を現場に導入して検証を繰り返しています。今後も在庫管理や寸法計測、キズ検知含めてあらゆる受発注の工程に当たって、圧倒的な品質や速度をお届けできるように挑み続けます。 リアルのモノを扱っている事業として、デジタルトランスフォーメーション(DX)というのは物理とデジタルの世界の橋渡しする事を通じて、今までは想像もできなかった世界を描くことだと思います。その世界を実現するために人と技術が協力して事業を推進出来ればと思っています。 データを活用するフェーズ 基本的なビジネスインテリジェンスはもちろん、設計図面等の画像情報の解析にも挑戦しています。伝統的なコンピュータビジョンから深層学習まで幅広い手法の活用を通じて、データを資産として扱える世界を描いています。 キャディの物量とデータと解析技術を活用すれば、非構造型データも構造化できますし、保険会社がリスクモデルを作るのと同様に不具合のリスク予測もできるようになります。大量生産ではパラメータの数が少なく、同じ作業を繰り返すためデータドリブンなアプローチが利用できますが、我々の向き合っている多品種少量生産になるとパラメータが数百倍増えるため伝統的な分析方法では難しくなってきています。だからこそ機械学習含む多次元での推論技術を利用して、商流のど真ん中にいるからこそ取得出来るデータを分析し、世界初の多品種少量生産のリスクモデルに挑戦したいと思ってます。 これを実現するために新たに仲間を探したくデータグループの立ち上げを真面目に検討してます。創業から4年近く蓄積してきた知見とデータを今後の投資としてもフル活用していく上で、データを中心に取り扱う仕組みを作るデータエンジニアリングとその分析を推進するデータサイエンスを深堀っていきます。 プラットフォームチームの立ち上げ 今年の7月にPlatform Engineeringチームを立ち上げましたが、それまでは組織構造としてプロジェクト単位に分かれていて、各プロジェクトでインフラ構築や監視からアプリケーション開発まで一気通貫で持っている状況でした。大体のプロジェクトがKubernetesを使っているんですが、学習コストも一定かかって気軽に新しいプロジェクトも始めにくい課題がありました。複数プロジェクトが利用している内製のミドルウェアもあるものの、明確なメンテナンスもない状態でした。 個人的な意見ですが、「インフラチーム」「基盤チーム」「SREチーム」は縁の下の力持ちという存在になり、「何かよしなにに色々やってくれる人たち」「夜オンコールしてくれる人たち」という感覚になりやすいと思います。そもそもプラットフォームチームのミッション自体がインフラに絞られているわけでもないので、そういう空気にはしたくはありませんでした。 プラットフォームチームのミッションは、開発組織のアジリティ向上とイノベーション推進だと思っています。共通化、標準化、自動化を通じて開発者の時間を生み出し、それをまた仕組みや挑戦に投資する事で一定リスクを背負いながらも、より高い目標を目指せると信じています。詳しくは こちらのブログ記事 を是非見て頂ければと思いますが、中期的な投資目線でなかなかリソースを割くのに苦労しているものの、ここにリソースを貼ることが開発組織の更なる成長にはクリティカルになってくると考えています。 デザインシステムを作りたい人、インフラが大好きな人、DevOpsをやってみたい人、nodejsエコシステムのエキスパート等々、とにかく開発組織全体の力になりたい方に助けて頂きたいです。興味ある方はお気軽にTwitterで @caddi.tech までDM下さい! 事業のグローバル展開 モノづくりは幸い言語や文化を問わず価値が伝わる業界で、日本の製造業は世界的にもトップクラスです。海外でも「Made in Japan」は「高品質」と同等に解釈される場面も多く、日本企業としてキャディが受発注網をグローバルに展開するのは創業期から視野に入れていました。 開発しているシステムも日本人ユーザから海外ユーザへと拡大する事で、ローカリゼーションもそうですし、通貨の取扱等ドメインモデルを大きく更新する必要が出てくる事を想定しています。また、物流や納期管理の難度が一気に上がり、よりデータドリブンなリスク管理を通じて、今まで実現不可能だったQCD(品質・価格・納期)を提供出来る事も期待しています。どこまで既存の外部会計システムを利用し、どこまでを内製開発するかまだ決まっていないフェーズですが、このような実世界の複雑な実態をソフトウェアで表現するチャレンジ自体がやりがいなのかなと思います。 新規事業の立ち上げ 私たちは祖業である、受発注プラットフォームの事業を通じていくつかの点に気づきました。 - 一つ目は、受発注の出し手でもあり受け手でもあるわたしたちが自分たちの事業のために内製してきた技術は、同様に受発注をしているメーカーや加工会社にも提供できる可能性があるということです。 - 二つ目は、図面という製造業において非常に重要なドキュメントの管理やデータ活用が、まだこの業界でほとんど解決されていないということです。 - そして一方で(これが三つ目ですが)、この図面というものを解析したり分析したりといった部分に、機械学習など近年で発展が大きい分野の技術が応用しやすいというポテンシャルが存在します。 私たちの新しい試みとして、図面データを軸にした 新しいプロダクト を始めます。既存事業が、受発注プラットフォームという仕組みを通じて最終的には製品(モノ)を納めるのに対して、今回はソフトウェアプロダクトそのものを提供するという形になると思います。そして、既存事業のデータや技術基盤との間に、強いシナジーが生まれる事業になります。 この新しい試みにとてもわくわくしています。 こんな人ウェルカム! 実際やりたい事は山ほどあります。普段これを求人という形で表現していますが、今回は折角なので別の切り口で紹介させてください。以下、全くMECEではないし、大分かぶりあると思いますがもし興味あれば是非! 技術が好きな人 最先端を攻めたい 誰もやったこと無いことを実現したい 裁量持って技術領域広げたい チーム開発が好きな人 お互い尊重してチームで課題を解決したい 周りが働きやすくする事でやりがいを感じる チームの結束や連携を良くすることで嬉しくなる 組織や人が好きな人 人を育てて成長支援するのが好き 事業の事情に合わせた人事制度を作ってみたい 広報活動や技術系のイベントが好き OSSやコミュニティ活動が楽しい 事業が好きな人 ユーザを喜ばし定性的なフィードバックが好き 技術を攻めなくてもしっかりと活用してインパクト出したい 業界問わず自分の腕っぷりで社会貢献したい ものづくり好きな人 自作PCやキーボード等ものづくりが好き 卓上旋盤やフライスでの加工が楽しい PLCやCNCの制御が面白いと思う もしご興味ありましたら、是非ご連絡下さい まずはカジュアルに話を聞いてみたいという方は カジュアル面談フォーム からご連絡ください 個人的に話をしてみたいという方はTwitterで @caddi.tech までDM頂いても構いません。 是非選考を受けてみたいという方は 求人ページ からエントリーください。
こんにちは。 SWE の飯迫 ( @minato128 ) です。 7月1日、CADDi で初めての Tech 組織横断チームとして、山田( @kei711_ ) と一緒に Platform チームを立ち上げました。一般的に Platform チーム/エンジニアの役割は、SRE ほど型化されておらず会社によってやることが異なりますが、過去 2 社の経験や他社事例をみると大きな技術的投資が必要になってくるタイミングで組成されることが多いようです。そこで、CADDi の Platform チームがどういう目的で何をやるチームなのか紹介します。 背景 現在開発チームは 5 つあり、各チームが責任を持ってプロダクトを開発運用しています。これまでは、SRE チームも Platform チームもなく、開発チーム間でよしなに連携して進めてきました。それでも一定なんとかなってはいたのですが、片手間で取り組むには大きすぎる課題はどうしても劣後せざるを得ませんでした。そこで、開発組織もプロダクトもスケールしていく中でも、開発速度や品質を落とさず Whole Product としてユーザーに価値を届けるための技術的投資ができるように、Platform チームを立ち上げることになりました。 ※抽象化した図なのでチーム数やプロダクト数は実際とは異なります 開発チーム それぞれのプロダクトの開発運用に責任をもつ Embeded SRE は存在しないが、それぞれのチーム内では実際その Role に近いことをやっている人もいる Platform チーム 全社共通プロダクトの開発運用に責任をもつ 開発チームと協調して、プロダクトに閉じない課題を解決する CADDi の Platform チームとは 戦略的に「技術課題を解く」ことを目的とする 戦略的に 短期的視点だけでなく、中長期的視点で価値のあることに投資する 4半期単位でできることだけにとらわれない 技術課題を解く インフラ共通基盤 アプリケーション共通基盤 社内標準化(属人性排除) 生産性 RASIS Observability などの課題を見つけて解いていく 直近の具体例をあげると、 認証認可基盤の設計・構築・運用 新規プロダクトの アーキテクチャ 設計・構築 ArgoCD 管理の全社展開 などがあります。 行動指針 技術課題を解くにフォーカスする インフラレイヤーでもアプリケーションレイヤーでもなんでもやる 安易にこぼれ落ちたボール拾って対応しない 柔軟に横断的に動く 組織的事業的に価値を発揮できるならなんでもやる 開発チームに一時的に入るとかもありえる 大きな課題は、横断的なプロジェクトチームの立ち上げてリードする 必要ならなんでも Learning / Unlearning 過去の成功体験、 固定観念 に縛られない 常にパラメーターが違うので、再現性があるか今やることに価値があるかを見極める Have Backbone; Disagree and Commit 信念を持ち、正しいと考えていることを言い続ける そして、決まったことにはコミットする 想定カバー範囲 やること 上の図の通り、カバーする範囲は広めに定義しています やらないこと 情シス的な業務 プロダクトの運用全般 各プロダクトの運用は開発チームが責任を持つ アラートの一次受けも開発チーム 認証認可基盤などの全社共通プロダクトは例外 Frotend, Algorithm, Data Engineering の Core Design/Implementation CADDi にはエキスパートがいるのでこれらは任せる 全体 アーキテクチャ 設計時に協調することはありえる 関わる人 プロジェクトごとに多少差異はあるかもしれませんが、基本的にはチーム内で優先度を決定し、そのプロジェクトの ステークホルダー と合意をとって進めます。 開発チームメンバー CTO PdM EM 開発の進め方 1 スプリントは 1 週間として、 スクラム で開発を進めています。また、タスク管理は Jira を利用し、直近だとこの 3 軸をメインにタスク化していっています。 中長期的にあるべき姿を描いてブレイクダウン アーキテクチャ や構成管理、アクセスコン トロール など 新規のプロダクト 立ち上げのための全体 アーキテクチャ の設計、構築など 開発チームから要望 DX 改善など おわりに CADDi では過去 1 年で見てもかなりのスピードで事業とプロダクトが動いています。だからこそ、まだできたばかりで少人数のチームではありますが、常に柔軟に、ときには中長期視点でエンジニアリングを進めていく必要があります。将来的に組織やプロダクトがスケールしていくと mercari さんの Platform チームのように分業化が進むのかなと想像していますが、今は自分たちのカバー範囲を定義しすぎず、コトに向かってなんでもやっていこうと考えています。 We’re hiring 一緒に課題に向き合ってくれる仲間を募集中です! https://corp.caddi.jp/recruit/
こんにちは😉 @ryokotmng です。 今日は社内ドキュメントの、Rust初心者向けのクックブックを公開しようと思います。 私自身コードを書くのに四苦八苦していた頃にとても助けられたので、Rustをはじめたばかりの方の参考になれば嬉しいです。 目次 [ toc ] はじめに この記事では、 The Book に記載されている知識を前提としています。 Rustを全く書いたことがない方は、先に読んでみることをお勧めします。 サンプルコードが結構長いこと、実行環境があった方が良い内容も多いことから、サンプルコードは大体Rust Playgroundのリンクとなっています。 ぜひご自身で修正して遊んでみてください。 単位つきの計算を型で厳格に縛る 例えば複数の長さの単位 (mm, cm, mなど) を扱う場合に、単位が合っていない長さ同士の計算をする場合、単位を揃える必要がありますね。 この時、最終的に欲しいのは1つの「長さ」、つまりプリミティブな数字のデータになるでしょう。 計算を行うとき、最終的に得たい「長さ」の単位は1つになるので、コード上では単位を比較して異なる場合はエラーを返す、もしくは、異なる単位の長さ同士の計算の場合はある単位に換算したうえで計算できる状態にする、のどちらかの処理が必要になります。 しかし、単位を型として定義すると、計算の実装をする際に単位をチェックするようなコードを書くことなく、異なる単位の長さ同士の計算を実装しようとしたら コンパイル エラーを出すことで、意図しない挙動を防いでくれます。 また、下記のサンプルコードのように、traitを使って単位の変換処理を実装することもできます。 サンプルコード 上記のサンプルコードには、型の書き方の他にも、以下のような多くの知識が詰め込まれています。 Rustの enum が、 JavaScript でいうUnion型のような使い方もできること ジェネリクス PhantomData (幽霊型) これらの概念を知らなくてもさらっと読んで雰囲気を掴むこともできますが、よく使うテクニックのはずなので、慣れていない方はそのような概念をひとつずつ調べながら読むことをお勧めします。 The Book にも記載されているので、ぜひ読んでみてください。 参考 (The Book): Enumを定義する 、 ジェネリクス 、 幽霊型パラメータ 参考 (外部ブログ): Rust で Phantom Type (幽霊型) なお、RustベースのWebエンジンである Servoの内部実装 でも、このパターンが使われています。よければ参考にしてみてください。 エラーハンドリング Rustは基本的に、 f() -> Result<T, E> 型でエラーを伴う処理を表します。 T が成功した場合の値, E がエラーだった場合の型です。 Result<T, E> 型は、パターンマッチで T と E のどちらに値が入っているのか判定できるので、以下のように使うことができます。 match f() { Err(e) => //エラー処理(eはE型の値), Ok(r) => // 成功したときの処理(rはT型の値), } これを利用してエラーハンドリングすると、以下のような処理を書くことが出来ます。 サンプルコード 1 更にRustでは、 ? オペレーターを利用して以下のように書くことも出来ます。 サンプルコード 2 ? を利用するために下準備で必要となるコード量が多いため、実際には以下のcrateを利用したりして使いやすくすることができます。弊社では errer crateを利用しています。 errer errer_derive OptionとResultに対する処理にcombinatorを使う Option と Result について処理を行う場合、match式で場合分けを行いながら処理を進めることも出来ますが、combinatorを使うと短く書くことができて便利です。 なお、Productionでは、 unwrap() , expect() は処理が失敗した場合にpanicを返すので原則使わない方が良いでしょう。 (テストやサンプルコードで使うのは問題ありません。 また紛らわしいですが、 unwrap_or***() 系のpanicを出さないものは問題なく使えます。) サンプルコード Productionコードでは、 Result を返す小さい関数を、 and_then() などのcombinatorを使って合成し、大きい処理を表したりするのに使います。 また単純なエラーハンドリングの場合は、combinatorで頑張らなくても、上記に挙げた ? オペレーターで処理もできるので、読みやすいように適宜調整すると良さそうです。 Rustでは厳密には Monad はありませんが、考え方は使えるので以下の記事などで少しでも理解しておくと理解しやすいでしょう。 参考: 箱で考えるFunctor、ApplicativeそしてMonad なお弊社では、 anyhow クレート を使用しています。エラーにコンテキスト情報を含める機能 ( with_context ) や便利なマクロ ( bail! など),独自のエラー型などを提供しています。 collect() で Vec<Result > と Result<Vec > を相互に変換できる イテレータ の処理で collect() を実行し、返り値として Result<Vec< >> がほしいと仮定します。 単純に collect() を実行した結果、 Vec<Result< >> が返り値となった場合でも、その逆の形に簡単に変換することができます。 以下引用 fn main() { // 全てSomeならSome(配列)を返し、どれかがNoneなら全体もNoneになる assert_eq!([Some(1), Some(2)].iter().cloned().collect::<Option<Vec<_>>>(), Some(vec![1, 2])); assert_eq!([None, Some(2)].iter().cloned().collect::<Option<Vec<_>>>(), None); } 引用元: RustでOptionやResultの配列ができてしまったときの一般的なテク4つ このテクニックを知らないままVecの複雑な処理に直面すると絶望的な気持ちになるので、すぐには使う場面がなくても、「こんなことができるんだな」くらいに覚えておく価値はあると思います。 _ の表す意味 変数名に使う => 変数が未使用であることを宣言する 型の一部として使う => 型推論 してねとRustにお願いする サンプルコード コードの公開範囲(public/private) Rustで定義したものはデフォルトでprivateで定義されます。定義されたモジュールの外で利用する場合は pub キーワードで公開することを宣言する必要があります。 サンプルコード また、pub(crate)などと指定することにより、公開する範囲を限定することができます。 参考: Visibility and Privacy 注意点 タプルも同様に、デフォルトの公開範囲はprivateです。 // Sampleタプルは外に公開されている // この場合、タプル内部のStringは非公開 pub Sample(String); // このように内部にpubをつけることで公開すことができる pub Sample(pub String); PRを出す前にやっておきたいcargoコマンド プッシュする前に、下記のコマンドを実行し、エラーがないことを確認しておきましょう。 cargo build アプリケーションをビルド cargo test テストコードを実行 cargo clippy Linterで構文チェック rust-lang/rust-clippy cargo fmt formatterにかける rust-lang/rustfmt 副作用のある処理をMock化して、実装を切り替えられるようにする 弊社では、 ビジネスロジック ( ドメイン 層など)などは、クリーン アーキテクチャ で言う外側の層に影響されないように記述しています。 例えば、 ビジネスロジック の 単体テスト を行うのに、DBや API など外部のシステムと連携したテストを作成するのは、環境構築等色々な前工程を行う必要が生じるため辛いことになります。 このため、外部リソースを使って計算を行うロジックはロジック部分と外部の連携部分を切り分けたくなります。 以下のサンプルコードでは、実際の ビジネスロジック は、その外部リソースを扱う処理に直接依存するのではなく、「外部リソースの扱い方を定義したtraitに依存するように記述する」ことで処理の分離を実現しています。 サンプルコード turbofish(::<>) turbofishとは型注釈の一種で、型を引数のように関数に対して与える表現方法です。 例えば、strに対する parse() メソッドの型定義は以下の通りです。 pub fn parse<F>(&self) -> Result<F, <F as FromStr>::Err> where F: FromStr, 出所: Primitive Type str ここで F は、 FromStr を実装している型となるように抽象化されています。 つまり、実際に使う時には コンパイラ が F の型を推定できないと、 str 型を何に変換すればいいか特定できないでしょう。 この時、推定できるように記述する方法が2つあります。 // 1. 型が決まるように束縛するxに対して型注釈をつける // なお、xの型注釈は Result<i32, _> のように省略可能 // => 理由は以下のturbofishの例に記述 let x: Result<i32, ParseIntError> = "10".parse() // 2. turbofishを使う // この際turbofishとしてはi32になることが特定できれば // エラー型はFromStrの定義からError型の具体型が特定できる let x = "10".parse::<i32>() 参考: Rustのturbofishを理解する (おまけ) cargoの独自コマンドを作る cargo bookに、「$PATHに cargo-XXX というバイナリが入っていたら cargo XXX でcargoのサブコマンドのように実行できる」との記述がありますが、これはバイナリに限った話ではなく実行権限がついていれば大丈夫です。なので、簡単なshellを登録しておいてcargoから実行することも可能です。 (例) #!/bin/sh rg -l todo: 上記のshellを$PATHの通る場所に置いておくと、 cargo todolist で todo: のあるファイルを探してくれます。出力がPATHになるので、弊社では、 VSCode のターミナルで実行 -> PathをCtrl+クリックで該当ファイルに飛ぶなどに利用している人もいます。 shell自体をPATHにあるところに置いて呼び出せばいいじゃんという声が聞こえてきそうですが、 cargo --list で利用できるサブコマンドの一覧を取れるところが強みです。 いかがでしたでしょうか? Rustはすらすら書けるようになるまでが難しい言語だと思います。私の場合は、Rustを書き始めて2ヶ月くらいの間は、他の言語ではスラスラと書けたようなロジックでも全然書けなくて、とても悲しい気持ちになりました。 ですが、書けるようになってみるとやっぱり良いところもたくさんありますし、勉強すればするほどその強力さがわかって楽しくなってくるなと感じています。 最後に、弊社でRustを長く書いているエンジニアに勉強のコツを聞いてみたのですが、「 The Book は論理から非常によくまとまっているため、何度も読むと良い」というアド バイス をいただきました。本記事のようなテクニック的なところではなく、Rustのコード自体読んでもよくわからないと言う方は、 The Book を読み直すと良いかもしれません。何度読んでも学びがある内容なので、損はないと思います。 この記事を読んでいる皆様が、Rustを楽しんでくれることを願っております! We’re hiring!!! キャディでは、エンジニアを含め全職種積極採用中です! Rustを使って開発がしたい方、会社に興味を持ってくださった方、気になるから話を聞いてみたいという方、ぜひ面談にお越しください。 弊社がRustを採用している背景や、実際に開発してみてのメリットデメリットなどは、 「Rust についてカジュアル面談で頻繁に訊かれる質問と、それに対する個人的な回答」 をご参考ください。 ご応募は こちら カジュアル面談のお申し込みは こちら 募集職種一覧 長文お読みいただき、ありがとうございました!
エンジニアリングマネージャーの村上 (@mura_mi) です。採用関連で面談に出ることが多いのですが、大体7割くらいの確率で 「なんで Rust 使ってるのですか?」「Rust 使っててどうですか」と聞かれるので先回りして書いておこうと思った記事です。 なんで Rust を選んだの Rust をエンジニアリングチームの武器の中心に据える意思決定がされたのは私の入社前ですが、伝え聞いている話しと自分の解釈を混ぜ合わせた話を書きます。 「データ指向アプリケーションを堅牢に作るのに必要な型システムを求めたこと」と、「キャディがもともと C++ の会社だったこと」の2つが、キャディが Rust を使っていることの背景にあったのだと理解しています。 後述しますが、キャディが 原価計算システム やサプライチェーン・マネジメントシステム を Rust を使って開発しはじめたのは、2019年の中頃だったと伝え聞いています。これらのシステムは、「キャディがどんなものを、いくら費やして、どのように製作し、いくらで販売するか」という、キャディのビジネスの根幹となるデータを扱った データ指向アプリケーション です。 扱うデータの複雑さや、常に変化の可能性の下にあるビジネスルールに立ち向かう手段はいくつか考えられるでしょうが、データストアを担う物理層だけでなく、アプリケーション層でデータ形式の制限をすることは常套手段のひとつでしょう。 静的型付けのないプログラミング言語を否定するつもりは毛頭ありませんが、具体的な技術選定に口出しをしない CTO も、”テストをたくさん書くことに時間を使わず、型を書いてバリエーションを自明にしてラクをしよう” とは, しばしば口にします。 ではなぜ、そこで Rust なのか?選択肢に挙がった言語たちをなぜ見送ったのかの具体的な理由を記述することは割愛しますが、選択肢の中に Rust があった背景には、CADDi がもともと C++ の会社だったことが挙げられます。 「CADDi」という社名の由来のひとつには “CAD から Direct” という意味がありました。この記事の執筆時点では、キャディがお客様からいただく設計図の情報は二次元図面がほとんどですが、創業当初のキャディのビジネスの中心にはCADデータの自動解析アルゴリズムがありました。この頃 CAD データの解析に使っていたのが C++ で、江添亮さんには一時期テクニカルアドバイザーになってもらっていた事もありました。 C++ のプログラマから見て、パフォーマンスを犠牲にせずに安全性を手に入れることができるのが Rust です。当時社内で Rust を推した一人である いなむさんのnote を読むと、少しだけ当時の雰囲気が垣間見えるかもしれません。 数多のテック企業が採用候補者を奪いあう戦国時代の中で、採用市場で「え、Rust を業務システムに使っているの?」でアテンションを惹けることは、キャディにとって良い副産物 でした。そもそも C++ と一口に言っても C++17 を 2018 年の段階で実戦投入したりしていた というのもあり、最新の技術を使っている組織だというイメージは強化されたのかなと思います。そういえば、筆者自身も「は?Rust でサービス作るの?」と釣り針に引っかかってカジュアル面談の話を聞きに行った一人でした。 Rust の用途とメリット 主に3つのエリアで使っていたり、使おうとしています。 1つは前述した、原価計算システムやサプライチェーン管理を行う「基幹システム」の開発です。 tonic (一部、まだ tower-grpc を使ってる部分もあります…😫 ) を用いて gRPC API サーバーを建てています。リレーショナルデータベースを接続してのデータ CRUD には、 diesel 上に構築した、データレコードの更新履歴も保存するフレームワークを独自に開発して利用しています。 ブラウザからユーザーがアクセスしたり、RDB のみならず Redis やメッセージキューなど多種多様なミドルウェアと接続されたシステムなので日々様々なトラブルは起きるのですが、Rust 自体が不明瞭な挙動を引き起こしたり、パフォーマンス劣化に悩まされる事象は見た記憶がありません。 (ビジネスルールの設計や実装に起因する問題だったり、データベースに異常な負荷を掛けてしまうケースが多い) 2つ目は、現在のキャディのビジネスの中心を流れる 「二次元図面」の解析を行うアルゴリズム の開発です。これに関しては、図面解析チーム (orama) のテックリードを務める寺田の記事 がとても良いので読んでほしい… のですが、Rust に関して彼の言っていることの抜粋が以下です。 Rust は目新しさがあるかもしれません。Rust は安全性とスピードを兼ね備えているので、パフォーマンスが求められるアルゴリズムの開発には本当に適していると実感しています。Python + OpenCV だけで賄えない理由は、ベクターデータの処理が必要になるからです。上で紹介した表の罫線認識では、まず画像をベクターデータに変換してから様々なアルゴリズムを組み込んでいます。ベクター化アルゴリズムを含めた各種のアルゴリズムの実装に、Rust が活躍しています それ以外に、まだ実戦投入していませんが、WASM を用いたブラウザアプリケーションの開発に利用できないかとフロントエンドエンジニア陣が試行錯誤をしています。ブラウザ上で2次元図面のデータを表示したり、その図面への注記の追加をブラウザ上でできるようにする (社内では 図面版Figma と呼ばれていたりします) ようなアプリケーションの開発が視野にあるのですが、このようなアプリケーションの開発にはどうしてもパフォーマンスを追求したくなるケースが出てくると思っており、そのときに備えて様々な技術調査をしています。 Rust を仕事で使って、「頑張らないといけない」ところ 現場で Rust をこれからも使い続けていくに際し、組織的に頑張らないといけない点もいくつかあります。 まず思い当たるのが ビルドや CI に時間が掛かる点。開発者にパワフルなマシンを貸与するという “札束勝負” もしつつ、並行ビルドが効くような工夫 も頑張っています。 独特な書き味であったり、ライフタイム、所有権など他の言語では馴染みのない概念も多く、新規参入メンバーに各種知識のキャッチアップをしてもらうのはどうしても大変です。 しかし、私達のチームは「誰もが Rust のキャッチアップを頑張る道を通った経験がある」ことが強みであると思っていて、より一層「学習の高速道路」を整備しなきゃいけないなと思っています。 言語仕様や性質から来る制約という観点では、外部通信のような比較的大きい副作用が絡むテストに於いて テストダブルを差し込むことは可能なのですが、かなりの労力が必要になる印象を持っています。これは、Rust の場合コンパイル時に Dependency Injection される具体的なデータ型が指定されている必要があることに由来しています。 (キャディでは CakePattern を使った DI を実装しています) 同様の理由で、The Clean Architecture の「domain のレイヤをピュアに保つ」という方針を徹底しきれないな、と思うこともしばしばあります。 とはいえ、長所と短所 を天秤に乗せても、ビジネスの基幹となる情報システムを、硬く・速く動くようにするという意味では良い選択肢だと思っているので、もっとうまく使いたいと思っているところです。 キャディの人って元から Rust 書ける人ばっかりなんですか? 否。 入社前から Rust を経験していたメンバーもいますが、 「がっつりチームで Rust 使った開発してた!」「ウェブアプリケーションの API サーバーを Rust で開発・運用していた」という人はいない はずです。皆それぞれのバックグラウンドをもって入社しています。先述したように、誰もがキャディに入社してから Rust 周辺についてキャッチアップをしてきた経験値を持っていることが、チームの強みなんじゃないかと思っています。 実際、 採用活動の中で候補者の方とお話する際には、Rust の経験があればもちろん良いのですが、それ以外にどういう経験してきたかを重視しています。サーバーサイドアプリケーションの開発をメインの領域とする自分からすると、しっかりとドメインモデリングに向き合ってきた経験や、他の言語で Clean Architecture をイチから書いたり、理解して使ってきた経験の方が重要だなーと思っています。 一方で、赤裸々なことを書くと、実際に社内の Rust のコードを読んでいても、API の設計をする際に参照と実態の区別をしっかりつけられていない (よって無用な .clone() の必要が生じる) ケースも結構見かけ、まだまだ Rustacean の集団として成熟しないといけないなぁと思うこともあります。 We’re hiring てなわけで、社内で書くコードが Rust オンリーというわけではないけど、キャディは「書きたい」と言えば十中八九 Rust を書ける環境だと思います。そんなCADDi の仲間になってくれる方を募集しています。「すぐ転職する感じじゃないんだけど Rust の話は聞きたい!」「Rust 書いたことないけど興味あります!」みたいな方でもお気軽にカジュアル面談からお申し込みください。 応募はコチラ https://caddi-careers.studio.site/jobs-tech-backend からどうぞ。 The post Rust についてカジュアル面談で頻繁に訊かれる質問と、それに対する個人的な回答 appeared first on CADDi Tech Blog.
はじめに こんにちは。キャディで原価計算システムの開発を担当しております、高橋です。 この記事は キャディ Advent Calendar 2020 の23日目です。前日は朱さんの 「【開発カルチャー発信 vol.1】原価計算システム開発チームの開発理念を大公開!」でした! さて本日は掲題の通り、私がスキルアップを兼ねて趣味的に取り組んでいる、コストモデル可視化システムの開発について紹介させていただきます。 目次 課題意識 弊社のビジネスの核は、コストモデル コストモデルは、名前の通り「コスト」の計算を「モデル」化したことで、原価計算という作業を弊社内で民主化しました。「正しい原価」を誰でもすばやく計算できるということです。 これが無くては弊社のビジネスがスケールアウトすることは不可能であり、スケールアウトしなければ受発注プラットフォームは作れません。従って、弊社のビジネスの核は1にも2にもコストモデルなのです。 コストモデルは生き物 しかしこの「正しい原価」というのが曲者です。正しいの定義は時々刻々と変わります。従って、コストモデルはこの正しさに常に追従する必要があります。 例えば材料費が高騰したら、原価は変わります。また、弊社が今まで対応していなかった新しい加工方法をコストモデルで取り扱えるように拡張しないと正しい原価が出せない場合もあります。このような事情で、コストモデルは常に改訂・拡張を繰り返しながら、あるべき原価を求めてさまよい続けています。 しかし、コストモデルは見えにくい このように、弊社ビジネスの核でありながらも常に動き続けるコストモデルですが、現状はRustのソースコードで実装されています。従って、コストモデルを常日頃からメンテナンスしている立場でない限り、具体的にコストモデルの定義がどうなっているのか把握しにくいというデメリットがあります。 コストモデルを作るのではなく扱う立場であっても、コストモデルが前提とするコストの構造・概念を理解していることは重要なのですが、ここで「コストモデルが見えにくい」ということが障壁になっていると私は考えています。 コストモデルをどうやって可視化するか 可視化したいものがコードの中にしか無い 上記の通り、現状ではコストモデルの定義はRustでハードコーディングされています。 こんな感じのものがたくさんコーディングされています。 コストモデルの定義の例(体積計算): 原価 = 加工時間 × 時間当たり原価 加工時間 = 加工工程 XXX の時間 + 加工工程 YYY の時間 時間辺り原価 = とある定数 加工工程 XXX の時間 = XXX加工単体の時間 × XXX加工の数 加工工程 YYY の時間 = XXX加工単体の時間 × XXX加工の数の2倍 抽象的に書いていますが、例えば穴あけ加工が1個1分かかってそれが2箇所、1分あたり100円なら、概算で200円、みたいなことを考えるとわかると思います(実際にはこんな単純ではなく、もっと複雑です)。 ここから可視化しようと思うと、Rustのコードをパースしてコストモデルの定義を抽出してくるような仕組みが必要ですが、あまり現実的ではありません。 コストモデルの定義をデータとして分離する コストモデルは数式のグラフ 上記の例から分かるように、コストモデルは数式の集合であり、それらは依存関係を持つことから、数式や定数(引数を持たない数式)をノードとする非循環有向グラフ(DAG)を考えることができます。これを数式グラフと呼ぶことにしましょう。 上記の定義を数式グラフとして表現した例: document.write("graph TD;\n原価-->加工時間;\n原価-->時間当たり原価;\n加工時間-->加工工程XXXの時間;\n加工時間-->加工工程YYYの時間;\n加工工程XXXの時間-->XXX加工単体の時間;\n加工工程XXXの時間-->XXX加工の数;\n加工工程YYYの時間-->XXX加工単体の時間;\n加工工程YYYの時間-->XXX加工の数;\n"); 例えば上図のように表現された数式グラフを計算する場合は、依存の階層の一番下から順番に計算していくと、最終的に原価を求めることができます。コストモデルの定義がどのようなものであれ、計算可能な関数と依存関係の集合であれば可能なことです。 グラフ構造をデータとして計算処理から分離できる さてこのように考えていくと、コストモデルの定義は数式グラフの中にしか登場せず、数式グラフの計算処理には登場しません。したがって、現状のようにプログラム中にコストモデルの定義をハードコーディングせずに、どこかにデータとして保存された数式グラフを計算実行処理に外側から注入して結果を得る、というやり方ができそうです。要は、コストモデルの定義を数式のグラフ構造のデータとして、計算処理から切り離すということです。 このやり方であれば、グラフ構造の保存・編集・可視化ができれば、コストモデルを可視化できると言えそうです。Rustのプログラムをパースしてコストモデルの定義を解析するよりも、大分現実味があります。 技術選定とシステム構成 上記のやり方を試すために、Frontend, backend, db を直列につないだ極めてシンプルな構成でシステムを組んでみることにしました。 component 仕様技術、ライブラリなど 役割 Frontend React + Typescript , G6.js, typed-rest-client 数式グラフと計算結果の表示 Backend Java + Spring boot + Spring Data Neo4j, mxParser 数式グラフのロードと計算 DB Neo4j 数式グラフの保存 まず、数式グラフを保存する手立てとして、グラフ構造をそのまま扱えるグラフ指向データベースを使うことにしました。とりあえず今回は、一番有名っぽいNeo4jにしました。 すると、Neo4jとエンティティ定義のマッパー(ObjectGraphMapping)に対応したフレームワークであるSpring Data Neo4j を使うのが一番楽につくれそうなので、バックエンドはJavaに決定。 バックエンドでは数式グラフを計算するので、Neo4jに文字列として保存された数式(簡単のために算術演算に限定)を動的にパースして計算する処理が必要になるのですが、そのようなライブラリをJavaから探した結果、mxParserというのがあるので使ってみました。「キャディのエンジニアなんだからパーサーくらい自分で書け」と言われそうですが、今はサクサク作って動かしたいので、あるものは最大限活用します。 UIは、業務上のスキルアップも兼ねてReact + Typescript を使うことにしました。グラフ構造を描画するUIライブラリとしては、G6.js を使ってみました。弊社で使用実績はなさそうなものの、MITライセンス・機能が豊富・公式ドキュメントが充実の3点で決めました。バックエンドのAPIを叩くクライアントは、本家が作ってて信頼できそうなのでtyped-rest-clientに(適当)。 実装 今回は簡単のため、UIからのコストモデルへのパラメータの入力は受け付けないものとします。 また、コストモデルの編集は実装が多いので、ここでは割愛します。 データベースに予め数式の定義と入力が保存されている状況から、計算と表示ができるところまでを紹介します。 特に難しいことはしていないので、同じものはこの記事を読みながらどなたでも作れると思います。 Backend ここでは、この記事のために「数式グラフのロード」「数式グラフの計算」の2つのAPIを用意してみます。 Entity定義 ノードに数式をもたせて、ノード間のエッジに、数式同士の依存関係と、依存先の数式が対応する依存元の数式中の変数名を保持させます。 SpringDataNeo4j のおかげで、クラスにアノテーションを付けるだけでグラフの構成要素として設定できます。 @NodeEntity public class ExprNode { @Id @GeneratedValue public Long id; public String name; public String expr; @Relationship(type = "SUBEXPR", direction = Relationship.OUTGOING) public ArrayList subExpressions = new ArrayList (); // 依存先の数式を定義する関数 // 第二引数で、数式中のどの変数に第一引数の数式の評価結果を割り当てるかを // 指定している public boolean setSubexpr(ExprNode node, String token) { Expression expr = new Expression(this.expr); this.subExpressions.add(new Edge(this, node, token)); } // 自身の値を計算する関数。 // 関数の引数の値が別の関数で求まる場合、再帰的に潜っていって計算する。 // Expression は mxParser が提供する数式型。 public Double evaluate() { // ここで文字列の数式をパースして関数を生成すると同時に、関数の引数に依存先の関数の評価結果を割り当てる Expression expression = new Expression(this.expr, this.subExpressions.stream() .map(edge -> new Argument(edge.startToken, edge.end.evaluate())).toArray(Argument[]::new)); return expression.calculate(); } } @RelationshipEntity(type = "SUBEXPR") public class Edge { @Id @GeneratedValue public Long id; @StartNode public ExprNode start; @EndNode public ExprNode end; // startnode の数式のどの 変数名に endnodeが対応するかを保存するフィールド public String startToken; } Repository Neo4jにアクセスするインターフェースを定義し、数式グラフをロードしてくる関数を定義します。 これもSpring DataNeo4jの恩恵を受けることが出来て、interface さえ定義すれば実装はSpringが勝手に作ってくれます。 public interface ExpressionRepository extends Neo4jRepository { // cyper query を直接書くこともできる @Query("MATCH p=(n:ExprNode)-[:SUBEXPR *]->(:ExprNode) RETURN nodes(p), relationships(p)") ArrayList getAllExprNodesWithEdges(); // query を書かない場合、実装は関数名から自動で定義される // デフォルトではグラフの深さ1までしか取ってきてくれないので、アノテーションで指定する @Depth(value=4) Option findByName(String name); } Service 作りたいAPIには、DBへの数式グラフのシード、数式グラフの取得、数式グラフ計算の3つの処理が必要なので、それをここで用意します。 @Service @Transactional @EnableNeo4jRepositories(basePackageClasses=ExpressionRepository.class) public class ExpressionService{ @Autowired ExpressionRepository expressionRepository; // 数式全体の取得 // 戻り値は適当に用意したResponse型 public ExpressionsResponse getAllExpressions() { ArrayList nodes = expressionRepository.getAllExprNodesWithEdges(); ArrayList nodeResponses = nodes .stream() .map(node -> { return new NodeResponse(node.id, node.name, node.expr); }).collect(Collectors.toCollection(ArrayList::new)); ArrayList edgeResponses = nodes .stream() .flatMap(node -> { return node .subExpressions .stream() .map(edge->{ return new EdgeResponse(edge.id, edge.start.id, edge.end.id, edge.startToken); }); }).collect(Collectors.toCollection(ArrayList::new)); return new ExpressionsResponse(nodeResponses, edgeResponses); } // 架空の体積計算を表す数式グラフを保存してみる public String seedExpressions() { // 数式ノード定義 ExprNode volume = new ExprNode("volume", "x * y * z"); ExprNode x = new ExprNode("x", "10"); ExprNode y = new ExprNode("y", "10"); ExprNode z = new ExprNode("z", "a + b"); ExprNode a = new ExprNode("a", "20"); // 定数 ExprNode b = new ExprNode("b", "30"); // 定数 // 数式中の変数に別の数式を割り当てる。 volume.setSubexpr(x, "x"); volume.setSubexpr(y, "y"); volume.setSubexpr(z, "z"); z.setSubexpr(a, "a"); z.setSubexpr(b, "b"); expressionRepository.save(volume); return volume.name; } // 数式グラフ中の指定した数式ノードの値を計算する public double calculateExpression(String name){ ExprNode rootNode = expressionRepository .findByName(name) .orElseThrow(() -> new RuntimeException()); return rootNode.evaluate(); } } Controller RestAPI経由でそれぞれのビジネスロジックを呼んでResponseを返すようにします。 @RestController @RequestMapping("/") // UI は yarn start で立てるので、そのアドレスをcorsに設定 @CrossOrigin(origins = "http://localhost:3000") @ResponseBody public class Controller { @Autowired ExpressionService expressionService; // 数式グラフをシードしてロード @RequestMapping(value = "/expressions", method = RequestMethod.GET) public ExpressionsResponse readExpressions(@PathVariable String version) { expressionService.seedExpressions(version); return expressionService.getAllExpressions(version); } // 数式グラフの計算 @RequestMapping(value = "/calculate", method = RequestMethod.GET) public String calculateSeededNode(@PathVariable String version) { // DB初期化 String rootNodeName = expressionService.seedExpressions(version); return String.format("calclation finished: %f", expressionService.calculateExpression(rootNodeName, version)); } }; UI こんな感じで、Reactのコンポーネント内でG6.jsのグラフオブジェクトを初期化した後、バックエンドからグラフ取得したグラフのデータを流し込んで描画します。(長いので一部省略しています) G6.js は pureJS ライブラリなので、Reactに組み込むのがちょっと手間です。公式のサンプルを元に実装しましたが、そのままだと警告が出るので一部手を加えました。 function GraphView() { const ref = React.useRef(null); const graph = React.useRef (null); useEffect(() => { if (!graph.current) { graph.current = new G6.Graph({ container: ReactDOM.findDOMNode(ref.current) as HTMLElement, layout: { type: 'dagre', // 有向グラフを階層的にレイアウトするためのアルゴリズム rankdir: 'LR', // レイアウトの向きを指定 ranksep: 70, // レイアウト方向のノード間隔を指定 }, // グラフ上で描画されるノードのデフォルト設定 defaultNode: { type: 'modelRect', anchorPoints: [ [0, 0.5], // source [1, 0.5], // target ], // ...その他設定は省略 }, // グラフ上で描画されるエッジのデフォルト設定 defaultEdge: { // ノードのアンカーポイントのどれとどれをつなぐかを // defaultNode の anchorPointsのindex指定で設定 sourceAnchor: 1, targetAnchor: 0, }, }); } let rest: trc.RestClient = new trc.RestClient('test', 'http://localhost:8080/'); rest .get (`expressions`) .then((res: trc.IRestResponse ) => { let data: GraphData = { nodes: res.result!.nodes.map(/* G6.js のノードの形式に変換*/), edges: res.result!.edges.map(/* G6.js のエッジの形式に変換*/), }; graph.current!.data(data); graph.current!.render(); }); return () => { graph.current!.destroy(); graph.current = null; } }, []); return ; } 起動 以上作ったものをローカルで起動させてみました。 まず、DBは公式ドキュメントに従うとDockerコマンド一発で立ち上がります。 $ docker run -p7474:7474 -p7687:7687 -e NEO4J_AUTH=neo4j/s3cr3t neo4j backend と Frontend はそれぞれ、gradle bootRun, yarn start として起動しました。 起動してみるとこんな感じです。簡素な画面ではありますが、数式のグラフを可視化できています。 上のコードでは省略していますが、G6.jsのプラグインで、画面左上に計算処理を叩くボタンを用意して、押すと計算結果を alertするようにしてみました。 volume = 10 * 10 * (20 + 30) = 5000 なので、確かにちゃんと計算できています。 これで一応、数式グラフの保存・ロード・表示まではできたことになります。 今後はもう少し作り込んで、あわよくば仕事につながったら面白そうに思っています。 おわりに 今回は、コストモデルの背景や課題に加え、それをReactとNeo4jを用いて可視化する試みをご紹介しました。 私は元々CADアルゴリズムグループとして採用されたので、Webエンジニアは初めてまだ1年程です。しかし、この記事のような小さなシステムを自分で一から一通り作ってみると、中々いい勉強になりました。 また、ここに書いた実装は私1人では達成し得なかったことで、色々な方のアドバイスを経ながらできたものです。弊社にはこのように一緒に技術を楽しんでくれるメンバーが揃っておりますので、興味を持っていただいた方は、ぜひご連絡いただければと思います。 The post React + Neo4j によるコストモデル可視化の取り組み紹介 appeared first on CADDi Tech Blog.
こんにちは、キャディでバックエンドエンジニアをしている朱です。 この記事は CADDi Advent Calendar 22 日目の記事です。昨日は、高藤さんによる「 tracing crateを利用したRustのlogging方法について 」でした! 今回は、私が所属している原価計算システム開発チームの開発理念についてお話ししようと思います。 この記事を通して キャディの開発チームに息づくカルチャーの一端を感じ取っていただければ 幸いです! (タイトルを勝手に vol.1 としていますが、反響が良ければきっと企画化されるはずでしょう…) [toc] 開発理念を定めた経緯 はじめに、私たちのチームで開発理念を定めるに至った経緯について簡単に触れておきます。 原価計算システム開発チームは現在、総勢9名(内 PM が1人、デザイナーが1人、残りがエンジニア)で社内で最大規模の開発チームです。 今年6月のプロダクトリリースを経て運用体制が一定周り始めたタイミングかつ、メンバーのアサイン変更や新たな仲間を迎えるにあたって、 これまでのチームのスタンスを保つ/発展させていくために最低限の明文化を図った というのが策定の背景です。 また、PM が他部署との兼任になったというのも理由の1つでした。 それまではユーザーとのコミュニケーションやそこから得られた示唆をもとにした意思決定は PM が主体となって行っていましたが、チームとして開発のスピードを落とさないためにも、なるべくエンジニア主導でのコミュニケーションや意思決定の比率を上げていく必要性がありました。明文化された理念は、こういった行動を取る際のある種の拠り所としても機能しています。 5つの理念 さて、ここからはチームに存在する5つの開発理念をそれぞれ深掘っていきたいと思います。 (社内外の)ユーザーからの信頼、期待値を毀損せず常に高めるべく努力すること 5つの中で最も重要かつ守るのが難しいのがこの一文だと感じています。 信頼、期待値というのは得てして積み上げるのは簡単ですが崩れ去るのは一瞬だからです。 プロダクトにとっての期待値の表れの1つに、ユーザーからの「こういう機能が欲しい!」という要望があります。 大前提として、 いただいたフィードバックは全て資産であり、チームとしては可能な限り要望に答えてプロダクトをより使いやすくしたい と考えています。(5つ目の理念が正にそれを明文化したものです) しかし、現実には開発リソースは有限なので中にはすぐには答えられない要望もあります。だからこそ、そのような要望をくれたユーザー1人ひとりにリスペクトを持って 「なぜ今その機能の開発に取り組まないのか/いつになればその負は解消されるのか」を丁寧に説明する ように心掛けています。 その上で、待望の機能をリリースした暁には Wow を届ける = 期待を上回る価値を提供して初めて、ユーザーからの信頼は保たれるのだと思うのです。 ユーザーに利便性を生むこと。利便性とはなにかをやらなくて良くなること 2つ目の理念は非常にシンプルで分かりやすいですね。 10X の矢本さんが 同じような趣旨のツイート をされていましたが、弊社のような toB の領域でプロダクトを提供している会社では特に意識する必要がある考え方だと思っています。 我々が挑んでいる課題は必ずしもプロダクト (= ここでは狭義のプロダクトでシステムの意) だけでは解決し得ないもの であり、両輪のもう片側である"ヒト"がより本質的な業務に時間を使えるようにするためにも、 プロダクトは出来る限り「やらなくて済むこと」を増やすのに心血を注ぐべき なのだ、と解釈しています。 何かをリリースする、変える上での説明は丁寧すぎるレベルでやること 私たちが作っているのは社内のオペレーションで用いる業務システムであり、 プロダクトの変更がすなわちオペレーションの変更に繋がります 。 もし、変更内容が社内に伝わりきらずにオペレーションが古いまま運用されてしまうと、我々にとってのエンドユーザーであるキャディの顧客・SPにまで間違った情報が渡ってしまうというリスクがあります。実際に、過去にはリリース内容の周知が不足していたことでバグの検知が遅くなり、顧客に間違った価格が流出しかけたという事象がありました。こうした背景を踏まえて、3つ目の理念は付け加えられています。 また、直近ではプロダクトの機能拡張によりステークホルダーが社内外に徐々に広がっており、今までよりも一層この理念を意識する必要が出てきています。長くなりがちなリリース内容をなるべくスムーズに認知してもらうための取り組みとして、 告知文に絵文字を用いることで関心を持ってもらおうというUXライティング的な動き も草の根的に始まっています。 Quipu というのが原価計算システムの名前で、インカ帝国で使われたそろばんのようなものが由来です(実は本邦初公開!?) リリースした後はリアルタイムにユーザーのそばでフォローすること。そして自分たちの目と耳で課題を見つけること 3つ目とも関連しますが、リリース時の周知の徹底だけでなくリリース後のフォローも大切な要素です。 これを実践するために、私たちのチームでは 週に一度ユーザーの業務を見学する会を行っています 。 見学会では、原価計算システムとその他の業務システムを用いた一連のオペレーションの様子を画面で共有してもらい、主にUX面での課題がないかを観察しています。やはり実際にプロダクトが使われているのを目にすると、思いもよらぬところで躓いてしまったり本来不要なはずの作業を繰り返してしまっていたり、当初のUX設計では考慮できていなかった点があぶり出されることが多いです。 このように一次情報を絶えず取得できる仕組みがあるのは開発側としては願ったり叶ったりの状態ですが、ユーザーにとっては負担にもなり得る取り組みを受け入れてもらっているのは、ひとえに日頃の信頼関係が築けているからこそだと思っています。 加えて、大きなリリースがあった場合はアドホックに説明会を開いたり、課題がないか個別でヒアリングしたり、 サポート体制には積極的に工数を割くようにしています 。 もらったフィードバックすべてに感謝しリスペクトをもって扱うこと。そして基本なるはやで対応すること。しかし、そのまま鵜呑みにして対応するのではなく、本質的に必要な対応を行うこと 最後の理念もユーザーからのフィードバックにまつわるものです。 フィードバックに対するチームのスタンスについては先述した通りなので、ここではもう少し具体的な話をしようと思います。 試しに直近1ヶ月のユーザーからのフィードバック件数を数えてみたところ、平均すると週に6-7件ほど要望が来ていることが分かりました。これは 対ユーザー数比率で言うと全体の約3割の方が週に一度はフィードバックをくださっている 計算になります。 また解決までのリードタイムについては、いただいた要望の内軽微な修正でUX向上が図れるものの多くは次のSprintでの解消を、中期的に取り組む必要があるものについても長くて1ヶ月程度のスパンで解消してきています。(中にはいち新機能として開発が必要になる要望も含まれるので、さすがにそれは本腰を入れて長期的に対応します) では、ユーザーの要望を取り入れ続けていわゆるキメラ的な開発を続けているのか?と言われると、そうではないと思っています。実際にユーザーの本質的な課題を特定して解決した事例として、先月行った とある機能を廃止した事例 を取り上げてみましょう。 原価計算システムには入力した諸々のデータを確定させ以降は編集できなくする、いわゆる「ロック機能」が存在しました。これは機能を設計した当初には製造業のバリューチェーンをウォーターフォール的に捉えていたために、後工程にデータを受け渡す際には情報を確定させることで不確実性を下げることを意図したものでした。 しかし、蓋を空けてみると実際のオペレーションではロックを行ったあとにも情報を修正することは度々行われており、本来はよりアジャイル的な情報の扱いができるUXを指向すべきだったことが明らかになります。もちろん、ロック後にもデータを修正できる手段としてバージョン管理の機能もセットで提供していたのですが、キャディとして取り扱う案件の規模が拡大するにつれて修正にかかるインタラクションコストが無視できないものになってきました。 ユーザーからはバージョンアップにかかる手間を削減できないか?という問い合わせが繰り返し寄せられましたが、チームで「元々この機能で解決したかった課題は何なんだっけ?」と議論を重ね、結果としてロックのタイミングの後倒しと改修が完了するまでの一時的な機能の停止を決定しました。 以上のように、 UXを毀損しているようなケースではたとえそれが時間をかけて開発した機能だとしても躊躇なく捨てる意思決定が出来ること が「本質的に必要な対応」なのだと考えています。 一緒にチームのカルチャーを育んでいく仲間を募集中! いかがだったでしょうか?冒頭でも述べましたが、この記事を読んでキャディの開発チームのカルチャーに興味を持っていただけた方が少しでもいらっしゃれば嬉しい限りです。 今回取り上げた開発理念はまだまだ発展途上のもので、 これからも既存のチームメンバーや今後キャディにJoinしてくださる皆さまの手によって育まれていくもの だと思っています。控えめに言っても最高のチームが集まっているので、まずは話しを聞いてみたいという方も Twitter などで気軽にご連絡いただければ幸いです! 明日はそんな原価計算システム開発チームの高橋さんによる「React + Neo4j によるコストモデル可視化の取り組み紹介」です。お楽しみに! キャディではエンジニア・デザイナー・PMなど幅広い職種の皆さまを募集しています! エンジニア採用サイトは こちら からどうぞ!
こんにちは。CADDi でバックエンドエンジニアをしている 高藤 です。 この記事は CADDi Advent Calendar 21日目の記事です。昨日は、寺田さんによる RustでRAMの動作原理をシミュレートする でした! 今回はRustのtracintg crateについて紹介したいと思います。 目次 はじめに キャディではバックエンドのAPIをgRPCを使って実装しています。 実装にはtonicというRustでは比較的新しいcrateを使っています。使いやすいこともあり比較的使ってみたなどの記事は散見されるのですが、今回は本番環境で運用するのに大事なloggingの観点で説明をしたいと思っています。 gRPCサーバの実装 まずはtonicのサンプルを紹介します。以下のコードはtonic にあるexamples/src/helloworld/server.rsをもとに説明を行います。 まずコードを見てみましょう。非常にシンプルなサーバです。Requestに名前を含めると返事をしてくれるそれだけのサーバですが、まずはこのコードを使っていくつか検証をして行こうと思います。 use tonic::{transport::Server, Request, Response, Status}; use hello_world::greeter_server::{Greeter, GreeterServer}; use hello_world::{HelloReply, HelloRequest}; pub mod hello_world { tonic::include_proto!("helloworld"); } #[derive(Default)] pub struct MyGreeter {} #[tonic::async_trait] impl Greeter for MyGreeter { async fn say_hello( &self, request: Request , ) -> Result , Status> { println!("Got a request from {:?}", request.remote_addr()); let reply = hello_world::HelloReply { message: format!("Hello {}!", request.into_inner().name), }; Ok(Response::new(reply)) } } #[tokio::main] async fn main() -> Result > { let addr = "[::1]:50051".parse().unwrap(); let greeter = MyGreeter::default(); println!("GreeterServer listening on {}", addr); Server::builder() .add_service(GreeterServer::new(greeter)) .serve(addr) .await?; Ok(()) } 挙動を確認するためにこちらのServerをまずは実行してみます。 ❯ cargo run --bin helloworld-server` サーバを起動すると GreeterServer listening on [::1]:50051 このように表示されてサーバが起動します。 このままRequestを送るためgrpcurlを使って見ます ❯ grpcurl -plaintext -d '{"name": "foo"}' -proto proto/helloworld/helloworld.proto -import-path ./proto localhost:50051 helloworld.Greeter/SayHello { "message": "Hello foo!" } サーバ側 Got a request from Some([::1]:58752) ちゃんと動いていますね。 あくまでサンプルですが、このコードにDBとの接続処理やロジックを記述していくことでサービスを提供できそうです。ですが、本番でちゃんと運用するにはログをちゃんと出力しないと難しいです。 上記の例では標準出力にprintln!を使ってRequestが来たことは出力されていますがtimestampもなくいつ処理されたものなのかもわかりません。 本番環境での運用を考えてgRPCサーバのloggingについて考えて見ようと思います。 env_loggerの利用 Rustではログの出力を行うためログ出力機能が抽象化されたlogcrateとその実装crateが存在しています。crates.ioでも上位にあるenv_loggerを使ってログの出力を行ってみます。 まず、Cargo.tomlに対して依存するcrateの追加を行います。dependenciesに以下の2つのcrateを追加します。 env_logger = "0.8.2" log = "0.4.11" env_loggerの初期化 main関数部分でenv_loggerの初期化処理を追加します。あわせてprintln!を使って標準出力を行っている部分をlog::info!に書き換えて見ましょう #[tokio::main] async fn main() -> Result > { // ここを追加 env_logger::init(); let addr = "[::1]:50051".parse().unwrap(); let greeter = MyGreeter::default(); log::info!!("GreeterServer listening on {}", addr); Server::builder() .add_service(GreeterServer::new(greeter)) .serve(addr) .await?; Ok(()) } こちらを改めて起動して確認をします。 起動直後 [2020-11-06T16:25:56Z INFO helloworld_server] GreeterServer listening on [::1]:50051 Requestの送信時 [2020-11-06T16:26:43Z INFO helloworld_server] Got a request from Some([::1]:34238) 無事timestampやlogレベルをあわせて出力することが出来ました。 ただし、これだけだと処理の内容がわからずlogを出力する意味があまりない状態なので処理の終了時にRequestの内容と処理が終わった旨を出力するように修正してみます。 #[tonic::async_trait] impl Greeter for MyGreeter { async fn say_hello( &self, request: Request , ) -> Result , Status> { let reply = hello_world::HelloReply { message: format!("Hello {}!", request.get_ref().name), }; log::info!("Request: {:?}, Done", request); Ok(Response::new(reply)) } } 実行結果 [2020-11-06T16:35:39Z INFO helloworld_server] GreeterServer listening on [::1]:50051 [2020-11-06T16:35:40Z INFO helloworld_server] Request: Request { metadata: MetadataMap { headers: {"content-type": "application/grpc", "user-agent": "grpc-go/1.30.0", "te": "trailers"} }, message: HelloRequest { name: "foo" }, extensions: Extensions }, Done これで意図したとおりに処理が終わった旨の出力とRequestの内容が表示されるようになりました。 もう少し実用的なアプリケーションを想定して処理を追加してみます 冒頭のimport宣言にCodeを追加します。 use tonic::{transport::Server, Code, Request, Response, Status}; 何かしらの処理を行う関数some_logic()の追加とそれを利用するようにsay_hello()メソッドの修正を行います。 #[tonic::async_trait] impl Greeter for MyGreeter { async fn say_hello( &self, request: Request , ) -> Result , Status> { let reply = hello_world::HelloReply { message: format!("Hello {}!", some_logic(&request.get_ref().name).await?), }; log::info!("Request: {:?}, Done", request); Ok(Response::new(reply)) } } async fn some_logic(name: &str) -> Result { log::info!("run some logic"); match name { "foo" => { log::error!("Failed some_logic"); Err(Status::new(Code::InvalidArgument, "who is foo")) } _ => Ok(name.to_string()), } } Requestに含む名前によってはエラーを出力するように修正を行いました。サーバを起動し、先程と同様にgrpcurlでRequestを投げると以下のような出力を得ることが出来ます。 [2020-11-07T00:39:25Z INFO helloworld_server] GreeterServer listening on [::1]:50051 [2020-11-07T00:39:36Z INFO helloworld_server] run some logic [2020-11-07T00:39:36Z ERROR helloworld_server] Failed some_logic 想定している通り失敗した時にERRORログが出力されることが確認できました。 しかしこの方法だと問題があります。 ERRORログにRequestの情報がないので複数のRequestを受けている時にどのRequestがエラーになったのか判断出来ない 今回の処理は全てasync fnにより非同期に実行されるため、ログの出力に1つのRequestからなる処理の内容がが混ざって表示される 愚直に問題を解決させるなら、RequestやRequestヘッダーにRequesを識別できるIdを含めてそれをsome_logic()関数に渡すことで解消はできます。 async fn some_logic(request_id: RequestId, name: &str) -> Result ただしこのやり方では更にlogicが複雑になったときなどに全てのlogicに対してRequestや識別子を持ち回す事を行わないと実現することが出来ません。 このような問題を解決するためにtracing crateを利用することが出来ます。 tracing crateの利用 Cargo.tomlにはすでにtracingの依存が含まれている状態なので、修正はserver.rsのみとなります。 #[derive(Default, Debug)] pub struct MyGreeter {} #[tonic::async_trait] impl Greeter for MyGreeter { #[tracing::instrument] async fn say_hello( &self, request: Request , ) -> Result , Status> { let reply = hello_world::HelloReply { message: format!("Hello {}!", some_logic(&request.get_ref().name).await?), }; tracing::info!("Done"); Ok(Response::new(reply)) } } async fn some_logic(name: &str) -> Result { log::info!("run some logic"); match name { "foo" => { tracing::error!("Failed some_logic"); Err(Status::new(Code::InvalidArgument, "who is foo")) } _ => Ok(name.to_string()), } } #[tokio::main] async fn main() -> Result > { tracing_subscriber::fmt() .with_max_level(tracing::Level::INFO) .init(); let addr = "[::1]:50051".parse().unwrap(); let greeter = MyGreeter::default(); log::info!("GreeterServer listening on {}", addr); Server::builder() .trace_fn(|_| tracing::info_span!("gRPC server")) .add_service(GreeterServer::new(greeter)) .serve(addr) .await?; Ok(()) } いくつかの修正を行っているため、修正点を列挙します。 MyGreeterにDebug traitを実装 say_helloメソッドに#[tracing::instrument]を追加 log::info, log::errorとしている部分をそれぞれtracing::info, tracing::errorとなるように修正 main関数で初期化していたenv_loggerの初期化処理を削除し、tracing_subscriberの初期化処理を追加 このコードを実行し先程のエラーが起きるRequestを送信するとログの出力が以下のようになります Nov 07 09:57:42.891 INFO helloworld_server: GreeterServer listening on [::1]:50051 Nov 07 10:02:51.585 INFO gRPC server:say_hello{self=MyGreeter request=Request { metadata: MetadataMap { headers: {"content-type": "application/grpc", "user-agent": "grpc-go/1.30.0", "te": "trailers"} }, message: HelloRequest { name: "foo" }, extensions: Extensions }}: helloworld_server: run some logic Nov 07 10:02:51.585 ERROR gRPC server:say_hello{self=MyGreeter request=Request { metadata: MetadataMap { headers: {"content-type": "application/grpc", "user-agent": "grpc-go/1.30.0", "te": "trailers"} }, message: HelloRequest { name: "foo" }, extensions: Extensions }}: helloworld_server: Failed some_logic 実際にログを出力しているsome_logic()関数内にはRequestの情報は渡していないにも関わらずログの出力にRequestの情報など付与されるようになりました。 どのような仕組みになっているのか少し説明をします。 tracing crateは In−Process Tracing機能を提供するcrateとなります。Microservice等の分散処理システムの文脈ではJaeger、Zipkinを始めとする分散トレーシングという技術を利用してどこのサービスからどのサービスへ通信がされたか、その処理時間はなどメトリクスを取得することが出来ます。tracing crateも同様にプロセス内部の処理を追跡できるような形で記録する仕組みを提供しています。 仕組みを理解する上で重要になるのが以下の3つの要素となります。 Span 処理を記録する期間を表します 名前やあわせて記録しておきたい情報を保持することができる Event Spanに記録するトレースしたい事象を表します 発生した事象を記録したい情報とあわせて保持することが出来ます Subscriber Spanや紐付いたEventを収集するための処理を表します 今回の例を上記3つの要素を明確に使ってsay_hello()メソッドの部分を書き直すと以下のようになります。 async fn say_hello( &self, request: Request , ) -> Result , Status> { let args = format!("{:?}", request); let span = tracing::span!(tracing::Level::INFO, "say_hello", request = args.as_str()); let _enter = span.enter(); let reply = hello_world::HelloReply { message: format!("Hello {}!", some_logic(&request.get_ref().name).await?), }; tracing::event!(tracing::Level::INFO, "Done"); Ok(Response::new(reply)) } 処理の冒頭でSpanを定義します 定義内容 名前: say_hello Spanに含める情報 = RequestをDebug traitを使って文字列にした情報 Span.enter()を行いSpanの中に入る事を表す。(enter()はRAIIガードオブジェクトを返し、DropされたタイミングでSpanを閉じます) event!()マクロを使って記録する内容を記述します。 上記の例からわかるとおり、#[tracing::instrument]の処理ではSpanの定義とSpan::enter()の処理を自動的に生成しています。また、tracing::info!()やtracing::error!()はEventの生成をlog crateと同様のI/Fで定義できるように作られています。 注意公式のドキュメントにも記述されていますが非同期処理内でのSpan::enter()の処理は慎重に利用するか避けることが明記されています。非同期関数の場合は#[tracing::instrument]を使った場合にただしく生成できるとドキュメントに書かれているように#[tracing::instrument]を利用することを推奨します。 tonicとの統合 すでに実装例で示していますが、tonicのServer::Builderにはtrace_fn()メソッドが用意されており、ここでRequest毎のSpanを生成しています。次の例ではRequest全体の情報をSpanに含めず、Request Headerにtrace_idという文字列の情報を出力するように変更しています。(もちろんClient側でRequestする際にIdをヘッダーに入れる必要があります) #[derive(Default, Debug)] pub struct MyGreeter {} #[tonic::async_trait] impl Greeter for MyGreeter { #[tracing::instrument(skip(self, request))] async fn say_hello( &self, request: Request , ) -> Result , Status> { let reply = hello_world::HelloReply { message: format!("Hello {}!", some_logic(&request.get_ref().name).await?), }; tracing::info!("Done"); Ok(Response::new(reply)) } } async fn some_logic(name: &str) -> Result { tracing::info!("run some logic"); match name { "foo" => { tracing::error!("Failed some_logic"); Err(Status::new(Code::InvalidArgument, "who is foo")) } _ => Ok(name.to_string()), } } #[tokio::main] async fn main() -> Result > { tracing_subscriber::fmt() .with_max_level(tracing::Level::INFO) .init(); let addr = "[::1]:50051".parse().unwrap(); let greeter = MyGreeter::default(); log::info!("GreeterServer listening on {}", addr); Server::builder() .trace_fn(|header| { let trace_id = header .get("trace_id") .map(|value| value.to_str().unwrap_or("Unknown")) .unwrap_or("Unknown"); tracing::info_span!("gRPC server", trace_id = trace_id) }) .add_service(GreeterServer::new(greeter)) .serve(addr) .await?; Ok(()) } 修正点 #[tracing::instrument(skip(self, request))] instrumentを使ってSpanを生成する場合、引数を全てSpanに含める挙動になりますが、今回はtraice_idのみを出力するために引数をSpanに含めないようにしています trace_fn(|header| ....) trace_fn()メソッドは引数にHeaderMap型をとり、Requestに含まれるHeaderの情報を取得することができます 実行結果 Nov 07 12:02:03.233 INFO helloworld_server: GreeterServer listening on [::1]:50051 Nov 07 12:02:06.875 INFO gRPC server{trace_id="Unknown"}:say_hello: helloworld_server: run some logic Nov 07 12:02:06.875 ERROR gRPC server{trace_id="Unknown"}:say_hello: helloworld_server: Failed some_logic Nov 07 12:02:12.284 INFO gRPC server{trace_id="xxxxxxxxxxxxxxxxxxxx"}:say_hello: helloworld_server: run some logic Nov 07 12:02:12.284 ERROR gRPC server{trace_id="xxxxxxxxxxxxxxxxxxxx"}:say_hello: helloworld_server: Failed some_logic 出力結果にHeaderから取得したtrace_idを含めることができました。 このようにtracing crateを利用することで非同期に実行される処理に対してContextを含めたログの出力を行うことが出来ます。 おわりに 今回取り上げたtracingにはこの処理機構を使って様々な処理を拡張するためのcrateが存在しており1つのecosystemが形成されて来ています。今回は単純にログを出力するだけのFmtSubscriberを利用しましたが、tracing-opentelemetry crateなどを利用すると前述した分散トレーシングシステムに対して出力することも可能です。 こちらは依存するopentelemetry crateの変更が激しく、今回割愛していますが興味がある方は試してみると面白いと思います。私が検証した内容では tracing-opentelemetry = "0.7" opentelemetry-jaeger = "0.7.0" opentelemetry = "0.8" 上記のような依存関係だとうまく実装が出来ましたが、すでにopentelemetry crateは0.10.0がリリースされている状態なので、本番への適用はもう少し様子を見たほうが良いかもしれません。 この記事がどこかの誰かの役に立つ日がくれば幸いです。 The post tracing crateを利用したRustのlogging方法について appeared first on CADDi Tech Blog.
頭おかしいタイトルですね。何を言っているんだお前は。 本記事は CADDi とは何の関係もありませんし、実用的価値も一切ありません。その点はご了承を。 あ、Rust が分からないからといって帰る必要はありません。この記事はほとんどRustと無関係です。なんらかのプログラム言語に親しんでいる方であれば雰囲気で読める程度の機能しか使っていないのでご安心ください。 nand2tetris 先日、こちらの記事が話題になっていました。Nand2Tetris(コンピュータシステムの理論と実装)でCPUからOSまで一気通貫で作るのが最高に楽しかった話 この記事にあるように、O’Reilly Japan – コンピュータシステムの理論と実装 、またの名を nand2tetris と呼ばれる本があります。NAND素子を出発点として簡単なゲームを作るまで(何故か作るのは名前に反してテトリスではない…)を一気通貫に説明してくれる本です。 上の記事の方は完走されたそうで、すごいですね。私は根気が続かず、途中でやめてしまいました…。お恥ずかしい。 しかしながら、やはりこの本が最高に楽しいのは前半のハードウェアのところではないかと思っています。私は本書に沿って、ハードウェアの動作をシミュレートするプログラムをRustで書いてみました。 やったのはずいぶん昔なのですが、上記の記事で思い出したので紹介してみます。キッカケを作ってくれた記事に感謝です。 NANDとフリップフロップ NAND pub fn nand(a: bool, b: bool) -> bool { !(a && b) } 言わずとしれたNAND素子です。 論理ゲートの入出力は電圧による 0/1 ですから、これを bool 型でシミュレートすることにします。この形式の関数で、2つの入力と1つの出力を持つ論理ゲートがシミュレートできることがわかると思います。 実装には && や ! といった演算子が使われていますね。こういった「高度な」演算子を使うのはここだけで、他の箇所では一切使いません。NAND素子を最もプリミティブな要素として、それを組み合わせて && のような論理演算をシミュレートしていくというのが目的なのですから、こういった演算子を使ってしまっては意味がありません。一方、nand() 関数だけはブラックボックスとして与えられるプリミティブな素子ですから、この実装だけはズルをするしかない、というわけです。 フリップフロップ nand2tetrisの紹介で「NAND素子を出発点として」と書きましたが、実はもうひとつ「フリップフロップ」も所与のものとして与えます。 コンピュータの状態遷移はクロック信号によって駆動されます。従って、コンピュータやそれを構成する部品は、次のようなクロック信号のループに駆動されて動くというモデルで考えていきましょう。 loop { hardware.clock(...); } これを踏まえて、フリップフロップを次のようなコードでモデル化します。 pub struct Flipflop { bit: bool } impl Flipflop { pub fn new() -> Self { Self { bit: false } } pub fn out(&self) -> bool { self.bit } pub fn clock(&mut self, a: bool) { self.bit = a; } } フリップフロップは1bitの状態を持っています。 入力 in が変化しても、すぐには出力 out には反映されず、内部で持っている bit の値を出力し続けます。そしてカシャッと clock が入力されたタイミングで、入力の値が内部に取り込まれます。 これをRustでシミュレートしたものが上記のコードです。out() 関数は単に self.bit を返す関数であり、clock() 関数によって内部状態を入力値に置き換えます。 clock() の実装において、値(状態)の代入という「高度な」操作が使われています。しかしこれ以降、Flipflop の内部以外では一切、mutable な状態変数への代入という操作は行いません。コンピュータは状態遷移機械であり、その「状態」を保持するための最もプリミティブな機構がこのフリップフロップです。それをシミュレートするのが目的ですから、Rust言語が備えている状態保持の機能を使ってしまっては意味がありません。フリップロップだけは、NAND素子と同様にブラックボックスとして与えられるものですから、その実装では「ズル」をしています。しかしこれ以降は、「状態」はすべてフリップフロップを組み合わせて表現していくことになります。 1bit レジスタを作る ではいよいよ、レジスタを作っていきましょう。まずは最も簡単な、1bitだけを保持するレジスタです。こんな形をしています。 フリップフロップと比較すると、load という入力が増えていることが分かります。clock のタイミングで内部の状態が遷移するという点はフリップフロップと同じです。 この構造をRustのコードにすると、次のようになります。 pub struct BitRegister { flipflop: Flipflop } impl BitRegister { pub fn new() -> Self { Self { flipflop: Flipflop::new() } } pub fn out(&self) -> bool { self.flipflop.out() } pub fn clock(&mut self, input: bool, load: bool) { ... } } BitRegister 型は、内部にフリップフロップを1つ保持しています。そして out() はフリップフロップの out() をそのまま返しています。 問題は clock() の実装です。この関数で、input と load という2つの入力に応じて内部の状態が遷移します。 実現したいのは次のような動きです。 impl BitRegister { pub fn clock(&mut self, input: bool, load: bool) { self.flipflop.clock(if load { input } else { self.out() }) } } 要するに load が true の場合のみ input が取り込まれて、load が false の時には状態は遷移しない、というわけですね。 しかし、上記は if 式を利用しています。これはズルです。物理デバイスに if を直接実現するものはありません。ですから、NANDを組み合わせて if に相当する回路を組まなくてはなりません。if すら使ってはいけないプログラミング、相当頭おかしい感じがしますが、やっていきましょう。 1bit レジスタを実現する回路は、下図のようなものです。(※ DFF と書かれているのは Data Flipflop の略で、要するに上で定義した Flipflop 型です。) Mux という素子が登場しています。これは multiplexor と呼ばれる素子で、if に相当する機能を担うものです。Rust で表現すると次のような動作をします。 pub fn mux(a: bool, b: bool, sel: bool) -> bool { if sel { b } else { a } } Mux は a, b, sel の3つの入力を持ち、sel の値に応じて a または b を出力します。上のコードは if を使ってズルをした実装になっていますが、これはあとで直すとして、まずはこの mux() を使って BitRegister::clock() の実装を書き換えてみましょう。 impl BitRegister { pub fn clock(&mut self, input: bool, load: bool) { // self.flipflop.clock(if load { input } else { self.out() }) self.flipflop.clock(mux(self.out(), input, load)) } } 上の回路図と見比べると、きちんと対応していることが分かるでしょう? これで BitRegister から if を取り除くことが出来ました。あとは mux() の実装のズルを取り除いて、全てNANDの組み合わせで実現できれば完了です。 mux() は次のように書き換えることが出来ます。 pub fn mux(a: bool, b: bool, sel: bool) -> bool { // if sel { b } else { a } (a && !sel) || (b && sel) } あとは &&, ||, ! という3つの論理演算子を nand() で表現できればOKです。 どん!答えは下記のとおりです。 pub fn not(a: bool) -> bool { nand(a, a) } pub fn and(a: bool, b: bool) -> bool { not(nand(a, b)) } pub fn or(a: bool, b: bool) -> bool { nand(not(a), not(b)) } pub fn mux(a: bool, b: bool, sel: bool) -> bool { or(and(a, not(sel)), and(b, sel)) } これで1bitのレジスタの完成です。 それにしても load という入力はどう役に立つのでしょうか? それは後ほどのお楽しみ。 16bit レジスタを作る 16bit を 1 word とするレジスタを作りましょう。まず Word を次のように定義しておきます。 pub type Word = [bool; 16]; [bool; 16] というのは長さ16(固定長)のboolの配列型を意味しています。 ところで、「配列」を使うのはズルではないのでしょうか。我々は if や && すら使ってはいけないプログラミングに取り組んでいます。「配列」は使ってはいけない「高度な」機能ではないのでしょうか。 心配は無用です。16本の導線を束にすれば、ハードウェアで [bool; 16] を実現することが出来ます。もちろん可変長の配列を使うことは出来ませんが(ハードウェアで動的に導線が増減したら怖い)、固定長なら問題ありません。 というわけで、16bit レジスタは下記のコードになります。 pub struct Register { bits: [BitRegister; 16] } impl Register { pub fn new() -> Self { Self { bits: [ BitRegister::new(), BitRegister::new(), BitRegister::new(), BitRegister::new(), BitRegister::new(), BitRegister::new(), BitRegister::new(), BitRegister::new(), BitRegister::new(), BitRegister::new(), BitRegister::new(), BitRegister::new(), BitRegister::new(), BitRegister::new(), BitRegister::new(), BitRegister::new(), ] } } pub fn out(&self) -> Word { [ self.bits[ 0].out(), self.bits[ 1].out(), self.bits[ 2].out(), self.bits[ 3].out(), self.bits[ 4].out(), self.bits[ 5].out(), self.bits[ 6].out(), self.bits[ 7].out(), self.bits[ 8].out(), self.bits[ 9].out(), self.bits[10].out(), self.bits[11].out(), self.bits[12].out(), self.bits[13].out(), self.bits[14].out(), self.bits[15].out(), ] } pub fn clock(&mut self, input: Word, load: bool) { self.bits[ 0].clock(input[ 0], load); self.bits[ 1].clock(input[ 1], load); self.bits[ 2].clock(input[ 2], load); self.bits[ 3].clock(input[ 3], load); self.bits[ 4].clock(input[ 4], load); self.bits[ 5].clock(input[ 5], load); self.bits[ 6].clock(input[ 6], load); self.bits[ 7].clock(input[ 7], load); self.bits[ 8].clock(input[ 8], load); self.bits[ 9].clock(input[ 9], load); self.bits[10].clock(input[10], load); self.bits[11].clock(input[11], load); self.bits[12].clock(input[12], load); self.bits[13].clock(input[13], load); self.bits[14].clock(input[14], load); self.bits[15].clock(input[15], load); } } 単に BitRegister を16個並べたものが Register です。動作は上のコードを読めばすぐに分かるでしょう。 それにしても Register::clock() の実装、これはひどいですね。for ループ使えや!と言いたくなります。 が、「ハードウェアに for ループはない!」という強い信念の元(?)、あえてループは使わずに実装しました。こうやってベタッと書いたほうが、回路図が透けて見える気がしませんか? 8ワード(16バイト)のRAMを作る Register を8個並べてRAMを作りましょう。骨組みは下記のようなコードになります。 pub struct RAM8 { registers: [Register; 8] } impl RAM8 { pub fn new() -> Self { Self { registers: [ Register::new(), Register::new(), Register::new(), Register::new(), Register::new(), Register::new(), Register::new(), Register::new(), ]} } pub fn out(&self, address: [bool; 3]) -> Word { ... } pub fn clock(&mut self, address: [bool; 3], input: Word, load: bool) { ... } } Register とよく似ていますが、よく見ると out() と clock() に address: [bool; 3] という引数が新たに加わっています。address は要するに、ポインタです。レジスタは8個ですから、3bit のアドレスで一意に指定することが出来ます。out() は address で指定されたアドレスのレジスタを読み取りますし、clock() は address で指定されたレジスタの値を書き換えるというわけです。 RAM8::out() 実現したいのはこういう動作です。 impl RAM8 { pub fn out(&self, address: [bool; 3]) -> Word { match address { [false, false, false] => self.registers[0].out(), [true, false, false] => self.registers[1].out(), [false, true, false] => self.registers[2].out(), ... [true, true, true ] => self.registers[7].out(), } } } もちろん match 式は「ズル」ですから、これを使わずに論理回路で分岐を実現しなくてはなりません。 1bit レジスタのときには、if 式を mux() に置き換えたのでした。ここでも mux() を組み合わせて拡張していきます。 まず次のような動作をする mux16() というものを作ります。 pub fn mux16(a: Word, b: Word, sel: bool) -> Word { if sel { b } else { a } } ほとんど mux() と同じに見えますが、入出力が bool から Word (16bit)に拡張されていることに注意してください。 これは、次のように mux() をひたすら16個ならべることで実装できます。 pub fn mux16(a: Word, b: Word, sel: bool) -> Word { [ mux(a[ 0], b[ 0], sel), mux(a[ 1], b[ 1], sel), mux(a[ 2], b[ 2], sel), mux(a[ 3], b[ 3], sel), mux(a[ 4], b[ 4], sel), mux(a[ 5], b[ 5], sel), mux(a[ 6], b[ 6], sel), mux(a[ 7], b[ 7], sel), mux(a[ 8], b[ 8], sel), mux(a[ 9], b[ 9], sel), mux(a[10], b[10], sel), mux(a[11], b[11], sel), mux(a[12], b[12], sel), mux(a[13], b[13], sel), mux(a[14], b[14], sel), mux(a[15], b[15], sel), ] } 続いて、次のような動作をする mux4way16() というものを作ります。 pub fn mux4way16(a: Word, b: Word, c: Word, d: Word, sel: [bool; 2]) -> Word { if sel[1] { mux16(c, d, sel[0]) } else { mux16(a, b, sel[0]) } /* 次のコードと等価 match sel { [false, false] => a, [true, false] => b, [false, true ] => c, [true, true ] => d, } */ } これは下記の実装で実現できることがすぐ分かるでしょう。 pub fn mux4way16(a: Word, b: Word, c: Word, d: Word, sel: [bool; 2]) -> Word { mux16(mux16(a, b, sel[0]), mux16(c, d, sel[0]), sel[1]) } 同様にして mux8way16() を作ることが出来ます。 pub fn mux8way16( a: Word, b: Word, c: Word, d: Word, e: Word, f: Word, g: Word, h: Word, sel: [bool; 3] ) -> Word { mux16( mux4way16(a, b, c, d, [sel[0], sel[1]]), mux4way16(e, f, g, h, [sel[0], sel[1]]), sel[2] ) } これを使って、RAM8::out() は次のように実装できます。 impl RAM8 { pub fn out(&self, address: [bool; 3]) -> Word { mux8way16( self.registers[0].out(), self.registers[1].out(), self.registers[2].out(), self.registers[3].out(), self.registers[4].out(), self.registers[5].out(), self.registers[6].out(), self.registers[7].out(), address) } } 以上で、指定された address のレジスタを読み取る回路が作れました。 RAM8::clock() address からの読み取りは出来ましたから、今度は address への書き込みを実装しましょう。実現したいのはこういう動作です。 impl RAM8 { pub fn clock(&mut self, address: [bool; 3], input:Word, load: bool) -> Word { match address { [false, false, false] => self.registers[0].clock(address, input, load), [true, false, false] => self.registers[1].clock(address, input, load), [false, true, false] => self.registers[2].clock(address, input, load), ... [true, true, true ] => self.registers[7].clock(address, input, load), } } } しかし、ちょっとこれは無理があります。このコードは address の値に応じてクロック信号を入力するレジスタを切り替える書き方になっていますが、クロック信号は常に全ての素子に入力し続けなくてはなりません。 ですので、こんなふうな方針に切り替えます。 impl RAM8 { pub fn clock(&mut self, address: [bool; 3], input:Word, load: bool) -> Word { let load8: [bool; 8] = match address { [false, false, false] => [load, false, false, false, false, false, false, false], [true, false, false] => [false, load, false, false, false, false, false, false], [false, true, false] => [false, false, load, false, false, false, false, false], ... [true, true, true ] => [false, false, false, false, false, false, false, load], }; self.registers[0].clock(input, load8[0]); self.registers[1].clock(input, load8[1]); self.registers[2].clock(input, load8[2]); self.registers[3].clock(input, load8[3]); self.registers[4].clock(input, load8[4]); self.registers[5].clock(input, load8[5]); self.registers[6].clock(input, load8[6]); self.registers[7].clock(input, load8[7]); } } 常に全てのレジスタにクロック信号が入力されていることが一目瞭然ですね。input も常に全てのレジスタに入力されています。 ではどうやって指定された address だけに書き込む制御をしているかというと、ここで load ビットが活躍します。指定された address のレジスタだけ load に true を入力することで、この制御をしています。BitRegister で仕込んだ load 入力の伏線を、ようやくここで回収することが出来ました。 では今までと同様に、if や match を使っている箇所(load8 を求めている箇所)を論理回路に置き換えていきましょう。 まず DMux (Demultiplexor)という素子を作ります。これは次のような動作をするものです。 pub fn dmux(input: bool, sel: bool) -> [bool; 2] { match sel { false => [input, false], true => [false, input], } } これは次のような論理回路で実現できます。 pub fn dmux(input: bool, sel: bool) -> [bool; 2] { [and(input, not(sel)), and(input, sel)] } これを組み合わせて、次の動作仕様の dmux4way() dmux8way() を作ります。 pub fn dmux4way(input: bool, sel: [bool; 2]) -> [bool; 4] { match sel { [false, false] => [input, false, false, false], [true, false] => [false, input, false, false], [false, true ] => [false, false, input, false], [true, true ] => [false, false, false, input], } } pub fn dmux8way(input: bool, sel: [bool; 3]) -> [bool; 8] { match sel { [false, false, false] => [input, false, false, false, false, false, false, false], [true, false, false] => [false, input, false, false, false, false, false, false], [false, true, false] => [false, false, input, false, false, false, false, false], ... [true, true, true ] => [false, false, false, false, false, false, false, input], } } これらから match 式を除去して論理回路としてどう実装できるか、考えてみて下さい。 これを使うと、RAM8::clock() は次のように実装できます。 impl RAM8 { pub fn clock(&mut self, address: [bool; 3], input: Word, load: bool) { let load = dmux8way(load, address); self.registers[0].clock(input, load[0]); self.registers[1].clock(input, load[1]); self.registers[2].clock(input, load[2]); self.registers[3].clock(input, load[3]); self.registers[4].clock(input, load[4]); self.registers[5].clock(input, load[5]); self.registers[6].clock(input, load[6]); self.registers[7].clock(input, load[7]); } } おわりに このあとは、RAM8 を8個並べて組み合わせて RAM64 を作り、RAM64 を8個並べて RAM512 を作り…、と続けてRAMを大きくしていきます。 そして、CPUを論理ゲートとレジスタの組み合わせから構成し、CPUとRAMを繋げて、ROMから機械語コードを読み出して実行するようにしていきます。 これが組み上がって動いたとき、何とも言えない感動を覚えたものです。特にレジスタやRAM周りの仕組みにワクワクしました。コンピュータというのは、クロック信号でカチカチと動いていく壮大なピタゴラ装置なんだということが実感できました。 書いたコードはここに置いてあります。ドヤァ!https://github.com/u1roh/nand2tetris …と思ったら、あれ?これ動かないっすね…。 ディスプレイをシミュレートするところを glium という OpenGL ラッパーで作ったのですが、久しぶりに動かそうとしたら動かない…。 今ちょっと原因を調べる時間も取れないので、すんません、ダサい感じの終わり方になりましたが、以上です。 The post RustでRAMの動作原理をシミュレートする appeared first on CADDi Tech Blog.
こんにちは。テクノロジー本部バックエンド開発グループの江良です。 この記事は CADDi Advent Calendar 19 日目の記事です。昨日は、狭間さんによる「GraphQL PaginationのNestJSでの実装」でした! 「バックエンド開発グループの〜」と自己紹介したばかりで恐縮なのですが、今日はフロントエンドの話をします。 はじめに これはなに まえがき 使用したライブラリのバージョン Apollo Client について Apollo Client のキャッシュとは Apollo Client 3.0 の新機能 ページネーションの設計 オフセットベース カーソルベース 閑話休題 サンプルコードで学ぶ Apollo Client 3.0 導入 概要 Offset-based なページネーションを実装する Cursor-based なページネーションを実装する 動作確認 おわりに はじめに これはなに Apollo Client の 3.0 で追加されたキャッシュ周りの新機能を試してみた記事です offsetLimitPagination と relayStylePagination について触れています 実際に手元で動かせるコードを使って、ステップ・バイ・ステップで説明します 能書きはいいからコードを見せてくれ、という人はこちらをご覧ください。 gushernobindsme/apollo-client-v3-practice まえがき 私が所属する原価計算システムの開発チームでは、 バックエンド BFF フロントエンド という構成でシステムを提供しています。 バックエンド・BFF 間は gRPC、BFF・フロントエンド間は GraphQL で通信しています。 フロントエンドから BFF の GraphQL サーバにアクセスする際に使用しているのが、今回お話する Apollo Client というライブラリです。 弊チームでは、現在 Apollo Client のバージョン 2.6.9 を使用しているのですが、 3.0 以降で登場したキャッシュ周りの機能がなかなか便利そうだったので、今後のバージョンアップに備えて試してみたことをまとめてみます。 使用したライブラリのバージョン 検証には以下のバージョンを使用しました。 @apollo/client: 3.3.4 graphql: 15.4.0 Apollo Client について Apollo Client のキャッシュとは Apollo Client は、GraphQL クエリの結果をインメモリのキャッシュに保存します。 クエリの結果は正規化して保存され、 InMemoryCache というクラスから簡単に操作できます。 InMemoryCache は 公式ガイド にも記載の通り、簡単に使い始められます。 import { InMemoryCache, ApolloClient } from '@apollo/client'; const client = new ApolloClient({ // ...other arguments... cache: new InMemoryCache(options) }); 保存されたキャッシュには InMemoryCache の以下のメソッドを使うことでアクセスできます。 readQuery readFragment writeQuery writeFragment また、Apollo Client 3.0 からはキャッシュ内の個々のフィールドを更新するために modify というメソッドが追加されています。「mutation を実行した後、その結果をキャッシュに書き戻したい」といったユースケースで便利です。 詳細は 公式ガイド のほか、弊社フロントエンドエンジニアの桐生さんの記事にも詳しく書かれていますので、気になる方は読んでみてください。 Apollo Client 3.0 の新機能 Apollo Client 3.0 ではいくつもの新機能が追加されています。 詳細は Apollo の公式ブログ と マイグレーションガイド に譲りますが、その中でも特にパワフルなのが Pagination helpers の追加です。 これは文字通りページネーションの実装を助ける便利なヘルパ機能になります。 ページネーションの設計 さて、ここでちょっと脱線してページネーションを実現する API の設計方針について考えてみましょう。 ページネーションの設計は数あれど、大まかなパターンとしては以下の二種類に整理できるかと思います。 オフセットベース(Offset-based pagination) カーソルベース(Cursor-based pagination) オフセットベース オフセットベースはいわゆる offset と limit を使ってページングを行うやり方です。 offset にデータの取得開始位置を指定し、 limit に取得するデータ件数を指定します(SQL を書いたことのある人には馴染みのあるアレですね)。 例えばこんな風に指定すると、 SELECT * FROM transactions LIMIT 10 OFFSET 20; 先頭の 20 行目から 10 件のデータを取得してください、という意味になります。 カーソルベース カーソルベースはデータの取得を開始する位置をインデックスではなく、トークンで指定するやり方です。 この方式では first にデータ件数、 after にデータの取得開始位置を表す base64 エンコードされたカーソルを指定します。 { user { id name friends(first: 10, after: "opaqueCursor") { edges { cursor node { id name } } pageInfo { hasNextPage } } } } この方式は GraphQL のサイトにて ベストプラクティス として紹介されているほか、GraphQL クライアントの Relay でも紹介されています。 GraphQL Cursor Connections Specification 閑話休題 さて、話を Apollo Client に戻します。 Apollo Client 3.0 では、上述した二種類の API のページング処理をいい感じにしてくれる便利な機能を提供しています。 Pagination helpers は InMemoryCache に対するオプションとして設定できます。 先ほど紹介した InMemoryCache のインスタンスを生成するコードを思い出してみましょう。 公式ガイド によると、ここに offsetLimitPagination を指定するとオフセットベースの API のページング処理がいい感じになります。 const cache = new InMemoryCache({ typePolicies: { Query: { fields: { comments: offsetLimitPagination(), }, }, }, }); また公式ガイドの このページ によれば、ここに relayStylePagination を指定するとカーソルベースの API のページング処理もいい感じになるそうです。 const cache = new InMemoryCache({ typePolicies: { Query: { fields: { comments: relayStylePagination(), }, }, }, }); 本当に、そんなうまい話があるのでしょうか? サンプルコードで学ぶ Apollo Client 3.0 導入 ということで、早速コードを書いて検証してみます。 やってみたことは以下の通りです。 オフセットベースのレスポンスを返す GraphQL のエンドポイントを実装する カーソルベースのレスポンスを返す GraphQL のエンドポイントを実装する Apollo Client 3.0 を組み込んだフロントエンドを実装し、Pagination helpers を設定する ここでは、ページングの動作を検証するためのシンプルな CRUD アプリケーションを実装してみます。 概要 ということで完成したのがこちらのリポジトリです。 gushernobindsme/apollo-client-v3-practice backend ディレクトリに NestJS 製の Graph サーバを実装 frontend ディレクトリに React 製のフロントエンドを実装 という構成になっています。 ページネーション以外の話題については、本記事では省略します。NestJS を使ったバックエンドの実装については、前日の狭間さんの記事に詳しく書いてありますので、是非読んでみてください! Offset-based なページネーションを実装する まず、オフセットベースの GraphQL の定義を用意します。 (バックエンド側の実装については割愛します。) type Query { sharks(offset: Int, limit: Int): [Shark] } type Shark { id: Int originalTitle: String japaneseTitle: String rate: Int } 次に、 offsetLimitPagination を設定した InMemoryCache を用意します。 const cache = new InMemoryCache({ typePolicies: { Query: { fields: { sharks: offsetLimitPagination(), } } } }) const client = new ApolloClient({ // other settings cache }); GraphQL のドキュメント定義を用意して、 const GET_SHARKS = gql` query getSharks($offset: Int, $limit: Int) { sharks(offset: $offset, limit: $limit) { id originalTitle japaneseTitle rate } } `; 戻り値の型を用意して、 interface SharksModel { sharks: Shark[]; } useQuery の hooks を実装します。 const { loading, error, data, fetchMore } = useQuery<SharksModel>( GET_SHARKS, { variables: { offset: 0, limit: 10 }, }, ); 最後に hooks を呼び出す component を実装して完成です。 // ... 略 <table> // ... 略 <tbody> {data && data.sharks.map((shark) => { return ( <tr key={shark.id}> <th>{shark.id}</th> <td>{shark.originalTitle}</td> <td>{shark.japaneseTitle}</td> <td> {shark.id && ( <Ratings id={shark.id} rate={shark.rate || 0} mutation={updateShark} /> )} </td> </tr> ); })} </tbody> </table> // ... 略 次の 10 件をフェッチするためのボタンも設置します。 次のデータの取得は fetchMore メソッドを呼ぶことで簡単に実装できます。 Core pagination API - Client (React) - Apollo GraphQL Docs <Button onClick={async () => { await fetchMore({ variables: { offset: data?.sharks.length, }, }); }} > fetch more </Button> Cursor-based なページネーションを実装する 次にカーソルベースの GraphQL の定義を用意します。 お作法にしたがって connection に edges と pageInfo を、 edges に node を定義してみます。 type Query { sharks(first: Int!, after: String): SharkConnection } type SharkConnection { edges: [SharkEdge] pageInfo: PageInfo } type SharkEdge { node: Shark cursor: String } type PageInfo { endCursor: String hasNextPage: Boolean } 次に、 relayStylePagination を設定した InMemoryCache を用意します。 const cache = new InMemoryCache({ typePolicies: { Query: { fields: { sharks: relayStylePagination(), } } } }) const client = new ApolloClient({ // other settings cache }); GraphQL のドキュメント定義を用意して、 export const GET_SHARKS = gql` query getSharks($cursor: String) { sharks(first: 10, after: $cursor) { edges { cursor node { id originalTitle japaneseTitle rate } } pageInfo { endCursor hasNextPage } } } `; 戻り値の型を用意して、 interface SharksModel { sharks: SharkConnection; } useQuery の hooks を実装します。 const { loading, error, data, fetchMore } = useQuery<SharksModel>( GET_SHARKS, { variables: { cursor: '' }, }, ); 最後に hooks を呼び出す component を実装して完成です。 // ... 略 <table> // ... 略 <tbody> {data && data.sharks && data.sharks.edges && data.sharks.edges.map((shark) => { const node = shark.node; return ( <tr key={node?.id}> <th>{node?.id}</th> <td>{node?.originalTitle}</td> <td>{node?.japaneseTitle}</td> <td> {node?.id && ( <Ratings id={node.id} rate={node.rate || 0} mutation={updateShark} /> )} </td> </tr> ); })} </tbody> </table> // ... 略 次の 10 件をフェッチするためのボタンはこんな感じです。 {data && data.sharks.pageInfo?.hasNextPage && ( <Button onClick={async () => { await fetchMore({ variables: { cursor: data?.sharks?.pageInfo?.endCursor, }, }); }} > fetch more </Button> )} 動作確認 実装が一通り書けたのでさっそく動かしてみましょう。 「fetch more」ボタンを押すと次の 10 件が表示されます。 さっそくデータを追加してみましょう。 追加できました。 ☆アイコンを押して評価をつけることもできます。 ( フランケンジョーズ は CG が本当にひどいので☆ 1 つです。) こちらも無事更新できました。 (ちょっとわかりにくいのですが)実際にうまくキャッシュが動作している様子は、先ほどご紹介したリポジトリをクローンして起動することでも検証できます。是非お手元で動かしてみてください。 おわりに ということで Apollo Client 3.0 で追加された新機能 Pagination helpers のご紹介でした。 明日は、寺田さんによる「RustでRAMの動作原理をシミュレートする」です。お楽しみに!
はじめに テオ。弾薬が尽きた。このまま突入する、さらばだ。ヴァルハラで会おう。Wir sehen uns in Walhalla! これは第二次世界大戦時のドイツ空軍のエースパイロット、ハインリヒ・エールラーの最後の言葉です。 北欧神話で戦死した人がラグナロク(終末の日)に備える場所とされるヴァルハラ。今日はそれにちなんで命名された解析ツールValgrindについて見ていきましょう。 申し遅れました。わたくしCADDiでコスト計算システムのバックエンドを担当している @kimu_di といいます。 こちら キャディAdvent Calendar 2020 17日目の記事となります。昨日は @yskeee000 の「 KleinというProductについて 」でした。 改めて、今回はValgrindという強力なコード解析・メモリデバッグツールについてお話します。 ここまで強力な解析ツールを使う必要はないことも多いですが、RustやC++を書く上で最後の最後のパフォーマンスの追い込みに力を発揮してくれること請け合いです。 改めてValgrindとは 普通、C++やRustで書いたプログラムはプロセッサに依存した実行可能ファイルへコンパイルされますね。Valgrindはそれを中間表現に変換し、それを再び機械語に翻訳し直して実行させるという手間を踏みます。パフォーマンスは低下しますが、中間表現を経由することで計測・解析・デバッグが容易になります。 ここにコードを追加することで様々なツールを機能させることが出来るようになっています。 よく使うツールをさらっとご紹介しましょう。 Memcheck 初期化されていない・あるいは解放されたはずのメモリの使用、境界外への読み書き、メモリリークを検出するツール Cachegrind キャッシュのプロファイラ。キャッシュのヒット率などを計測できる Callgrind コールグラフの作成、ブランチ予測などのツール Helgrind マルチスレッドのエラー検出ツール Massif ヒーププロファイラ 導入 Ubuntuでは $ sudo apt install valgrind のように導入します。 ビジュアライザも入れましょう。 $ sudo apt install kcachegrind massif-visualizer このあたりがあると幸せになれそうです。 kcachegrindでコールグラフを読み解く まずはコールグラフを見てみましょう。 プログラムをvalgrindに食べさせながら実行します。 $ valgrind --tool=callgrind プログラム名 引数 Rustで開発している途中ならこうなるでしょう。 $ cargo build --relase $ valgrind --tool=callgrind target/release/binary_name まさか本番環境でdebug版を動かしていることはないでしょうから、リリースビルドを選ぶのを忘れないようにします。 そのあとvalgrindにプログラムを渡して実行ですね。 実行するたびに callgrind.out.12345 のような名前でファイルが出力されます。 ls コマンドで確認できると思います。 このファイルの中身をビジュアライザで見てみましょう。 $ kcachegrind callgrind.out.12345 これでツールが起動します。 このような画面が表示されましたね。 メニューバーには真新しいものはありませんが、View -> Cycle DetectionだけはOFFにしておくことをおすすめします。 この機能は再帰的な相互呼び出しをヒューリスティックに解析してコストを表示してくれるのですが、ONになっていると実際の関数が <Cycle1> のような計測結果に覆い隠されてしまいます。関数別の呼び出しコストを見たいので、OFFで良いでしょう。 左側がflat profileと呼ばれる領域で、ここにコールスタックのようなものを表示してくれます。実際にこのようなコールスタックがあるわけではないですが、現在の関数から見て呼び出す関数/呼び出される関数を上下に並べてコストが高い順に表示されます。 右側には視覚化された「ツリーマップ」やコールグラフ、呼び出し/呼び出される関数のリストが並びます。 Caller というのは、今扱っている関数を呼び出している親玉のこと、 Callee は今扱っている関数が呼び出す子分のことを指します。 また、 CallGraph を選ぶと呼び出し階層をグラフ構造で表示してくれます。 この例ではripgrepというRust製の検索ツールをvalgrindで実行した結果を表示しています。 コールマップを見るとmain関数の中でばっくりと2つ大きな関数が占めていて、残りが小さめの関数で占められている事がわかります。 マップをクリックすると、それぞれの色分けされている領域がどの関数に対応しているかがわかります。 試しに押してみましょう。左側の大きな領域をクリックするとハイライトされますね。それは ignore::walk::WalkParallel::visit という関数で、すべての呼び出しのうち38%のコストを占めている事がわかります。 ダブルクリックするとコールマップが変わりました。 その関数の中に絞って表示してくれるというわけです。 このツールのソースコードを見れば、きっと重い実装であることが想像できますね。 crossbeam ってなんでしょうと思って見てみると、並行プログラミングのライブラリです。つまり、これ自体にあまり意味はないですね。 なので、さらにその内側を見るとエイホ・コラシックという関数が見えます。これは文字列探索アルゴリズムの名前です。きっとこのあたりが処理の中核だと当たりをつけ、どれくらいのコストが掛かっているか、何を呼び出しているかがわかるでしょう。 massifでヒーププロファイリングを覗いてみる ヒープとはご存知の通り、実行時に任意のタイミングで確保や解放ができるメモリ領域のことですね。 このプログラムがどれくらいのメモリを使うか探ってみましょう。 今度はmassifを使います。 $ valgrind --tool=massif --time-unit=B --stacks=yes プログラム名 引数 massif.out.23456 のようなファイルが出力されるので、これをビジュアライザに掛けます。 $ massif-visualizer massif.out.34567 例によってripgrepのメモリ消費を見てみました。 一目瞭然ですね。 右側でメモリ確保/解放の詳細を見ることができます。 今回はたいへん明確にメモリを使ったあと終了と同時に解放する右肩上がりのグラフになっていますね。 実務にて 業務にて、以上2つのツールを活用してコスト計算システムのコアのプロファイリングを行いました。 計算量の多く複雑なロジックを内包するコンポーネントだったのでボトルネックになっていることが予想されていましたが、意外にも、さほどメモリを消費せず、コールグラフを見るとメモリアロケーションが大半のコストを占めていたことが判明しました。 (残念ながらその結果は公開できないですが……。) ボトルネックを探る手段としてたいへん強力な手段になった一方で、使い方がやや難しいといいますか、タバコに火を付けるのに火炎放射器を使うこともないですから、まずは簡単にミリ秒単位での出力から始めると良いでしょう(と、ボスからアドバイスを受けました) 通常のバックエンド開発としてはAPI単位で時間計測していくほうが遥かに簡単で効果的ですが、難解なアルゴリズムを用いたソフトウェア開発で力を発揮するかもしれません。 主にC++を書く方へ valgrindの目玉ツールとして memcheck があります。 $ valgrind --leak-check=full プログラム名 本来valgrindと言ったらコレがまっさきに出てくる機能です。主にメモリリークの検出に使われるものなのですが、Rustを普通に書いているとまず遭遇しないので……。まあ見ていきましょう。 メモリ解放忘れ 以下のようなコードで発生させてみます。 #include <iostream> int main() { int *p = new int; *p = 0; std::cout << *p << std::endl; } これをmemcheckに掛けると、 definitely lost: 確実に解放漏れがある、と怒られます。 delete p を呼んでいないからですね。 未初期化 今度は *p=0 を消してみました。 #include <iostream> int main() { int *p = new int; std::cout << *p << std::endl; delete p; } Use of uninitialised value of ...と怒られますね。 二重解放 解放済みのポインタをもう一度解放してみます。 #include <iostream> int main() { int *p = new int; *p = 0; std::cout << *p << std::endl; delete p; delete p; } Invalid free() / delete / delete[] / realloc() と怒られました。そのとおり。 不正な読み込み ポインタをずらして読んでみます。 #include <iostream> int main() { int *p = new int; *p = 0; std::cout << *(p + 1) << std::endl; delete p; } 4つ分後ろ見てるよ、と怒られています。 不正な書き込み 最後です。初期化に続けて違う領域に書き込んでみます。 #include <iostream> int main() { int *p = new int; *p = 0; *(p + 1) = 0; std::cout << *p << std::endl; delete p; } Invalid write of ...と怒られますね。 memcheckまとめ このように、厳格にメモリの使い方を指摘してくれますのでC++erの各位におかれましてはぜひお手元のプログラムを一度memcheckに掛けてみることをオススメします。 そして何よりも、メモリ安全なRustに切り替えることを考えると良いでしょう! おわりに Valgrindについて見てきました。 これであなたもプロファイリングマスターです。バイナリにビルドされているプログラムならたいてい解析できますので、お試しに身近なコマンドやプログラムを覗いてみてはいかがでしょうか。
えこんにちは!CADDiでプロダクトマネージャーとしている @yskeee000 です。 もうそろそろ仕事納めしたい 本記事は、キャディ Advent Calendar 2020 – Qiita の16日目の記事です。昨日の記事は松田さんの「 Rust入門者がrust-analyzerへのコントリビュートを達成するまで 」でした。 この記事では、私がプロダクトマネージャーとしてCADDiで携わっているプロダクトについて素朴に紹介したいと思います。 私はCADDiに入社して約1年半となりますが、ほぼ一貫して本記事で紹介する Klein というプロダクト開発に、スクラッチの段階から関わってきました。 現状では、内部に閉じたProductであり外部の方になかなか知られることがないですが、CADDiの根幹をなす重要なProductですので、出来るだけ平易に紹介できればと思います。 プロダクトの背景となるCADDiのビジネスプロセス ①顧客からの受注 ②パートナーへの発注 ③サプライチェーンの設計 ④製造~納品までの実行と実績の管理 KleinというProduct Kleinの持つ役割 果たす価値 これからの発展 名前の由来 P.S. 【余談】もう一つの名前の由来 プロダクトの背景となるCADDiのビジネスプロセス Kleinというプロダクトはいくつかの側面を持っていますが、 受発注管理 生産管理 サプライチェーン管理 物流管理 といったキーワードが内包する概念として近いでしょうか。 こういったビジネスプロセスが発生する、ということがこのプロダクトの価値の前提なのでまずは簡単にそこから出発したいと思います。 ここでは特に関わる範囲のプロセスに絞ってご紹介します。(全体像については以前noteに投稿した こちら ) ①顧客からの受注 顧客から、金属加工品の製造を受注します。 例えば、工場やプラントの中で使う産業装置の部品となる以下のようなものです。 CADDiでは 特注品 と言われるものを扱っておりますので、カタログに規格化された部品ではなく、顧客それぞれが装置ごとのために設計した図面を元にオーダーメイドのものとして受注します。 大きな案件では一案件につき、数千点にのぼる部品数になります。 それぞれについてどういう前提で、いくらで受注したかという情報が発生します。 ②パートナーへの発注 CADDiは自社で製造能力を持たない事業構造のため、パートナーに発注を行ってファブレスにこれらの部品の製造を進めます。 (※ パートナー: CADDiではアラインアンスを結んだサプライヤー(加工会社)をこのように読んでいます。) この時、それぞれのパートナーの強み(=価格・品質などの面で競争力が高い)に合わせて、特性にあった部品を発注していきます。 強みは 鉄/ステンレス/アルミといった材質 板金/旋盤/フライスなどの加工の種類 どれだけのロット数を作るか 加工物のサイズ 求められる品質や精度 などの様々な切り口によって別れていきます。 また、この時の発注の仕分けは部品ごとの粒度ではありません。 例えば、金属自体を切削などによって削る部分はパートナーA、塗装やメッキなど表面への加工はパートナーBのようにその部品に施すべき 工程 というより小さい粒度によります。 結果として、一つの案件において数十のパートナーに発注が分割されることもあります。 ③サプライチェーンの設計 製品は最終的には顧客に納品されなければなりません。 それまでに、例えばパートナーから次の加工のパートナーにわたる、東西にあるCADDiの物流拠点において検品を行う、顧客に送るなどの物流の情報が発生します。 いつ/どこに/何を/どれだけ送るか、というものの流れの情報を、誰が/どの工程を担当するかの情報に加えて管理する必要があります。 大量生産品のように、ものが量産されていく一つの大きなサプライチェーンを確立するのではなく、案件ごとに最適なサプライチェーンをその時ごとに設計して管理する必要があります。 弊社の顧客となる装置メーカー様などでは、部品を納品した後に組み立てる工程が待っており、そのペースに合わせて納品スケジュールを分割していくことが必要な場合も多いので、より複雑になり得る要素も含んでいます。 ④製造~納品までの実行と実績の管理 ③でサプライチェーンの設計ができたとして、製造と輸送が進むにつれて実際にいつどこに何が届いたか、何を納品したか、という実績も管理していく必要が出てきます。 望ましくはないことですが、時に不良や納期遅延が発生するので、それらをきちんと記録として残しつつ、そういったイレギュラーに対応しながらトラブルシューティングしていくことも求められます。 KleinというProduct その中において、顧客とパートナーを繋ぐリボンモデルを繋ぐ存在として業務プロセスに伴走するのがKleinというProductです。 リボンモデルとはいうものの、よく弊社のビジネスそのものが勘違いされるのですが、単なるマッチングではなく、納品・品質責任を持つ存在として、オペレーションを遂行するという責任を弊社は追っています。 顧客はキャディに発注し、パートナーはキャディという顧客から受注するということです。 この大きな違いがあるためにこのProductは要請されます。 Kleinの持つ役割 こういったプロセスの中で、KleinというProductが持つ役割はどのようなものでしょうか? 現状から一部を抜粋すると例えば以下のようなものです。 受発注管理 受注した製品とそれに求められる工程(=発注して手配すべきもの)の情報が管理できます それらの製品に対応する図面のデータを管理することができます どのパートナーにどの工程を依頼するかの判断を助けることができます それを元にして、パートナーに対して工程を簡単に割り振り、発注データを作ることができます 発注もれや二重発注をチェックすることができます 帳票として発注データを元にして発注書の発行ができます サプライチェーン管理 どこから/どこに/何を/どれだけ運ぶかという物流の情報を管理できます 結果として形成されたサプライチェーンのグラフ構造を直感的に図解することができます サプライチェーンのものの流れが整合的かをチェックすることができます あまり多くの情報を記載することができませんが、例えばサプライチェーン全体を管理するのは以下のようなUIを通じて行うなどします。 果たす価値 結果として、このプロダクトが果たしている役割はどんなものでしょうか。 受発注業務のプロセスに伴走するシステムとして、業務プロセスの 標準化 を促進する 自動化プロセスを組み込んでいくことにより 業務工数を削減 する 直感性的UXやデータ/ロジックによって、 人間の正しく迅速な判断をアシスト する 受発注/サプライチェーンにおいて発生するデータを正しい形で蓄積することで データドリブンな 業務/システム改善、また データによって生み出される新たな価値 の土台となる こういった点があげられると思います。 さらに、個人的な観点でいうと、よりマクロで見た時に サプライチェーンを構築・管理する工数を圧倒的に下げることにより、より小さい粒度、例えば部品単位ではなく工程単位での流動性を高めることで、受発注のトラフィックをより強みに合わせて最適に調整する ということが価値であると考えています。 流量をより細分化しても低負荷で最適化できる水路のようなイメージです。 ソフトウェア技術によって、ビジネスにおけるバックエンド情報をより複雑な場合でもリーンに扱うことができるようにすることによって、取引の単位をより小さい単位で、より最適にマッチング ~ 実行することができるようになっていくのです。 これからの発展 KleinはCADDiという受発注プラットフォームにおいて、扇の要となるようなプロダクトと考えています。 実際に使う一次ユーザー以外にも、他のプロダクトやデータを活用するユーザーにも価値を伝播させていくことを前提に土台から設計しており、進化の方向性は複雑ですが多方面にわたります。 今現段階での展望にすぎませんが、以下のような9つのベクトルを意識して、個別の具体的な実装の補助線としています。 名前の由来 最後に、Kleinという名前の由来ですが、これはクラインの壺 ( wiki ) というものに由来します。 ちょっときもいですね。 この壺は表面を辿っていくと表と裏が入れ替わるという構造を持っています。 (メビウスの輪の3次元版のイメージです) CADDiは顧客から受注し、パートナーに発注するという受注側/発注側のどちらのロールも内包しています。 このことから得られるソフトウェアとそれを前提にしたオペレーションのドッグフーディングが、将来的により直接的に顧客/パートナーへのテクノロジーによる価値提供に繋がるというvisionから名付けました。 パートナーへの発注者としてのナレッジの形式知化が、同じ発注という業務を行う顧客への転用につながっていくと考えており、その逆も然りということです。 まだまだやることは無限に山積みですが、一緒に遠くへこのプロダクトを連れて行ってくださる方をまだまだ募集しています! P.S. 【余談】もう一つの名前の由来 Kleinというのは一つのProductですが、そのシステムのなかの一つのコンポーネントとして、 h20e というコンポーネントがあります。 このコンポーネントは案件管理という役割を担っているのですが、Kleinのリポジトリなどにはよくこの h20e というワードが登場します。 Kleinの立ち上げ当時、上記の業務プロセスを統括していたのは創業メンバーの一人でもある 幸松大喜 という人物です。 仕事ができてリーダシップがあるばかりでなく、なんと縁起のいい名前なのでしょうか。 幸せで、最上で(松竹梅)、大きな喜びなのです。結婚式ではぜひ乾杯を頼みたい。 この縁起の良さにあやかるべく、 happy pine great pleasure -> h20e という名前を授けました。 これから長い間このproductが使われていく中で、この魂も脈々と受け継がれていくということでしょう。
はじめに はじめまして、テクノロジー本部バックエンド開発グループの松田です。 この記事は、 CADDi Advent Calendar 15日目の記事です。昨日は、和田さんによる「 Rust と nalgebra で MLP を実装した話 」でした! CADDiでは、バックエンドの主要な開発言語としてRustを採用していますが、エンジニアの多くはRust未経験の状態でCADDiに入社し、入社前後にRustの勉強を開始しています。11月に入社した私も、Rustを本格的に利用するのはCADDiが初めてで、入社が決まって以降、Rustの勉強を行っていました。 また、これは個人的な話ですが、これまでにOSSへのコントリビュートを一度も行ったことがなく、いつかはOSSコントリビュートを行ってみたいと思っていました。 今回、Rust入門とOSSコントリビュート達成という2つの目標を同時に追求するため、「何でもいいからRustのOSSのIssueをひとつ選んでコントリビュートする」ということを行ってみました。 その模様を、毎週火曜日の午前に行っているCADDiの社内勉強会である「STUDDi」で発表しましたので、ダイジェストをここにまとめたいと思います。 伝えたいこと OSSへのコントリビュートに興味があるがコントリビュート経験がない読者様に対し、 手頃なIssueを探せば、その言語や技術の熟練者でなくてもOSSにコントリビュートすることは可能 簡単に見えるIssueであっても、取り組んでみると勉強になる ことをお伝えすることがこの記事の目的です。 逆に、既にOSSへのコントリビュートの経験がある方にとっては、あまり得られるものがないかもしれません。 いろいろなコントリビュート OSSへのコントリビュートを行うきっかけとしてよくあるのは、「あるOSSを自プロダクトで利用したいが、必要な機能が存在しない」という状況だと思います。この場合、どのOSSに対してどのようなコントリビュートを行うべきかは明確です。 一方、私の場合、身も蓋もなく言えば、「何でもいいからRust製OSSへのコントリビュートをしてみたい」というモチベーションだったため、何から手を付ければ良いか分かりませんでした。 このため、私はまず、Rustの公式ウェブサイトの コントリビュータ向けページ に飛びました。 すると、一口に「コントリビュート」といっても以下のような様々な方法があることが紹介されていました。 問題を捜し、仕分け、修正する ドキュメント コミュニティの形成 ツール、IDE、インフラ ライブラリ 言語、コンパイラ、標準ライブラリ 国際化 このように、「コントリビュート」には、ソースコードを書いてPRを提出することだけでなく、ドキュメントの整備や、コミュニティの形成といった取り組みも含まれます。 ただし、私のモチベーションとしては、とりあえず何らかRustのソースコードに手を入れてみたい気持ちがありました。そのため、上記のうち、「問題を捜し、仕分け、修正する」の方法に取り組むことにしました。 初心者向けのIssueを探そう さて、問題を捜し、仕分け、修正すると言っても、私はRust入門者ですから、いきなり複雑度の高いIssueに取り組むのは困難です。幸い、Rustのコントリビュータ向けページでは、初心者向けのIssueの探し方が紹介されていました。 それは、E-easyまたはE-mentorというラベルが付いたIssueを探すことです。 E-easyは文字通り、難易度が低いことを示すラベルです。一方、E-mentorは、そのIssueに関連するコードや、修正の方針について相談に乗ってくれるメンターがアサインされていることを示します。 また、上記の他、ソースコードの該当箇所や、修正の方向性などが分かりやすくブレークダウンして記載されていることを示すE-has-instructionsタグや、そのOSSに初めてコントリビュートする人向けのIssueであることを示すgood first issueラベルなども存在します。 good first issueを求めて 私は、とりあえず上記のラベルが付けられたIssue(例えば、 good first issueタグが付いたIssueの一覧 )を片っ端から見て回り、自分でも取り組めそうなIssueを探すことにしました。 ここで直面したのは、手軽に取り組めそうなIssueほど、既に他の開発者が着手してしまっているという問題です。 業務のプログラミングであれば、新規メンバー向けの手頃なタスクを先輩メンバーが用意してくれたりするものですが、OSSでは誰かがタスクをあてがってくれるわけではないので、この点はどうしようもありません。 私は、自分が取り組めそうで、かつ、他の開発者が着手していないIssueを手に入れるため、 rust-lang/rust , rust-lang/rustfmt , rust-analyzer/rust-analyzer など、思いついたRust製OSSのリポジトリのIssueをひたすら探してまわりました。 誰も着手していないIssueを発見 その結果、発見したのが、rust-analyzerの このIssue です。 これは、rust-analyzerの機能のうち、Assistと呼ばれる入力補助機能群の一つに関するテストについて、冗長になっているものを最小化せよというIssueです。 具体的には、テスト対象の関数内に分岐Aと分岐Bが存在して、それぞれM通りとN通りのパスに分かれているとき、分岐A内の処理と分岐B内の処理の内容が独立であればテストケースの数はM+N個で済むはずのところ、MxN個のテストケースが書かれてしまっていることが問題となっていました。 整数リテラルの基数を変換するAssist機能 テスト対象のAssist機能は、具体的には、エディタ上に入力された整数リテラルの基数を変換するという機能です。 rust-analyzerをインストールしたエディタ上で整数リテラルを入力し、その上にカーソルを合わせると、電球のマーク(VSCodeの場合)が表示されると思います。この電球のマークをクリックすると、 Convert integer base という選択肢が表示されます。これを選択することにより、 100 を 0b1100100 に、あるいは 100 を 0x64 に、といった具合に基数を変換することができます。 テストの肥大化 今回のAssist機能の場合、 基数は2, 8, 10, 16の4通り 整数リテラルがunderscoreで区切られているかどうかで2通り Rustでは、 100_000 のようにunderscoreで整数リテラルを区切ることができます 型名がsuffixとして付いているかどうかで2通り Rustでは、 100i64 のように型名をsuffixとして付けることができます という分岐が存在するところ、 変換元の基数 * underscoreの有無 * suffixの有無 の組み合わせ全パターンのテストが存在する状態となっていました。 rust-analyzerのコード規約においても、テストは最小化することとされており、これは コード規約 にも反している状態でした。 Issueを完遂できるか検証する 簡単そうなタスクだと思ったら、依存関係が複雑に広がっていて想定の何倍も手がかかった、という経験は日常業務においても誰しもしていることだと思います。 OSSの場合、休日に趣味として取り組んでいるわけで、軽い気持ちで手を出してみたもののなかなか完遂できず、しかし一度着手したものを投げ出すのも……という気持ちででダラダラと作業を続ける、という状態になってしまうと精神的に辛そうです。 そこで、このIssueが本当に自分の想定の範囲内で完遂できるかどうか、事前に検証することにしました。 幸い、 修正対象のコード の位置は、Issueから貼られたリンクにより示されていました。初心者向けのタグが付けられたIssueの場合、このようにコードの該当箇所へのリンクが示されている場合が多いようです。 また、 docs/dev/ 以下に置かれたドキュメントを参照しつつ、rust-analyzer全体のアーキテクチャを確認しました。その結果、今回の対象となるAssist機能は、rust-analyzerのコアな部分から疎結合な設計となっており、rust-analyzer全体を理解しなくても着手できそうだということが分かりました。 以上を踏まえ、休日の1日の間に完遂できそう、と判断しました。 手を挙げる やるときめたらすぐに手を挙げておかないと、他の開発者が同じIssueに着手してしまいます。 そこで、すぐに手を挙げました。 すると、10分も経たないうちにメンテナの方から返信があり、このIssueは私が担当することが決まりました。 テスト対象機能内の各分岐は独立 晴れてIssueを獲得したので、さっそく修正に着手します。 テスト対象の機能においては、変換元の基数、underscoreの有無、suffixの有無に応じて分岐が生じていることを上述しましたが、これらの分岐が相互に独立していると言えるのかどうか、改めて検証してみました。 その結果、これら3つの分岐は相互に独立していることが確認できました。 具体的には、テスト対象の機能においては、以下のような順序で処理が行われていました。 まず、underscoreとsuffixを除去する その際、suffixについては、その有無と内容をOption型の変数に保持 underscoreとsuffixが除去された整数リテラルの基数を変換 suffixが付いていた場合、変換後の整数リテラルに変換前と同じsuffixを付加 したがって、underscore有りの場合およびsuffix有りの場合については、どれか1つの基数の場合についてのみテストを行えば足りるということが分かりました。 実際のコードは以下のとおりです。それぞれの処理は、他の分岐でどのパスを通ったかに影響されないことが分かるかと思います。 underscoreの除去 let buf; if text.contains("_") { buf = text.replace('_', ""); // underscoreの除去 text = buf.as_str(); }; let value = u128::from_str_radix(text, radix as u32).ok()?; Some(value) 基数の変換とsuffixの付け直し // 変換先の基数に応じて変換 let mut converted = match target_radix { Radix::Binary => format!("0b{:b}", value), Radix::Octal => format!("0o{:o}", value), Radix::Decimal => value.to_string(), Radix::Hexadecimal => format!("0x{:X}", value), }; let label = format!("Convert {} to {}{}", literal, converted, suffix.unwrap_or_default()); if let Some(suffix) = suffix { converted.push_str(suffix); // suffixを付け直す } 不要なテストケースをひたすら消していく あとは、不要なテストケースをひたすら消していくだけのお仕事です。 具体的には、underscore有りの場合およびsuffix有りの場合については、変換元の基数が10進数の場合のテストのみを残し、それ以外の基数のテストは削除しました。 また、これまでの説明では省略しましたが、テスト対象の機能について、私が作業を行う1週間ほど前にリファクタリングが行われていたところ、それによって不要になったはずのエッジケースに対応するテストなども残存していたので、これらもまとめてお掃除しました。 リポジトリをforkしてPR 通常の社内リポジトリの場合、作業対象のリポジトリ内で feature ブランチなどを切り、 master/main ブランチなどに対してPRを送るのが一般的だと思います。 これに対し、OSSの場合、作業対象リポジトリを自分のGitHubアカウント内にforkし、自分のアカウント内で作業を行ったものをfork元リポジトリに対してPRする、という手順が採られることが多いようです。 このようにするメリットとして、PRがマージされない限り、作業対象リポジトリに対して一切影響を与えないことが保証されるという点が挙げられます。 完成したPR 完成したPR は以下のとおりです。 rust-analyzerの場合、PRやコミットメッセージの書き方に明確なルールはないようだったので、よしなに書きました。 なお、私の場合、英語でのコミュニケーションにあまり自信がありません。そのため、PRの内容に細かい質問をされて英語でのディスカッションが始まると辛いなと思ったので、初めから丁寧な説明を書いて、それを読んで全てを理解してもらう、という戦略を採りました。 diffは以下のような感じです。 ほとんど削除しただけなので真っ赤ですね。 Merged! PR提出後、ドキドキしながら晩ごはんを食べていたのですが、30分も経たないうちにメンテナの方によりマージされました。 削除するだけの簡単な内容ですが、Perfectと言ってもらえると嬉しいですね。 まとめ 初心者でもOSSへのコントリビュートは可能 OSS、それも、rust-analyzerのようにある程度複雑で規模の大きいOSSへのコントリビュートというと、その言語を初めたばかりの初心者には難しいのではないかと思ってしまいがちです。 しかしながら、自分の力量にあったIssueを探せば、初心者でもOSSへのコントリビュートは十分に可能と感じました。 逆に言えば、そのような手頃なIssueを探すことが最も大変だとも言えます。簡単なIssueほどすぐに着手されがちであり、空いていることが少ないからです。 この点については、初心者向けのタグが付いたIssueを片っ端から当たっていくしかないのではないかと思います。 簡単なIssueであっても、やってみると勉強になる 漠然とコードリーディングをするのと、1行でも変更するつもりで読むのとでは、当事者意識が大きく違ってくると感じました。 今回の場合、やったことはテストを削除しただけですが、あるテストケースを削除して良いかどうか判断するためには、テスト対象の処理を注意深く読む必要があり、その結果として、テスト対象の処理そのものについても理解を深めることができました。 自分のPRがマージされたときの達成感は大きい 自分のPRが国籍も所属会社も異なる海外のメンテナによって評価され、マージされるという体験は、日常業務とはまた異なる達成感がありました。とても楽しいので今後も挑戦していきたいです。 最後に CADDiでは、フロントエンド,バックエンド,アルゴリズム,SREと幅広くエンジニアを募集しております.興味をお持ちいただけた方は, こちら からご連絡ください! 明日は、 @yskeee000 さんが、CADDiのサプライチェーン管理システムについてご説明する予定です!
1. Rust Christmas, I gave you my heart はじめに 折角クリスマスなので、楽しい近未来感のある技術を検証してみようと思います。お仕事で使っているRust、 Kubernetes 、そして個人的に興味のあるWebAssemblyをガッチャンコ出来ないか考えてみた。技術者としては常に勉強をする事が重要だと思っているので勉強兼ねて 「highly experimental」 と注意書きのある技術の紹介をしたいと思います。 本日は会社の アドベントカレンダー ということもあり仕事しているふりをする必要があるため、ポエムも混ぜながら Kubernetes 上でWebAssemblyのモジュールを実行するKrustletを使ってみたお話をします。 Krustletとは? GitHubのページ 曰く: Krustlet is a tool to run WebAssembly workloads natively on Kubernetes . Krustlet acts like a node in your Kubernetes cluster. 流行りの言葉を並べた感じになりますが、今年の4月に Microsoft Azure部門で始まった Krustlet プロジェクト。WebAssemblyモジュールを直接 Kubernetes クラスタ ー上でPod同様に実行する Kubernetes のNode同等のプログラム。 通常の Kubernetes クラスタ ーはマスターと、複数のノードによって構成され、各ノード内のコンテナをKubeletがマスターからの指示の元手配する。新しくコンテナを クラスタ ー上で動かしたい場合マスターに依頼を送れば、後はマスターが勝手に適切なノード上で動かしてくれます。 環境構築と設定 まずは Kubernetes の クラスタ ーが必要です。今回はminikubeで立ち上げました。こちらの図で描かれているように、既存のコンテナ実行するNodeと、新たにKrustletを利用してWasm実行するNodeの2つを並行で利用します。 $ minikube start 立ち上がったら動作確認しましょう。minikubeは勝手にkubectlの設定もしてくれるので、そのまま試せるはずです: $ minikube status host: Running kubelet: Running apiserver: Running kubectl: Correctly Configured: pointing to minikube-vm at 192.168.99.100 $ kubectl cluster-info Kubernetes master is running at https://192.168.99.100:8443 KubeDNS is running at https://192.168.99.100:8443/api/v1/namespaces/kube-system/services/kube-dns:dns/proxy To further debug and diagnose cluster problems, use 'kubectl cluster-info dump'. Kubernetes dashboard も立ち上げることが出来ます。 kubectl で取得できる情報ばかりですが、見やすいので一応裏で立ち上げておきます。 $ minikube dashboard 🔌 Enabling dashboard ... 🤔 Verifying dashboard health ... 🚀 Launching proxy ... 🤔 Verifying proxy health ... 🎉 Opening http://127.0.0.1:36305/api/v1/namespaces/kube-system/services/http:kubernetes-dashboard:/proxy/ in your default browser... ブラウザー 開くとこんな感じの ダッシュ ボード。 Kubernetes dashboard 手元で Kubernetes クラスタ ーが動いているので、続いてkrustletを手元で実行します。 GitHub からバイナリーを直接ダウンロード出来ます。 wasi 版と wascc 版がありますが、今回は wasi で行きます。違いをすごく雑に纏めると、Wasmのモジュールが外部環境とインタフェースする上で必要な接続 プロトコール が2種類あり、それに適したバイナリーを利用する必要がある。 こちらのバイナリーは、kubelet同様 Kubernetes のNode上でPod実行出来る環境を用意するものです。普段だとNode上でコンテナが実行されますが、Wasmの場合はKrustletがWasmを実行する形になります。 Kubernetes のcontrol planeにアクセスして、「該当するPodは実行出来るので待っています」という登録をする必要があり、こちら少し分かりにくいんですがささっと進めていきます。 まずはBootstrapから始めます。詳細はこちらを参考にしてください。 $ bash <(curl https://raw.githubusercontent.com/deislabs/krustlet/master/docs/howto/assets/bootstrap.sh) 裏ではまずKrustletが Kubernetes のマスターと通信出来るように 秘密鍵 等を整備している。こちらの情報が最終的には bootstrap.conf に書き出され、これを参考にして krustlet は立ち上がる。 $ export KUBECONFIG=~/.krustlet/config/kubeconfig $ krustlet-wasi --node-ip 192.168.99.100 --cert-file=~/.krustlet/config/krustlet.crt --private-key-file=~/.krustlet/config/krustlet.key --bootstrap-file=~/.krustlet/config/bootstrap.conf BOOTSTRAP: TLS certificate requires manual approval. Run kubectl certificate approve silicon-tls 最後に、Krustletが Kubernetes マスターと通信した上で、 CSR (certificate service request)を手動で承認する必要があります。別ターミナルで指示通り クラスタ ーの CSR 承認しましょう $ kubectl certificate approve silicon-tls certificatesigningrequest.certificates.k8s.io/silicon-tls approved するとついにkrustletが稼動します! BOOTSTRAP: TLS certificate requires manual approval. Run kubectl certificate approve silicon-tls BOOTSTRAP: received TLS certificate approval: continuing [2020-12-14T14:51:51Z ERROR wasi_provider::states::registered] Cannot run kube-proxy [2020-12-14T14:51:56Z ERROR wasi_provider::states::registered] Cannot run kube-proxy [2020-12-14T14:52:01Z ERROR wasi_provider::states::registered] Cannot run kube-proxy [2020-12-14T14:52:06Z ERROR wasi_provider::states::registered] Cannot run kube-proxy こちらエラー出ているのは一旦無視して大丈夫です。ちなみに、krustlet-wasi実行時にはほぼログメッセージが出ないので、気になる方はRustのロギング 環境変数 を設定して RUST_LOG=info で実行してみて下さい。 最後確認のため、ノードとして登録されているか確認しましょう。 $ kubectl get nodes -o=wide NAME STATUS ROLES AGE VERSION INTERNAL-IP EXTERNAL-IP OS-IMAGE KERNEL-VERSION CONTAINER-RUNTIME minikube Ready master 29h v1.14.0 10.0.2.15 <none> Buildroot 2018.05 4.15.0 docker://18.6.2 silicon Ready <none> 29h 0.5.0 192.168.99.100 <none> <unknown> <unknown> mvp いいですね、 silicon という別ノードが立ち上がっています。少し詳細を見てみましょう。 $ kubectl describe node silicon Name: silicon Roles: <none> Labels: beta.kubernetes.io/arch=wasm32-wasi beta.kubernetes.io/os=linux kubernetes.io/arch=wasm32-wasi kubernetes.io/hostname=silicon kubernetes.io/os=linux type=krustlet アーキテクチャ としても wasm32-wasi 対応されている事が分かります。 wasmを kubernetes 上で実行 色々と整備できたので、待ちに待ったデプロイです。krustletのデモをそのまま使ってみました。実際に自分でも コンパイル してみたんですが、OCIコンテナにパッケージする必要もあるため省略します。サンプルの Yaml ファイルはkrustletのレポジトリにあります: https://github.com/deislabs/krustlet/tree/master/demos/wasi/simpleserver $ kubectl apply -f simpleserver.yaml YAML ファイルの中には Tolerations がありますが、こちらはある意味 Kubernetes のハック。 wasm32-wasi の アノテーション があるPodに関しては、 NoSchedule, NoExecute ということで、普通のkubeletは実行しないようにする指示が入っています。この Tolerantions が入っていないとコンテナでも無いWasmをコンテナ扱いしてしまい、実行が失敗してしまいます。 Kubernetes はコンテナの オーケストレーション するものなので、今回のようにwasmを無理やりコンテナに突っ込んだものを扱う仕様は恐らく想定されないはずですから、krustletの開発者はちょっとした工夫をしたんですね。 ここで、先程スケジュールしたPodのログを取りに行きましょう。 $ kubectl logs simpleserver Hello World! Hello World! Hello World! Hello World! Hello World! という訳で、wasmモジュールの出力が kubernetes のログとして見れました!ただの Hello World ですが、これが実現出来ている背景には Kubernetes の素晴らしい設計と、多大なる努力があることを感じた。 waSCCでウェブサーバー動かそう WASIではネットワーキング対応出来ていませんが、折角なのでwaSCCを試してみましょう。krustlet-wasccを同じオプションで実行して: $ ~/Downloads/krustlet-v0.5.0/krustlet-wascc --node-ip 192.168.99.1 --cert-file=~/.krustlet/config/krustlet.crt --private-key-file=~/.krustlet/config/krustlet.key --bootstrap-file=~/.krustlet/config/bootstrap.conf 別ターミナルでサンプルの yaml ファイルをデプロイ:  $ kubectl apply -f uppercase-wascc.yaml pod/uppercase created $ kubectl get pods NAME READY STATUS RESTARTS AGE uppercase 0/1 ImagePull 0 2s こちらのステータスが ImagePull から Running になったらリク エス トを送ってみましょう: $ curl localhost:8080/?hello-world {"original":"hello-world","uppercased":"HELLO-WORLD"}a そしてwasmのログの方を確認すると: $ kubectl logs uppercase 11:47:47 [ INFO] [MDFC3LZ2YAGPTI452SEKDZ3D5D6QJD62R5KDPPJVPDL5B6DFFQKM3B62] Query String: hello-world サンプルの ソースコード を見てみましょう。 fn uppercase(r: codec::http::Request) -> HandlerResult<codec::http::Response> { info!("Query String: {}", r.query_string); let upper = UppercaseResponse { original: r.query_string.to_string(), uppercased: r.query_string.to_ascii_uppercase(), }; Ok(codec::http::Response::json(upper, 200, "OK")) } ウェブアプリケーション になってるじゃん!サンプルの ソースコード は本当に簡単なんですが、これがWebAssemblyになって Kubernetes 上で動いているって、ワクワクしますよね。 2. But the very next day, you gave it away Krustletの実用性 最初は楽しく Krustlet とはお付き合いさせて頂きましたが、やはり highly experimental で実は動かすだけで数日かかりました。相当細かい Kubernetes 内部の仕様も勉強した上で挑むべきだったと反省はしていますが、セキュリティ周り含めて幅広い知見が無いと躓いた瞬間に時間が溶けます。 日々の業務で Kubernetes 使っているので、そちらで利用しようとした結果、色々と苦労して諦めてREADMEどおりにminikubeで立ち上げました(笑) READMEには本番環境では使うなと記載もありますし、WASIの規格が決まっていない時点で本番運用は論外だと思いますが、サーバアプリケーションをWasmに コンパイル してそれを既存のインフラ上で実行させる世界感が見えてきましたね。。。 WebAssembly と Runtime 達 WASI 規格は未完成ですし現時点でもネットワーク通信出来ないし、実用性のあるアプリケーション作ることは出来ない。これからの API の安定やエコシステムの拡大に期待すべきでしょう。今回はネットワーキングサポートの有る waSCC ランタイムを利用して簡単な ウェブアプリケーション を立ち上げたが、こちらもまだデータベースへアクセスも出来ない。 時間注ぎ込んだ結果すぐには実らないかもしれない。クリスマスは、おとなしくコード書くべきだったのかもしれない。 3. Maybe next year, I’ll give it to someone special それでも勉強し続ける アプリやウェブ上のプロダクトを作る時って、ユーザさんには「アプリが使いやすい」とか「サービスすごく便利」と言われますが、「インフラすごく格好いい?」なんて褒めてもらえないですよね。「今日もホームページ動いている、拍手!」なんて言われたら逆に期待値の低さを感じてしまう。インフラのユーザ インパク トはどちらかというと減点方式であり、改善に向けた努力の結果がどうしてもユーザには理解してもらえない部分はある。 インフラの仕事はUI作るのと違い因果関係が見えにくく、長い時間軸で価値発揮する仕事だと思っている。ボタン押してすぐ結果が出ないので、ある程度自分の行動がユーザの為になっている事を自信持って周りとコミュニケーション出来ないと、周りの信頼を失いやすい。デプロイ速度を上げたり、セキュリティ強化したり一般人には通じないが非常に大切な縁の下の力持であることは覚えておきたい。 そんなインフラ開発者に似ているのが日本の製造業に関わっているメーカーや町工場さん。 自作PC 作りながら「このPCのケース、キズ無くていいね!」なんて思わないですよね。「何かネジが入らないんだけど!」という不具合が記憶に残りやすいが、このようなミスを極力下げるために日々プロセス改善を続け、加工技術を磨いてらっしゃる製造業を支えるために弊社ではアプリケーション開発をしております。