ペンギン村 Tech Blog

技術をこよなく愛するエンジニア集団が在住するペンギン村から、世界へ役立つ(かもしれない)技術情報を発信する技術系ブログです。某アラレちゃんが済む村とは一切関係ありません。んちゃ!

1. 環境構築 :: Swift プログラマのための Haskell 入門

さて、この記事では VSCode と stack を利用した Haskell の開発環境の構築を行っていきます。この環境構築を飛ばしても、次回以降の記事を読むのに支障はありませんが、コードを実行・評価できる環境が整っていると学習が捗るのでおすすめです。

概要

インストールする対象は以下のとおりです。

最近(でもないですが)の VSCode は Remote Containers が登場したこともあり、ナウなヤングは Docker を使って開発環境を構築する人も多いのかもしれません。しかし、Remote Containers は Preview 段階ですし、Docker は Mac のバッテリー消費も激しいので、この記事では直接マシンにインストールしていきます(トラブルに自分で対処できる人は Remote Containers でも良いかもしれません)。

なお、VSCode と Homebrew はすでにインストール済みと仮定して進めていきます。

Stack のインストール

Stack は Haskell におけるビルドツールのようなものです。

大雑把に言うと、コンパイラである GHC とパッケージマネージャである Cabal のラッパーみたいな感じになっています。現時点では、とりあえず Stack さえ入れておけば Haskell で開発する環境は整うということだけ抑えておけばよいでしょう。

Homebrew でインストールできます。

$ brew install haskell-stack

次に stack setup を実行すると、グローバルプロジェクトの設定が行われ、GHC が利用できる状態になります。

$ stack setup

GHC には REPL(対話的にコード片を試せる環境)が用意されており、この時点で利用できる状態になっています。任意のディレクトリで stack ghci すると起動できます。

$ stack ghci
...
Prelude> 

もしかすると Prelude> の部分は表示が異なるかもしれませんが、現時点では気にしなくて大丈夫です。

せっかくなので、少しだけ利用してみましょう。

Prelude> 1 + 1
2

Prelude> length "Hello, GHC."
11

これで単純な Haskell コードであれば REPL で試せるようになりました。REPL 上で :q と入力するか、Ctrl + D で REPL を終了することが出来ます。

Prelude> :q
Leaving GHCi.

REPL の基本的な使い方については後述したいと思います。

ちなみに余談ですが、Swift でちょっとしたコードを試すときには Playground を利用することが多いかもしれませんが、Swift でも REPL は提供されています。

$ swift
...
  1> 1 + 1
$R0: Int = 2

VSCode拡張:Haskell のインストール

VSCode拡張:Haskell から Install ボタンをクリックするか、VSCode 上から検索してインストールしましょう。

f:id:yu_dotnet2004:20210218193705p:plain

Haskell は Language Server が開発されており、この拡張もそれを利用して動作するようになっています。Language Server はこの拡張をインストールすると一緒に自動的にインストールされるので、別途インストールする必要はありません。

せっかくなので新しく Haskell プロジェクトを作成して VSCode 上で確認してみましょう。 stack new コマンドを利用して新しい Haskell プロジェクトを作成することが出来ます。

$ stack --resolver lts-17.2 new hello-world

—resolver というオプションは今は考えなくてもよいのですが、気になる方のために少しだけ説明しておくと GHC とパッケージの構成を指定するものです。省略した場合は自動的に最新の構成(resolver)が利用されるのですが、執筆時点において最新のバージョンである LTS Haskell 17.4 (ghc-8.10.4) を利用すると GHC 8.10.4 が使用されるのですが、前述の Language Server が 8.10.3 までしか対応していないため、少し前のバージョンを指定しています。

さて、そんな細いことはさておき、VSCode で開いてみましょう。

$ cd hello-world
$ code .

code コマンドが使えないよ!」という方はVSCode 上から普通に開くか、VSCode 上で Cmd + Shift + P でコマンドパレットを表示して Shell Command: Install … を選択して code コマンドを利用可能にしましょう。

f:id:yu_dotnet2004:20210218193732p:plain

VSCode 上でツリーを展開すると、次のような構成になっているかと思います。

f:id:yu_dotnet2004:20210218193743p:plain

