Toyokumo Tech Blog

トヨクモ株式会社の開発本部のブログです。

ビジネスモデルがソフトウェアエンジニアリングの現場を決めるーー受託・SLG・PLGから考える

こんにちは。CTOの木下です。

この記事のテーマは、どんなビジネスモデルの元で働くのがソフトウェアエンジニアにとって望ましいのかです。

結論を先に言えば、ビジネスモデルこそ意思決定の構造を決め、それが開発現場に降りてきます。その影響力は、スクラムを採用してるかどうかなんかよりも圧倒的に大きいです。そして数あるビジネスモデルの中で、PLG(Product-Led Growth: 営業ではなく製品それ自体が製品を売るモデル)という選択は、ソフトウェアエンジニアリングの理想に最も近い構造を作ります。

違和感の出発点

私は日本に五万とあるだろう受託開発を行っている会社でキャリアをスタートしました。

より良いソフトウェア開発をしたいと思い、達人プログラマー(第1版)、情熱プログラマーハッカーと画家プログラマのためのサバイバルマニュアルアジャイルサムライなどを読んでいました。これらの書籍に共通する視点として「ソフトウェアの利用者にとって、より良いものを作るためには」という観点があると思います。今とは背景が変わっている部分も多いでしょうが、この観点は今にも通じるはずであり、今も私の追う理想です。

しかしながら当時の私の開発の現実はだいぶ遠いところにありました。

  • Excelで書いた設計書を印刷する。そこに定規を当ててはみ出していることを指摘される
  • 新しい言語機能を使うとそれがわからない人がいるため保守性が下がるという理由でバージョンを上げられない
  • 品質保証担当者は設計やコードの品質ではなく、Excelで書かれたテスト計画書に添付されたスクリーンショットの網羅を気にしている

これはもう10年ちょっと前のことなので今はもうこのような環境は少なくなっているでしょう。ただ問題は、なぜこんなことが起こるのか、です。

なぜこんなことが起こるのか

書籍に書かれているソフトウェア開発と、自分の仕事内容があまりにも違う。どうしてこうなってしまうのか。

当時の私は最初、自分の会社の問題だと考えました。しかし周囲を見渡しても悪意のある人はおらず、むしろ真面目な人ばかりでした。真面目な人たちが集まって、なお理想から遠い仕事になってしまっていました。

ここで気づいたのは、真面目さや能力の問題ではなく、合理的な判断の積み重ねがこういう帰結になる何かが働いている、ということでした。組織論の古典とも言える『学習する組織』でピーター・センゲは「構造が行動を規定する」ことについて書いています。システムの中にいる人々は自分の意思で選んでいると思っていても、実際は構造がその選択を強いている、という視点です。

次に考えたのは、業界慣習の問題ではないかということでした。受託開発業界には特有の慣習があり、それに縛られているのではないか。

そこで私は弊社に転職し自社製品(SaaS)開発をすることになりましたが、そこを通して気がついたのは、同じIT業界であってもビジネスモデルが違えば現場の状況は全く違うということでした。業界ではなくもっと上位の構造、すなわち「誰に、何を、どうやって売るのか = ビジネスモデル」が、開発現場を規定しているということです。

受託開発の構造

受託の本質は作業の代行です。成果物が欲しい人が自社に作るリソースがない場合に、作業リソースを外部から調達するというのが受託のビジネスモデルの基本構造です。

この構造から2つのことが言えます。

第一に、最終成果物から収益を得られないという点です。作ったソフトウェアがどれだけ売れようとも、受託した側の利益に直接影響しません。 製作費としての対価を受け取って終わりです。 この構造では、クライアントが求める以上の品質で成果物を仕上げるインセンティブがありません。

そうなると、ビジネスとしての論点は「最もバリューがあるソフトウェアはどんなものか」ではなく「クライアントが許容する最低ラインを超えているか」に収束します。 もちろん個人として高い品質を追求するエンジニアは存在しますが、それは構造に逆らう行為であり構造に支えられていない分、例えば良いソフトウェアを追求するための行為が人事評価にポジティブに影響しなかったりします。

第二に、料金が人の単価と稼働時間から計算されるという点です。 受託の料金は、最終成果物が作る価値ではなく、投入された人の稼働時間から計算されます。 とすると安く仕入れて高く売るビジネスの原則に従えば、売上を最大化する方法は「安い賃金の人に長く働いてもらう」になります。 これは関係者の倫理感の問題ではなくまた構造の問題です。 プリンシパル=エージェント理論が示す通り、インセンティブ構造がその方向を向いている限り、合理的な判断の積み重ねがこの結果に収束します。

たとえスクラムをしてもペアプログラミングをしても、ビジネスモデルが変わらなければ、開発現場の根本的な性格は変わりません。

自社製品開発ならば良いのか

私はソフトウェアエンジニアやデザイナーのカジュアル面談に出ているのですが、「受託から自社製品開発へ」という軸で転職活動をしている方に出会うことが多くあります。実際、私自身もその考えで転職活動をしていました。

直観的には、自社製品開発なら構造は変わります。作ったソフトウェアが売れれば会社の収益になるわけですから、「最低ラインを超えるかどうか」ではなく「最大の価値を届けるにはどうするか」が論点になるはずです。

しかし自社製品開発にも異なるビジネスモデルがあり、違った構造を作り出します。

SaaSにはSLGとPLGがある

SaaS業界のビジネスモデルは、SLGとPLGに大別できます。

SLG(Sales-Led Growth)は、営業担当主導で商品・サービスを販売するビジネスモデルです。具体的な販売の流れとしては、製品HPで資料請求してもらいリードを獲得、インサイドセールスが電話・メールでアポを取り、フィールドセールスが商談し契約まで持っていき、契約後はカスタマーサクセスが導入定着を支援するというものです。

PLG(Product-Led Growth)は、プロダクト自体が売るモデルです。資料は自由に閲覧でき、無料お試しは何度でも、基本的に商談はなく、気に入ってくれたユーザーが自分で契約する。人が直接介在せず、ユーザー自身の行動のみで購入の判断をしてもらいます。

同じSaaSという言葉で括られていますが、この2つは構造としてまったく別のビジネスモデルです。誰に、何を、どう売るかが違うので、当然ながら開発現場に降りてくる構造も違ってきます。

SLG型自社製品開発の構造

SLGでは、営業組織が顧客と開発の間に入ります。

この構造から、開発現場には固有の問題が降りてきます。

第一に、判断の起点が「営業が売れると言っているかどうか」に収束していきます。営業は顧客と直接向き合っているので、現場の声として「この機能があれば契約が取れる」「あの競合にはあってうちにはない機能がある」という情報を持ち込みます。これ自体は有用な情報です。しかし開発の論点は「顧客群(マーケット)にとって最大の価値は何か」ではなく「(今商談している)顧客がこう言っている」になっていきます。

第二に、販売計画が開発計画を規定するようになります。売上目標を達成するために、この時期までにこの機能をリリースしなければならない。この逆算は事業運営上健全に見えますが、開発側の自由度を徐々に削っていきます。

