LoginSignup
42
13

More than 3 years have passed since last update.

キメる Clojure チーム開発

Posted at

この記事は Clojure Advent Calendar 2019 18 日目の記事です。二年前に書いたキメるClojure高速開発が未だに読まれている気配があるので、情報を最新化し、現時点での意見をまとめようと思います。

当時の意見は特に Clojure 開発の高速性に注目し、短いスパンで進める個人開発に特に向いているというものでした。一方で経験にないため憶測でしたが、チームで進める中規模以上の開発にはあまり向かないのではないかと考えていました。しかし現在はチームとして Clojure を使って開発を行っているため今回はその観点から「Clojure を使ったチーム開発ってどうなの?」に対する意見を書きます。

私と Clojure のそれから

2014 年に上司の指導で Clojure に入門してから五年が経ちました。

  • 2018
    • 引き続きシリコンバレー駐在。
    • プロトタイプ開発などで許される限り Clojure/Script を使う。
    • Clojure/conj 2018 参加。
    • ミートアップで Clojure の紹介をしたり Clojure 勉強会をやったり草の根活動。
    • Clojure 転職活動開始。
  • 2019
    • Clojure 転職成功。
    • 帰国と同時に前職を退職。
    • 毎日 Clojure を書いている。

一番大きなアップデートはClojure 転職に成功したことです。日米ともに転職活動を進めていたのですが、最終的に Clojure で開発を行うサンフランシスコの Fin Tech スタートアップに拾ってもらいました。

転職活動については「まだ Clojure やらずに日本で消耗してるの?」みたいな煽り記事を書けるほど一般化出来る内容はありませんし、当時は将来への不安で太ったり痩せたりしていました。米国企業への転職には数多のハードルがあり、更にそれを超えた上で運次第という、やはり気軽に人に勧められるものではありません。私はとにかく人との繋がりを含めて Clojure に助けられまくりました。

しかしある意味 AI/Blockchain エンジニアより貴重な Clojure エンジニアとして転職活動をするのはニッチ戦略として成功だったと思います。それに Clojure 求人自体も決して少なくはありませんでした。転職活動で主に使ったのは LinkedIn ですが、何をしていいのか判らずとりあえずレジュメを公開したところ、続々と Clojure 一本釣りメッセージが届き、すぐに電話面接にこぎつけることが出来ました。また大手でも Clojure エンジニアの公開求人があり、例えば某 A 社がデータサイエンティスト系の Clojure エンジニアを募集していたようです。

とにかく Clojurian としての人生を永らえることが叶いました。現在はビザの都合で日本からリモートで働いていますが、ビザが下り次第渡米する予定です。

現在の Clojure 技術スタック

一人でのプロトタイプ開発からチーム開発に移行しましたが技術スタックに大きな変化はないです。敢えて上げるならエディタとして Emacs により強く依存するようになってしまったことと、ビルドツール周りが Clojure CLI, shadow-cljs など新しいものに移行したことなどでしょうか。ちなみにチーム全員 Emacs ユーザというわけではありません。

  • エディタ/ IDE
  • ビルドツール
    • Clojure CLI ... 新しい Clojure ビルド CLI。tools.deps という新しい依存解決機構を持つ。
    • shadow-cljs ... 新しい ClojureScript ビルドツール。npm パッケージを自然に利用できる。
    • leiningen
    • figwheelh
    • boot
  • その他ツール
    • eastwood
    • cljfmt
    • kibit
    • clj-kondo ... 新しい Clojure リンター。コードをこんまりしてくれる。
    • joker ... Golang 製の Clojure インタプリタ・リンター。高速に起動し Go の標準ライブラリが利用できる。
    • closh ... Clojure が使える Shell。
  • フレームワーク
    • integrant ... 最早外せないデータドリブンなモジュールベースフレームワーク。
    • duct ... integrant でサーバサイドアプリを開発するためのフレームワーク。
    • lacinia ... GraphQL の Clojure 実装。
  • ライブラリ
    • clojure.spec
    • umlaut ... スキーマ定義言語。共通スキーマ定義から様々なスキーマ定義ファイルを生成できる。
    • jackdaw ... Kafka クライアント、Kafka streams ライブラリ。Clojure らしい書き方で Kafka topology を定義できる。
    • clara-rules ... ルールエンジン。ルールを Clojure コードで書け Immutable なスタイルで実行することが出来る。
    • ring
    • pedestal
    • aws-api ... Cognitect 謹製の新しい AWS ライブラリ。API 定義から自動生成を行うことで AWS 側のバージョン変更にも追従。
    • timbre ... ロギングライブラリ。
  • フロントエンド
    • reagent
    • re-frame ... reagent 上で Redux 的アーキテクチャを実現するフレームワーク。
    • re-graph ... re-frame のハンドラとして利用できる GraphQL クライアント。
    • garden
  • その他
    • Datomic On-Prem
    • Datomic Cloud
    • Metabase ... データ可視化ツール。Clojure 製というだけではなく後述の Datomic Analytics により Datomic のデータ可視化にも利用できるようになった。

