Как я могу отслеживать ошибки GHC "Не удалось совместить ожидаемый тип"?

Этот код Haskell содержит ошибку типа, тупую ошибку с моей стороны, которая будет очевидна после ее просмотра.

Я понял, но это было сложно. Мой вопрос: Как я должен был диагностировать это?

class Cell c where
  start :: c
  moves :: c -> [c]

score :: Cell c => (c -> Float) -> Int -> c -> Float
score estimate limit x =
  foldr (scoreRed (limit - 1)) (-1) (moves x)
  where
    scoreRed limit x best =
      max best $ foldr (scoreBlue limit best x) 1 (moves x)
    scoreBlue limit best x worst =
      if limit <= 0
      then estimate x
      else min worst $ foldr (scoreRed (limit - 1)) best (moves x)

main = return ()

Примечания:

  • Все, что называется limit, имеет тип Int.
  • Все имена с именем x имеют тип c, экземпляр Cell.
  • best и worst - Float.
  • score, estimate, scoreRed и scoreBlue все возвращаются Float.
  • Я удалил кучу кода, чтобы упростить его для этого вопроса. Сосредоточьтесь на типах, а не на рабочем времени.

Сообщение об ошибке из GHC 7.6.3:

[1 of 1] Compiling Main             ( Game.hs, Game.o )

