Неоднозначное разрешение типов в классах типов по умолчанию - PullRequest
3 голосов
/ 29 января 2020

Почему именно следующий код не набирается?

{-# LANGUAGE AllowAmbiguousTypes, MultiParamTypeClasses #-}

module Main where

class Interface a b c where
  get :: a -> [b]
  change :: b -> c

  changeAll :: a -> [c]
  changeAll = map change . get

main = return ()

Если я закомментирую экземпляр по умолчанию для --changeAll = map change . get, все будет хорошо. Однако с созданием экземпляра я получаю эту ошибку:

GHCi, version 8.6.5: http://www.haskell.org/ghc/  :? for help
[1 of 1] Compiling Main             ( test.hs, interpreted )

test.hs:10:19: error:
    • Could not deduce (Interface a0 b0 c)
        arising from a use of ‘change’
      from the context: Interface a b c
        bound by the class declaration for ‘Interface’ at test.hs:5:7-15
      The type variables ‘a0’, ‘b0’ are ambiguous
      Relevant bindings include
        changeAll :: a -> [c] (bound at test.hs:10:3)
    • In the first argument of ‘map’, namely ‘change’
      In the first argument of ‘(.)’, namely ‘map change’
      In the expression: map change . get
   |
10 |   changeAll = map change . get
   |                   ^^^^^^

test.hs:10:28: error:
    • Could not deduce (Interface a b0 c0) arising from a use of ‘get’
      from the context: Interface a b c
        bound by the class declaration for ‘Interface’ at test.hs:5:7-15
      The type variables ‘b0’, ‘c0’ are ambiguous
      Relevant bindings include
        changeAll :: a -> [c] (bound at test.hs:10:3)
    • In the second argument of ‘(.)’, namely ‘get’
      In the expression: map change . get
      In an equation for ‘changeAll’: changeAll = map change . get
   |
10 |   changeAll = map change . get
   |                            ^^^

Я что-то упускаю здесь очевидное?

1 Ответ

9 голосов
/ 29 января 2020

Все ваши методы напечатаны неоднозначно.

Чтобы лучше проиллюстрировать проблему, давайте сведем пример к одному методу:

class C a b c where
    get :: a -> [b]

Теперь представьте, что у вас есть следующие экземпляры:

instance C Int String Bool where
    get x = [show x]

instance C Int String Char where
    get x = ["foo"]

А затем представьте, что вы пытаетесь вызвать метод:

s :: [String]
s = get (42 :: Int)

Из сигнатуры s компилятор знает, что b ~ String. Из параметра get компилятор знает, что a ~ Int. Но что такое c? Компилятор не знает. Нигде не найти этого.

Но подождите! Оба экземпляра C соответствуют a ~ Int и b ~ String, так что выбрать? Не ясно. Не хватает информации. Неоднозначно.

Это именно то, что происходит, когда вы пытаетесь вызвать get и change в map change . get: недостаточно информации о типе для компилятора, чтобы понять, что a, b и c предназначены для вызова get или change. Да, и имейте в виду: оба эти звонка могут исходить из разных случаев. Нельзя сказать, что они должны быть из того же самого экземпляра, что и changeAll.


Существует два возможных способа исправить это.

Во-первых, вы можете используйте функциональную зависимость , которая означает, что для определения c достаточно знать a и b:

class C a b c | a b -> c where ...

Если вы объявите таким образом, компилятор отклонит несколько экземпляров для одних и тех же a и b, но разных c, и, с другой стороны, он сможет выбрать экземпляр, просто зная a и b .

Конечно, вы можете иметь несколько функциональных зависимостей в одном классе. Например, вы можете объявить, что знания двух любых переменных достаточно для определения третьей:

class C a b c | a b -> c, a c -> b, b c -> a where ...

Имейте в виду, что для вашей функции changeAll даже этих трех функциональных зависимостей будет недостаточно, потому что реализация changeAll "глотает" b. То есть, когда он вызывает get, единственный известный тип - a. И точно так же, когда он вызывает change, единственный известный тип - c. Это означает, что для того, чтобы такое «проглатывание» b сработало, оно должно определяться только a, а также только c:

class Interface a b c | a -> b, c -> b where ...

Конечно, это будет возможно только в том случае, если logi c вашей программы действительно обладает таким свойством, что некоторые переменные определяются другими. Если вам действительно нужно, чтобы все переменные были независимыми, читайте дальше.


Во-вторых, вы можете явно указать компилятору, какие типы должны быть , используя TypeApplications:

 s :: String
 s = get @Int @String @Bool 42  -- works

Нет больше двусмысленности. Компилятор точно знает, какой экземпляр выбрать, потому что вы сказали это явно.

Применение этого к вашей реализации changeAll:

changeAll :: a -> [c]
changeAll = map (change @a @b @c) . get @a @b @c

(ПРИМЕЧАНИЕ: для того, чтобы иметь возможность ссылаться Переменные типа a, b и c в теле функции подобным образом, вам также необходимо включить ScopedTypeVariables)

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

foo = changeAll @Int @String @Bool 42
...