改めて Clojure はどんな開発に向いているか

二年前の意見としては、Clojure は REPL などのエコシステムを利用した高速開発が特に魅力で、個人でのプロトタイプ開発などに向いているという意見でした。一方で私自身にチームとしての Clojure 開発経験が少なく、また Clojure には型がないためチームで使っていくのは現実的には大変そうだなあという印象でした。

その後、前述の通り Clojure を全面的に採用する企業に参画したため、自分にとって未知なる Clojure チーム開発を始めることになりました。結果から言うと Clojure でのチーム開発についてはそれほどのストレスを感じていません。むしろ Clojure には型を捨てて余りあるチーム開発を支える言語特性があると感じられるようになったので、それらを紹介していきます。

1. 関数型的な書き方の強制

まずこれは主観ですが Clojure による開発では関数型的な書き方が強制されるため、OOP とのハイブリッド的な書き方が出来る Java や Scala と比べて個人間の書き方に大きな差異が出ない気がします。そのため人の書いたコードを理解するオーバーヘッドが比較的少ないです。更に duct などのフレームワークを使って開発すると、どこにどの関数があるか宣言的に記述できるため更に読みやすくなります。

ClojureサーバサイドフレームワークDuctガイド

2. 全てがデータ

これは前回記事でも触れましたが、Clojure の「全てがデータ」という特性はチーム開発と相性がいいです。システムの内部的なデータがほぼ全て EDN として表現されており、設定値、スキーマ定義、関数の入出力などを文字列スニペットとして簡単に共有出来るためです。また Clojure のコード自体もデータとして表現されるため、意味のある単位で抜き出して共有することが容易です。共有されたスニペットは REPL に打ち込むことで簡単に状況を再現することが出来ます。

怪しい箇所に予めデータを出力するログを仕込むことで、問題が発生した際の状況を簡単に再現して原因を特定するような手法はチームでも多く使われています。個人的に気に入っている出力方法は前述の timbre というロギングライブラリの spy というマクロです。taoensso.timbre/spy は引数に指定したフォームが実行時にどの様な値に解決されるかをログ出力し、かつその値をそのまま返してくれます。スレッディングマクロに挿入したり、引数を包んだりすると既存のコードに影響を与えずに実行時の中間の値を見ることが出来ます。

