代数プログラミング入門 Ch2 環境構築
1 Haskellセットアップ
言語の特徴や意味を色々と説明してきましたが,習うより慣れろということで,そろそろHaskellを利用してみましょう.Haskellの開発環境には様々なものがありますが,現在良く使われているものとしてCabal + GHCupあるいはStackの2つがあります. CabalとStackはプロジェクトのビルドを行うためのアーキテクチャであり,GHCupは周辺環境のインストーラーです. どちらで開発を行ってもいいのですが,本稿ではStackを用います.
Stackは現在のHaskellの標準的なコンパイラである,Glasgow Haskell Compiler(GHC)に基づいたビルド環境です(cabalもGHCですが). 他の言語と同様にHaskellでも様々なpackage(ライブラリ)を利用するのですが,package毎に他のpackageや,GHC(Haskellのコンパイラ)との依存関係があります.それらを使用するpackage事に調整することが人間には至難の業であり, 特定のpackageの依存関係を満たせば他のpackageの依存関係が満たされなくなるという試行錯誤を永遠と繰り返すことをcabal hellなどと呼びます.
Stackにはそのようなpackage間の依存関係を満たすバージョンの組み合わせ(resolver)を利用して,自動で解決してくれる機能があり,Haskellでのブロジェクトの開発を容易にしてくれます. resolverの集まりをStackageといい, resolverで扱われるpackageをまとめて管理するレポジトリのことをHackageといいます.
1.1 環境構築
Stackの環境構築の方法は基本的には,公式サイトに従ってください. 使用しているOS毎にインストール方法が異なるので注意しましょう特にMacユーザーはIntel Mac と Apple silliconでインストール方法が異なるので正しい方を選択するようにしてください.
インストールが終わったら,以下のコマンドでstackを最新版にupgradeします.
stack upgrade次に,開発用のディレクトリに移動して,開発用のプロジェクトを作成していきます. Stackでは,新しいプロジェクトの作成はstack new [project-name] コマンドで行われます. stack new [project-name]コマンドで新しいプロジェクトを作成すると,必要なファイルが含まれた[project-name]という名前のディレクトリが作成されます. 作成されたディレクトリに移動しましょう.
> ls
> stack new hello-world
> ls
hello-world
> cd hello-world作成されたディレクトリの構成は以下のようになっています.
❯ tree
.
├── CHANGELOG.md
├── LICENSE
├── README.md
├── Setup.hs
├── app
│ ├── hello.hs
├── hello-world.cabal
├── package.yaml
├── src
│ └── Lib.hs
├── stack.yaml
└── test
└── Spec.hsそれぞれの用途と意味は以下のとおりです.
appフォルダの中には,実行可能ファイル用のプログラムプロジェクトをbuildすると,
Main.hsから実行可能ファイル(executable)が生成されますこの後,
Main.hsの中身を編集してHello World用のプログラムを作成します.
srcフォルダ内には,実行可能ファイルで利用するライブラリが格納されます.- ここに自分で開発したライブラリを含めることも可能です.
package.yamlファイルはプロジェクトの設定を記入するファイルです.Hackageなどの外部のライブラリを利用する場合には,
package.yaml内のdependencies:部分に,使用したいライブラリを記述します.Stackは
stack setupコマンドによって,package.yaml内に記述されたライブラリの依存関係を解決するresolverを自動で選択しますが, 自分で使いたいresolverをpackage.yaml内のresolver:に続けて書くことで,指定することも可能です.その他実行可能ファイルの設定や,コンパイルオプションなどを指定することができます.
package.yamlの設定に従って,プロジェクトの設定ファイルtest.cabalが自動で作成されます. 基本的にstackを使っている範囲では.cabalファイルを自分で編集することはありません.
stack.yamlファイルは,stackの設定を記入します- resolverに含まれないライブラリ(自分のGitHub上にあるライブラリなど)を指定する,あるいはあえてresolverとは異なるバージョンを利用するときなどには
extra-deps:に続けて,使用したいライブラリのレポジトリやバージョンを明示します.
- resolverに含まれないライブラリ(自分のGitHub上にあるライブラリなど)を指定する,あるいはあえてresolverとは異なるバージョンを利用するときなどには
これらの利用法は,今後ライブラリを使用し始めたときに改めて学習すれば大丈夫なので,取り敢えずプログラムを作成してきましょう.
1.2 Hello World
環境構築が上手くできているかを確認するために,Hello World用のプログラムを作成してみましょう.
まずは,app/Main.hsをテキストエディタで開いて編集します.
app/Main.hsを開くと,以下のようなファイルになっているかと思います. Haskellのプログラムをコンパイルした実行可能ファイルでは,main = 内の記述が実行されます.
module Main (main) where
import Lib
main :: IO ()
main = someFunc現在はsumFuncという関数が実行されます. sumFuncは import Lib の記述によって, src/Lib.hsからimportされています. src/Lib.hsを開くと,
module Lib
( someFunc
) where
someFunc :: IO ()
someFunc = putStrLn "someFunc"という風にsomeFuncが定義されています. プログラム内の someFunc :: IO () はsomeFuncの型注釈です. IO () というのは,標準入出力 IO において, アクション () を実行するという意味ですが,ここではそれぞれの詳細は省きます. putStrLn は文字列を引数にとり,標準入出力IOに受け取った文字列を出力するというアクション()を返す関数であり,ここでは,"someFunc"という文字列が出力されます. この"someFunc" 部分を "Hello World"に書き換えれば,Hello Worldは実行できます.関数の定義はこのあと徐々に扱いますが, someFuncは,引数を取らないので関数というよりは実際には値です.
Lib.hs にhelloWorldと出力する値helloWorldを追加し,全体を以下のように書き換えましょう.
module Lib
( someFunc
, helloWorld
) where
someFunc :: IO ()
someFunc = putStrLn "someFunc"
helloWorld :: IO ()
helloWorld = putStrLn "Hello World"module Lib () where はモジュール宣言で,他のプログラムからimport Libで,src/Lib.hs内に定義された関数や値などの内 ()内に記述されたものを読み込むことができるようにします.
作成した値helloWorldを()内にhelloWorldを追加することを忘れないようにしましょう.
併せて app/Main.hs を書き換えて,作成したhelloWorldを実行しましょう.
module Main (main) where
import Lib
main :: IO ()
main = helloWorldこのプログラムをコンパイルして得られる実行可能ファイルの名前などは,package.yaml内で定義されています.
ghc-options:
- -Wall
- -Wcompat
- -Widentities
- -Wincomplete-record-updates
- -Wincomplete-uni-patterns
- -Wmissing-export-lists
- -Wmissing-home-modules
- -Wpartial-fields
- -Wredundant-constraints
library:
source-dirs: src
executables:
hello-world-exe:
main: Main.hs
source-dirs: app
ghc-options:
- -threaded
- -rtsopts
- -with-rtsopts=-N
dependencies:
- hello-worldghc-options: 以下の項目はghcのコンパイルオプションであり,Wで始まるいずれのオプションもコンパイル時のWarningを追加するものです. これらのコンパイルオプションがあると,プログラムの品質を高めることができますが, 利用していてWarningが邪魔に感じた場合は,すべて削除しても問題ありません(
その場合は以下のように,ghc-options:部分を#でコメントアウトしてください.)
#ghc-options:
library:
source-dirs: src
executables:
hello-world-exe:
main: Main.hs
source-dirs: app
ghc-options:
- -threaded
- -rtsopts
- -with-rtsopts=-N
dependencies:
- hello-world特に,本講義資料では,品質よりも分かりやすさを優先してできるだけシンプルな実装を紹介する他,事例としてあえて間違ったコードを入力する場面も存在します. そのままサンプルを入力すると多数のWarningが表示されることになるので,以下の説明中で登場する出力結果ではこれらのオプションはすべて切った状態のものとなっている点に留意してください.
library:以下の記述で,利用するライブラリのPATH,executables:以下の記述で実行可能ファイルについて記述されています. ここでは, executableとして’app’フォルダ内にある’Main.hs’が’hello-world-exe’という名称でコンパイルされることが書かれています.ghc-options:以下は,コンパイル時のオプションを設定していますが,ここでは詳細は省略します.
Main.hs以外のファイルをここに追加すれば,いくらでも実行可能ファイルは増やすことができます.
hello-world-exe部分をもっと短い名前に変更することも可能です.なお生成される実行可能ファイルはMacではhello-world-exe,Windowsではhello-world-exe.exeになるので注意してください.
それでは,以下のコマンドでこのプロジェクトをbuildして,実行してみましょう.
stack build
stack exec hello-world-exestack buildのあと,プログラムにミスがなければ以下のように出力されるはずです(一部省略しています).
❯ stack build
hello-world> build (lib + exe) with ghc-9.6.4
Preprocessing library for hello-world-0.1.0.0..
Building library for hello-world-0.1.0.0..
[1 of 2] Compiling Lib [Source file changed]
Preprocessing executable 'hello-world-exe' for hello-world-0.1.0.0..
Building executable 'hello-world-exe' for hello-world-0.1.0.0..
[1 of 2] Compiling Main [Source file changed]
[3 of 3] Linking .stack-work/dist/x86_64-osx/ghc-9.6.4/build/hello-world-exe/hello-world-exe [Objects changed]
hello-world> copy/register
Registering library for hello-world-0.1.0.0..どこかで,タイプミスなどがあると例えば以下のようなエラーが表示される可能性もあります(一部省略しています).
hello-world> build (lib + exe) with ghc-9.6.4
Preprocessing library for hello-world-0.1.0.0..
Building library for hello-world-0.1.0.0..
Preprocessing executable 'hello-world-exe' for hello-world-0.1.0.0..
Building executable 'hello-world-exe' for hello-world-0.1.0.0..
[1 of 2] Compiling Main [Source file changed]
/Users/akagi/Documents/Programs/Haskell/blog/hello-world/app/Main.hs:6:8: error: [GHC-88464]
Variable not in scope: hellWorld :: IO ()
Suggested fix: Perhaps use ‘helloWorld’ (imported from Lib)
|
6 | main = hellWorld
| ^^^^^^^^^
Error: [S-7282]
Stack failed to execute the build plan.
While executing the build plan, Stack encountered the error:
[S-7011]
While building package hello-world-0.1.0.0
Process exited with code: ExitFailure 1上のエラーでは, Main.hsの6行目で使用されている,hellWorldが定義されていないという意味になります.
helloWorldとoを追加して正しい名称にしたあともう一度 stack buildをしてみましょう.
stack exec hello-world-exeの後,Hello Worldと出力されていれば成功です.
なお,build と exec を併せて一つのコマンドstack run で代替することも可能です.
❯ stack run hello-world-exe
hello-world> build (lib + exe) with ghc-9.6.4
Preprocessing library for hello-world-0.1.0.0..
Building library for hello-world-0.1.0.0..
Preprocessing executable 'hello-world-exe' for hello-world-0.1.0.0..
Building executable 'hello-world-exe' for hello-world-0.1.0.0..
hello-world> copy/register
Registering library for hello-world-0.1.0.0..
Hello World