これも営業の倫理感や能力の問題ではなく、受託開発のときと同様に構造から来るものです。

プリンシパル=エージェント理論で見ると、SLGではエージェンシーの連鎖が二重になります。本来のプリンシパルである顧客と開発の間に、営業という中間エージェントが入る構造です。

営業には営業のインセンティブがあります。例えば、1万5千円の契約を10件獲得した営業と、100万円の契約を1件獲得した営業のどちらが評価されるでしょうか。また営業はどちらを目指したくなるでしょうか。このインセンティブ構造から、SLGはサービスを高単価にし、大企業向けの開発を優先する圧力がかかります。

PLGという構造がもたらすもの

PLGでは、プロダクト自体が顧客と対話する構造なので、営業という中間エージェントを介さずに、顧客の反応が直接開発者に返ってきます。

この構造的な違いは、開発現場に4つの変化をもたらします。

第一に、判断基準が一元化されます。「営業が売りやすいか」や「販売計画に間に合うか」ではなく、「特定顧客ではなく顧客群(マーケット)にとっての価値があるか」が唯一の判断軸になります。社内で議論が分かれたときに立ち戻る場所が、「使ってくれるユーザーにとって良いかどうか」になります。そしてPLGでは結局マーケットで売れるのかに判断が収束します。これが一番大きな変化です。

第二に、エンジニアが直接結果を得ます。作ったものが良ければユーザーが増え、契約が増える。悪ければそうならない。この手応えが、営業という媒介なしにエンジニアに直接返ってきます。結果の手応えが伝聞ではなく、エンジニア自身の経験になります(ただし多くは数値を通じた経験ですが)。

第三に、主体的な開発計画になります。販売計画から逆算した締め切りがありません。リリース計画は開発側が決定します。これは「好き勝手にやる」という意味ではなく、「顧客価値に照らして最適な判断を自分たちで下す責任を負う」という意味です。最善の開発を磨き続ける責任があります。

第四に、コスト構造が優位に働きます。PLGでは売上の増加に対して営業コストが比例しません。だから利益が出やすく、結果として低価格でも提供できます。これは単に価格競争力があるという話にとどまりません。弊社のミッションは「すべての人を非効率な仕事から解放する」です。"すべての人"に届けるには低価格であることは必要条件です。また利益があれば継続した昇給が可能になります

この4点が揃うと、冒頭に挙げた「ソフトウェアの利用者(たち)にとって、より良いものを作る」という私が読んでいた書籍に共通する理想が、個人の奮闘に依らず、構造上の帰結として成立しはじめます。

PLGは楽な道ではない

ここまでPLGの良さを書いてきましたが、トレードオフとしての難点もあります。

第一に、責任がエンジニアに返ってきます。直接結果に向き合うということは、ダメだったときの責任も感じるということです。他責することが構造上できません。

第二に、要求水準が上がります。要件を満たすだけでは不十分で、"売れるもの"を作らなければいけません。機能比較表を満たすだけでは不十分で、体験が良いなど選ばれる理由も作る必要があります。

第三に、直接のフィードバックが得づらくなります。営業が常に購買に直接つながる顧客の声を集めてきてくれる構造ではないので、改善につながる情報を得る難易度が上がります。ユーザーの行動データや問い合わせなどから、"真に解くべき問題は何か"を推察する力が求められます。

第四に、新サービスの立ち上げが難しくなります。PMF(Product-Market Fit)するまでの試行錯誤に使える情報や初期ユーザーの獲得が難しく、暗中模索の中をなんとか切り開く必要があります。

総じて責任の所在が明確になり、言われたものを作るのではなく、主体としてビジネスに参画することが求められます。

これを重荷と感じるか、手応えと感じるかで、PLGが向いているかどうかが分かれると私は考えています。

どういうエンジニアに向いているか

これまで採用活動を行ってきて、PLGが向いているエンジニアには共通する傾向があると感じています。

  • 営業の要求や上司の指示よりも、顧客価値を判断の軸にしたい人
  • 結果に対する責任を負担ではなく手応えとして楽しめる人
  • 自律的に判断することを求められる状況を歓迎する人

もしここまで読んで、自分のことかもしれないと感じたら、一度お話しさせてください。

また、弊社とご縁がなくても本記事が迷われている状況を整理する一助になると幸いです。


トヨクモでは一緒に働いてくれる技術が好きなソフトウェアエンジニアや、デザインが好きなデザイナーを募集しております。

採用に関する情報を公開しております。 またインタビュー等の記事もあります。 気になった方はこちらからご応募ください。

平均年収1,000万円が見えてきたことのご報告及び背景にある考え方についてご紹介

こんにちは。CTOの木下です。

だいぶ久しぶりの記事更新になってしまいました。

この記事は、もう4年弱経ってしまいましたが以下の記事の続編です。

tech.toyokumo.co.jp

前回の記事では、小さなスタートアップが年収1,000万円という金額を払えるようになるまでのストーリーと、弊社の報酬体系についてお伝えしました。

今回は、あれからも年収アップは順調に継続できているということと、それを継続させるための背景・思想、そして今後のさらなる目標などについてお伝えしようと思います。

現状:平均年収1,000万円超えが射程圏内に

まずはこの画像をご覧ください。

平均年収推移 - 会社紹介資料より

このように順調に平均年収は増加を続けており、昨年度実績で947万円と、1,000万円を今年か来年には超える見込みであることが見て取れると思います。

もちろんこの金額は全社員の平均であり、ソフトウェアエンジニア以外のメンバーや新卒メンバーの年収も含まれています。 よって、ソフトウェアエンジニアの平均年収はこの画像の数値を上回っていると考えて頂いて相違ありません。

※弊社ではソフトウェアエンジニアは原則として新卒採用を行っていないものの、例外はあります

また前回記事では

当面の目標としては、日本のSaaS業界でトップの平均年収の会社になることを新たな目標として、事業の成長と昇給の両輪を回し続けられるように努力していきます。

と書きました。他社の動向はそこまで見ていませんが、こちらも見えてきたと言っても良いのではないでしょうか?

原資はどこから?:無い袖は振れない

いくら高い給与を払いたくても、原資がなくてはどうにもなりません。次のグラフをご覧ください。

事業成長の様子 - IR資料より

ありがたいことにおかげさまで弊社の事業は成長を続けており、これが給与アップを実現するための原資になっています。 もちろん、収益増 = 給与増 という図式が世の中で必ずしも定まっているわけではありませんが、弊社は メンバーの成長・優秀なメンバーの入社 -> プロダクトの成長 -> 事業成長 -> 収益増 という論理を成立させることにこだわっており、またそれが望ましいことであるという信念を持っています。

利益成長こそ重要だ

ところで、弊社は死ぬとか死なないとか言われているSaaS事業を行っていますが、代表的なSaaS企業の成長モデルに、営業利益(= 売上 - 原価 - 販管費)が赤字のまま売上成長を続け、将来の期待利益で投資してもらい、それを原資に給与を払うというモデルがあります(ありました)。