(require '[taoensso.timbre :as log])

(def a {:a 1 :b "2"})
(log/spy a) ;; => {:a 1 :b "2"}
;; 19-12-18 12:15:11 Kazuki-MBP.local DEBUG [test-app.handler.example:22] - a => {:a 1, :b "2"}

(defn test-fn [arg]
  (-> arg
      log/spy
      (assoc :id 1)
      log/spy
      (assoc :country "Japan")
      log/spy))

(def data {:name "Kazuki"})
(test-fn (log/spy data)) ;; => {:name "Kazuki", :id 1, :country "Japan"}
;; 19-12-18 12:19:04 Kazuki-MBP.local DEBUG [test-app.handler.example:34] - data => {:name "Kazuki"}
;; 19-12-18 12:19:04 Kazuki-MBP.local DEBUG [test-app.handler.example:?] - arg => {:name "Kazuki"}
;; 19-12-18 12:19:04 Kazuki-MBP.local DEBUG [test-app.handler.example:?] - (assoc (log/spy arg) :id 1) => {:name "Kazuki", :id 1}
;; 19-12-18 12:19:04 Kazuki-MBP.local DEBUG [test-app.handler.example:?] - (assoc (log/spy (assoc (log/spy arg) :id 1)) :country "Japan") => {:name "Kazuki", :id 1, :country "Japan"}

また DB 接続などの通常データとして扱えない情報であっても、システムを integrant/duct プロジェクトとして開発しているのであれば、それらの情報が system という一つのマップで管理されるためスニペット化が可能です。例えば以下のような profile の duct アプリだとします。

config.edn
{:duct.profile/base
 {:duct.core/project-ns test-app

  :duct.router/cascading
  [#ig/ref [:test-app.handler/example]]

  :test-app.handler/example
  {:db #ig/ref :duct.database/sql}} ;; db 接続を利用する ring ハンドラ

 :duct.profile/dev   #duct/include "dev"
 :duct.profile/local #duct/include "local"
 :duct.profile/prod  {}

 :duct.module/logging {}
 :duct.module.web/api
 {}
 :duct.module/sql ;; duct の db 接続モジュール。接続情報を :duct.database/sql に展開。
 {}}

REPL を (dev)(go) で起動後、integrant.repl.state/system からシステムの状態を参照可能です。そのため REPL から実行可能な、DB 接続を含む関数実行を以下のようにスニペット化出来ます。受け取った側は同じプロジェクトで REPL を起動してこのスニペットを実行することができます。

(defn get-customer [db]
  ;;...
  )

(def db (:duct.database/sql integrant.repl.state/system))
(get-customers db)

この様に Clojure では評価可能な単位でのスニペット共有が容易で、チーム開発における潤滑油的な役割を果たしてくれます。

3. スキーマ定義共通化

上記の様に Clojure では全てがデータ(EDN)として表現可能です。そのため、Clojure 製の多くのライブラリやフレームワークではスキーマ定義も EDN で行われることが多いです。EDN であれば自動生成することも簡単であり、スキーマ定義を自動生成するようなライブラリが存在します。代表的なものとしては前述の umlaut があり、チームでもこれを活用しています。

umlaut を使うことで共通のスキーマ定義から Datomic スキーマ、Lacinia スキーマ、clojure.spec 定義ファイルなどを生成することが可能で、Generator を書くことで自ら拡張することも出来ます。少しの不満としては共通定義スキーマは EDN ではありません。

schema.umlaut
@doc "Wrestler of Grand Sumo Tournament"
type Rikishi {
  id: ID
  shikona: String
  banduke: String
  heya: String
}

@lang/lacinia identifier query
type QueryRoot {
  rihishi_by_id(id: ID?): Rishiki? {
    @doc "Access a Rikishi by its unique id, if it exists."
    @lang/lacinia resolver query_rikishi-by-id
  }
}

上記の共通定義スキーマから Lacinia, Datomic などのスキーマを生成できます。

lacinia-schema.edn
{:objects
 {:Rikishi
  {:fields
   {:id {:type (non-null ID), :isDeprecated false},
    :shikona {:type (non-null String), :isDeprecated false},
    :banduke {:type (non-null String), :isDeprecated false}
    :heya {:type (non-null String), :isDeprecated false}},
   :implements [],
   :description "Wrestler of Grand Sumo Tournament"}},
 :enums {},
 :interfaces {},
 :mutations {},
 :queries
 {:rikishi_by_id
  {:type :Rikishi,
   :description "Access a Rikishi by its unique id, if it exists.",
   :isDeprecated false,
   :args {:id {:type ID}},
   :resolve :query_rikishi-by-id}}}
datomic-schema.edn
(#:db{:ident :rikishi/id,
      :valueType :db.type/string,
      :cardinality :db.cardinality/one,
      :unique :db.unique/identity}
 #:db{:ident :rikishi/shikona,
      :valueType :db.type/string,
      :cardinality :db.cardinality/one}
 #:db{:ident :rikishi/banduke,
      :valueType :db.type/string,
      :cardinality :db.cardinality/one}
 #:db{:ident :rikishi/heya,
      :valueType :db.type/string,
      :cardinality :db.cardinality/one})

