はじめに
この記事では、Haskellの基本文法を身につけた皆さんに向けて、これから実用的なHaskellアプリを開発したり、既存のHaskellコードを読むにあたって役に立つ事柄を紹介します。
またこの記事を読むと、この記事で紹介していないものについても、混乱せずに自分で調べて学んでいけるように(たぶん)なります。
必ずしも頭から読む必要はなく、知りたいと思った節から読んでいくのが良いかと思います。
目安として、ウォークスルー Haskellを全て学んでいる相当の知識を前提とします。
目次
- Readerモナド, Writerモナド, Stateモナドの簡単な解説
- これらはIO, Maybe, List以外で特に重要なモナドです。
- モナドを合成する手段であるモナドトランスフォーマーの説明もします。
- Haskellの文字列事情の解説記事へのリンク
- Haskellの例外事情の解説記事へのリンク
- Haskellの基本的なイディオムの紹介
- Lens, Hasパターン
- 特に重要なGHC拡張の紹介
- TupleSections, NamedFieldPuns, MultiParamTypeClasses
- その他の重要なGHC拡張へのリンク
- Haskellerがよく使う周辺ツールの紹介
- ウェブサイト、環境構築ツール、Lintなど
mtl
mtlとは、いくつかの基本的なモナドを提供するライブラリです。
Haskellでよく利用するモナドは実はそう多くありません。
標準ライブラリで提供されるIO
モナド、Maybe
モナド、リストモナドに加えて、mtlが提供するReader
モナド、Writer
モナド、State
モナドの使い方が分かれば、後はこれらを組み合わせて自作モナドを作っていくことが可能です。
IO
モナド、Maybe
モナド、リストモナドについてはご存知かと思うので、mtlが提供する基本的なモナド3種について簡単に紹介します。
Readerモナド
イミュータブルな環境r
を持ち、いつでも文脈からr
を読み出すことができるモナドです。
定義は次の通りです。関数にReader
と名前をつけただけの、シンプルな構造です。
newtype Reader r a = Reader
{ runReader :: r -> a
}
ask :: Reader r r
ask = Reader id
Readerモナドの文脈中では、ask
アクションを用いて好きに環境r
を取ってくることができます。
しばしば利用される例として、プログラム全体をReaderモナドでラップして、コマンドライン引数やアプリケーションの設定などを利用できるようにすることが挙げられます。
コマンドライン引数などからEnv
を作成して、メインの処理は全てReaderT
の文脈で実行する例です。
プログラム内部では、ask
を利用してverbose
の設定値を読み取り、それによってデバッグログを出力するかどうかを判断しています。
main :: IO ()
main = do
let env = Env {envVerbose = True}
flip runReaderT env $ do
verbose <- envVerbose <$> ask
when verbose $
lift . print $ "DEBUG: Hello World!"
lift . print $ "Your application is running!"
なお、ReaderT
と末尾にT
がついているのは、後述するモナドトランスフォーマーと呼ばれる変種を使っているからです。
コード全文はこちらです。
Writerモナド
Writerモナドは、文脈中でモノイドを書き出していくことができるモナドです。
定義は次の通りです。ペアにWriter
と名前をつけただけの、シンプルな構造です。
newtype Writer w a = Writer
{ runWriter :: (a, w)
}
tell :: w -> Writer w ()
tell w = Writer ((), w)
Writerモナドの文脈では、tell
アクションを用いてモノイドw
を書き出すことができます。
よくあるユースケースとしては、たくさんのモノイドを手続き的な書き方で結合させたい場合があります。
let (_, html) = runWriter $ do
tell "<html>"
tell "<p>Hello World</p>"
tell "</html>"
これは次のコードと同じ意味です。
let html2 = "<html>"
<> "<p>Hello World</p>"
<> "</html>"
あるいは、Data.Foldable
のfold
関数を使って、次のように書くこともあります。
let html3 = fold
[ "<html>"
, "<p>Hello World</p>"
, "</html>"
]
モノイドは様々な場所で使われるため、場合に合わせて最も読みやすくなる書き方を選択します。
コード全文はこちらです。
Writerモナドについての注意点として、素直な実装のWriterモナドにはスペースリークの問題があるため、下に貼った私の実装を含め、独自実装は使わないようにしてください。
mtlに含まれるWriterモナドはこの問題を回避するため、安心して使うことができます。
Stateモナド
文脈中でミュータブルな状態s
を扱うことができるモナドです。
定義は次の通りです。関数にState
と名前をつけただけの構造です。
newtype State s a = State
{ runState :: s -> (a, s)
}
get :: State s s
get = State $ \s -> (s, s)
put :: s -> State s ()
put s = State $ const ((), s)
modify :: (s -> s) -> State s ()
modify f = get >>= put . f
Readerよりもやや複雑な関数に見えますが、「状態を受け取って、計算結果と新しい状態を返す」ような関数がStateモナドとみなせる、ということです。
Stateのアクションは3つあり、現在の状態を取得するget
、新しい状態を設定するput
、現在の状態に変更を加えるmodify
があります。
これにより、定数しか使えないHaskellの中で疑似的に変数を扱うことができるようになります。
Stateモナドのユースケースとしては、自作モナドのプリミティブとして使うことが多いと思います。
自作モナドについては、続くモナドトランスフォーマーで説明します。
モナドトランスフォーマー
モナドトランスフォーマーとは、モナドを合成可能にする仕組みです。
Readerモナドの例で出てきたコードは、まさにモナドトランスフォーマーを使った例です。
この例では、Reader
とIO
を合成して、環境を持つReaderの特性と標準出力が可能なIOの特性を両立するモナドを作成しています。
main :: IO ()
main = do
let env = Env {envVerbose = True}
flip runReaderT env $ do
verbose <- envVerbose <$> ask
when verbose $
lift . print $ "DEBUG: Hello World!"
lift . print $ "Your application is running!"
モナドトランスフォーマーは、各モナドごとに作成します。
例えばReaderT
は次のように実装されています。
newtype ReaderT r m a = ReaderT
{ runReaderT :: r -> m a
}
ask :: Monad m => ReaderT m r r
ask = ReaderT pure
Readerの定義と見比べると型パラメータにm
が増えており、これが合成対象のモナドとなっています。
Readerモナドの使用例と称して使ったモナドの型は実際にはReaderT Env IO
であり、これがReaderモナドとIOモナドを合成したモナドということになります。
また、モナドトランスフォーマーは一般にMonadTrans
という型クラスを実装します。
class MonadTrans t where
lift :: Monad m => m a -> t m a
モナドトランスフォーマーによるモナドの合成には、上下関係があります。
例えば、ReaderT r IO
というモナドは、IOの上にReaderTを被せています。この上にWriter
モナドを被せれば、WriterT w (ReaderT r IO)
となります。これをモナドスタックと呼びます。
lift
は、下位のモナドのアクションを一つ上のモナドのアクションに持ち上げます。
例えばReaderT Env IO a
モナドの中でprint
などのIO
のアクションを実行したい場合、lift . print
とすることで戻り値のIO ()
をReaderT Env IO ()
に変換します。
モナドを自作するというと、ほとんどの場合、モナドトランスフォーマーを使って必要な性質を組み合わせて一つのモナドを合成することを言います。
次は、典型的な自作モナドの例です。
newtype MyApp a = MyApp (ReaderT MyEnv (StateT MyState IO) a)
モナドトランスフォーマーの問題点とExtensible Effects
モナドトランスフォーマーにはいくつかの問題点が指摘されています。
- 速度が遅い
-
lift
が面倒くさい - 合成の順序によってモナドの特性が変わる
- 合成の順序を入れ替えることができない
これを克服する代替のモナドの合成手段として、Extensible Effectsが存在します。
Extensible Effectsについては発展的な内容になるため、必要になったら調べてみてください。
モナドトランスフォーマーの問題点とExtensible Effectsについて、より詳しくは次の記事が参考になります。
文字列ライブラリ
残念なことに、Haskellの文字列事情は、非常に込み入っています。
ここで解説するにはあまりにも複雑なので、いくつかの記事を紹介します。
基本的にはtextパッケージを使い、必要に応じてbytestringパッケージを使います。
トイプログラム以外では標準のString
は使わないのが無難です。
例外
ウォークスルーHaskellには出てきませんが、Haskellにも例外という概念はあります。
Maybe
モナドやEither
モナド、List
モナドには例外処理という側面がありますし、例えば非網羅的なパターンマッチやIO処理の失敗から送出される例外もあります。
しかし、Haskellは例外事情も込み入っており、例外処理に標準ライブラリではなく代替のライブラリを使うことが推奨されています。
例外事情については以下の記事が参考になります。
イディオム
Lens
Haskellの貧弱なレコードを補うイディオムです。
次のLens
という型を用いると、HaskellでGetterやSetterをオブジェクト指向の言語のように扱うことができます。
type Lens s t a b = forall f. Functor f => (a -> f b) -> s -> f t
type Lens' s a = Lens s s a a
使い心地は次のような感じです。
let p = Person{name = "John", age = 20}
print $ "name : " <> p^.nameL
print $ "age : " <> show (p^.ageL)
let q = p & nameL .~ "Jeanne"
print $ q
コード全体は次に置いてあります。
この通りシンプルなイディオムではあるのですが、様々なバリアントと膨大な取り巻きを擁する巨大なライブラリが用意されています。
実用上は軽量なmicrolensパッケージを使うのがおすすめです。
Hasパターン
Haskellの貧弱なレコードを、Lensと型クラスを使って補うイディオムです。
私が書いた記事で恐縮ですが、以下にまとめています。
GHC拡張
Haskellに新しい文法を導入したり、既存の文法を変更したりする、コンパイラ独自の拡張です。
Haskell関連の記事や既存のコードを読んでいて、全く知らない文法が登場したら、恐らくそれはGHC拡張です。
GHC独自の拡張ではあるのですが、GHCがデファクトスタンダードであることと、もはやなくてはならない機能が多数含まれるために、学ぶことが必須となっています。恐らくですが、世界中のHaskellコードのうちGHC拡張を一切使っていないコードは1%も存在しないと思います。
ここでは、主要なGHC拡張のうち特に重要なもののみを紹介します。
TupleSections
Haskellでは、セクションという記法を用いることで、二項演算子に対しても部分適用を利用することができます。
TupleSectionsを用いると、このセクションをタプルに対しても利用することができるようになります。
(, True) -- ==> \x -> (x, True)
NamedFieldPuns
フィールドを変数に束縛するとき、しばしば次のようにフィールド名と同名の変数を宣言することがあります。
getName :: Person -> String
getName Person {name=name} = name
NamedFieldPunsを使うと、これを次のように書けます。
getName :: Person -> String
getName Person {name} = name
また値の分解時だけでなく、構築時にもNamedFieldPunsを使えます。
フィールドの多いレコードを扱うときには非常に便利な拡張です。
let name = "John"
in Person {name}
MultiParamTypeClasses
ここまではただ便利な構文を紹介してきましたが、拡張機能がなければ実現できない拡張をいくつか紹介します。
MultiParamTypeClassesは、複数のパラメータを取る型クラスを書けるようにする拡張です。
型クラスは通常、ある型についての特性を記述します。
例えばShow
を実装する型a
はshow
メソッドを通じて文字列にすることができます。
複数のパラメータを取る型クラスは、複数の型の間の関係を記述します。
次のIso
型クラスは、型a
と型b
が同型であることを表しています。(同型は数学の用語ですが、直感的には「ほぼ同じ」という意味です)
class Iso a b where
iso :: a -> b
iso' :: b -> a
この型クラスを実装でき、かつ条件iso . iso' == id
とiso' . iso == id
を満たす場合、型a
と型b
は同型であるといえます。
- 例えば、
(String, Int)
と(Int, String)
は同型であることは自明です。 -
Int
とString
は同型ではなさそうです。-
Int -> String
は実装できそうですが、逆の変換はString -> Maybe Int
となってしまい、実装できなそうです。
-
-
a -> b -> c
と(a, b) -> c
同型です。-
Iso
のインスタンスを書くことはできるでしょうか? -
Iso
のインスタンスを書くにはFlexibleInstances拡張を有効化する必要があることに注意してください。 - MultiParamTypeClassesを制約として書くにはFlexibleContexts拡張を有効化する必要があることに注意してください。
-
もう少し実用的な例として、mtlのMonadReader
型クラスを見てみましょう。
class Monad m => MonadReader r m | m -> r where
ask :: m r
MonadReader
型クラスは、Reader
モナドを型クラスに一般化した型クラスではあるのですが、字面に注目すれば環境r
の型とモナドであるような型m
の関係を表しています。
なお、| m -> r
の部分はFunctionalDependenciesといい、m
が決まればr
は一意に定まることを意味しています。
その他の拡張
主要なGHC拡張は実際のところまだまだあります。
不自由なくHaskellを書こうと思ったら、少なくとも以下のGHC拡張は必須だと思います。(これでも厳選しています)
- MultiParamTypeClassesに付随して必要となる拡張群
-
OverloadedStrings
- ダブルクォートで囲った文字列を
String
以外の型にも推論できるようにします。 - Haskellの貧弱な文字列を扱う上で必須です。
- ダブルクォートで囲った文字列を
-
GeneralisedNewtypeDeriving
-
newtype
で新たな型を作成するとき、元の型が持っていたインスタンスをderiving
キーワードを使って実装できるようになります。
-
-
GADTs
- 一般化された代数的データ型を定義できるようになります。
もし他に使うべきGHC拡張が知りたい場合、rioというライブラリがデフォルトで有効化するべきと主張するGHC拡張のリストが参考になります。(ライブラリを使う必要はありません)
GHC拡張について、より詳しくはGHCのドキュメントを参照してください。
周辺ツール
GHCup
GHCやStack, Cabal, Haskell Language Serverなどの、Haskell開発の周辺ツールのバージョンを管理ができるツールです。
- StackやCabalは、Haskellプロジェクトを管理するためのツールです。
- 依存関係やプロジェクト構成などを管理できます。
- 一時期はStackが有力でしたが、最近再びCabalが盛り返しつつあります。
- Haskell Language Server (HLS) は、IDEがソースを解析を行うための諸情報を提供します。
- これを導入することで、VSCodeなどお好みのエディタをIDEにすることができます。
- HIE (Haskell IDE Engine) は古いのでお勧めしません。
もしHaskellの開発環境をセットアップしていない場合は、こちらのブログが参考になります。
Hoogle
Hoogleは、定数名や関数名、関数のシグネチャからドキュメントをライブラリ横断的に検索することができるサイトです。
特にググラビリティの低い演算子の検索に重宝すると思います。
試しに>>=
演算子を検索してみましょう。
この演算子のシグネチャがどんなシグネチャをしていて、どの型クラスのメソッドで、どのパッケージに含まれていて、どのライブラリに含まれているか、ということが分かります。
Wandbox
小さいコード片をさっと実行したり、Haskellコードを共有してみたりするのに便利です。
もし使い慣れたオンラインコンパイラがHaskellに対応していれば、それでも全然問題ないと思います。
Haddock
Haskell用のドキュメント生成ツールです。
JavaDocやJSDocのように、専用の記法を使うことでソースコードからドキュメントを生成できます。
いつもお世話になっているHackage上のドキュメントは、このHaddockを使って生成されています。
Stylish HaskellとHLint
Stylish Haskellはコードの自動整形ツールです。
新しい言語を始めたばかりの頃は特に、その言語での文化圏ではどのようなコードスタイルが一般的なのかが分からないと思います。
他の言語に漏れず、Haskellの文化圏でもコードスタイルは人によってまちまちではあるのですが、Stylish Haskellに従っておけばとりあえずは間違いありません。
HLintはHaskell向けのLintです。
他のLintと同じようにコードの問題点を解析してくれるのですが、使い道が初見では分からないような関数が多いHaskellでは、初心者とって極めて有用です。
まるで先輩にコードレビューしてもらっているように、標準ライブラリに存在する関数を新たに知ることができたり、標準ライブラリにある関数の使い方を初めて理解できたりするケースがあります。
Stylish HaskellやHLintについて知るには、次の記事もおすすめです。
ブログなど
Haskell-jp
Haskellの日本語話者のユーザーグループです。
Haskellの情報を発信したり、勉強会やイベントの開催をしています。
Slackチームに参加すれば、分からないことを質問したり、共にHaskellを学ぶ人を見つけることができるかもしれません。
Haskell荘園
Haskellに関する攻略情報が共有されています。
頻出パターンやベストプラクティス、Haskellの問題点などが雑多に載っています。
用語
Haskellをやっているとよく出てくるが、他のプログラミング言語にはあまり出てこない用語。
- 文脈
- (特にdo文を使って)モナディックな値を生成する過程を、モナドの文脈などという。
- 文脈ではそのモナドのアクション(後述)を使うことができる。
main :: IO () main = do -- ここはIOモナドの文脈にある putStrLn "Hello World" -- IO () という値を生成する flip runReaderT "Bonjour Monde" $ do -- ここは ReaderT Env IO モナドの文脈にある greeting <- ask lift $ putStrLn greeting -- ReaderT String IO () という値を生成する
- アクション
- モナディックな値、またはモナディックな値を返す関数のことを指します。
- 例えば、
putStrLn
はString -> IO ()
であるため「IOのアクションを返す関数」あるいは単に「IOのアクション」などと言ったりします。 - しばしば型クラスに括り出されて一般化されます。
- 単純にアクションを型クラスに括り出すだけでも、アクションをモックモナドの上で動かすことができるようになったりします。
- アクションについてはこちらの記事も参考になります。