Как я могу отслеживать ошибки 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)
И все еще проверяет тип.