Toyokumo Tech Blog

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

Vim で始める Clojure 開発

Vim/Neovim 向けに Clojure の開発環境を提供するプラグイン vim-iced を使って Clojure 開発環境の構築をしてみましょう。 なお vim-iced は個人的に開発している拙作の Vim プラグインで、実際に私が業務で(勝手に)利用しているという実績があります。

前提

  • Vim もしくは Neovim がインストールされていること
    • Vim は 8.1.0614 以降、Neovim は 0.4.0 以降が必要であることにご注意ください
  • Java がインストールされていること
  • macOS もしくは Linux 環境(本記事では Ubuntu を想定)であること
    • Windows 環境は vim-iced 自体がサポートしていないので、VM などで Linux 環境を用意してください

ゴール

本記事の内容を実践したことで以下の状態になっていることをゴールとします。

  • Vim を使った Clojure の開発環境が構築でき、REPL 駆動開発ならびにテスト駆動開発が体験できていること

やること

  • Clojure 実行環境のインストール
  • vim-iced のセットアップ
  • サンプルプロジェクトの作成
  • REPL 駆動開発のお試し

Clojure 実行環境のインストール

Clojure の実行環境は複数ありますが、今回は多くのプロジェクトで使われている Leiningen (ライニンゲン)を使った手順を紹介します。

なにはともあれインストールから始めましょう。

# macOS の場合
brew install leiningen

# Linux の場合
curl -fLo lein https://raw.githubusercontent.com/technomancy/leiningen/stable/bin/lein
chmod +x lein
sudo mv lein /usr/local/bin   # パスが通っているディレクトリであればどこでも可

インストールができたら簡単に Clojure のコードを実行して試してみましょう。

  • lein repl コマンドを実行して REPL(Read Eval Print Loop) を起動します。
    • 初回は起動に必要なライブラリ類をダウンロードするので時間がかかります。 user=> が出てくるまでしばしお待ちください。
  • user=> が出てきたら Read の状態なので、何かコードを書いて評価(Eval)してみましょう。結果が表示(Print)されるはずです。
    • E.g. (+ 1 2 3 4 5) を評価すると 15 が表示されます。
  • 確認ができたら Ctrl + d を押すか、(exit) を評価して REPL を閉じましょう。

vim-iced のセットアップ

vim-iced は Vim プラグインとして提供しています。そのためプラグインマネージャーを利用していない場合はまずその導入から始めましょう。 すでにプラグインマネージャーを利用している場合は次のセクションは飛ばして構いません。 なお本記事では vim-plug を使った例を紹介します。

vim-plug のインストール

Vim/Neovim で設定ファイルのパスが異なるので vim-plug の配置先も異なることにご注意ください。

# Vim の場合
curl -fLo ~/.vim/autoload/plug.vim --create-dirs \
    https://raw.githubusercontent.com/junegunn/vim-plug/master/plug.vim

# Neovim の場合
curl -fLo ~/.local/share/nvim/site/autoload/plug.vim --create-dirs \
    https://raw.githubusercontent.com/junegunn/vim-plug/master/plug.vim

各種プラグインのインストール

Vim/Neovim の設定ファイルを用意します。 Vim の場合は ~/.vimrc、Neovim の場合は ~/.config/nvim/init.vim です。 すでに設定ファイルがある場合は「追加する設定」の部分だけの追加で問題ありません。

# Neovim の場合は先にディレクトリを作成しておく
# mkdir -p ~/.config/nvim

cat <<EOT > ~/.vimrc
set nocompatible
set encoding=utf-8
scriptencoding utf-8
filetype plugin indent on

" ==== 追加する設定 ここから ====
" プラグインのインストール先ディレクトリは必要に応じて変更してください
call plug#begin('~/.vim/plugged')
" 次のうちいずれかが必要
Plug 'ctrlpvim/ctrlp.vim'
"Plug 'junegunn/fzf'
"Plug 'liuchengxu/vim-clap'

" 必須
Plug 'guns/vim-sexp',    {'for': 'clojure'}
Plug 'liquidz/vim-iced', {'for': 'clojure'}
call plug#end()

" vim-iced でのデフォルトキーマップを有効化
let g:iced_enable_default_key_mappings = v:true

