Как не применять ограничения экземпляра к ограниченной функции, которая использует более общую функцию? - PullRequest
10 голосов
/ 24 мая 2019

Допустим, у меня есть функция:

logResult :: (MonadIO m, ToJSON a) => Either MyError (Response a) -> m ()
logResult = ...

В этой функции, если я получу:

  1. Right (Response a) - я вызываю toJSON, чтобы записать результат.
  2. Left MyError - Я тоже это регистрирую.MyError уже определен экземпляр ToJSON.

Теперь я хочу написать вспомогательную функцию:

logError :: (MonadIO m) :: MyError -> m ()
logError err = logResult (Left err)

Но GHC жалуется на что-то вроде следующего:

    • Could not deduce (ToJSON a0) arising from a use of ‘logResult’                                                                    
      from the context: MonadIO m                                                                                                       
        bound by the type signature for:                                                                                                
                   logError :: forall (m :: * -> *).                                                                                    
                               MonadIO m =>                                                                                             
                               L.Logger                                                                                                 
                               -> Wai.Request
                               -> MyError
                               -> m ()

...
...
The type variable ‘a0’ is ambiguous 

Я понимаю, что ошибка в том, что logResult должен гарантировать, что для a в Response a должен быть определен экземпляр ToJSON.Но в logError я явно передаю Left MyError.Разве это не должно быть неоднозначным?

Можно ли как-нибудь написать вспомогательную функцию logError?

PS: Я упростил сигнатуры типов в примере.Сообщение об ошибке содержит подробную информацию.

1 Ответ

11 голосов
/ 24 мая 2019

Почему эта функция?Если поведение этой функции так четко разделяется на две части, тогда должно быть двумя функциями.То есть вы написали одну монолитную функцию и пытаетесь определить более простую функцию как утилиту, используя ее.Вместо этого напишите простую функцию и напишите монолитную функцию как ее композицию с другой.Тип в значительной степени просит его: Either a b -> c изоморфен (a -> c, b -> c).

-- you may need to factor out some common utility stuff, too
logError :: (MonadIO m) :: MyError -> m ()
logResponse :: (MonadIO m, ToJSON a) => Response a -> m ()

logResult :: (MonadIO m, ToJSON a) => Either MyError (Response a) -> m ()
logResult = either logError logResponse

logResult все еще имеет свое применение;если вы получите Either MyError (Response a) из какой-то библиотеки, то logResult сможет справиться с этим без особых хлопот.Но, в противном случае, вы не должны писать logResult (Left _) или logResult (Right _) очень часто;в сущности, logResult . Left и logResult . Right рассматриваются как их собственные функции, что возвращает вас к фактическому написанию их как отдельных функций.

Но в logError я явно передаю Left MyError.Разве это не должно быть неоднозначным?

Нет, не должно.Конец и начало проблемы в том, что logResult выглядит следующим образом:

logResult :: (MonadIO m, ToJSON a) => Either MyError (Response a) -> m ()

Когда вы вызываете его, реализация не имеет значения, один щелчок.Тип говорит, что вам нужно ToJSON a - вам нужно предоставить ToJSON a.Вот и все.Если вы знаете, что вам не нужны значения ToJSON a для значений Left, то вы обладаете полезной информацией, которая не отражена в типе.Вы должны добавить эту информацию к типу, что в данном случае означает разделение ее на две части.Было бы (IMO) на самом деле плохой языковой дизайн, чтобы позволить то, о чем вы думали, потому что проблема остановки должна сделать невозможным правильное выполнение.

...