Toyokumo Tech Blog

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

Clojureで作るAPI ライフサイクルと依存関係を管理できるようにする

[連載]Clojureで作るAPIの4記事目です。

前回の記事はこちらです。

tech.toyokumo.co.jp

この記事ではREPLを再起動することなくWebサーバーを再起動できるようにしていきます。

なぜ再起動できる必要があるのか

そもそもなんでそんなことが必要なのかというと、コードを書き換えた後で、REPLに持たせている状態を今のコードに合わせて即座に置き換えたいからです。

前回の記事(jetty/run-jetty ring-handler {:port 8000}) という式を使ってWebサーバーを起動しました。 引数に与えている関数 ring-handler と マップ {:port 8000} は立ち上がったサーバーが保持していますから、サーバーを再起動しないことには置き換えることはできません。しかしそのためにいちいちREPLを再起動していては時間がかかってしまって生産的ではありません。

また、このような状態を持つものが複数あり、かつその起動が順序通りに行われる必要がある場合に注意しながら手動で起動していくなんてことはやりたくありません。

そこでそういったことを可能にするライブラリを使って宣言的に再起動できるようにしていきます。

使用するライブラリを追加

ライブラリComponentを追加します。

github.com

Componentは、

'Component' is a tiny Clojure framework for managing the lifecycle and dependencies of software components which have runtime state.

とあるようにまさに今回の目的のために作られたライブラリです。 frameworkとありますが、フレームワークと聞いたときにイメージする例えばRuby on Railsのようなものではなく、シンプルで学習コストも小さいのでご安心ください。

deps.ednを次のようにします。

;; ./deps.edn
{:paths ["src"]
 :deps {org.clojure/clojure {:mvn/version "1.11.1"}
        info.sunng/ring-jetty9-adapter {:mvn/version "0.17.6" :exclusions [org.slf4j/slf4j-api]}
        spootnik/unilog {:mvn/version "0.7.30"}
        ;; 追加
        com.stuartsierra/component {:mvn/version "1.1.0"}}}

Componentを実装

ライブラリComponentにおいてはLifecycleというプロトコルを実装しているレコードなどをコンポーネントと呼びます。