試しに Lib.hs を開いて、putStrLn という関数にカーソルをホバーさせてみましょう(初回は Language Server の起動に少し時間がかかるかもしれません)。

f:id:yu_dotnet2004:20210218193757p:plain

試しに新しい行に put と入力すると、コード補完も機能することが確認できます。

f:id:yu_dotnet2004:20210218193811p:plain

いい感じですね!

implicit-hie のインストール / hie.yaml の自動生成

さて、おそらく先ほどの VSCode 上での手順を進める際、VSCode の右下に以下のようなポップアップが表示されたかと思います。

f:id:yu_dotnet2004:20210218193825p:plain

現時点で Haskell Language Server は hie.yaml という設定ファイルを必要とする作りになっています。先ほど見てきたように、hie.yaml が無くても動作はするのですが、構成によっては正しく動かなくなってしまう可能性もあります。

hie.yaml は自分で記述してもよいのですが、自動生成してくれる implicit-hie という Haskell 製のツールが用意されています。

以下のように stack install コマンドでインストールできます。

$ stack install implicit-hie
...
Copied executables to /Users/xxxx/.local/bin:
- gen-hie

コマンド名は gen-hie とツール名と異なっているので注意しましょう。 ~/.local/bin にインストールされるので、パスを通していない方は ~/.zshrc などに追記しておくと良いでしょう。

export PATH="$HOME/.local/bin/:$PATH"

さて、 hie.yaml を生成しましょう。

$ gen-hie > hie.yaml

これで No cradle found for … というポップアップは表示されなくなったかと思います。

ただし、ファイル構成を変更した場合は再生成が必要になることもあるので、なんだか VSCode の調子がおかしいなと思ったら、とりあえずこのコマンドを再実行しておくとよいかと思います1

JetBrains Mono フォント(任意)

Haskell のコードでは記号が非常に多く登場します。そのため、合字(リガチャ)に対応したフォントの方がコードが見やすいという人もいるかもしれません。

例えば、以下のようなコードの場合、

f:id:yu_dotnet2004:20210226134044p:plain

合字に対応した JetBrains Mono を使用すると以下のようになります。

f:id:yu_dotnet2004:20210226134056p:plain

どちらが見やすいかは完全に好みですが、記号がゴチャゴチャしてコードが読みづらいと感じたら試してみても良いかもしれません。

VSCode で JetBrains Mono を使用する手順は以下のとおりです。

  1. JetBrains Mono をインストール。
  2. VSCode 上で Cmd + , で設定画面を開く。
  3. Font Family の項目の先頭に 'JetBrains Mono', を追加する。

f:id:yu_dotnet2004:20210226134109p:plain

おすすめの学習方法

大体の Haskell 入門書では、最初に REPL を活用した解説が出てきます。

REPL は短いコードを試すのには便利なのですが、複数行の関数を定義するようになってくると REPL では辛くなってきます。また、REPL では基本的に書き捨てになってしまうので、自分で書いたコードを後から見直したいときにも不便です。

ここでは、個人的におすすめな学習方法を書きたいと思います。

REPL の活用

先ほど VSCode 上で開いた Lib.hs が格納された src ディレクトリに、新しく Calc.hs を作成して次のコードを入力してみましょう。

module Calc where -- 拡張子を除いたファイル名と合わせる

add :: Int -> Int -> Int 
add x y = x + y

ここではコードの解説は行いませんが、1行目の module xxx wherexxx の箇所はファイル名と合わせることだけ覚えておきましょう(お察しのとおり -- はコメントです)。

次に Shift + Ctrl + ^ で VSCode のターミナルを開き、 stack ghci を実行しましょう。

$ stack ghci
...
*Main Calc Lib Paths_hello_world> 

途中で Warning が表示されたりするかもしれませんが、とりあえず REPL が使用可能になれば大丈夫です。

プロジェクトのルートディレクトリで stack ghci を実行すると、プロジェクトに含まれるコードが import された状態で REPL が起動します。そのため、先ほど定義した add 関数が利用できます。

*Main Calc Lib Paths_hello_world> add 1 2
3

いい感じですね!

REPL は開いたままで、新たに sub 関数を追加してみましょう(保存を忘れずに!)。

module Calc where

add :: Int -> Int -> Int 
add x y = x + y

sub :: Int -> Int -> Int 
sub x y = x - y

