LoginSignup
18
10

Haskellの基本文法を学んだあとに読む記事

Last updated at Posted at 2023-07-19

はじめに

この記事では、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.Foldablefold関数を使って、次のように書くこともあります。

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モナドの例で出てきたコードは、まさにモナドトランスフォーマーを使った例です。
この例では、ReaderIOを合成して、環境を持つ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

モナドトランスフォーマーにはいくつかの問題点が指摘されています。

  1. 速度が遅い
  2. liftが面倒くさい
  3. 合成の順序によってモナドの特性が変わる
  4. 合成の順序を入れ替えることができない

これを克服する代替のモナドの合成手段として、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を実装する型ashowメソッドを通じて文字列にすることができます。
複数のパラメータを取る型クラスは、複数の型の間の関係を記述します。

次のIso型クラスは、型aと型bが同型であることを表しています。(同型は数学の用語ですが、直感的には「ほぼ同じ」という意味です)

class Iso a b where
    iso  :: a -> b
    iso' :: b -> a

この型クラスを実装でき、かつ条件iso . iso' == idiso' . iso == idを満たす場合、型aと型bは同型であるといえます。

  • 例えば、(String, Int)(Int, String)は同型であることは自明です。
  • IntStringは同型ではなさそうです。
    • Int -> Stringは実装できそうですが、逆の変換はString -> Maybe Intとなってしまい、実装できなそうです。
  • a -> b -> c(a, b) -> c同型です。
    • Isoのインスタンスを書くことはできるでしょうか?
    • Isoのインスタンスを書くにはFlexibleInstances拡張を有効化する必要があることに注意してください。
    • MultiParamTypeClassesを制約として書くにはFlexibleContexts拡張を有効化する必要があることに注意してください。

もう少し実用的な例として、mtlMonadReader型クラスを見てみましょう。

class Monad m => MonadReader r m | m -> r where
    ask :: m r

MonadReader型クラスは、Readerモナドを型クラスに一般化した型クラスではあるのですが、字面に注目すれば環境rの型とモナドであるような型mの関係を表しています。
なお、| m -> rの部分はFunctionalDependenciesといい、mが決まればrは一意に定まることを意味しています。

その他の拡張

主要なGHC拡張は実際のところまだまだあります。
不自由なくHaskellを書こうと思ったら、少なくとも以下のGHC拡張は必須だと思います。(これでも厳選しています)

もし他に使うべき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 () という値を生成する
    
  • アクション
    • モナディックな値、またはモナディックな値を返す関数のことを指します。
    • 例えば、putStrLnString -> IO ()であるため「IOのアクションを返す関数」あるいは単に「IOのアクション」などと言ったりします。
    • しばしば型クラスに括り出されて一般化されます。
      • 単純にアクションを型クラスに括り出すだけでも、アクションをモックモナドの上で動かすことができるようになったりします。
    • アクションについてはこちらの記事も参考になります。
18
10
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
18
10