弊社は今来ている生成AIによる(スタートアップ業界の投資への考え方に対する)変遷の波を感じる前から、経営の責任とは利益を生み出すことであるという考え方で経営してきており、上記のモデルには反する立場を取ってきました。すなわち、事業価値とは利益であり、利益額を増加させることこそ企業成長であるという立場です。

余談として私自身の考え方ですが、利益成長することは社会貢献性がとても高いと考えています。なぜなら納税額を増やすことができるからです。税というのは個人の選好というフィルタを通さず、(間接的ではあるが)総意によって使い方が決定されるという意味で、直接"社会"に還元している感覚になれます。ですので、利益成長を希求するトヨクモの考え方はとても好きです。この感覚は後述する特定の福利厚生を会社側が選択しないことにも通じていると考えています。

利益成長の継続のためには、すべてを噛み合わせ続けなければいけない

さて、あると嬉しい利益ですが、黙っていて増え続けるということもありません。

継続するのが難しいのですが、継続して成長し続けるためにはあらゆるポイントで狙った通りに狙ったことが噛み合い続ける必要があると思っています。

対象はプロダクトの機能・仕様や技術選定だけではありません。

組織や制度の作り方やワークフローの選び方といった抽象的・長期的なことはもちろん、誰に何を任せるか、このタスクにどれくらいの時間軸を設定するか、ある人を採用するのかしないのかといった日常の意思決定まで、すべての整合性が上手くはまり続けることが大切であるという感覚が強くあります。

弊社において、こういった意思決定に対して強く芯を通す原則にビジネスモデルの徹底があります。弊社はビジネスモデルとしてPLG(Product-Led Growth: 営業ではなく製品それ自体が製品を売るモデル)にこだわっています。このこだわりにより、ソフトウェアエンジニア・デザイナーを中心とした開発メンバーのパフォーマンスを最大限事業成長に活かすことが可能になっています。

tech.toyokumo.co.jp

すべての人にオススメできるわけではない

ここまで良いことばかり書いてきましたが、正直に申し上げると、自社のことであるものの年収が高いからといって、弊社で働くことがすべての人にオススメできるとは言えません。

どんなことに対してもそうでしょうが、あることを実現するにはトレードオフに対して一貫性のある選択を積み重ねる必要性があります。そしてその選択の論理が、人によって合う合わないを作り出します。

成長・成果への継続した要求が存在

弊社が目指す組織は、大人数を集めてマネジメントし総和としての成果を大きくする組織ではなく、人数が少なくても生産性が高く、1人当たり利益が大きく、そして平均年収が高い組織です。どの職種もその職種としてのプロフェッショナルであることに期待します。

そうすると必然的に1人1人への成果期待が大きくなり、裁量が広く責任が重くなります。

個人の好みよりも全体最適化された生産性が高い状態を優先するので、働き方は出社中心で、密度高く働くことを求めています。

仕事はなるべくスピード感を持って進め、評価は成果主義的に行います。

メンバーを子供扱いせず、大人として責任ある振る舞いを求めます。

※ピープルマネジメントの考え方については別記事で詳しく紹介します。

成果主義をベースに裁量がありスピード感のある中で仕事をしたい人には合っていると思います。

合理的なカルチャー故の割り切りあり

上位ポジションでも経費枠ゼロ

この記事をご覧の皆さんの中には、自社のどなたかが、ほんとに事業成長につながるのか不透明な会食を経費で落としている方がいるということを、"まぁそういうものだよな"という感覚で受け止めていることがあるかも知れません。

弊社はこういうことに対して非常に厳しい目線を持っており、役員であっても接待交際費の予算はゼロです。 接待がないと良い仕事ができないなんていうことはないと思っていますし、むしろそこから発生した仕事はそうでない仕事に比べて良いものではないというのが弊社の感覚です。

自由に使える経費というのは、(税がかからない)実利として、またある種権力欲を満たす仕組みとして機能するかもしれないですが、弊社ではそういうのはないです。公明正大な活動を行うことを優先順位高く考えている現れの1つです。

※公明正大は弊社のバリューその1です。

今後、弊社に良いポジションで入社された方で、自由に使える経費がないことに驚くことがあるかも知れませんが、変えるつもりのない大事にしているカルチャーの1つです。ですので弊社に入って昇進したからといって、雑な勤務をしたり、気ままに経費を使ったりはできません。

一見優しそうな福利厚生なし

また、弊社には人によって好みが分かれる福利厚生に経費を使わないというカルチャーもあります。

例えば、ウォーターサーバー、ランチ無料、お菓子無料、飲み物無料といった福利厚生は弊社はやりません。

これは、人によって嬉しい・いらないという好みが分かれるものにお金を使うのであれば、全部給与の予算にしてしまおうという考え方があるからです。

※福利厚生費と給与は、会計上同じ販管費、つまり同じ財布です。同じ財布から出すなら余計な出費しないで給与にしたいという考え方を持っています。お金の使い方は自分で決めてくださいということですね。

面談等で上記のような福利厚生が以前あって嬉しかったという声も聞いたことがあるので、ここも好みが分かれるポイントになると思います。

合理的な考えを徹底するところを好ましく思う人には合っていると思います。

終わりに:今後の目標

色々書いてきましたが、今の弊社の年収は"外資系"企業と比べるとまだまだ高くないのが事実だと思います。

グローバルテック企業は低い限界費用を背景に広大なマーケットから収益を得ることで高い報酬を実現しています。

一方で現在の弊社の売上のほとんどは日本国内からです。

しかし、そんな弊社でさらに成長を続け、今後年収2,000万円に到達するソフトウェアエンジニアがごろごろいるような企業を目指していきたいと考えています。

そのために日々1つ1つやるべきことを重ね、既存も新規も両方、良いサービスを作っていきたいと考えています。

また何年か先にこの目標達成の報告ができるようにがんばっていきます。


※この記事はAI時代だからこそあえてすべて手書きし、手触り感のある文章を目指してみました。

トヨクモでは一緒に働いてくれる技術が好きなソフトウェアエンジニアや、デザインが好きなデザイナーを募集しております。

採用に関する情報を公開しております。 またインタビュー等の記事もあります。 気になった方はこちらからご応募ください。

退職エントリ トヨクモ株式会社

この度2025年6月末をもって、トヨクモ株式会社を退職することになりました。アプリケーション開発チームの鈴木です。 2022年より新卒として入社したため正社員としては3年強の期間になりますが、2019年のちょうど今頃の季節にアルバイトとして入社しており、合計でおよそ6年間という期間でした。

退職理由

起業します。トヨクモとは全く関係ない業界です。 トヨクモで働きつつ個人開発として始めたNoveLandというサービスを事業としてやっていきます。内容は小説をはじめとした物語のための創作プラットフォームです。

novel-land.com

学生時代に就職活動を行っていた頃、元々はこの業界でエンジニアをやることも考えていたのですが、給料や環境など働いていくことを考えたら明らかにトヨクモの方が良かったためそのまま就職させていただきました。 今にして思えば自分がこだわりのある分野で思い通りにやれず葛藤を抱えるよりかは、趣味の時間でのびのびと個人間発をやれたからこそ継続できたのかもしれません。