REPL では :r を入力することでリロードが行われるので、 追加した sub 関数も利用可能になっています。

*Main Calc Lib Paths_hello_world> sub 1 2
-1

いい感じですね!

このように事前に REPL を起動した上で、

  1. ソースコードを編集。
  2. :r で再読み込みして、REPL で評価。

というループを構築しておくと、コードを書き換えながら試すことができて学習効率が良いかと思います。

なお、新しいファイルを追加したときは :r による再読み込みでは反映されないので、:l ファイル名 で明示的に読み込むか、REPL を再起動する必要があります。

テストコードの活用

新しいプログラミング言語や API を学ぶ際には、テストコードも一緒に記述すると使い方が記録として残るので効率的という話を聞きます。ここではドキュメンテーションコメント上で記述できる、手軽なテストコードの書き方について紹介しておきます。

Haskell には doctest というドキュメンテーションコメント上にテストを書けるツールが存在します。

以下のコマンドでインストール出来ます。

$ stack install doctest

実行するには stack exec doctest {対象ファイル名} というコマンドを使用します。試しに、先ほど作成した Calc.hs に対してテストを実行してみます。

$ stack exec doctest ./src/Calc.hs
Examples: 0  Tried: 0  Errors: 0  Failures: 0

まだ何もテストが存在しないため、1つも実行されていないのが分かります。

試しに add 関数にテストを記述してみましょう。最初は意図的に失敗させます。

-- | 足し算
-- >>> add 1 2
-- 0
add :: Int -> Int -> Int 
add x y = x + y

ドキュメンテーションコメントは -- | {関数の説明} から開始します。関数の説明は省略しても良いのですが、-- | を書き忘れるとテストとして認識されないので注意しましょう。テストは >>> の後に実行したい式を記述し、直後の行に期待結果を記述します。

もう一度、テストを実行してみましょう。

$ stack exec doctest ./src/Calc.hs
./src/Calc.hs:4: failure in expression `add 1 2'
expected: 0
 but got: 3
          ^

Examples: 1  Tried: 1  Errors: 0  Failures: 1

add 1 2 という式を評価し、 0 という結果を期待したものの、実際には 3 が返却されたため、テストが失敗しているのが分かります。

期待値を正しい 3 に変更して、テストがパスすることを確認しましょう。

$ stack exec doctest ./src/Calc.hs
Examples: 1  Tried: 1  Errors: 0  Failures: 0

いい感じですね!

なお、>>> を使ったテストは複数記述できます。

-- |
-- >>> add 1 2
-- 3
-- >>> add 0 1
-- 1
add :: Int -> Int -> Int 
add x y = x + y

また、詳細についてはここでは割愛するのですが、>>> の代わりに prop> を使用することで QuickCheck を利用した Property-based Testing も同時に記述できます。

以下は、足し算が「結合法則を満たす」ことを検証しています。

-- |
-- >>> add 1 2
-- 3
-- prop> add x y == add y x
add :: Int -> Int -> Int 
add x y = x + y

Property-based Testing については、以前 try! Swift 2019 TOKYO で発表したスライド(英語)や Qiita 記事があるので、興味のある方はご参照ください。

speakerdeck.com

qiita.com

ちなみに、VSCode の Haskell 拡張をインストール済みの場合、>>> 式 だけ記述して Evalute... をクリックすると評価結果が自動的に生成されます。

f:id:yu_dotnet2004:20210226134136p:plain

生成された後は Refresh... ボタンに変わっているので、クリックすると再評価されます。

f:id:yu_dotnet2004:20210226134147p:plain

エディタ上の簡易な REPL 的な使い方をしても良いかもしれません。

REPL の基本と設定

さて、最後に REPL について軽く説明しておきたいと思います。

ヘルプ

まず :? でヘルプを見ることが出来ます。

*Main Calc Lib Paths_hello_world> :?
 Commands available from the prompt:

   <statement>                 evaluate/run <statement>
   :                           repeat last command
   :{\n ..lines.. \n:}\n       multiline command
...

利用可能なコマンドが列挙されるので、何か分からないことがあれば :? でヘルプを見ると良いでしょう。

代表的なコマンド

ここでは代表的なコマンドを列挙したいと思います。

まずは REPL 全体の操作に関するものです。

コマンド 説明
:load 指定したファイルの読み込み
:reload リロード
:module モジュールの追加・削除
:quit REPLを終了
`:help ヘルプを表示(:?と同等)

