Почему GHC не дает предупреждение о компиляции для исключения "Нет соответствия в выборе записи"?

Когда я запускаю этот баггированный код...

data Person = Adult { pName :: String}
            | Kid   { pName :: String
                    , pAge  :: Int
                    } deriving Show

getAge :: Person -> Int
getAge p = pAge p

getName :: Person -> String
getName p = pName p

main :: IO ()
main = do

  let p1 = Kid "fred" 5
      p2 = Adult "john"
      ps = [p1, p2]

  names = map getName ps
  ages = map getAge ps

  putStrLn $ "names: " ++ show names
  putStrLn $ "ages: " ++ show ages

... Я получаю это в ghci:

names: ["fred","john"]

ages: [5,* * * Exception: No match in record selector pAge

Я знаю, как избежать этой ошибки, но мне интересно, почему компиляция с помощью "ghc -Wall" не предупредила меня об этой проблеме. Есть ли другой инструмент, который может помочь мне предотвратить этот тип ошибок?

Ответ 1

Есть ли инструмент [a], который может помочь мне предотвратить этот тип ошибок?

Нет, но может быть.

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

data Person = Adult { pName :: String}
            | Kid   { pName :: String
                    , pAge  :: Int
                    } deriving Show

создает функции pName :: Person -> String и pAge :: Person -> Int. Теперь предположим, что у Хаскелла есть подтипирование. Если это так, то Kid может быть подтипом Person, а pAge может иметь более подходящий тип Kid -> String. Однако у Haskell нет подтипирования, и поэтому нет типа Kid.

Теперь, учитывая, что Person -> String является наиболее конкретным типом, который мы можем дать pAge, почему бы не предупредить, что pAge является частичной функцией во время компиляции? Позвольте мне отвлечь вопрос, обратившись к примеру List

data List a = Cons { head :: a, tail :: List a } | Empty

В этом примере head и tail являются частичными функциями: двумя компонентами непустого списка, но (из-за отсутствия подтипов Haskell) бессмысленных аксессуаров в пустом списке. Итак, почему нет предупреждения по умолчанию? Ну, по умолчанию вы знаете код, который вы написали. Компилятор не предоставляет предупреждения, если вы используете unsafePerformIO, потому что вы программист здесь, вы должны использовать такие вещи ответственно.

So tl; dr: если вы хотите получить предупреждение здесь:

getAge :: Person -> Int
getAge p = pAge p

тогда вам не повезло, потому что система типов не имеет достаточной информации, чтобы вывести, что это проблема.

Если вы хотите получить предупреждение здесь:

data Person = Adult | Kid { pAge :: Int }

то я уверен, что было бы тривиально реализовать: просто убедитесь, что данное поле существует в некоторых конструкторах, но не в других. Но я не предвижу, что это предупреждение широко полезно для всех; некоторые могут жаловаться, что это будет просто шум.