当初は独立するのはもっと先の話になるだろうと思っていたのですが、一緒にやってくれるという方に出会ったことをきっかけに起業することにしました。

また、もう一つの大きな理由は最近のAI関連の目覚ましい技術発展です。 自分自身が使うという観点でも、自分が詳しくない分野について自分で考えて闇雲に行動するよりも、とりあえずAIに聞いて出てきた回答を試すことによって、平均以上の結果を出しつつ間違っていたら次の経験に活かすというサイクルをたくさん回せるようになりました。 そして、NoveLandのサービスの観点についても元々自分が大学時代に研究していた分野がAIだったということもあり、来たる時代に備えてAIを活用できる仕組みをコツコツ準備していたため、こういった状況からやるとしたら今しかない、そう思いました。

トヨクモでやったこと

アルバイト時代は製品のバグ修正や細かい機能追加をやりつつ、Next.jsとTailwindcssを使い、CMS機能を持つ会社や製品のホームページのリプレースや保守をやっていました。 正社員として入社してからはそれらを引き継ぎつつ、当時リリースしたばかりだったトヨクモスケジューラーのアプリ版を担当しました。 今でこそ会社の技術スタックはNext.jsが使われるようになってますが、Next.jsだったりReact Nativeだったり当時会社で他にやっている人が少ない技術を中心に触らせていただいたように思います。

そうして経験を重ねるうちにToyokumo kintoneAppアカウントをメインの担当者としてやらせていただきました。 kintone連携製品の全製品のユーザーが使用するサービスであり、その分利用者も多かったため良い意味でプレッシャーとなり、やりがいがありました。 中でも一番印象に残っているのはテンプレートギャラリーという機能です。 インフラ、バックエンド、フロントエンドの全工程の設計の叩き台作る作業を初めて自分の手で行ったため、とても思い入れがあります。 また、開発中にテンプレートギャラリーというプラットフォームを通じて、どんなことをやっていきたいか開発本部長の方から聞く機会があったのですが、自分が物語の分野で実現させたいと思っていたことと重なる点も多く、本当にあと一つ何かが違えば自分はトヨクモに居続けただろうなと思いました。

その他にも、細かいものを含めればほぼ全ての製品にPRを出したり、アルバイトの採用面接をやらせていただいたりと振り返ってみると幅広く担当させていただきました。

トヨクモで働いてどうだったか

とにかく学びが多かったです。 技術レベルをはじめとしたさまざまな点での当たり前の水準が高く、日々の業務は良い刺激の連続で、辞めるのが惜しいほどでした。

また、働き方についても目的を明文化し、必要なことを必要なだけやるという合理的な社風で、この仕事は何のためにやっているんだろう、といった疑問を抱くことなく業務に専念できました。

そして、正社員として働き始めるまでは、エンジニアにコミュニケーションスキルはそこまで必要ないだろうと高を括っていましたが、実際にはそんなことはありませんでした。 通常業務に関しては、プログラムを書くときにこのコードもっとああすればよかったなという後悔よりも、もっと要件を事前に確認したり方針についてテックリードの方に相談した方がよかったなという後悔をすることの方が多く、働き方としてその辺りを中心に改善することを意識していました。 その他にも、HPの作成一つでも他部署の方との調整が必要で視野が広がりましたし、アルバイトの採用や研修、マネジメントを通じてされる側からする側の立場になり新しい視点で組織を見る力が養われました。

おわりに

お世話になった皆様、本当にありがとうございました!

こうして振り返ってみると本当に会社によくしていただいたのだと改めて気付かされました。 活動していく中で、またいつか良い形で恩返しできるように頑張ります。

背景色と文字色でどの色の組み合わせが見やすいか?Webデザイナもプログラマも迷わなくなる方法

北川です。

先日、任意の色の背景がある時、テキストの文字色を黒か白どちらを使うのが適当か?という問題に当たりました。最終的にまあまあ満足のいく結果が得られたので記事にしました。

背景色と文字色の問題

人間が文字色を決めた

これは色々な背景色について黒か白の文字色の選択を筆者の主観に基づき決めた画像です。社内でフィードバックを求めたところ「黄緑や水色が見にくい」と不評でした。

このように当人としては見やすいと思っても、他の人から見やすいとは限りません。

またよくある問題として、一律黒文字テキストでスタイリングしていたがボタンの塗りつぶしが濃い色のときに見辛くなってしまった、ということもあると思います。

文字色が黒か白かに限らずそれ以外の任意の文字色でも同様な一般的な問題かと思います。

毎回どうすれば良いか判断するのも手間がかかって仕方がありません。何か良い解決方法はないものでしょうか?

WCAG

さて、WCAGというものがあります。WCAG(Web Content Accessibility Guidelines)は、ウェブコンテンツのアクセシビリティを向上させるための国際的な標準です。

今回の問題も文字の見やすさといったアクセシビリティに関連した話なので、ここにヒントがありそうです。

文字色決定戦略を決める

Web Content Accessibility Guidelines (WCAG) 2.2

The visual presentation of text and images of text has a contrast ratio of at least 4.5:1, except for the following

Contrast (Minimum) の項に関連しそうなことが書いてありました。contrast ratio というものが少なくとも 4.5:1 であるべきだと。なるほどまったく分かりません。

Contrast (Enhanced) の項には、contrast ratio は少なくとも 7:1 であるべきともあります。どうも contrast ratio は高ければ高いほど良さそうです。

次に、contrast ratio とは何かが気になります。

(L1 + 0.05) / (L2 + 0.05), where

L1 is the relative luminance of the lighter of the colors, and

L2 is the relative luminance of the darker of the colors.

と、contrast ratio の項にありました。どうやら contrast ratio は relative luminance という変数に依存した式で表せるようです。

さらに掻い摘んだ説明をChatGPTに聞いてみると「コントラスト比は、テキストや図形などの要素とその背景の間の明るさの差を示します。これは、テキストを読みやすくするために必要な要素である」と教えてくれました。コントラストと文字色との話がつながりましたね。この比が 4.5:1 または 7:1 より高ければクッキリハッキリ見えやすいということです。

次に、relative luminance とは何かが気になります。これが分かればついに文字色決定戦略を決められる予感がします!

the relative brightness of any point in a colorspace, normalized to 0 for darkest black and 1 for lightest white

For the sRGB colorspace, the relative luminance of a color is defined as L = 0.2126 * R + 0.7152 * G + 0.0722 * B where R, G and B are defined as:

と、relative luminance の項にありました。さっきの式よりも複雑ですが、知らない変数がありません!RGB値に依存した値が relative luminance のようです。

さらに掻い摘んだ説明をChatGPTに聞いてみると「相対輝度(Relative Luminance)は、色の明るさを表す指標の一つです。色の相対輝度は、その色がどれだけ明るいかを示し、通常、0から1までの範囲で表されます。0は完全な黒を示し、1は完全な白を示します。」と教えてくれました。要するに色の明るさのことでした。

「ある背景色について、文字色1と文字色2のどちらがよりアクセシビリティの観点で見やすいか?」