" 任意) 見やすさのためスクリーンキャプチャ内では有効にしています
set splitright
let g:iced#buffer#stdout#mods = 'vertical'
let g:iced#buffer#error#height = 5
" ==== 追加する設定 ここまで ====
EOT

上記内容の設定ファイルが作成できたら、Vim/Neovim を起動している場合、一度再起動して :PlugInstall コマンドを実行してください。 もしくはターミナル上から vim -c PlugInstall -c qa を実行するのでも可です。

上記の例だと ~/.vim/plugged 配下に各種プラグインが配置されたことが確認できます。

ls ~/.vim/plugged

iced コマンドにパスを通す

最後に vim-iced が提供している iced コマンドへのパスを通す必要があります。 vim-iced のインストール先が ~/.vim/plugged である場合は以下のコマンドでパスを通してください。

export PATH=$PATH:~/.vim/plugged/vim-iced/bin

なお iced コマンドはインストールディレクトリ配下のファイルを参照するので、すでにパスが通っているところへ iced コマンドをコピーしても正しく動作しませんのでご注意ください。 以下を実行して何かしらバージョン番号が表示されれば正しく動作しています。

iced version

サンプルプロジェクトの作成

ここまでで Vim で Clojure 開発を始められる準備は整いました。 Leiningen を使ってサンプルプロジェクトを作って実際に動かしてみましょう。

lein new hello-iced
cd hello-iced

サンプルプロジェクトのディレクトリ構成を簡単に説明すると以下の通りですが、今回使うのはソースコードのみです。

Dir/File Description
src ソースコードはこちら
test テストコードはこちら
project.clj プロジェクト名、バージョン番号、依存ライブラリなどを記述する設定ファイル

REPL 駆動開発への入り口

まずはソースコードを開きます。Leiningen で作成されたプロジェクトでは プロジェクト名.core という名前空間が用意され、名前空間のドット区切りがそのままディレクトリ/ファイル名に反映されます。 唯一の例外がハイフン(-)で、これはディレクトリ/ファイル名上ではアンダーバー(_)に変換されます。

vim src/hello_iced/core.clj

開いたファイルは以下のようになっているかと思います。

(ns hello-iced.core)

(defn foo
  "I don't do a whole lot."
  [x]
  (println x "Hello, World!"))

REPL への接続

接続とIcedEvalの実行例

ソースコードを開いたらおもむろに :IcedJackIn コマンドを実行してみてください。 iced コマンドにパスが通っていれば、ステータスラインに OK: Leiningen project is detected のようなメッセージが表示され、しばらくすると Connected. が表示されます。 この Connected が出た状態が Vim の裏側で起動している REPL に vim-iced が接続している状態です。

※ 2022/05/27追記 ここから

:IcedJackIn は Vim 内で REPL のプロセスを起動しますが、別途 REPL を起動しておいてそちらに接続する方法もあります。 プロジェクト配下で iced repl コマンドを実行してみてください。 すると lein repl 同様に REPL が起動するので、Vim 上で :IcedConnect コマンドを実行すると別途起動した REPL に接続できます。 こちらの方法だと Vim を終了しても REPL は起動したままなので、REPL を再度立ち上げる必要がないというメリットもあります。

※ 2022/05/27追記 ここまで

試しに :IcedEval (+ 1 2 3) コマンドを実行してみてください。Vim 上で 6 という結果が表示されることが確認できるはずです。

コードを評価してみる

ソース上のコード評価の実行例

次にソースの末尾に (comment (foo "iced")) を追記し、その括弧の中にカーソルを移動して、ノーマルモードで <Leader>et (:IcedEvalOuterTopList)とタイプしてみてください。

これはカーソル配下のトップレベルのフォームを対象に評価することを意味しています。 なお <Leader> はデフォルトでバックスラッシュ(\) なので \et となります。

なおここで comment フォームを使っているのは、名前空間 hello-iced.core が読み込まれたときに無駄に評価されないようにするためです。 vim-iced の <Leader>et ではデフォルトで comment フォーム内のコードを評価するようになっているので、既存のコードを評価して試してみたいときには comment フォームを使うと便利です。

(ns hello-iced.core)

(defn foo
  "I don't do a whole lot."
  [x]
  (println x "Hello, World!"))