次に Haskell のコードに関するものです。

コマンド 説明
:type 型を表示
:kind カインドを表示
:info 識別子の情報を表示

よく分からないものも多いと思いますが、それは必要なときに説明されると予想されるので(著者に期待したいところです)、現時点では理解しなくても大丈夫です。

さて、先ほど :r でリロードできましたが、実は :reload の短縮形だったのです。REPL では :xxx とした場合、最初にマッチしたコマンドが選択されるようになっているため、大体のコマンドは1文字で入力できます。:q なら終了ですし、:t なら型を表示しますし、:k ならカインドを表示します。

複数行の入力

REPL 上で複数行にわたるコードを記述したい場合には :{ で開始して、 :} で終了します。

以下のような感じですね。

*Main Calc Lib Paths_hello_world> :{
*Main Calc Lib Paths_hello_world| mul :: Int -> Int -> Int
*Main Calc Lib Paths_hello_world| mul x y = x * y
*Main Calc Lib Paths_hello_world| :}
*Main Calc Lib Paths_hello_world> mul 2 3
6

プロンプト表示のカスタマイズ

さて、そろそろプロンプトの表示がムダに長いと感じてきた頃でしょう。

*Main Calc Lib Paths_hello_world>

並んだ文字列から予想された方もいるかも知れませんが、これはファイルが増えるほど長くなっていき、非常に見づらくなってしまいます。

:set prompt:set prompt-cont でカスタマイズすることが出来ます。

*Main Calc Foo Lib Paths_hello_world> :set prompt "> "
> :set prompt-cont "| "
> :{
| x = 1
| y = 2
| :}

ご想像のとおり :set prompt は通常のプロンプト、 :set prompt-cont は複数行時のプロンプトをそれぞれ設定できます。

しかし、REPL を起動するたびに入力するのは面倒という人も多いでしょう。~/.ghci に記述しておくことで自動的に反映されるようになります。

:set prompt "λ "
:set prompt-cont "| "

私は上記のとおり λ 記号を使用しています。なんだか通っぽい感じがでますよね?

おわりに

さて、今回のまとめです。

  • Stack は Haskell におけるビルドツール。
  • VSCode の Haskell 拡張を入れれば、Language Server も一緒に入ってすぐに使える状態になる。
  • stack new xxx でプロジェクト作成。
  • No cradle found for … という通知を消したければ hie.yaml を作成する。
  • implicit-hie を使えば自動生成できる。
  • お好みで JetBrains Mono などの合字に対応したフォントもオススメ。
  • VSCode と REPL を活用した学習方法。
  • doctest を使うとドキュメンテーションコメント上にテストを書ける。
  • REPL のヘルプは :? で見られる。
  • 複数行の入力は :{ で始めて、 :} で終える。
  • プロンプトは :set prompt:set prompt-cont でカスタマイズできる。
  • ~/.ghci に記述しておけば自動的に反映される。

さて、次回は関数の基本的な書き方を見ていく予定です。

blog.penginmura.tech

VSCode トラブルシューティング

おまけとして、VSCode のトラブルシューティングについて記載しておきます。

補完やホバーが動かない

考えられる要因と対策は以下のとおりです。

  • Language Server の起動に時間が掛かっている。
    → 待つ。
  • Language Server がサポートしていない GHC バージョンを利用している。
    --resolver で明示的にバージョンを指定する(記事中のlts-17.2 であれば動くと思います)。
  • hie.yaml の定義が正しくない。
    gen-hie > hie.yaml で更新する。

それでも解決しない場合は、VSCode のログなどを確認してみると良いでしょう。

なんかコード全体がエラー扱いに・・・

とくに GHC の言語拡張 {-# LANGUAGE ... #-} を記述しようとすると起こることが多いのですが、コード全体に赤ラインが引かれてまともに動かなくなってしまうことがあります。

その場合は、VSCode を再起動すると直ることが多いようです。

あとがき

コードを試したくなったら REPL だ。REPL を使わないと損だぞ!

Written by tobi462.


  1. 本格的に開発する際には git コマンドにフックしても良いかもしれません。