これまでの要素を踏まえると以下のように言い換えられそうです。

「色1について、色2と色3のどちらがよりコントラスト比(contrast ratio)が高いか?」

ここまでくると、このお題を解決する戦略が決められそうです。その戦略はこうです。

  • step1: 色1,2,3 についてそれぞれのRGB値を計算する
  • step2: 色1,2,3 についてそれぞれの相対輝度(relative Luminance)を step1 で求めたRGB値を使って計算する
  • step3: 色1,2のコントラスト比と、色1,3のコントラスト比(contrast ratio)を step2 で求めた相対輝度を使って計算する
  • step4: よりコントラスト比が大きい方がアクセシビリティの観点で見やすいので採用する

実装

// 色1について、色2と色3のどちらとのコントラスト比が高いかを返す
const selectHigherContrastRatio = (c1: string, c2: string, c3: string) => {
  // step1: それぞれのRGB値を計算する
  const [r1, g1, b1] = getRGB(c1);
  const [r2, g2, b2] = getRGB(c2);
  const [r3, g3, b3] = getRGB(c3);

  // step2: それぞれの相対輝度を計算する
  const L1 = getRelativeLuminance(r1, g1, b1);
  const L2 = getRelativeLuminance(r2, g2, b2);
  const L3 = getRelativeLuminance(r3, g3, b3);

  // step3: c1とc2とのコントラスト比とc1とc3とのコントラスト比を計算する
  const contrast1 = getContrastRatio(L1, L2);
  const contrast2 = getContrastRatio(L1, L3);

  // step4: どちらとのコントラスト比が高いか。高い方を返す。
  return contrast1 > contrast2 ? c2 : c3;
};

実装の細部は置いておくとして、戦略を直裁に書き下すとこのようになります。c1, c2, c3 は #ffffff といったカラーコードの形状の文字列を想定しています。

次は getRGB です。

const getRGB = (color: string) => {
  // color は '#ffffff' の形式
  const c = color.substring(1, 7);
  const r = parseInt(c.substring(0, 2), 16);
  const g = parseInt(c.substring(2, 4), 16);
  const b = parseInt(c.substring(4, 6), 16);
  return [r, g, b];
};

あまり説明することがないですね。次は getRelativeLuminance です。

// https://www.w3.org/TR/WCAG22/#dfn-relative-luminance
const getRelativeLuminance = (R8bit: number, G8bit: number, B8bit: number) => {
  const RsRGB = R8bit / 255;
  const GsRGB = G8bit / 255;
  const BsRGB = B8bit / 255;

  const R = RsRGB <= 0.03928 ? RsRGB / 12.92 : ((RsRGB + 0.055) / 1.055) ** 2.4;
  const G = GsRGB <= 0.03928 ? GsRGB / 12.92 : ((GsRGB + 0.055) / 1.055) ** 2.4;
  const B = BsRGB <= 0.03928 ? BsRGB / 12.92 : ((BsRGB + 0.055) / 1.055) ** 2.4;

  return 0.2126 * R + 0.7152 * G + 0.0722 * B;
};

これは relative luminance について調べる過程で実装例も一緒に見つけてしまったのでコピーぺしました。参考にしたコードもリンクさせていただきます。https://gist.github.com/jfsiii/5641126

最後に getContrastRatio です。

// https://www.w3.org/TR/WCAG22/#dfn-contrast-ratio
// コントラスト比の公式
// (L1 + 0.05) / (L2 + 0.05)
// L1: lighter color の relative luminance
// L2: darker color の relative luminance

const contrastRatioW3C = (L1: number, L2: number) =>
  L1 > L2 ? (L1 + 0.05) / (L2 + 0.05) : (L2 + 0.05) / (L1 + 0.05);

getContrastRatio では明るい色をL1に代入し暗い色をL2に代入しなければならないので、最初にふたつの相対輝度(色の明るさ)を比較します。そして明るい方をL1として、暗い方をL2として計算します。

これですべての関数を実装し終え selectHigherContrastRatio が完成しました!

冒頭の画像を思い出してください。色々な背景色について黒か白の文字色付けていく操作を完成した selectHigherContrastRatio を使ってもう一度やってみます。

selectHigherContrastRatio('いろいろな背景色', '#ffffff', '#000000');

WCAGの基準に沿って決めた

うん!なんだか良さそうです!特に不評だった黄緑と水色の文字色は黒になり、やはりアクセシビリティの観点で良くなかったと判明しました!

最後に注意点として、selectHigherContrastRatio によって選ばれた色の組み合わせが必ずしもアクセシビリティの観点で良いというわけではないということです。

そういうものが欲しい場合は、改善として contrast ratio が 4.5:1 か 7:1 を越えるよう検証するコードをどこかに入れると良さそうですね。

おわり

WCAG に沿った決定をした結果、ユーザー体験が向上し、デザイナと実装者の両方の頭を悩ませる負担を軽くすることができました。

よかったですね。


トヨクモ株式会社ではUXに真摯に向き合いたいデザイナ、Clojureを書きたいエンジニア、PHP/Reactを書きたいエンジニア、技術が好きなエンジニアを募集しております。

よろしければ採用ページをご覧ください。

4月のTokyo Clojure会を開催しました!

自己紹介

こんにちは!アーロンと申します。4月1日にトヨクモのエンジニアとして入社させていただいて主にClojureについてブログを書いてる人です。アメリカ人で純粋な日本語話者ではないのでよろしくお願いします! Github - aburd (Aaron B) · GitHub

概要

4月11日にトヨクモのオフィスで4月のTokyo Clojure会を開催させていただきました!

元々の開催者はPaul Chunという方なんですが、開催することはできなくなりました。このグループで僕はClojure勉強できたので、無くなったら残念に思ってグループの管理者の責任を渡してもらいました。ちょうど僕がトヨクモに入社したので、Clojure使ってる会社として会場を貸すことになりました!

今月のテーマはHTMXでした! HTMXの紹介

内容

1. 僕はHypermediaとSPAのアーキテクチャーの違いとHypermedia中心のアーキテクチャーがどうやってClojure・HTMXができるのがというLTをさせていただきました

docs.google.com

2. HTMXとReititとHiccupに基づいたフレームワークsimpleui.ioの作成者Matthew MolloyさんからのSimpleUIの紹介をしていただきました

Matthew MalloyはSimpleUIで作ったゲームについて話してくれた

Matthew MalloyはSimpleUIで作ったゲームについて話してくれた

Matthew MalloyはSimpleUIを紹介してくれた

3. Pizzaを食べながらLTのこととWeb開発にどういう影響がありそうなのかというような会話できました

円になってピザを食べながらHTMXとWebの次の進化はどうなるのかを議論した

僕からの感想・学んだこと

やっぱり現在ウェブアプリを作るなら、SPAフロントエンドJSONを吐き出すバックエンドが通念になってるのですが、何を作るかによってそこまでやる必要がありません。むしろある条件によって大変なことにあう可能性があり、気をつけなければならないです。主にソフトチームはFacebook・Googleの規模ではないし、同じ規模の問題を解決してないかもしれないので、多分アーキテクチャーを真似する必要がありません。HTMXを使えば、コードベース行数がかなり減るしデプロイすることも減るだろうしブラウザーはすでに入ってる機能が利用できるし、何を作ってるかによってかなり複雑性が減る可能性があります。