(comment (foo #_カーソルはここ "iced"))

結果はどうでしょうか?恐らく期待とは違い nil が表示されたかと思います。 vim-iced では関数の戻り値を Popup ならびにステータスラインに表示して、標準出力は別の場所に表示するようにしています。 ノーマルモードで <Leader>ss (:IcedStdoutBufferOpen) とタイプすると標準出力の表示先(以下、StdoutBuffer)が別ウインドウで表示され、期待した結果が出力されていることが確認できると思います。

変更内容を即座に確認する

ソースを修正しその内容を即座に確認する実行例

ではサンプルプロジェクトの foo 関数で println の引数を以下のように変更してみましょう。 この変更で StdoutBuffer に出力される内容が期待したものに変更されるかを確認したいと思います。

(defn foo
  "I don't do a whole lot."
  [x]
  (println x "is awesome!!!")) ;; 出力内容を修正

変更できましたか? できたら foo 関数の中にカーソルを移動して <Leader>etfoo 関数を再評価してください。 REPL 駆動開発では起動している REPL 内に評価された結果が保持されて、それを使って開発を進めていきます。 なので単にコードを変更しただけでは REPL 内の foo は変更前のままなので再度評価してあげる必要があります。

再評価できたら末尾の comment フォームを改めて <Leader>et で評価してみてください。 StdoutBuffer に変更後の内容が出力されることが確認できるかと思います。

このようにフォーム単位で簡単・迅速に挙動が確認できるのがREPL駆動開発の強みです。

上記の例では関数単位ですが、さらに言うと例えば (def x "bar") を評価しておいて、 println フォーム内で <Leader>ee (参考: Evaluation ranges)で評価すると関数内のフォーム単位でも動作確認可能です。

テスト駆動開発への入り口

次にテスト駆動開発に触れてみましょう。 ここでは整数のリストを渡して、その合計値を返すという簡単な関数を題材としてみます。

まずは core.clj に以下の関数を書いて評価してください。 この時点では合計を算出する処理はまだ書かずにとりあえず 0 を返すだけです。

(defn sum [ls]
  0)

テストの作成

テストの実行例

では hello-iced.core/sum のテストを書いてみましょう。 テストコードはサンプルプロジェクト直下の test ディレクトリ配下にあります。

ファイルを探して開いても良いのですが、ここでは :IcedCycleSrcAndTest コマンドを実行してみましょう。 これによりソースファイルに対応するテストファイルとして test/hello_iced/core_test.clj を自動的に開くことができます。(ファイルが実在しなくても名前空間からパスを推測して新規作成も可能です)

開いたファイルは以下のようになっているかと思います。

(ns hello-iced.core-test
  (:require [clojure.test :refer :all]
            [hello-iced.core :refer :all]))

(deftest a-test
  (testing "FIXME, I fail."
    (is (= 0 1))))

サンプルプロジェクトでは必ず失敗するテストしか書かれていないので、この a-test は以下のように書き換えてください。

(deftest sum-test
  (is (= 6 (sum [1 2 3])))
  (is (= 10 (sum [1 2 3 4]))))

ここでは hello-iced.core/sum[1 2 3] と、[1 2 3 4] というリストを渡して、 それぞれの結果が期待したものになるかをテストしています。 このフォーム上にカーソルを移動させてノーマルモードで <Leader>tt (:IcedTestUnderCursor)とタイプしてみてください。 これはカーソル配下のトップレベルのフォームを対象にテストを実行することを意味しています。

ステータスラインにテスト結果の概要が表示されると共に、別ウインドウで期待した結果と実際の戻り値が異なるエラーが2つ表示されることが確認できるかと思います。

ソースの修正

テストの再実行の例

ではテストが通るようにソースを修正していきましょう。 改めて :IcedCycleSrcAndTest コマンドを実行してソース・ファイルに戻りましょう。

まずはすこしズルをして以下のように sum を修正してみてください。

(defn sum [ls]
  6)

修正できたら <Leader>et で再度 sum を評価し、 テストも再実行します。 もう1度 :IcedCycleSrcAndTest で移動、 <Leader>tt をタイプとしても良いのですが、ちょっと面倒なので今度はソースコードを開いたままノーマルモードで <Leader>tr (:IcedTestRedo)とタイプしてみてください。

これは直前に失敗したテストを再実行することを意味しています。 すると今まで2つ出ていたエラーが1つになったかと思います。 あと1つのエラーが残っていますが、今度はズルをせずにきちんと書きましょう。

(defn sum [ls]
  (apply + ls))

再度評価してもう1度 <Leader>tr でテスト実行してみてください。 今まで別ウインドウで表示されていたエラーはウインドウと共に消えて、ステータスラインに成功した旨が表示されたかと思います。

ここまででテストの書き方、実行方法、そしてテスト駆動による修正方法がわかったかと思います。

リンター/フォーマッター の設定

※ 2022/06/01追記

REPL駆動以外にも開発時にあって欲しいのはリンターとフォーマッターのサポートです。 より具体的には以下の2点は開発において必須と言っても過言ではないかと思います。

  • clj-kondo による静的解析が常にかかる
  • cljstyle によるフォーマットがファイル保存時に行われる

clj-kondo による静的解析の設定

Vim においてリンターの結果を表示するためのプラグインはいくつかあり、デファクトスタンダードと言えるものはありません。

主要なプラグインに関しては clj-kondo の以下のドキュメントにまとまっているので、こちらを参考に設定すると良いでしょう。

github.com

なお筆者はこのドキュメントにおける coc.nvim の設定を採用しています。

cljstyle によるフォーマットの設定

フォーマッターについては vim-iced 自体がデフォルトで cljfmt を使ったフォーマットに対応しています。 しかしチーム内でコーディングスタイルを統一したい目的で cljfmt 以外を使いたいニーズもあり cljstyle を使ったフォーマットも勿論サポートしています。

https://liquidz.github.io/vim-iced/#formatting_customize

vim-iced でのフォーマッターの設定

vim-iced にてフォーマッターとして cljstyle を使いたい場合の設定は簡単で ~/.vimrc などに以下の1行を追加するのみです。

let g:iced_formatter = 'cljstyle'

もし cljstyle がインストールされていない場合は :IcedFormat のようなフォーマットに関するコマンドを実行することで 「iced コマンドにパスを通す」でパスを通したディレクトリ配下にインストールするかという案内が始まるので、それに従うと簡単にインストールすることが可能です。

保存時にフォーマットする設定

vim-iced が提供するフォーマット関連のコマンドは基本的に非同期でバッファ上のテキストを更新しますが、保存時は非同期にしてしまうとフォーマット結果が正しくファイルに保存されなくなってしまいます。 そのため vim-iced では同期的に動くフォーマットコマンドも用意しています。

これらのコマンドを BufWritePre に対する autocmd で使うことで保存時のフォーマットを実現できます。

以下ドキュメントからの抜粋ですが設定例です。

https://liquidz.github.io/vim-iced/#format_on_writing_files

aug VimIcedAutoFormatOnWriting
  au!
  au BufWritePre *.clj,*.cljs,*.cljc,*.edn execute ':IcedFormatSyncAll'
aug END

なお環境やフォーマット対象のコードによってはバッファ全体のフォーマットに時間がかかり Vim がフリーズする時間ができてしまうこともなきにしもあらずです。 その場合は :IcedFormatSyncAll の代わりに :IcedFormatSync コマンドを使うとカーソル配下のフォームだけを対象にフォーマット可能です。 フォームを編集 → 保存をこまめに行っている場合だとこちらの設定でフリーズはほぼ回避できますが、保存時のフォーマット対象がカーソル配下のフォームに限定されるのでフォーマット漏れが出る可能性があることに注意してください。

最後に

最初に掲げたゴールを振り返ってみましょう。

Vim を使った Clojure の開発環境が構築でき、REPL 駆動開発ならびにテスト駆動開発が体験できていること

簡単ではありましたがゴールの通りの体験ができたでしょうか? これをきっかけとして Clojure での開発に興味をもっていただけたなら幸いです。

なお vim-iced 自体にも興味を持っていただけたなら、 ここで紹介していない多くの便利な機能がまだあるので、ぜひドキュメントを読んでみていただければと思います。

Spacemacsを利用している方はこちらを参考にしてください。

tech.toyokumo.co.jp


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

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