はじめに

Data.MonoidLast 型が定義されています。

Last モノイドは First モノイドとほとんど同じですが、<> で結合した時に最後の値を返すという部分が異なります。

使い方は簡単。

Semigroup law の確認

Case (1) (b == Nothing, c == Nothing)

Case (2) (b == Just b’, c == Nothing)

Case (3) (c == Just c’)

Monoid Law

Case (1) (a = Nothing)

Case (2) (a = Just a’)

具体例: Partial Options Monoid

コマンドライン引数によりオプションを受け取り、指定されなかったオプションの値に対してはデフォルト値を利用するという場面で Last モノイドが活用できそうです。

コード

optparse-applicative を使った具体的なサンプルはこんな感じです。

{-# LANGUAGE RecordWildCards #-}
module Main (main) where

import Data.Monoid
import Options.Applicative

data Options = Options
  { oInputPath  :: FilePath
  , oOutputPath :: FilePath
  , oLogLevel   :: Maybe Int
  } deriving (Show, Eq)

data PartialOptions = PartialOptions
  { poInputPath  :: Last FilePath
  , poOutputPath :: Last FilePath
  , poLogLevel   :: Last (Maybe Int)
  } deriving (Show, Eq)

instance Semigroup PartialOptions where
  x <> y =
    PartialOptions
      { poInputPath  = poInputPath  x <> poInputPath  y
      , poOutputPath = poOutputPath x <> poOutputPath y
      , poLogLevel   = poLogLevel   x <> poLogLevel   y
      }

instance Monoid PartialOptions where
  mempty = PartialOptions mempty mempty mempty

defaultPartialOptions :: PartialOptions
defaultPartialOptions = mempty
  { poInputPath  = pure "input"
  , poLogLevel   = pure Nothing
  }

lastOption :: Parser a -> Parser (Last a)
lastOption = fmap Last . optional

partialOptionsParser :: Parser PartialOptions
partialOptionsParser = PartialOptions
  <$> lastOption (strOption (short 'i'))
  <*> lastOption (strOption (short 'o'))
  <*> lastOption (Just <$> option auto (short 'l'))

lastToEither :: String -> Last a -> Either String a
lastToEither errMsg = maybe (Left errMsg) Right . getLast

mkOptions :: PartialOptions -> Either String Options
mkOptions PartialOptions {..} = do
  oInputPath  <- lastToEither "Missing input path"  poInputPath
  oOutputPath <- lastToEither "Missing output path" poOutputPath
  oLogLevel   <- lastToEither "Missing loglevel"    poLogLevel
  return Options {..}

main :: IO ()
main = do
  options <- execParser $ info partialOptionsParser mempty
  case mkOptions (defaultPartialOptions <> options) of
    Left  msg -> putStrLn msg
    Right opt -> print opt

defaultPartialOptions でオプションの初期値を用意しておきます。ここで指定されなかったフィールドの値はオプションで必ず指定しなければなりません。今回の例では poOutputPath が必須オプションになっています。

また Last モノイドが効いている部分は defaultPartialOptions <> options です。mempty = Last Nothing となるため、期待通りの動作が得られます。

デフォルト値の無いオプションが省略された場合にエラーメッセージが表示される理由としては lastToEithergetLast した際に Nothing となるためです。

実行結果

実行結果は見やすく整形しています。

# オプション無しで実行
λ> stack run ex3
Missing output path

# 必須オプションの -o のみ指定 (他はデフォルト値)
$ stack run ex3 -- -o "oDir"
Options
  { oInputPath  = "input"
  , oOutputPath = "oDir"
  , oLogLevel   = Nothing
  }

# 必須オプションの -o と -i を指定
$ stack run ex3 -- -o "oDir" -i "myDir"
Options
  { oInputPath  = "myDir"
  , oOutputPath = "oDir"
  , oLogLevel   = Nothing
  }

# オプションを全部指定
$ stack run ex3 -- -o "oDir" -i "myDir" -l 10
Options
  { oInputPath  = "myDir"
  , oOutputPath = "oDir"
  , oLogLevel   = Just 10
  }

参考