SPA・JSONバックエンドのアーキテクチャーに向いたアプリ

  • 複雑なUI (例えばWebのスプシーアプリ、UIに更新が多く)
  • クライアントが多く、HTMLよりもJSONがパース・シリアライズことでバックエンドCPUを節約する必要がある場合 (これでも大きい規模でしか効果でない臭さがあり)
  • オフラインでも使えるようなアプリ

HTMXに向いたアプリ

  • 大体の「ビジネス用のアプリ」
  • UIにユーザー対話数は中級レベルのアプリ
  • クライアントはダウンロード速度が遅くても電波が悪くても使えることが必要 (HTMXのJSはただ14KB)

コロナ時代から対面のイベントがなくて、みんな久しぶりに会えたし、新しい顔も見えたし、楽しかったです 毎月・2っヶ月に1回としてのイベントをしていこうと思っておりまして、東京にいるならぜひ来てください!

Clojureを静的型付け言語にする

こんにちは、開発本部の松尾です。

Clojureは標準ライブラリに使いやすい道具が充実していて、可読性の高い抽象的なコードが書きやすく、Javaとの相互運用のお陰で使える資産も多いため実用的で、良い言語だと思っています。

しかし、おこがましくも1点だけ改善出来るところがあると思っています。それは、「静的型解析が出来ない(弱い)」という点です。

Pythonの型ヒント、Ruby3の静的型チェッカーの例に見られるように、静的型解析によるメリットは、パフォーマンスという観点を抜いても、バグを減らして開発効率を上げる観点から、もはや見過ごすことが出来ないのが時代の潮流だと感じています。特に中規模〜大規模の業務でのコードになってくると、正確さやドキュメント製の観点から重要度が増してくるかと思います。

ということで、静的型解析を出来るようにしていきたいと思いますが、なぜやる必要があるのか。既に他の手段は無いかについて次の項でお話します。

本題を見たい方は読み飛ばしてください。

なぜやる必要があるのか、代替手段の認識と反省

私が認識している中で、Clojureに型という概念を導入する方法は4つあります。

1. メタデータのタグ

^string, ^bytes などのように、シンボルの前につけているやつです。 しかし、Javaとの相互運用の際に最適化に使われるという目的が大きく、バグをへらす為の静的解析に使うにはあまりにも表現力が低いです。例えば、ある形を持ったマップの配列、といったClojureのオブジェクトの型を表現することが出来ません。

2. schema.core

これはトヨクモでも採用しているライブラリです。プリミティブな型やマップの型はもちろん、それらの配列や、Javaの型も扱うことが出来るので表現力は十分です。解析しきれない時は Anyを指定するか、単純に型を付けないかを選べば良く、書いた分だけ見返りが得られて、使っていない部分の開発を妨げることもありません。とても現実的だと思います。しかし、型があっているかどうかは実行時にしか分かりません。主に関数の入力と出力の際に検証をしているので、出せるエラーメッセージには限界があります。また、コードにおける原因と結果が遠いときは問題箇所を見つけづらいという問題意識があります。 また、全てのコードパスを確認するためには、多くのテストを書く必要があります。

3. core.typed Clojureのコードを静的解析することが出来ます。

しかし、以下のリンク先では、CircleCIが実際に導入してから2年後、実用を通して分かった問題点が挙げられています。これは重要参考文献だと思います。 circleci.com

簡単にまとめると以下の通りです。

  1. 解析が遅い
  2. 型を解析しきれないことがあり、その際に型チェックを無視するか、複雑な型アノテーションをつけるかを選ばなくてはならない
  3. サードパーティーコードには型がない。

後発のtypedclojureも出ていますが、開発が非常に盛んというわけではなく、基本的な方針は変わっていないようです。 また、より広く使われているschema.coreを採用している企業にとって、移行コストがかなり高くつくことが予想されます。

4. malli + clj-kondo

これは今回やろうとしていることに近いかと思います。 スキーマとバリデーションの機能を提供するmalliというライブラリが、clojureのリンタである clj-kondoと連携することによって、実行前にスキーマを静的に解析した結果を知ることが出来るようです。

github.com

特に大きな欠点はなさそうですが、強いて言えば、他のスキーマライブラリを使っているプロジェクトにおいては移行コストが高く付きそうです。

結論

ということで、既存でも型を解析するツールチェインは存在していますが、トヨクモでも利用していて、比較的広く使われているschema.coreのスキーマを静的解析する、ということには意義がありそうです。

大まかな方針として、既にあるものから漸進的に解析出来る情報を増やしていけば、既存コードに影響を与えることなく、静的解析で得られる情報を増やしていけるのではないか、という算段です。

サードパーティーコードに型がないという問題点は致し方がないと思いますが、他の例を見てみて、解析に必要な情報が足りないときにエラーを出したり、明示的に無視をするのを要求するのではなく、Any(なんでも許容する)という寛容な方に倒し、間違っている確証が得られる箇所のみをエラーとするベストエフォート方式が良いのではないかと思いました。

とはいえ、型が分からないものをそのままにしておくと、Anyが伝播していって全体的に型付けが弱くなってしまったり、Anyの場合にバグを検出することが出来ないという問題があります。 この問題に関しては、型アノテーションで補足をすることで解決出来るかと思います。静的解析が出来ない場合はschema.coreの実行時解析が役に立ちそうです。

速度についてですが、例えば、HTTPのパース、JSONのパースを始めとする何度も実行されるコードはやはり高速であればあるほど良く、そうある努力をすべきだと思っています。 コードの解析もそれに含まれるのではないかと思います。

今回は開発言語にRustを用います。 解析の性質上、取りうる型を網羅的に検査する為に、Rustの強い型付けや代数データ型は非常に有用です。また、解析の速度におけるオーバヘッドにならず、特に意識しなくてもそこそこ高速なことを期待しています。

読み進める前に

この記事は、Clojureを静的型付け言語にした際の機能や実装方法の概要を紹介するものですが、あまり実装の詳細を書いていくと理解しづらくなってしまうため、実装の代わりに「どのようなデータ構造を扱っているのか」という側面で解説していきます。 また、実装の詳細が気になる方の為に、合わせて実際のコードのリンクを近くに貼っておきます。

1. Clojureのパーサーを作る。

解析の準備の為に、Clojureのソースコードの文字列から、Clojureの構文を表すデータ構造に変換する必要があります。文字列からそのようなデータ構造に変換する機構のことをパーサーと呼びます。(大雑把な説明)

Rust製Clojureパーサーは無い気がしたので、まずはClojureのパーサーを作ります。 本題ではありませんので実装方法は割愛しますが、例えば、以下のソースコードを次のように変換出来るように実装します。

(s/defn add-one :- s/Str ;; サンプルコード
  [a :- s/Int]
  (+ a 1))