Game.hs:13:12:
    Couldn't match expected type `c -> c' with actual type `Float'
    In the return type of a call of `estimate'
    Probable cause: `estimate' is applied to too many arguments
    In the expression: estimate x
    In the expression:
      if limit <= 0 then
          estimate x
      else
          min worst $ foldr (scoreRed (limit - 1)) best (moves x)

Почему я нашел это трудно:

  • Фактическая ошибка не указана в строке 13 и не имеет ничего общего с estimate.

  • Казалось, что ошибка может быть вызвана почти любым идентификатором в программе.

  • Иногда добавление явных объявлений типов ко всему помогает, но не здесь: я не знаю, как писать объявления типа для scoreRed и scoreBlue. Если я напишу

    scoreRed :: Int -> Float -> c -> Float
    

    тогда GHC думает, что я вводил новую переменную типа c, не ссылаясь на переменную типа c в score. Я получаю разные сообщения об ошибках, но не лучшие.

Похоже, что "пожалуйста, дай мне рыбу" версия этого вопроса была задана десятками раз. Как насчет того, что мы научим меня ловить рыбу. Я готов.

Ответ 1

Для чего это стоит, вот как я мысленно обрабатываю ошибку.

Я начинаю с c -> c vs Float и реализую там проблему с количеством аргументов где-то: применяется некоторая нефункция или функция пропускает слишком много аргументов (что то же самое, из-за каррирования).

Затем я рассмотрю, где ошибка указывает на: estimate x. Я проверяю тип для esitmate, чтобы обнаружить, что estimate принимает ровно один параметр. (Шаг броунов-подъема здесь.) Я делаю вывод, что этот код в порядке, но он используется в контексте, который передает слишком много аргументов, что-то вроде

(if ... then estimate x else ...) unexpectedArg

К счастью, estimate используется внутри определения функции

scoreBlue limit best x worst = ...

Здесь, где я добавляю сигнатуру типа к этому определению перед дальнейшим исследованием. Как вы заметили, выполнение этого в этом случае не является тривиальным, поскольку вы справляетесь с одним из простых недостатков Haskell: - К счастью, как отмечает @bheklilr в комментарии, вы можете написать подпись в любом случае, если вы включите ScopedTypeVariables расширение. (Лично я надеюсь, что следующий стандарт Haskell включает это (и несколько других очень распространенных расширений).)

В этом случае, поскольку у меня нет редактора, открытого с помощью кода, я проверяю, где используется scoreBlue, отмечая, что foldr выше слишком много передает один аргумент. (... но это не вопрос, о котором идет речь.)

Честно говоря, в моем собственном коде я часто добавляю аннотации типов в определениях let/where, возможно, слишком оборонительно. В то время как я иногда опускаю их, когда код прост, при написании функции с множеством аргументов, например scoreBlue, я бы, несомненно, написал тип, прежде чем начинать с фактического определения, поскольку я бы рассмотрел этот тип как фундаментальное руководство и документацию к фактический код.

Ответ 2

Для такой проблемы вы можете легко использовать расширение ScopedTypeVariables и изменить подпись типа score, начиная с forall c. Cell c => ..., но я предпочел бы извлечь эти функции на верхний уровень. Для этого вам нужно добавить estimate в качестве аргумента как для scoreRed, так и scoreBlue:

score :: Cell c => (c -> Float) -> Int -> c -> Float
score estimate limit x =
    foldr (scoreRed estimate (limit - 1)) (-1) (moves x)

scoreRed estimate limit x best =
    max best $ foldr (scoreBlue estimate limit best x) 1 (moves x)

scoreBlue estimate limit best x worst =
    if limit <= 0
        then estimate x
        else min worst $ foldr (scoreRed estimate (limit - 1)) best (moves x)

И теперь вы получите ошибки

jason_orendorff.hs:9:25:
    Couldn't match type ‘Float’ with ‘Float -> Float’
    Expected type: Float -> Float -> Float
      Actual type: c -> Float
    In the first argument of ‘scoreRed’, namely ‘estimate’
    In the first argument of ‘foldr’, namely
      ‘(scoreRed estimate (limit - 1))’

jason_orendorff.hs:17:18:
    Occurs check: cannot construct the infinite type: r ~ r -> r
    Relevant bindings include
      worst :: r (bound at jason_orendorff.hs:14:37)
      x :: r (bound at jason_orendorff.hs:14:35)
      best :: r (bound at jason_orendorff.hs:14:30)
      estimate :: r -> r -> r (bound at jason_orendorff.hs:14:15)
      scoreBlue :: (r -> r -> r) -> a -> r -> r -> r -> r -> r
        (bound at jason_orendorff.hs:14:5)
    In the expression:
      min worst $ foldr (scoreRed estimate (limit - 1)) best (moves x)
    In the expression:
      if limit <= 0 then
          estimate x
      else
          min worst $ foldr (scoreRed estimate (limit - 1)) best (moves x)
    In an equation for ‘scoreBlue’:
        scoreBlue estimate limit best x worst
          = if limit <= 0 then
                estimate x
            else
                min worst $ foldr (scoreRed estimate (limit - 1)) best (moves x)

Что еще говорит о проблемах с использованием estimate. На этом этапе я прокомментирую scoreRed и scoreBlue, затем поставлю знак подчеркивания перед вызовом scoreRed в score, сделав его именованным отверстием:

score :: Cell c => (c -> Float) -> Int -> c -> Float
score estimate limit x =
    foldr (_scoreRed estimate (limit - 1)) (-1) (moves x)

Что говорит нам, что _scoreRed должен иметь тип (c -> Float) -> Int -> c -> Float -> Float. Итак, теперь мы можем поместить это как подпись типа и объявление функции с отверстием для scoreBlue:

score :: Cell c => (c -> Float) -> Int -> c -> Float
score estimate limit x =
    foldr (scoreRed estimate (limit - 1)) (-1) (moves x)

scoreRed :: Cell c => (c -> Float) -> Int -> c -> Float -> Float
scoreRed estimate limit x best =
    max best $ foldr (_scoreBlue estimate limit best x) 1 (moves x)

Компиляция сообщает нам, что _scoreBlue :: (c -> Float) -> Int -> Float -> c -> c -> Float -> Float, и именно там я вижу проблему, scoreBlue ожидает два аргумента c, когда на самом деле я уверен, вы хотите, чтобы он принимал только один. Вы хотите fold через scoreBlue, когда в качестве аргументов нужны только x и worst, но вы уже предоставили его x. Если мы удалим это из fold и раскомментируем scoreBlue:

score :: Cell c => (c -> Float) -> Int -> c -> Float
score estimate limit x =
    foldr (scoreRed estimate (limit - 1)) (-1) (moves x)

scoreRed :: Cell c => (c -> Float) -> Int -> c -> Float -> Float
scoreRed estimate limit x best =
    max best $ foldr (scoreBlue estimate limit best) 1 (moves x)

scoreBlue :: Cell c => (c -> Float) -> Int -> Float -> c -> Float -> Float
scoreBlue estimate limit best x worst =
    if limit <= 0
        then estimate x
        else min worst $ foldr (scoreRed estimate (limit - 1)) best (moves x)

И теперь все проверки типов. Я не знаю, правильно ли это поведение, система типов может помочь только в определенной точке, но этот код будет работать. Затем вы можете реорганизовать это обратно для использования локальных функций вместо верхнего уровня:

score :: Cell c => (c -> Float) -> Int -> c -> Float
score estimate limit x =
    foldr (scoreRed (limit - 1)) (-1) (moves x)
    where
        scoreRed limit x best =
            max best $ foldr (scoreBlue limit best) 1 (moves x)
        scoreBlue limit best x worst =
            if limit <= 0
                then estimate x
                else min worst $ foldr (scoreRed (limit - 1)) best (moves x)

И все еще проверяет тип.