Отключить приведение типа Haskell

У меня есть пример, основанный на hyperloglog. Я пытаюсь параметризовать свой Container размером и использовать отражение, чтобы использовать этот параметр в функциях контейнеров.

import           Data.Proxy
import           Data.Reflection

newtype Container p = Container { runContainer :: [Int] }
    deriving (Eq, Show)

instance Reifies p Integer => Monoid (Container p) where
  mempty = Container $ replicate (fromIntegral (reflect (Proxy :: Proxy p))) 0
  mappend (Container l) (Container r) = undefined

Мой хромой экземпляр Monoid определяет mempty на основе овеществленного параметра и делает некоторые "типобезопасные" mappend. Он отлично работает, когда я пытаюсь суммировать контейнеры разного размера, терпя ошибку типа.

Однако его все еще можно обмануть с помощью coerce, и я ищу способ заблокировать его во время компиляции:

ghci> :set -XDataKinds
ghci> :m +Data.Coerce
ghci> let c3 = mempty :: Container 3
ghci> c3
ghci> Container {runContaner: [0,0,0]}
ghci> let c4 = coerce c3 :: Container 4
ghci> :t c4
ghci> c4 :: Container 4
ghci> c4
ghci> Container {runContainer: [0,0,0]} 

Добавление роли типа не помогает

type role Container nominal

person Eugene Zhulenev    schedule 18.05.2016    source источник
comment
GHC 7.10 выдает много ошибок из-за отсутствия аннотаций LANGUAGE в исходном коде. Не могли бы вы добавить их?   -  person Zeta    schedule 18.05.2016
comment
Вы проверили, что произойдет, если вы сделаете это в файле .hs? GHCi может быть странным в отношении границ модулей. Я совершенно уверен, что hyperloglog имеет собственное объявление роли.   -  person dfeuer    schedule 18.05.2016
comment
Странно... аннотация роли действительно должна предотвращать эти принуждения.   -  person chi    schedule 18.05.2016
comment
Здесь это не имеет значения, и вы, вероятно, уже знаете, но я хотел бы отметить, что это недопустимый экземпляр моноида, потому что (mempty<>mempty) :: Container 1 дает Container [0,0] ≠ mempty. Вы можете сделать это действительным, например. mappend ∝ zipWith (+).   -  person leftaroundabout    schedule 18.05.2016
comment
@Zeta gist.github.com/ezhulenev/f7913740e8971f9a60ce4c5677a0f3f3 - там много всего, не все они имеют отношение к этому конкретному вопросу, хотя   -  person Eugene Zhulenev    schedule 18.05.2016
comment
@leftaroundabout о да, мне лучше положить туда undefined   -  person Eugene Zhulenev    schedule 18.05.2016
comment
@dfeuer да, я могу проделать тот же трюк в файле .hs и скомпилировать его   -  person Eugene Zhulenev    schedule 18.05.2016
comment
О, я упустил тот факт, что ваш пример основан на hyperloglog, а не на его использовании. Ха. Примечание к ответу Антала: вы можете условно предоставить экземпляр Coercible, упакованный как Coercion, из модуля, определяющего абстрактный тип. hyperloglog, например, предлагает функцию, которая проверяет, отражают ли два типа распределения одну и ту же конфигурацию, и, если это так, дает вам Coercion между ними.   -  person dfeuer    schedule 18.05.2016


Ответы (1)


Проблема в том, что newtype можно принудить к своему представлению, пока конструктор находится в области видимости — действительно, это большая часть мотивации для Coercible. А ограничения Coercible подобны любым другим ограничениям класса типов, они автоматически ищутся и объединяются для вас, только в еще большей степени. Таким образом, coerce c3 обнаруживает, что у вас есть

instance Coercible (Container p) [Int]
instance Coercible [Int] (Container p')

для всех p и p', и с радостью создадим для вас составное принуждение с помощью

instance (Coercible a b, Coercible b c) => Coercible a c

Если вы не экспортируете конструктор Container — как вы, вероятно, все равно захотите сделать! — тогда уже не известно, что newtype равно его представлению (вы теряете первые два экземпляра выше), и вы получаете желаемую ошибку в других модулях:

ContainerClient.hs:13:6:
    Couldn't match type ‘4’ with ‘3’
    arising from trying to show that the representations of
      ‘Container 3’ and
      ‘Container 4’ are the same
    Relevant role signatures: type role Container nominal nominal
    In the expression: coerce c3
    In an equation for ‘c4’: c4 = coerce c3

Однако вы всегда сможете сломать свои инварианты в модуле, где вы определяете newtype (через coerce или иным образом).


В качестве примечания: вы, вероятно, не хотите использовать здесь метод доступа в стиле записи и экспортировать его; который позволяет пользователям использовать синтаксис обновления записи для изменения кода из-под вас, поэтому

c3 :: Container 3
c3 = mempty

c3' :: Container 3
c3' = c3{runContainer = []}

становится действительным. Вместо этого сделайте runContainer отдельной функцией.


Мы можем убедиться, что получаем композицию двух ограничений представления newtype, взглянув на ядро ​​(через -ddump-simpl): в модуле, который определяет Container (который я также назвал Container), вывод (если мы удалим список экспорта)

c4 :: Container 4
[GblId, Str=DmdType]
c4 =
  c3
  `cast` (Container.NTCo:Container[0] <GHC.TypeLits.Nat>_N <3>_N
          ; Sym (Container.NTCo:Container[0] <GHC.TypeLits.Nat>_N <4>_N)
          :: Container 3 ~R# Container 4)

Это немного трудно читать, но важно видеть Container.NTCo:Container[0]: NTCo является newtype приведением между newtype Container p и его типом представления. Sym переворачивает это, а ; составляет два ограничения.

Назовите окончательное ограничение γₓ; тогда весь вывод типизации

Sym :: (a ~ b) -> (b ~ a)
-- Sym is built-in

(;) :: (a ~ b) -> (b ~ c) -> (a ~ c)
-- (;) is built-in

γₙ :: k -> (p :: k) -> Container p ~ [Int]
γₙ k p = Container.NTCo:Container[0] <k>_N <p>_N

γ₃ :: Container 3 ~ [Int]
γ₃ = γₙ GHC.TypeLits.Nat 3

γ₄ :: Container 4 ~ [Int]
γ₄ = γₙ GHC.TypeLits.Nat 4

γ₄′ :: [Int] ~ Container 4
γ₄′ = Sym γ₄

γₓ :: Container 3 ~ Container 4
γₓ = γ₃ ; γ₄′

Вот исходные файлы, которые я использовал:

Контейнер.hs:

{-# LANGUAGE FlexibleContexts, UndecidableInstances, ScopedTypeVariables,
             RoleAnnotations, PolyKinds, DataKinds #-}

module Container (Container(), runContainer) where

import Data.Proxy
import Data.Reflection
import Data.Coerce

newtype Container p = Container { runContainer :: [Int] }
    deriving (Eq, Show)
type role Container nominal

instance Reifies p Integer => Monoid (Container p) where
  mempty = Container $ replicate (fromIntegral (reflect (Proxy :: Proxy p))) 0
  mappend (Container l) (Container r) = Container $ l ++ r

c3 :: Container 3
c3 = mempty

-- Works
c4 :: Container 4
c4 = coerce c3

ContainerClient.hs:

{-# LANGUAGE DataKinds #-}

module ContainerClient where

import Container
import Data.Coerce

c3 :: Container 3
c3 = mempty

-- Doesn't work :-)
c4 :: Container 4
c4 = coerce c3
person Antal Spector-Zabusky    schedule 18.05.2016