これは何?
上記の10日目の記事です。
最近流行りのNix Flakeを使ってHaskellAtCoder環境を作ってみました。
開発者目線でも嬉しいですが、Nix Flakeを使うことでサプライチェーンのセキュリティ問題も解決できそうなので個人的に注目しています。
(アドカレタイトルとの関連も薄いですが、枠余っているし、一応AtCoder用の環境のリプレースなのでご容赦ください)
環境
- Ubuntu22.04 LTS
- Ubuntu24.04 LTS
- Ubuntu 22.04(WSL): ホストOSはWindows11
Nixセットアップ
Nixインストール
公式ドキュメントどおりにインストールする。
特にSingle Userにする理由もないのでMulti Userでインストールした。Multi Userでインストールするとsudo時にも使えるなどのメリットがあるよう。
sh <(curl --proto '=https' --tlsv1.2 -L https://nixos.org/nix/install) --daemon
nix-commandとflakesを有効化する
毎回、
nix --experimental-features 'nix-command flakes' develop
のように長いオプションを書くのは面倒なので、公式手順に従い、~/.config/nix/nix.confに以下を追記することでnix-commandとflakesを有効にする。
experimental-features = nix-command flakes
これで上の長いコマンドが
nix develop
で良くなる。
flake.nixの作成
リポジトリトップにflake.nixを作成して記述していく。
{
description = "Haskell dev environment AtCoder";
inputs = {
nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";
flake-utils.url = "github:numtide/flake-utils";
};
outputs =
{
self,
nixpkgs,
flake-utils,
}:
flake-utils.lib.eachDefaultSystem (
system:
let
pkgs = import nixpkgs { inherit system; };
in
{
formatter = nixpkgs.legacyPackages.${system}.nixfmt-tree;
devShells.default = pkgs.mkShell {
packages = [
pkgs.zsh
(pkgs.haskell.packages.ghc96.ghcWithPackages (ps: [
ps.vector
ps.containers
ps.bytestring
]))
pkgs.haskell.packages.ghc96.cabal-install
pkgs.haskell.packages.ghc96.haskell-language-server
];
};
}
);
}
以下に試したことを書いておく。
GHCのバージョン
AtCoder用にGHCのバージョンを9.4.5似合わせてみようかと思ったが、language serverの互換性がサポート外になっていたので9.6を選択。
外部ライブラリのインストール
自分はまだあまり外部ライブラリを使っていないのと、競技プログラミングだと真面目にバージョン管理するモチベーションが低いので、Data.Vector等のライブラリをflake.nix管理にしてみた。
nix developで環境作成
flake.nix作成後に以下を実行すると環境が作られて、bashが起動する。
nix develop
環境変数としてhttp_proxyとhttps_proxyを設定していただけで、proxy経由しないとインターネットに出られない環境でもうまくいった。
Dockerよりもproxy設定も楽で素晴らしい
補足: zshを使いたい場合
shellHookに書いても良さそうだが、以下のようにしたらzshが起動した。
nix develop -c zsh
追記: 後述のdirenvの設定をやるなら不要。
動作検証
Nixで入れたもののPATHが通っているか
nixで入れたrunghcが使われることを確認した。
nix develop
which runghc
/nix/store/k7bpzkib01ippnpwh7ljpdvzvpf40jfl-ghc-9.6.7-with-packages/bin/runghc
外部ライブラリは動くか
外部ライブラリである、Data.Vectorを入れたコードが動いたので一旦ヨシとする。
import Control.Arrow ((>>>))
import Control.Monad.ST (runST)
import Data.Vector qualified as V
import Data.Vector.Mutable qualified as MV
import Debug.Trace
-- Data.Vectorを使うバージョン
-- 2次元配列を1次元に平坦化: (r, c) -> r * n + c NOTE: Vector自体は1次元しかサポートしていない
solve :: Int -> [[Int]]
solve n = runST $ do
arr <- MV.replicate (n * n) 0 -- n*n の要素を0で初期化
-- debug用
debugArr <- V.freeze arr
traceM $ show (V.toList debugArr) -- [0,0,0,0,0,0,0,0,0]
let startC = (n - 1) `div` 2 -- cの初期値
let idx r c = r * n + c -- 2次元 -> 1次元のインデックス変換
let go r c k
| k > n * n = return ()
| otherwise = do
MV.write arr (idx r c) k
let r' = (r - 1) `mod` n
c' = (c + 1) `mod` n
v <- MV.read arr (idx r' c')
if v == 0
then go r' c' (k + 1)
else go ((r + 1) `mod` n) c (k + 1)
-- goを呼び出し NOTE: doブロックの中ではletにinを使わないのでin goと書かなくて良い
go 0 startC 1
frozen <- V.freeze arr
traceM $ show (V.toList frozen) -- debug [8,1,6,3,5,7,4,9,2]
-- 1次元Vectorを2次元リストに変換
return
[ [ frozen V.! (idx i j)
| j <- [0 .. n - 1]
]
| i <- [0 .. n - 1]
]
main :: IO ()
main =
interact $
(read :: String -> Int) >>> solve >>> map (unwords . map show) >>> unlines
VS CodeでLSPやフォーマッタ、Linterは動くか
私の環境では、過去にghcupでインストールしたものが動いていたのでNixでインストールしたものに置き換えた。
workspaceの設定を以下のように修正。
{
"[haskell]": {
"editor.defaultFormatter": "haskell.haskell",
"editor.formatOnSave": true
},
"haskell.manageHLS": "PATH", // GHCupから変更
"haskell.formattingProvider": "ormolu",
"haskell.ghcupExecutablePath": "", // PATHが通っていたので消した。
"haskell.upgradeGHCup": false, // GHCupによる更新をしない
"haskell.serverExecutablePath": "haskell-language-server", // 現在PATHが通っているHLSを使用する
nix developしていない状態でVS Codeを立ち上げるとLSPやフォーマッタ、Linterが使えないので注意。
nix develop
code .
追記: 後述のdirenvを使った設定をするとcode .だけですむようになる。
毎回nix developしなくていいようにdirenvを使う
こちらの記事を参考にセットアップしたので、元記事をご参照ください。
一応メモ程度のことは書いておく。
direnvインストール
sudo apt install -y direnv
nix-direnvをインストール
nix profile install nixpkgs#nix-direnv
# 上記ブログの内容を丸コピしました
if [ -f "$HOME/.nix-profile/share/nix-direnv/direnvrc" ]; then
source "$HOME/.nix-profile/share/nix-direnv/direnvrc"
elif [ -f "/nix/var/nix/profiles/default/share/nix-direnv/direnvrc" ]; then
source "/nix/var/nix/profiles/default/share/nix-direnv/direnvrc"
elif [ -f "/run/current-system/sw/share/nix-direnv/direnvrc" ]; then
source "/run/current-system/sw/share/nix-direnv/direnvrc"
fi
shellにフックを追加
こちらも上記のブログ参照。
自分はzshを使っているため以下のように設定
eval "$(direnv hook zsh)"
.envrcの作成
こちらも上記のブログと同様に.envrcを作成して以下を記載
use flake
対象のリポジトリに移動した際に自動でnix developされるか確かめる
初回はエラーがでるのでdirenvを有効化すること
cd myproject
direnv: error /home/sigma/myproject/.envrc is blocked. Run `direnv allow` to approve its content
direnv allow
# GHCupでいれたものが有効
which runghc
/home/sigma/.ghcup/bin/runghc
cd myproject
# direnvによりPATHが切り替わる
which runghc
/nix/store/k7bpzkib01ippnpwh7ljpdvzvpf40jfl-ghc-9.6.7-with-packages/bin/runghc
おまけ: nix fmt
flake.nixのためのフォーマッタも一応導入した。
nix fmt
nix fmtを実行しても謎にハングする現象があったが、以下を参考にして修正したら動いた。
nix fmt
# ずっと終了されない
# これは動く
nix run nixpkgs#nixfmt-rfc-style -- flake.nix\n
# AIに作ってもらって動かなかったやつ
formatter = flake-utils.lib.eachSystem systems (system:
let pkgs = import nixpkgs { inherit system; };
in pkgs.nixfmt-rfc-style
);
# 動いたやつ
formatter = nixpkgs.legacyPackages.${system}.nixfmt-tree;
nix fmtが動かない時に調べて参考になったURL