List(
  Symbol {ns: Some("s"), name: "defn"},
  Symbol {ns: None, name: "add-one"},
  Keyword { ns: None, name: "-"},
  Symbol {ns: Some("s"), name: "Str"},
  Vector (
     Symbol {ns: None, name: "a"},
     Keyword {ns: None, name: "-"},
     Symbol {ns: "s", name: "Int"}
  )
  List (
     Symbol {ns: None, name: "+"},
     Symbol {ns: None, name: "a"},
     IntegerLiteral(1)
  )
)

このとき、コードに影響を及ぼさないコメントや空白などの情報は削ぎ落とされています。 このようなデータ構造を、言語処理系の用語で「抽象構文木」と呼びます。

英語で AST(abstract syntax tree)と略すことが多いので、以降はASTという言葉を使います。

詳細を見たい方は以下のファイル内の型を見るとよりイメージが湧くかもしれません。実際は、位置の情報なども含まれています。

github.com

2. ASTを解析しやすいデータ構造に変換する

先程パースしたデータはあくまでリストの中にシンボルなどの下位のASTが入っているものに過ぎません。 つまり、関数ということが分かっているのではなく、「ただのリスト」に過ぎないのです。

他のLisp以外の言語は構文の種類がより多く、ASTの時点で意味合いが具体的に決まっていることが多いため、文字列からパースした段階でその情報を得られることが多いですが、Clojure(Lisp)は構文がリスト、ベクタ、リテラルぐらいしかなく、自由度が非常に高いので、ここから更に意味を抽出していきます。

Clojureの内部構造としては、def, defn, if, let などの特殊形式はマクロとして定義されていますが、これを展開して解析しようとするとキリが無いので、ある程度決め打ちでこちらが分かる範囲のデータに持ち込むというわけです。

先程のASTから以下のようなデータを生成します。

Function (
   decl: FunctionDecl,
   name: "add-one",
   arguments: [
      (Binding::Simple("a"), Some(Scalar("Int"))) // Int型
   ]
   return_type: Some(Scalar("Str")),
   body: [
     Call {
       func_expr: Symbol {ns: None, name: "+"},
       args: [
          SymbolRef {ns: None, name: "a"]
          IntegerLiteral(1)
       ]
     }
   ]
)

例えば、defnから始まるリストが、Function、それに続くシンボルが名前、その後のベクタが引数、 + から始まるリストが Call、というように、defnマクロが持つ意味に従って再解釈されています。

他にも特殊形式はあると思います。例えば、ifやwhenなどがありますが、これらもそれぞれのマクロが持つ意味に従って解釈していきます。

詳細を見たい方は以下のファイル内の型を見るとよりイメージが湧くかもしれません。

github.com

3. データを用いて型を解析していく

さて、ここまで作成してきたデータにより、型を解析するための準備は揃っています。 後は以下のことを出来るようにするだけです

  • 関数、変数、スキーマの型解決
  • 型シンボルを比較可能な形式に解決
  • 型が他の型に代入できるかのロジックの実装
  • 関数呼び出し、定義、代入、関数の戻り値などに関して、型が定義されたものに一致するかを検査する

関数、変数、スキーマの型解決について 簡単な実装方法を紹介します。 変数の参照先の解決という問題は、「スコープ」という概念と、あるスコープの中で参照出来る変数というデータを表現することによって解決することが出来ます。

例えば以下のような 関数定義があったとします

