Необязательные параметры вместе с оператором case

Я узнал о hslogger здесь.

initLogging :: Priority -> IO ()
initLogging level = do
    stdOutHandler <- streamHandler stdout level >>= \lh -> return $
            setFormatter lh (simpleLogFormatter "[$prio:$loggername:$time] $msg")
    updateGlobalLogger rootLoggerName (setLevel level . setHandlers [stdOutHandler])

Я хотел бы создать функцию «quicklog», которая просто требует меньшего набора текста для отладки: если logtype указан как «I», будет использоваться infoM, а если указано как «D», будет использоваться debugM и т. д., и по умолчанию INFO, если уровень ведения журнала не указан.

Я могу сделать версию, которая делает то, что мне нужно:

qLog :: String -> String -> IO ()
qLog l msg = log' "" msg where
  log' "I" msg = infoM "" msg
  log' "D" msg = debugM "" msg
  log' "E" msg = errorM "" msg
  log' "W" msg = warningM "" msg
  log' _ msg = infoM "" msg

И это работает:

λ> qlog "D" "Thing!"
[DEBUG::2017-01-18 20:38:19 EST] Thing!

λ> qlog "I" "Thing!"
[INFO::2017-01-18 20:38:19 EST] Thing!

Но не могу заставить работать выбор инфоМ, если не указан уровень, а-ля:

λ> qlog "Thing!"
[INFO::2017-01-18 20:38:19 EST] Thing!

Это просто терпит неудачу, ожидая «Вещь!» является необязательным уровнем ведения журнала, а не аргументом msg. Я считаю, что для этого требуется «Может быть», чтобы сообщить, что первый аргумент может вообще не существовать, но, похоже, не может понять его правильно. Вот мои попытки:

qLog2 :: Maybe String -> String -> IO ()
qLog2 l msg = case l msg of
     (Just "D") msg -> debugM "" msg
     (Just "I") msg -> infoM "" msg
     Nothing  msg -> infoM "" msg

Or

qLog3 :: Maybe String -> String -> IO ()
qLog3 l msg 
  | Just "D" msg = debugM "" msg
  | Just "I" msg = infoM "" msg
  | Nothing  msg = infoM "" msg

Кажется, я не могу достаточно сформулировать комбинацию «как может быть» в объявлении типа + либо case, либо guard.

ОБНОВЛЕНИЕ 1

qLog4 "D" msg = debugM "" msg
qLog4 "I" msg = infoM "" msg
qLog4 msg  = infoM "" msg

Дает мне:

Equations for ‘qLog4’ have different numbers of arguments

ОБНОВЛЕНИЕ 2:

Отказ от дел, пока я выясняю необязательную часть аргумента через здесь

import Data.Optional

newtype LogLevel = LogLevel { getLogLevel :: Char } 

qLog' :: Char -> String -> IO ()
qLog' "I" msg = infoM "" msg
qLog' "D" msg = debugM "" msg
qLog' "W" msg = warningM "" msg
qLog' "E" msg = errorM "" msg
qLog' _ msg = infoM "" msg

qLog5 :: (Optional LogLevel) -> String -> IO ()
qLog5 (Specific l) msg = qLog' (getLogLevel l) msg 
qLog5 (Default) msg = qLog' "I" msg


<interactive>:251:7-9: error:
    • Couldn't match expected type ‘Char’ with actual type ‘[Char]’
    • In the pattern: "

person Mittenchops    schedule 19.01.2017    source источник
comment
Ваш второй последний вариант ближе всего. Вам просто нужно, чтобы l и msg были в кортеже, и шаблон соответствовал этому (только case ветви на основе one выражения). Тем не менее, почему бы вообще не избавиться от case и просто иметь несколько вариантов?   -  person Alec    schedule 19.01.2017
comment
@Alec, я пробовал это в обновлении, но компилятор жаловался на несколько аргументов, что мне и нужно.   -  person Mittenchops    schedule 19.01.2017


Ответы (3)


Если я правильно понимаю вашу проблему, то вы можете сделать так:

