この記事の目的
現在おそらくまだ、主流なMonadインスタンスの作り方である、GenerarizedNewTypeDerivingを用いた方法の理解の手助け。
軽く検索した限り、本記事タイトルについての具体例が(わかりやすさ特化な例はありましたが、具体特化の例は)見つからなかったので、紹介してみます。
あと、Monadを作ることの楽しさの布教。
GenerarizedNewTypeDeriving拡張を学ぶ筋道と、実例を示します!
そもそもGenerarizedNewTypeDerivingとは?
Haskellのnewtypeは、元となる型と(圏論的な)同型を作るキーワードです。
(値構築子と、パターンマッチの解凍 もしくは レコードの関数、を同型射とした同型。間違ってたら教えてください ><)
つまりは「元となる型ができることはnewtypeで作る型もできる」ってことなのですが、newtypeがstrong typedefであるが故に、型クラスの情報をnewtypeは忘れ去ってしまいます。
その情報を思い出させるのがGenerarizedNewTypeDeriving
拡張です!
- strong typedef とは? ==> 本の虫: C++1yに提案されている不透明エイリアス(opaque alias)
具体的には、以下を可能にします。
{-# LANGUAGE GeneralizedNewtypeDeriving #-}
{-# LANGUAGE OverloadedStrings #-}
import Data.String (IsString)
import Data.Text (Text)
import qualified Data.Text.IO as TIO
newtype MyString = MyString { unMyString :: Text }
deriving (IsString) -- GenerarizedNewTypeDerivingがないと、ここを書けない
txt :: MyString
txt = "アホ毛"
main :: IO ()
main = TIO.putStrLn . unMyString $ txt
実際どのように、自分用Monadを作るのか?
これについては、こちらがわかりやすいかなと思います。
具体例
さて本題ですが、今回僕が作ったものは
ログと、構文上のブロックのネスト数という状態を保持した、不純なパーサです。
実際使っているプロジェクトはここにあります(ただしWIP)
まあ…不純なパーサは個人的には好ましくないとは思いますが……。
ここは言い訳なので読まないで良いんですが
なぜ良くないと思うのにParsecTをMonadStateとして使ったのかというと、パースログを取りたかったんですよね。
でもmegaparsecのParsecTはMonadWriterではなかったので、MonadStateを使おうということに決めました。 苦し紛れ。
そしたらやっぱり、状態も使いたいじゃないですか。 めっちゃ見やすくしたくなった。
ということで使いました……。
実際はログへのみの副作用として状態が使われています。
状態を読み出してパース結果に影響を与えたりしてないので、セーフセーフ!!
-- | Current state of ElinParser
data ElinState = ElinState
{ _parseLogs :: [ParseLog]
, _parseNestLevel :: Int
} deriving (Show)
-- | Parser with parsing logs
newtype ElinParser a = ElinParser { _runElinParser :: ParsecT Dec Text (State ElinState) a }
deriving ( Functor, Applicative, Monad
, Alternative, MonadPlus
, MonadState ElinState, MonadParsec Dec Text
)
-- | A log of ElinParser
data ParseLog = Message Text -- ^ Simple message
| ParsedItem Text -- ^ Ex: '(', ''', ')' or some symbol
deriving (Show)
-- | Run parser and extract result and logs
runElinParser :: ElinParser a -> Text -> (Either (ParseError Token Dec) a, [ParseLog])
runElinParser parser source =
let initialState = ElinState [] 0
bareness = _runElinParser parser
(result, ElinState logs _) = flip runState initialState $ runParserT bareness "" source
in (result, reverse logs)
ElinParser a
は
- ElinStateという状態を持ち
- Textを食べる
というパーサです。
ElinStateは後にmakeLensesされます。
おわり
Operationalモナドも使ってみたいんですが、まだ使ったことないんですよね〜。