Lecture

関数型プログラミング Ch2 環境構築

資料

プログラミング用の設定

本章では,実際にHaskellを利用したプログラミングを体験してみます. しかし,Haskellを使う前に,一般的にプログラミングを行うために必要となる準備をしておきましょう.

テキストエディタ(VSCode)のインストール,IME,CLI,基本コマンドなどの設定は他の講義と共通するため,別ページにまとめています.

プログラミング用の設定はこちら

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といいます.

Stackの役割

Haskellの各packageは, 動作可能なGHCのバージョンや依存packageのバージョンに制約を持っています. プロジェクトで使う複数のpackageを同時に動かすには, すべてのpackageの制約を満たすGHCと依存packageの組合せ, すなわち各制約集合の 積集合 に属する組合せを選ばなければなりません. この組合せを人手で探そうとして泥沼にはまるのが cabal hell です.

package A 対応 GHC・依存 package B 対応 GHC・依存 package C (対応 GHC・依存) 使用可能な 組合せ

Stackでは, この積集合の代表点を予め選んで固定したものが resolver (Stackage が提供する組合せの snapshot) です. プロジェクトはこのresolverに従い, 選ばれたGHCとpackage群がプロジェクト固有のもの (.stack-work/ ディレクトリ以下) として配置 され, 他のプロジェクトと干渉しません.

Stackプロジェクト (resolver で固定) 選択された GHC package A package B package C .stack-work/ ディレクトリ以下にプロジェクト固有として配置

従来 Cabal + GHCup の構成では, GHCバージョン管理・package管理・依存解決をそれぞれ別の役割で分担していましたが, Stack はこれらをまとめて扱えます.

GHCバージョン管理 パッケージ管理 依存解決 プロジェクト分離
Cabal + GHCup GHCup Cabal (Hackage) Cabal solver Cabal store
Stack Stack Stack (Hackage) resolver (Stackage) .stack-work

環境構築

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:に続けて,使用したいライブラリのレポジトリやバージョンを明示します.

これらの利用法は,今後ライブラリを使用し始めたときに改めて学習すれば大丈夫なので,取り敢えずプログラムを作成してきましょう.

Hello World

環境構築が上手くできているかを確認するために,Hello World用のプログラムを作成してみましょう.

まずは,app/Main.hsをテキストエディタで開いて編集します.

app/Main.hsを開くと,以下のようなファイルになっているかと思います. Haskellのプログラムをコンパイルした実行可能ファイルでは,main = 内の記述が実行されます.

module Main (main) where

import Lib

main :: IO ()
main = someFunc

現在はsumFuncという関数が実行されます. sumFuncimport 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.hshelloWorldと出力する値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-world

ghc-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-exe

stack 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が定義されていないという意味になります. helloWorldoを追加して正しい名称にしたあともう一度 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
ce0f13b2-4a83-4c1c-b2b9-b6d18f4ee6d2