{-# LANGUAGE GADTs #-}

qlog :: LogResult a => a
qlog = log' []

class LogResult a where
    log' :: [LogArg] -> a

class IsLogArgument a where
    toLogArg :: a -> LogArg

data LogArg = LogLevel LogLevel
            | LogMessage String

data LogLevel = D | I | W | E

instance IsLogArgument LogLevel where
    toLogArg = LogLevel

instance (a ~ Char) => IsLogArgument [a] where
    toLogArg = LogMessage

instance (IsLogArgument arg, LogResult result) => LogResult (arg -> result) where
    log' args arg = log' ((toLogArg arg) : args)

instance (a ~ ()) => LogResult (IO a) where
    log' args = tell "" msg
      where
        tell = case level of
                D -> debugM
                I -> infoM
                W -> warningM
                E -> errorM
        level = fromMaybe I $ look logLevel
        msg   = fromMaybe "No message" $ look logMessage

        logLevel (LogLevel x) = Just x
        logLevel _            = Nothing

        logMessage (LogMessage x) = Just x
        logMessage _              = Nothing

        look = listToMaybe . catMaybes . flip map args

А также:

λ> qlog D "Thing!"
[DEBUG::2017-01-18 20:38:19 EST] Thing!

λ> qlog "Thing!"
[INFO::2017-01-18 20:38:19 EST] Thing!
person freestyle    schedule 19.01.2017
comment
Ух ты! Это ужасно много кода для необязательного аргумента. - person Mittenchops; 20.01.2017
comment
Да, но вы можете сделать больше. Например, вы можете просто добавить необязательный LogName и освободиться от порядка аргументов. - person freestyle; 20.01.2017

Фиксированная версия qLog2 будет

qLog2 :: Maybe String -> String -> IO ()
qLog2 l msg =
  case l of
    Just "D" -> debugM "" msg
    Just "I" -> infoM "" msg
    Nothing  -> infoM "" msg

Если бы вы собирались перегрузить qlog, чтобы принять необязательный параметр для уровня, вам нужно было бы использовать класс типов. Эта функция будет раздражать в использовании. Было бы двусмысленно, когда вы пишете qlog "I", если вы ожидаете, что это будет иметь тип String -> IO () или IO (). Может быть, вы создали функцию журнала для информационного уровня, или, может быть, вы записывали сообщение «Я».

person glguy    schedule 19.01.2017
comment
Теоретически я понимаю проблему, но я использую это для отладки сообщений, поэтому будет легко избежать включения qlog "I" в мои отладки. - person Mittenchops; 19.01.2017
comment
Это не то, что вы можете игнорировать. Та же проблема, если у вас был qlog "A Log Message". Это неоднозначно (без дополнительного контекста), если это сообщение журнала или уровень отладки. Вы можете учитывать типы только при проведении этого различия. Значения не имеют значения. - person glguy; 19.01.2017
comment
Наверное. Но я могу решить, что с Char это другой тип, чем [Char], верно? Если я не буду создавать односимвольные отладочные сообщения, потому что это бесполезно для отладки, на самом деле не будет никакой двусмысленности, учитывая предполагаемое приложение. - person Mittenchops; 19.01.2017

Вам нужно только провести анализ случая для аргумента l:

qLog :: Maybe String -> String -> IO ()
qLog l msg = case l of
    Just "D" -> debugM "" msg
    Just "I" -> infoM "" msg
    -- etc.
    Nothing  -> infoM "" msg

(Если вам на самом деле нужно было выполнить анализ случая и для msg, вы можете сделать это с помощью кортежа — case (l, msg) of — как предлагает Алек, или просто заменить оператор case несколькими уравнениями.)


Несвязанные предложения: вместо строк вы можете использовать настраиваемый тип для представления ваших уровней журнала. Это проясняет ситуацию и устраняет риск передачи "G" или "foobar" в качестве уровня журнала.

-- Feel free to use less terse names.
data LogLevel = D | I | W | E
    deriving (Eq, Show)

qLog :: Maybe LogLevel -> String -> IO ()
qLog l msg = case l of
    Just D -> debugM "" msg
    Just I -> infoM "" msg
    Just W -> warningM "" msg
    Just E -> errorM "" msg
    Nothing -> infoM "" msg

Вы также можете исключить большую часть повторяемости:

-- I added a few superflous parentheses to emphasise what is going on.
qLog :: Maybe LogLevel -> String -> IO ()
qLog ml msg = case ml of
     Just l -> (fLog l) "" msg
     Nothing -> infoM "" msg
     where
     fLog :: LogLevel -> (String -> String -> IO ())
     fLog l = case l of
         D -> debugM
         I -> infoM
         W -> warningM
         E -> errorM

Или, используя maybe, чтобы аккуратно исключить возможность:

GHCi> :t maybe
maybe :: b -> (a -> b) -> Maybe a -> b
-- There are also some superflous parentheses here.
qLog :: Maybe LogLevel -> String -> IO ()
qLog ml msg = (maybe infoM fLog ml) "" msg
     where
     fLog :: LogLevel -> (String -> String -> IO ())
     fLog l = case l of
         D -> debugM
         I -> infoM
         W -> warningM
         E -> errorM
person duplode    schedule 19.01.2017
comment
Кажется, это не работает для меня, когда я пытаюсь опустить необязательный аргумент, я получаю: λ> qLog "test" • Couldn't match expected type ‘Maybe String’ with actual type ‘[Char]’ - person Mittenchops; 19.01.2017
comment
@Mittenchops Это потому, что вам все равно нужно указать аргумент Maybe String, даже если это Nothing. Если вы хотите полностью опустить его, либо пропустите Maybe и определите отдельную функцию, которая вообще не использует уровень журнала (например, qLogDef :: LogLevel -> String -> IO (); qLogDef = qLog I), либо используйте трюк varargs в ответе фристайла. - person duplode; 19.01.2017
comment
(Обратите внимание, что я опечатался в подписи qLogDef в комментарии выше — должно быть qLogDef :: String -> IO ().) - person duplode; 20.01.2017