「GenerarizedNewTypeDerivingで自分用Monadを作る」ってなに!?(具体例編)

  • 2
    いいね
  • 0
    コメント

この記事の目的

 現在おそらくまだ、主流なMonadインスタンスの作り方である、GenerarizedNewTypeDerivingを用いた方法の理解の手助け。
軽く検索した限り、本記事タイトルについての具体例が(わかりやすさ特化な例はありましたが、具体特化の例は)見つからなかったので、紹介してみます。

あと、Monadを作ることの楽しさの布教。

 GenerarizedNewTypeDeriving拡張を学ぶ筋道と、実例を示します!

そもそもGenerarizedNewTypeDerivingとは?

 Haskellのnewtypeは、元となる型と(圏論的な)同型を作るキーワードです。
(値構築子と、パターンマッチの解凍 もしくは レコードの関数、を同型射とした同型。間違ってたら教えてください ><)

つまりは「元となる型ができることはnewtypeで作る型もできる」ってことなのですが、newtypeがstrong typedefであるが故に、型クラスの情報をnewtypeは忘れ去ってしまいます。
その情報を思い出させるのがGenerarizedNewTypeDeriving拡張です!

 具体的には、以下を可能にします。

{-# 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モナドも使ってみたいんですが、まだ使ったことないんですよね〜。