(s/defn say-hello [to :- s/Str]
   (println to)

このとき、[to :- s/Str] の後の (println to) の部分が関数のボディにあたりますが、この中では、to という変数を参照出来るようになっていて、s/Str型を持っているということも分かるはずです。 この情報を defn に対応するデータを読み込んだ際にマップに保存していきます。

スコープはネストされていくものなので、深さによって変数が指す値が変わることがあります。そのため、マップの可変長配列 というデータ型で、関数や変数、スキーマを解決するためのデータを表現することが出来ます。

以下に実際のコードの関数の解析の例を載せておきます。

pub fn analyze_function(errors: Errors, context: Context, func: &Function) {
    let func_ty = get_func_type(context.clone(), &func.decl);
    context
        .borrow_mut()
        .variable_scopes
        .last_mut()
        .unwrap()
        .insert(func.decl.name.clone(), func_ty);
    variable_scope!(context, {
        for (arg_binding, opt_arg_ty) in &func.decl.arguments {
            match &arg_binding.value {
                semantic_parser::semantic_ast::Binding::Simple(name) => {
                    let arg_ty = if let Some(arg_ty) = opt_arg_ty {
                        context.borrow().resolve_type(&arg_ty).clone()
                    } else {
                        ResolvedType::Unknown
                    };
                    context
                        .borrow_mut()
                        .variable_scopes
                        .last_mut()
                        .unwrap()
                        .insert(name.clone(), arg_ty);
                }
                semantic_parser::semantic_ast::Binding::Complex { keys, alias } => todo!(),
            }
        }
        for expr in &func.exprs {
            analyze_expression(errors, context.clone(), expr);
        }
    });
}

このような関数を書いていきます。特にClojureは ifもwhenも様々なものが「値」なので、値の解析については再帰的に解析することになります。 その際も let forなど変数を束縛するマクロがあれば、スコープを追加して値を設定していきます。そして、それらの値も解析が終わる際にスコープをpop(削除)します。

この実装により変数などの型が解決出来るので、例えば、関数呼び出しの際に、以下のように型が間違っているかどうかが判定出来るようになります。

(say-helllo ;; s/Strを受け取る関数であることが分かっている
  1) ;; リテラルはそのまま s/Intであることが分かるので、これはエラーとなる

実際の実装のリンクも貼っておきます。

github.com

github.com

エラーを標準出力する

以上で紹介した実装方法によって、型が間違っている箇所をエラーとして蓄積していきます。 あとは集まったエラーを標準出力に出力するだけです。以下のようなイメージです。

for error in errors {
   println!("{}:{}:{}: {}", file_path, error.location.line, error.location.col, error.message)
}

エディタで表示する

標準出力だけでも有用かもしれませんが、普段の開発に組み込むことを考えれば、エディタ上でエラーを表示出来るのが望ましいです。 今回は弊社で広く使われている IntelliJ での方法を紹介します。

まず、IntelliJのプラグインであるFileWatcherを導入します。

設定 > ツール > FileWatchersを開き、リストに設定を追加します。

肝はこの部分です。この部分を標準出力の形式に合わせるだけで、保存時に出た標準出力をパースして、該当位置に波線が引かれるようになります。

ホバーすると、$MESSAGE$ の内容のツールチップを表示してくれます。

最後に

今回は、Clojureのパーサーから書いた為に結構な実装量になりましたが、これでClojureの静的解析をするための基礎が出来ました。まだ初歩的なものだしバグも多いかもしれませんが、このプログラムを漸進的に成長させて、実際に業務で使えるところまで持っていきたいという所存でいます。

他にも

  • 他のファイルやライブラリの読み込み、namespaceの解決
  • 標準ライブラリへの組み込み型付け
  • サードパーティーコードへの型付けの提供

などやることが多く考えられます。 もし、Clojureの静的解析に興味を持っていただけた方は、ご意見、感想を頂けると励みになります。 最後まで読んで頂きありがとうございます。


トヨクモでは一緒に働いてくれる技術が好きなエンジニアを募集しております。

採用に関する情報を公開しております。 気になった方はこちらからご応募ください。

Babashka pods から使う clj-kondo

開発本部の飯塚です。

この記事は Clojure Advent Calendar 2022 5日目の穴埋めに向けた記事です。

今回は clj-kondo を Babashka pods として利用する方法を簡単に解説したいと思います。

用語の説明

clj-kondo

clj-kondo とは何か、どう使うのかについては以下の記事でまとめているので もし知らなければ先にこちらを参照することをおすすめします。

tech.toyokumo.co.jp

Babashka

Babashka は Clojure のインタプリタです。 GraalVM を使ってネイティブイメージとして動くため起動がとても速く Clojure の機能のほとんどがそのまま使えるためスクリプトとしての用途などで広く利用されています。

Babashka pods

Babashka pods とは Babashka にて Clojure ライブラリとして使える「プログラム」です。

あえて強調した通り「プログラム」なので Clojure で書かれている必要は勿論ありません。 Go で作られたものもあれば C# で作られたものもあります。

Babashka pods として動くプログラムは nREPL をベースとしたプロトコルで Babashka とやりとりし動作します。 このプロトコルでは nREPL 同様に Bencode を使っていて、メッセージのやりとりは標準入出力で行われます。 なので基本的には Bencode さえ扱えればどの言語でも Babashka pods として動くプログラムは作れます。

参考までに拙作の Dad でも Babashka pods に対応しているので、その部分へのリンクだけ貼っておきます。 https://github.com/liquidz/dad/blob/main/src/dad/pod.clj

Babashka pods としての clj-kondo

前置きが長くなりましたが、clj-kondo は Babashka と同じ Borkdude 氏によるものなので当然 Babashka pods としても動きます。

一応 clj-kondo 自体が clojars にデプロイされているのでライブラリとしても勿論利用できはするのですが、 Babashka pods として利用することで手軽に便利スクリプトが作れるのでその利点と方法が紹介できればと思います。

今回はあるプロジェクト配下で private にできそうな public var を検出するスクリプト を例に説明します。

準備

まずは準備です。例えば foo.clj のようなファイルを用意してみましょう。 ファイルの保存先は任意のプロジェクトルート直下を想定しています。

(ns foo
  (:require
   [babashka.pods :as pods]))

;; Babashka pods として clj-kondo を読み込む
;; clj-kondo コマンドへのパスが通っている必要あり
;; もし clj-kondo コマンドを持っていない場合は Pod registry も利用可能
;; https://github.com/babashka/pod-registry
(pods/load-pod "clj-kondo")

;; 読み込んだ pod で提供されている ns を require
(require '[pod.borkdude.clj-kondo :as clj-kondo])

これだけで Babashka pods として clj-kondo を使う準備は完了です。 clojars のライブラリから使う場合は Leiningen や Clojure CLI を使って project.clj なり deps.edn なりから用意する必要がありますが、 Babashka pods から使う場合はファイル1つだけなのでかなり手軽であることがわかると思います。

エラーが無いかは bb コマンドを使って bb foo.clj のように実行しても確認はできますが、 いくら Babashka の起動が速いとは言っても非効率なので Clojure 開発環境から Babashka の REPL に接続することをおすすめします。 そうすることでフォーム単位での評価ができ、 REPL 駆動でスクリプトを書くことが可能になります。

なお最近の Clojure 開発環境であれば大抵は Babashka に対応しているはずなので、 どうやって Babashka の REPL に接続するのかは各開発環境のドキュメントを参照してください。 例えば拙作の vim-iced では IcedInstantConnect コマンドでREPLの起動と接続が可能です。

なおもし Clojure の開発環境がない場合は以下のまとめ記事を参考にすると良いでしょう。

tech.toyokumo.co.jp

プロジェクトの解析

次に実際に clj-kondo を使って解析データを取得してみましょう。

:lint で指定しているディレクトリは検出したいプロジェクトに応じて変更してください。 今回説明するスクリプトの中で一番時間がかかるのがここの clj-kondo による解析処理なので、例えば analysis-data として束縛しておけばその後の解析データを使った処理でデータ構造の確認や情報の抽出が楽になります。

(def analysis-data
  (-> {:lint ["src"]
       :config {:output {:analysis true}}}
      (clj-kondo/run!)
      (:analysis)))

public な var の抽出

解析データが取得できたら次は public な var の抽出です。

今回は :var-definitions という var の定義情報を利用します。 解析データに他にどのようなものが含まれるのかの詳細は clj-kondo のドキュメントを参照してください。

(def public-vars
  (->> (:var-definitions analysis-data)
       ;; public なものだけにしたいので private は除外
       (remove :private)
       ;; 必要な情報(ns名, var名)だけにする
       (map #(select-keys % [:ns :name]))
       (distinct)))

結果の出力

これで最後です。

「privateにできそう」というのは言いかえると「varが定義されているns以外で使われていない」ということです。 それをそのまま条件として書き出して、該当する var を出力します。

var の利用状況は解析データの :var-usages にあります。

(doseq [v public-vars]
  (let [;; public な var を使っている箇所を抜き出す
        usages (filter #(= (:name v) (:name %)) (:var-usages analysis-data))]
    ;; var の利用元の ns 名がすべて定義されている ns と一致するならば、それは「privateにできそうな var」
    (when (every? #(= (:ns v) (:from %)) usages)
      (println (format "%s/%s" (:ns v) (:name v))))))

これで public だけど private にできそうな var が表示できました。

勿論ライブラリ等のコードで意図的に public にしているものも表示されてしまうとは思いますが、 例えば public-vars の抽出時に特定の var は除外するみたいなことは自由にできるので、自身のプロジェクトに応じてカスタマイズすれば使えるスクリプトになるかと思います。

これ以外にも例えば以下のようなことも可能です。

  • テストコードの無い public var を表示する
  • ns のエイリアスとして他ファイルと異なるエイリアスを使っている箇所を表示する

今回紹介したスクリプトの全体は以下の Gist に保存してありますので、とりあえず試してみたい方はこちらからコピーしてください。

gist.github.com

最後に

clj-kondo の解析データはコマンドからも取得可能で、それをパイプしてワンライナーであれこれする方法もありますが、 試行錯誤のしづらさが個人的にネックでした。

それと比べて以上のようなことが1ファイルで、かつ REPL 駆動で書けるのは嬉しい人も多いのではないでしょうか?

clj-kondo は解析データからはいろいろな情報が取得可能なので、この記事内で紹介したこと以外でも便利な使い道があるはずです。

リンターであるという認識が強いのか解析データを利用する方面での記事はあまり多くない印象なので、これを機にこんなこともできて便利だよ!という記事が増えたら良いなと思います。


トヨクモでは一緒に働いてくれる技術が好きなエンジニアを募集しております。

採用に関する情報を公開しております。 気になった方はこちらからご応募ください。