この様にスキーマ定義を集約することで複数チームにまたがる開発であっても、それほどの負担を感じずにデータモデルの開発を進めていくことが出来ます。また実装の検討時には umlaut スキーマ定義をいじりながら推敲し、データモデルが固まった後はチームメンバーがそれぞれ Lacinia Resolver や Datomic Query など別の箇所の実装を独立して進めることが可能になります。

4. Clojure CLI (tools.deps)

現在、システムは多数のマイクロサービスに切り出して開発していますが、サービス、ライブラリ間での依存は複雑であり、複数チームに跨る激しい開発はセマンティックバージョニングではうまく行きそうにありませんでした。そのため社内リポジトリ間の依存関係解決は、Clojure CLI (tools.deps) の Git Coordinate を採用しました。この機能を使うことで他ライブラリの依存を git のコミットハッシュ(sha)として指定することが出来ます。

deps.edn
{:deps
 {github-yourname/time-lib {:git/url "https://github.com/yourname/time-lib"
                            :sha "04d2744549214b5cba04002b6875bdf59f9d88b6"}}}

例えば一つのサービスに対する変更のためにライブラリを変更した際にそのライブラリに依存する他のサービスに影響を与えずに開発を続けることが出来たり、ロールバックが容易に出来るなどのメリットが得られます。またライブラリ自身にリリースの必要がなくなりバージョン管理から開放されます。ただし、Git Coordinate を使うためには参照先リポジトリも Clojure CLI プロジェクトである必要があるので Clojure ならではの手法と言えるでしょう。

※ しかしこの構成も色々と限界を迎え、現在は一つのリポジトリで全ライブラリ、全サービスを管理する monorepo という方法を取っています。こちらについては別記事でまとめようと思います。

5. どこでも Clojure が使える

チームにはフロントエンドエンジニアもいますが Web UI は ClojureScript で開発しています。基本的にサーバサイドとの分業はされていますが、両方に参加するエンジニアもいるのは Clojure らしいです。前述の umlaut の成果物はフロントエンドからも利用出来るため、ClojureScript の採用によりデータモデルの二重管理を省くことが出来ています。

また、開発者向けツールの多くも Clojure で書かれています。コマンドラインツールは元来起動の遅い Clojure(JVM) で直接書くことは多くありませんでしたが、GraalVM の Native Image ビルドにより Git Commit Hooks などで高頻度で呼び出されるコマンドも Clojure で書けるようになりました。また前述の closh や joker なども利用しています。

多くのプロダクトが Clojure で書かれていることで、自分の参加していない開発物でもいざとなれば見ることが出来るという安心感があります。

一方で Clojure を採用し過ぎるとプロダクトマネージャーや QA など Clojure を直接書かない人に負担を強いることも事実です。具体例を出すと Datomic の内容の参照が難しいという問題がありました。Datomic は Clojure からしか接続出来ないわけではありませんが、付属の Datomic Console も使い易いとは言えず、Clojure エンジニア以外に簡単に参照できるものではありませんでした。

しかし、最近 Datomic が Datomic Analytics という機能をプレリリースしました。今年の Clojure Advent Calendar でも hden さんが記事に詳しくまとめてくれていますが、Datomic がクエリ検索エンジン Presto 接続をサポートしたことで SQL により Datomic のデータを参照出来るようになりました。