(defprotocol Lifecycle
  :extend-via-metadata true
  (start [component]
    "Begins operation of this component. Synchronous, does not return
  until the component is started. Returns an updated version of this
  component.")
  (stop [component]
    "Ceases operation of this component. Synchronous, does not return
  until the component is stopped. Returns an updated version of this
  component."))

startに起動時の処理を書き、stopに停止時の処理を書きます。

コンポーネントという言葉が繰り返し出てきて紛らわしいですが、本記事では次の2つの意味で使っています。

  • ライブラリのComponent
  • ライブラリのComponentの要求に従って作ったコンポーネント

まずはRing handlerを提供するコンポーネントを実装します。 今は単純な関数なのでわざわざコンポーネントにする意味が感じられないですが、例えばテスト環境と本番環境で挙動を変えたい時などでコンポーネントにしておくと切り替えができて便利です。

;; ./src/cljapi/component/handler.clj
(ns cljapi.component.handler
  (:require
   [com.stuartsierra.component :as component]))

(defn- ring-handler
  [_req]
  {:status 200
   :body "Hello, Clojure API"})

(defrecord Handler [handler]
  component/Lifecycle
  (start [this]
    (assoc this :handler ring-handler))
  (stop [this]
    (assoc this :handler nil)))

次にサーバーを起動停止するコンポーネントを実装してみます。

;; ./src/cljapi/component/server.clj
(ns cljapi.component.server
  (:require
   [com.stuartsierra.component :as component]
   [ring.adapter.jetty9 :as jetty]))

(defrecord Jetty9Server [handler opts server]
  ;; handlerは :handler をキーにもつマップ(= コンポーネント)であることを期待している
  component/Lifecycle
  (start [this]
    (if server
      this
      (assoc this :server (jetty/run-jetty (:handler handler) opts))))
  (stop [this]
    (when server
      (jetty/stop-server server))
    (assoc this :server nil)))

Systemを構築する

ライブラリComponentではシステムというマップを定義して、作成したコンポーネントの依存関係を定義し、まとめて起動停止できるようにします。 依存関係を component/using を使って定義しておくと、ライブラリ側でよしなに起動順序(startを呼ぶ順序)を制御してくれます。

;; ./src/cljapi/system.clj
(ns cljapi.system
  (:require
   [cljapi.component.handler :as c.handler]
   [cljapi.component.server :as c.server]
   [com.stuartsierra.component :as component]))

(defn- new-system []
  (component/system-map
   :handler (c.handler/map->Handler {})
   :server (component/using
            (c.server/map->Jetty9Server {:opts {:join? false
                                                :port 8000}})
            ;; component/usingの第二引数で依存しているコンポーネントを宣言している
            [:handler])))

(defn start []
  (let [system (new-system)]
    (component/start system)))

(defn stop [system]
  (component/stop system))

(defonce system (atom nil))

(defn go []
  (when @system
    (stop @system)
    (reset! system nil))
  (reset! system (start)))

component/start を実行すると起動したコンポーネントが返ってくるので、それをatomで保持しておくことで後から停止できるようにしています。 また、go という関数で繰り返し起動させることができるようにしています。 namespaceを移動して go を実行してみます。

※namespaceを評価しておくのを忘れないようにしてください。

user> (in-ns 'cljapi.system)
#namespace[cljapi.system]
cljapi.system> (go)
12:51:40.387 [nREPL-session-e6ae6d15-5885-4f3b-912b-741243dab0a0] DEBUG org.eclipse.jetty.util.component.ContainerLifeCycle - QueuedThreadPool[qtp773034985]@2e1393e9{STOPPED,8<=0<=50,i=0,r=-1,q=0}[NO_TRY] added {org.eclipse.jetty.util.thread.ThreadPoolBudget@5eabf21,POJO}
12:51:40.390 [nREPL-session-e6ae6d15-5885-4f3b-912b-741243dab0a0] DEBUG org.eclipse.jetty.util.component.ContainerLifeCycle - Server@4f689547{STOPPED}[10.0.9,sto=0] added {QueuedThreadPool[qtp773034985]@2e1393e9{STOPPED,8<=0<=50,i=0,r=-1,q=0}[NO_TRY],AUTO}
# 出力が続く...

{:handler {:handler #function[cljapi.component.handler/ring-handler]},
 :server
 {:handler {:handler #function[cljapi.component.handler/ring-handler]},
  :opts {:join? false, :port 8000},
  :server
  #object[org.eclipse.jetty.server.Server 0x4f689547 "Server@4f689547{STARTED}[10.0.9,sto=0]"]}}

最後に表示されているのがgoの戻り値、つまりstartしたシステムマップです。 :server に起動した( :handler が生成されている)handlerコンポーネントが注入されていることがわかります。

curlで確かめてみます。

$ curl http://localhost:8000
Hello, Clojure API

最後にmain関数も置き換えておきましょう。

;; ./src/cljapi/core.clj
(ns cljapi.core
  (:require
   [cljapi.system :as system]))

(defn -main
  [& _args]
  (system/start))

main関数はサーバー起動時に使う関数で、停止する必要がないので、goではなくstartを使っています。

おわりに

これでREPLを起動したまま、サーバーのように状態を持つものを再起動できるようになりました。

コードはGitHubのリポジトリに上げてあります。 04_ライフサイクルと依存関係を管理できるようにするというタグからご覧ください。

次は開発時に使う関数を分離して、より開発を進めやすくしていきます。


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

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

Clojureで作るAPI Web サーバーを立ち上げる

[連載]Clojureで作るAPIの3記事目です。

前回の記事はこちらです。

tech.toyokumo.co.jp

この記事ではWebサーバーを立ち上げていきます。

Webサーバーのライブラリを追加

まずは依存関係にWebサーバーのライブラリを追加します。 deps.edn を次のようにしてください。

{:paths ["src"]
 :deps {org.clojure/clojure {:mvn/version "1.11.1"}
        info.sunng/ring-jetty9-adapter {:mvn/version "0.17.6" :exclusions [org.slf4j/slf4j-api]}
        spootnik/unilog {:mvn/version "0.7.30"}}}

ClojureのWebサーバーはRingに沿って作ります。 Ringがどういうものなのかは次の記事で詳しく解説しているのでご覧ください。

tech.toyokumo.co.jp

ここで info.sunng/ring-jetty9-adapterJettyをRingの仕様に適合させてくれているライブラリです。 slf4j-api を除外したり、Clojureのロギングライブラリである spootnik/unilog を加えたりしているのはJavaの複雑なログライブラリの事情によるものなのですが、そこを解説していると別の記事になってしまうので、ここでは一旦触れないでおきます。

最初のWebサーバー

ライブラリが追加できたら core.clj を次のようにします。

;; ./src/cljapi/core.clj
(ns cljapi.core
  (:require
   [ring.adapter.jetty9 :as jetty]))

(defn ring-handler
  [_req]
  {:status 200
   :body "Hello, Clojure API"})

(defn -main
  [& _args]
  (jetty/run-jetty ring-handler {:port 8000}))

(comment
  (-main) ;; (1)
  )

ここまで書けたらREPLを立ち上げて、ファイル全体を評価した後で、(1)の式を評価します。 そうすると大量のログがREPLに出力されつつ、Webサーバーが立ち上がります。

curlを使って確認してみます。

$ curl http://localhost:8000
Hello, Clojure API

localhostの指定したポートにWebサーバーが起動していて、アクセスすると定義した関数(Ring handler)が呼び出されていることが確認できました。

おわりに

とても簡単にWebサーバーを立ち上げることができました。

コードはGitHubのリポジトリに上げてあります。 03_Webサーバーを立ち上げるというタグからご覧ください。

ClojureでAPIを作ることは、言ってしまえば上記で定義した ring-handler に機能を足していくだけの作業です。 とてもシンプルであるおかげで、一度作り上げれば継続したメンテナンスが楽になるということが少しだけでも伝わるでしょうか?

次は開発の生産性を上げるために、REPLを再起動しなくてもWebサーバーの再起動ができるようにしていきます。


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

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

Clojure で作るAPI 開発環境を構築する

[連載]Clojureで作るAPIの2記事目です。

まずは開発環境を作るところからやっていきましょう。

要件

今回は次の要素を必須の要件として構築を進めます。

  1. REPLが動いて、エディタ上から式を評価して結果を見ることができる
  2. ファイル保存時に自動でフォーマットされる
  3. コードを書いているときに自動でリンターが走る

特にフォーマッターとリンターは最初から自動的に動作するようにしておくと後で楽です。

インストール

この4つをインストールしてください。 JDKについてはまだインストールされていなければsdkmanを使うのが便利でおすすめです。

# macなら次のようにインストールできます
$ brew install clojure/tools/clojure
$ brew install --cask cljstyle
$ brew install borkdude/brew/clj-kondo

# 確認
$ clojure -version
Clojure CLI version 1.11.1.1113
$ cljstyle version
mvxcvi/cljstyle 0.15.0
$ clj-kondo --version
clj-kondo v2022.05.31

REPLを動かす

なにはともあれREPLを動かしましょう。

REPLはClojure開発において(だけではなくLisp全般でも)とても大切な開発環境です。 エディタ上で今書いているコードはもちろん、他の名前空間の関数もその場で評価(その場で式を実行すること)できるので、実際に動かしながら開発を進めることができます。 一度慣れてしまうとREPLのない状態が逆に違和感を感じてしまうようになる程です。

エディタ自体の設定に関しては次の記事からお好みのものを選んで設定を済ませるようにしてください。

tech.toyokumo.co.jp

まずは deps.edn というファイルを作ります。 これはJavaなら build.gradlepom.xml、 JavaScriptなら package.json、PHPなら composer.jsonに当たるもので、依存するライブラリやビルド等のコマンドを定義するファイルです。 詳しくは公式のガイドをご覧ください。

;; ./deps.edn
{:paths ["src"]
 :deps {org.clojure/clojure {:mvn/version "1.11.1"}}}

この状態でプロジェクトルート(以後 ./)で clj でREPLを立ち上げてみると指定したバージョンのClojureでREPLが立ち上がるのが確認できます。

$ clj
Clojure 1.11.1
user=>

確認できたらこのREPLは閉じておきます。

次に適当なソースコードを用意してエディタとREPLを接続します。

;; ./src/cljapi/core.clj
(ns cljapi.core)


(defn -main
  [& _args]
  (println "Hello, Clojure API!"))


(-main)

ここまで書けたらREPLに繋いで評価してみます。

./src/cljapi/core.clj

repl

※繰り返しになりますが、評価とは式(上記の例では (-main) )をREPLに送信して実行することを指しています。やり方はエディタによって異なりますので、事前に設定方法を参照いただき、設定を済ませておくようにお願いします。

First commit

ここまでで git init してコミットしておきましょう。

# .gitignoreの例
.lsp
.cpcache
.nrepl-port
target
classes

フォーマッターの設定

cljstyleの設定をしていきます。

cljstyleの設定についてはこちらの記事で詳細に解説しています。

tech.toyokumo.co.jp

;; ./.cljstyle
{:rules {:indentation {:list-indent 1
                       :indents {defrecord [[:inner 0] [:inner 1]]}}
         :whitespace {:remove-surrounding? true
                      :remove-trailing? true
                      :insert-missing? true}
         :blank-lines {:trim-consecutive? true
                       :insert-padding? true
                       :padding-lines 1}
         :eof-newline {:enabled? true}
         :comments {:enabled? false}
         :vars {:enabled? false}
         :functions {:enabled? false}
         :types {:enabled? false}
         :namespaces {:indent-size 1}}}

cljstyle fix やエディタとの連携がうまく行っていれば保存時にフォーマットが走ることが確認できると思います。

cljstyle demo

リンターの設定

clj-kondoの設定をしていきます。

;; ./.clj-kondo/config.edn
{:skip-comments true}

ひとまずこれだけでOKです。

エラーになるような式を書くと警告が出ることが確認できます。

clj-kondo demo

Makefileにまとめる

エディタの外からも繰り返し実行できるようにMakefileにコマンドを書いておきます。

# ./Makefile
.PHONY: format
format:
  cljstyle check

.PHONY: lint
lint:
  clj-kondo --lint src

.PHONY: static-check
static-check: format lint

おわりに

ここまでで開発環境が構築できました。コミットしておきましょう。

コードはGitHubのリポジトリに上げてあります。 02_開発環境を構築するというタグからご覧ください。

次回はさっそくWebサーバーを動かしてみましょう。


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

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

Clojureで作るAPI はじめに

これから、「Clojureで作るAPI」と題して、Clojureを使ってAPIを作る方法をステップバイステップで解説していきたいと思います。

内容としては開発環境の構築から始め、必要な事柄を理解しながら徐々に機能を足していき、最終的には本番環境で使えるようなAPIを作れるところまで到達したいと思っています。 作っていくものも、本番で使うことができないようなものではなく、実際に弊社が本番環境で行なっていることに近い構成を取りながら、認証・認可といった機能も持ち、今後何をしていけば自分が必要な機能を足していくことができるのかわかる水準を目指します。

本筋と関係のない必要な知識も適宜リンクを提示して補足していくので、Clojureの経験がない人、Webアプリケーションサーバーを作ったことがない人であっても、記事の内容を実践していくことでClojureでAPIを作れるようになることを目指していきます。

目次は次のまとめ記事を参照してください。

tech.toyokumo.co.jp

来週の金曜日からなるべく毎週1記事の公開を目指して進めていきたいと思います。

それでは楽しんで始めていきましょう。

続きはこちら。

tech.toyokumo.co.jp


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

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

昇給によって年収1000万円を超えるエンジニアを産み出すという目標の達成報告

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

「昇給によって年収1,000万円を超えるソフトウェアエンジニアを産み出す」という目標の達成報告をしたいと思います。

実は初めて達成してからかなり時間が経ってしまっているのですが、これまで、そしてこれからの取り組みによってさらに多くのメンバーが達成していく見込みであることを受け、区切りとして記事に残しておきたいなと考えてこの記事を書くことにしました。

遠い目標

私が入社したのは2016年3月なのですが、当時CTOの落合は「昇給によって年収1,000万円を超えるエンジニアを産み出す」ということを口にしていました。

しかしながら2016年3月以前にいた社員数は3人程度で、売上規模も小さい会社でした。 そのため人件費にかけられる金額も絶対額として大きくなく、特段給与水準の高い会社ではありませんでした。

私は弊社にはClojureで自社製品開発がしたくて入社を決めたのですが、給与もたくさん欲しいと当然のことながら思っていました。 しかし現実には上述したような規模でしたし、いただいたオファーの金額とのギャップを考えると距離がある目標に思えていました。 どうやってそこに到達できるか、そのためにいかに早く事業を大きくできるか、焦りと共に真剣に考えたことをありありと覚えています。

賞与の導入と増額へ

弊社は昇給において、基本的に月給(= 基本給+固定残業代)を上げていくことをよしとしています。 月給ベースで考えることは弊社のようなSaaSビジネスと相性が良く、受け取り側にとっても生活の計画を立てやすいというメリットがあります。 そのため2019年まで昇給は月給アップに使っており、賞与は少額を上乗せしているだけでした。

しかしそれでは望ましい年収アップペースに届かないため、まず固定賞与の導入を決め、次に特別賞与を導入し、大きく年収を上げていくことにしました。

弊社における固定賞与と特別賞与の定義は次の通りです。

  • 固定賞与:月給と同額を支給する賞与
  • 特別賞与:全社員の月給の合計額を予算とし、評価に応じて0〜上限なしで支給する賞与

昇給の推移

昇給の推移をまとめると次のようになります。

年次 固定賞与 特別賞与 賞与予算合計
2016 なし 少額 0
2017 なし 少額 0
2018 なし 少額 0
2019 1ヶ月 少額 1ヶ月
2020 2ヶ月 少額 2ヶ月
2021 2ヶ月 1ヶ月 3ヶ月
2022 3ヶ月 1ヶ月 4ヶ月
2023 4ヶ月 1ヶ月 5ヶ月

このように2019年から1ヶ月分ずつ賞与を増やすことで、平均給与を10%前後上げ続けることができました。

目標達成

「昇給によって年収1,000万円を超えるソフトウェアエンジニアを産み出す」という目標は、月給の昇給と賞与の増額で達成することができました。 上述のように、来年もさらに賞与を増やすことを予定しているため、今後も1,000万円を超えるメンバーが誕生していく見込みです。

また基準も上がり、2023年度は特別賞与を除いても、エンジニア/デザイナーは年収672万円〜となりました。

なぜ昇給にこだわったのか?

ソフトウェアエンジニアは転職すると収入が増えるバグがあるなどと言われたりしています。 しかしながら転職活動にはリスクがあります。 リスクとしては、転職した先の文化が肌に合うかは働いてみるまでわからないですし、そこの開発で力を発揮できるかどうかも不確実です。 また転職活動にはコストもかかります。準備し、面談・面接を複数回受け、時には選考課題をこなす必要もあるでしょう。そうした時間をかけるよりも、自社のプロダクトなりをよくするための活動に時間を割いて報われるのであれば、それが本来は経済合理的なはずです。

私としては、環境・文化が自分に合っているのであれば、給与を上げるための転職をするよりも、昇給によって給与が上がっていくのが最も望ましいと思っています。ささやかでも弊社の取り組みが転職によって収入が上がるバグへのパッチになるといいなと思っています。

また、弊社は自社製品を販売している会社です。 同じ製品を長く開発していくので、長く働いて製品に詳しくなっているメンバーが居てくれると非常にありがたいです。 上述したような思い切った昇給を続けることができた背景には弊社は着実な成長を続けることができたことがあります。 これは継続して貢献してくれたメンバーのおかげに他なりません。 ですので、そうしたメンバーに報いていくことが、理にかなったお金の使い方だと思っています。

これから

振り返ってみるとかなり給与水準を上げることができてきましたが、まだ満足したわけではありません。

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


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

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

Clojure記事まとめ

弊社で書いたClojure関連記事をまとめます。

リンクになっていないものは今後執筆予定です。

開発環境

エディター

フォーマッター

リンター

  • Clojureのリンターclj-kondoの使い方

プロジェクト

Clojure基礎知識

  • Clojure基礎文法最速マスター

Clojureを使ったWebアプリケーション開発のための基礎知識

[連載]Clojureで作るAPI

  • Clojureで作るAPI はじめに
  • Clojureで作るAPI 開発環境を構築する
  • Clojureで作るAPI Webサーバーを立ち上げる
  • Clojureで作るAPI ライフサイクルと依存関係を管理できるようにする
  • Clojureで作るAPI 開発用のコードを分離する
  • Clojureで作るAPI ビルドして実行できるようにする
  • Clojureで作るAPI 設定をednで管理する
  • Clojureで作るAPI ロギングできるようにする
  • Clojureで作るAPI テストできるようにする
  • Clojureで作るAPI Routerを追加する
  • Clojureで作るAPI Ring Middlewareを追加してAPIらしくする
  • Clojureで作るAPI RESTful APIを追加する
  • Clojureで作るAPI GraphQL APIを追加する
  • Clojureで作るAPI 開発環境にDatabaseを追加する
  • Clojureで作るAPI Databaseのマイグレーションができるようにする
  • Clojureで作るAPI Databaseへのアクセスを整備する
  • Clojureで作るAPI フロントエンドを追加する
  • Clojureで作るAPI CORS に対応する
  • Clojureで作るAPI 認証ができるようにする
  • Clojureで作るAPI 認可ができるようにする
  • Clojureで作るAPI おわりに

ライブラリ活用

その他

まとめ

Clojureの日本語情報は多くないため、Clojureを使ってみたいと思った方は情報を探すのに苦労することもあるかと思います。

弊社は日本では数少ないClojureで自社製品を開発している企業です。

今後とも弊社にとって重要なClojureの活用をしやすいように情報提供を続けて、日本のClojure活用が広まる一助になれるように情報提供を続けていこうと思います。


トヨクモではClojureをはじめとして、TypeScriptやPHPを使って一緒に働いてくれる技術や製品開発が好きなエンジニアを募集しております。

採用に関する情報を公開しております。最近更新が滞って居たので最新化しました。 気になった方はこちらからご応募ください。

Spacemacsで始める Clojure 開発

こんにちは。開発本部の木下です。

先日ClojureアプリケーションをLSPで開発するための方法について書きました。

tech.toyokumo.co.jp

そこで本記事では上記のような Clojure アプリケーションをSpacemacsで開発するための設定について記載します。

なお、Spacemacs は現在のメインブランチである develop ブランチを使用していることを前提としています。

共通設定

Clojure アプリケーションを Spacemacs で開発する場合に必須で満たしてほしい要件は次のようなものです。

  • Clojure LSPが動作し、LSP が提供する機能を使うことができる
  • CIDERが動作し、REPL を使うことができる
  • clj-kondoによる静的解析が常にかかる
  • cljstyleによるフォーマットがファイル保存時に行われる

これらを実現しつつ、Clojure を書いていく上で必要な設定をしていきます。

前提として Clojure CLI、clj-kondo、cljstyle はインストールして実行可能にしておきます。

$ clojure -version
Clojure CLI version 1.11.1.1113
$ clj -version
Clojure CLI version 1.11.1.1113
$ clj-kondo --version
clj-kondo v2022.04.25
$ cljstyle version
mvxcvi/cljstyle 0.15.0

~/.spacemacs に次のように設定します。

(defun dotspacemacs/layers ()
  (setq-default
   ;; 次のレイヤーは最低限有効にします
   dotspacemacs-configuration-layers
   '(
     auto-completion
     lsp
     syntax-checking
     (clojure :variables
              clojure-enable-linters 'clj-kondo))

   ;; cljstyle を動作させるためのパッケージ cljstyle-mode をパッケージリストに加える
   dotspacemacs-additional-packages
   '((cljstyle-mode :location "~/projects/lib/cljstyle-mode/"))))

(defun dotspacemacs/init ()
  (setq-default
   ;; 編集中(挿入モードまたは dotspacemacs-editing-style が emacs の場合)にカッコのバランスが崩れるのを防いでくれます。
   ;; 有効にしておきましょう。
   dotspacemacs-smartparens-strict-mode t
   dotspacemacs-activate-smartparens-mode t
   dotspacemacs-smart-closing-parenthesis t))

(defun dotspacemacs/user-config ()
  ;; 忘れずに cljstyle-mode を読み込ませる
  (use-package cljstyle-mode)
  (use-package clojure-mode
    :init
    ;; clojure-mode で cljstyle-mode を有効化
    (add-hook 'clojure-mode-hook 'cljstyle-mode))
  ;; d$ など通常の vim のコマンドを実行したときにカッコのバランスを維持してくれます。
  ;; dotspacemacs-editing-style が vim または hybrid の場合は有効にしておきましょう。
  (spacemacs/toggle-evil-safe-lisp-structural-editing-on-register-hook-clojure-mode))

cljstyle の設定だけはデフォルトで用意されているやり方だけでは不可能なので上記のように個別で設定する必要があります。 cljstyle-modeを任意のディレクトリにクローンしてそこをdotspacemacs-additional-packages に指定してください。dotspacemacs-additional-packages の書き方には GitHub を直接参照させるやり方もありますが、筆者としてはローカルの適当なディレクトリにクローンして参照させるこのやり方が安定していると思っています。

これで基本的な設定は完了です。

プロジェクト固有の設定

いくつかのケースでプロジェクト固有の設定が必要になります。 CIDER のドキュメントでは設定値をまとめたページがないので個別のページを見ていく必要があって手間がかかります。 設定が必要なケースは様々あると思いますが、ここではイメージを持ってもらうことを目的として、弊社であった 2 つの実例をご紹介します。

ケース 1: alias を指定した REPL への接続

CIDER は deps.edn を使ったプロジェクトで cider-jack-in-clj すると通常は次のようなコマンドで REPL を立ち上げようとします。

/usr/local/bin/clojure \
-Sdeps '{:deps {nrepl/nrepl {:mvn/version "0.9.0"} cider/cider-nrepl {:mvn/version "0.28.3"}} :aliases {:cider/nrepl {:main-opts ["-m" "nrepl.cmdline" "--middleware" "[cider.nrepl/cider-middleware]"]}}}' \
-M:cider/nrepl

CIDER を機能させるためにnREPLなどのライブラリを加えているのがわかります。 ここで問題となるのは alias を使って状況によって読み込みたいコードやライブラリ分けて管理している場合です。 下記の例では開発時だけ必要なディレクトリとライブラリを :dev alias として定義しており、REPL を立ち上げる時には alias を指定する必要があります。

{:paths ["src" "resources"]
 :deps {org.clojure/clojure {:mvn/version "1.11.1"}}
 :aliases {:dev {:extra-deps {lambdaisland/kaocha {:mvn/version "1.65.1029"}
                              lambdaisland/kaocha-cloverage {:mvn/version "1.0.75"}}
                 :extra-paths ["dev" "test"]}
           :test {:main-opts ["-m" "kaocha.runner"]}}}

上述したデフォルトの CIDER のコマンドでは alias は考慮されていませんから、このまま cider-jask-in-clj しただけではうまくいきません。

1 つ目の解決策は .dir-locals.el を用意することです。 .dir-locals.elEmacs で特定のディレクトリやそのサブディレクトリに対してローカル変数を設定する方法です。

;; .dir-locals.el
((clojure-mode . ((cider-clojure-cli-aliases . ":dev")
                  )))

cider-clojure-cli-aliases で alias を指定しています。この上で cider-jack-in-clj を実行するとコマンドに次のように alias が付与されます。

/usr/local/bin/clojure \
-Sdeps '{:deps {nrepl/nrepl {:mvn/version "0.9.0"} cider/cider-nrepl {:mvn/version "0.28.3"}} :aliases {:cider/nrepl {:main-opts ["-m" "nrepl.cmdline" "--middleware" "[cider.nrepl/cider-middleware]"]}}}' \
-M:dev:cider/nrepl

2 つ目の解決策は REPL を立ち上げてそこに cider-connect-clj をすることです。 REPL を立ち上げるコマンドは上記のものを Makefile 等に記載しておいて実行してもいいですし、CIDER のドキュメントにあるようにalias を追加するのも良いです。

{:paths ["src" "resources"]
 :deps {org.clojure/clojure {:mvn/version "1.11.1"}}
 :aliases {:cider-clj {:extra-deps {cider/cider-nrepl {:mvn/version "0.28.3"}}
                       :main-opts ["-m" "nrepl.cmdline" "--middleware" "[cider.nrepl/cider-middleware]"]}
           :dev {:extra-deps {lambdaisland/kaocha {:mvn/version "1.65.1029"}
                              lambdaisland/kaocha-cloverage {:mvn/version "1.0.75"}}
                 :extra-paths ["dev" "test"]}
           :test {:main-opts ["-m" "kaocha.runner"]}}}

deps.edn に上記のように :cider-clj alias を追加し、clj -M:dev:cider-clj で REPL を立ち上げて cider-connect-clj で接続します。

ケース 2: shadow-cljs と deps.edn を組み合わせて使っている場合

モノレポなプロジェクトにおいてshadow-cljsを使って ClojureScript 開発をしている場合、shadow-cljs は :local/root を使ったローカルのモジュールの参照に対応していないためshadow-cljs と deps.edn を組み合わせた依存関係の管理をすることになります。

この場合は CIDER のドキュメントにそのままのやり方が書かれています。

docs.cider.mx

まとめ

ここまでの内容で Spacemacs を使って Clojure 開発を効率よく進められる設定ができるようになると思います。

Spacemacs は安定的で、一貫性があり、効率的でありながら指に優しいコーディング体験を提供してくれる非常に優れたエディタです。 Clojure だけでなく他の多くの言語の開発環境も提供してくれますから、何かの言語で一度慣れてしまえば長く連れ添うことのできる相棒になると思います。 興味があってまだ使ってみたことのない方はこれをきっかけに入門してみてはいかがでしょうか。

Pure Vimmer な方はこちらの記事を参考にしてください。

tech.toyokumo.co.jp


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

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