presto:my_db> select * from rikishi;
 id | shikona | banduke | heya |
----+---------+---------+-------
 0  | 鶴竜     | 横綱    | 陸奥
 1  | 白鳳     | 横綱    | 宮城野
 2  | 豪栄道   | 大関    | 境川

Presto をサポートする Metabase と接続することで UI からインタラクティブにデータをインスペクトすることも可能で、レポーティング用途に使うことも可能です。Datomic の参照に関してはこの機能で負担を軽減することが出来ました。しかしこの手の問題で大きかったのはこれくらいで、割とエンジニア以外も「Clojure 書けるんじゃないの?」と思うレベルで Clojure に対する理解がある気がしています。

ちなみに Datomic Analytics では、グラフで表される Datomic のスキーマ構造を SQL でクエリ可能なスキーマ構造に変換するために Metaschema と呼ばれるマッピング定義ファイルを用意する必要があります。一般的な用途では人間が直接考えて記述しなければいけないような内容はないため機械的にすることが可能であり、チームでは umlaut により Datomic スキーマと共通化して自動生成を行っています。

Clojure チーム開発におけるデメリット?

1. 型がない

今の会社は未だに激しいサービス開発の途上にあり、Rich Hickey のスピーチにあるように強い型付けによりデータ構造がコードベースに強く埋め込まれコードベースの変更が難しくなることをよしとしませんでした。
Effective Programs - 10 Years of Clojure - Rich Hickey
ここにも勿論賛否はありますが、既に Clojure が採用された状態からの参画だった立場から言うと、耐えきれないほどの不便さは感じないという感想です。

変更に対して開かれたデータのチェックとして Clojure は clojure.spec を提供しており、少なくともサービスの境界で spec チェックを行っているため、データモデルの変更時にも大きな不安は感じません。ただし現状の spec ではデータ中の余分なキーを検出するようなチェックが出来ないため、それに不便を感じることは多少あります。(Datomic への投入時など)こちらは athos さんの記事にある通り、spec2 に期待しています。

2. コードレビュー

S 式は読みづらくありません。

しかしコードレビューする際に S 式の構造的な変化があると diff を読み解くのが難しくなることがあります。渡された値で条件分岐するようなコードは、defmethod を使って書くようにすると拡張性が高く、変更のわかりやすいコードに出来ます。

また、spec チェックされていないマップを関数に渡すような場所でキーワードの typo があると、致命的なのにコードレビューでキャッチしづらく、その後の動作確認をまたねばならないことなどがあります。これは IDE の補完を使えばある程度避けられますが、割と起きがちな印象です。

3. Clojure エンジニア不足

転職活動中にもよく聞かされたことですが、やはり採用する側からすると Clojure エンジニアを探すのは難しいです。ある意味で Clojure を採用する上での最大のネックかも知れません。しかし同時に転職活動中に聞いた話では、Clojure を好き好んで学ぶ人であればエンジニアとしてある程度高いレベルにあることが期待出来るため選考コストは低く済むそうです。

また様々な Clojurian の働きで Clojure を学ぶ素材もかなり増えており、0 から Clojure エンジニアを育てるというのも現実的になっているのではないでしょうか。先月参加した clojure.tokyo の Clojure ハンズオンには 0 から Clojure を勉強したいという人が多数参加しており裾野の広がりを感じました。dosync radio渋谷 Lisp など、今年も Clojure を広げようと頑張ってくれた人たちには頭が上がりません。

最後に

振り返ってみるととても幸せな Clojurian 人生を送れているなと思います。

前回記事以降の Clojure 活動を振り返り、自分の中の Clojure に対する意見を書きました。私としては Clojure はチーム開発にも向いているという結論です。Clojure の採用はまずそこに辿り着くまでが大変ではありますが、一旦体制が出来て動き始めれば大きな問題なく Clojure の力を活かしてチーム開発を進めていくことが可能です。

皆でキメましょう。

42
13
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
42
13