Классы закрытого типа

Можно ли создать класс типа, который больше не может допускать новых членов (возможно, используя границы модулей)? Я могу отказаться от экспорта функции, необходимой для полного определения экземпляра, но это приводит только к ошибке выполнения, если кто-то создает недопустимый экземпляр. Могу ли я сделать ошибку времени компиляции?

Ответы

Ответ 1

Я считаю, что ответ является квалифицированным да, в зависимости от того, чего вы пытаетесь достичь.

Вы можете воздержаться от экспорта имени класса класса из своего интерфейса интерфейса 1 при этом все еще экспортируйте имена функций класса типа. Тогда никто не может создать экземпляр класса, потому что никто не может его назвать!

Пример:

module Foo (
    foo,
    bar
) where

class SecretClass a where
    foo :: a
    bar :: a -> a -> a

instance SecretClass Int where
    foo = 3
    bar = (+)

Недостатком является то, что никто не может написать тип с вашим классом как ограничение. Это не полностью мешает людям писать функции, которые будут иметь такой тип, потому что компилятор все равно сможет вывести тип. Но это было бы очень неприятно.

Вы можете смягчить недостатки, предоставив еще один пустой класс типа, с вашим "закрытым" классом в качестве суперкласса. Вы делаете каждый экземпляр вашего исходного класса также экземпляром подкласса, и вы экспортируете подкласс (вместе со всеми функциями класса типа), но не суперкласс. (Для ясности вы, вероятно, должны использовать "публичный" класс, а не "секретный" во всех типах, которые вы публикуете, но я считаю, что он работает в любом случае).

Пример:

{-# LANGUAGE FlexibleInstances, UndecidableInstances #-}

module Foo ( 
    PublicClass,
    foo,  
    bar   
) where 

class SecretClass a where 
    foo :: a
    bar :: a -> a -> a

class SecretClass a => PublicClass a

instance SecretClass Int where 
    foo = 3
    bar = (+) 

instance SecretClass a => PublicClass a

Вы можете обойтись без расширений, если вы хотите вручную объявить экземпляр PublicClass для каждого экземпляра SecretClass.

Теперь клиентский код может использовать PublicClass для записи ограничений класса типа, но для каждого экземпляра PublicClass требуется экземпляр SecretClass для одного и того же типа и без возможности объявить новый экземпляр SecretClass no можно сделать любые экземпляры типов PublicClass 2.

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


1 Чистое мнение: обычно неплохо иметь отдельный внутренний модуль со страшным именем, которое экспортирует все, чтобы вы могли получить его для тестирования/отладки, с интерфейсным модулем, который импортирует внутренний модуль и экспортирует только то, что вы хотите экспортировать.

2 Я предполагаю, что с расширениями кто-то может объявить новый дублирующий экземпляр. Например. если вы предоставили экземпляр для [a], кто-то может объявить новый экземпляр PublicClass для [Int], который будет копировать в экземпляр SecretClass для [a]. Но, учитывая, что PublicClass не имеет функций, и они не могут записать экземпляр SecretClass, я не вижу, что с этим можно многое сделать.

Ответ 2

Так как GHC 7.8.1, закрытые типы семейств могут быть объявлены, и я думаю, с их помощью, и ConstraintKinds, вы может сделать это:

type family SecretClass (a :: *) :: Constraint where
  SecretClass Int = ()

SecretClass a формирует ограничение, эквивалентное классу типа, и поскольку это семейство не может быть продлен кем-либо, никакие другие экземпляры "класса" не могут быть определены.

(На самом деле это просто спекуляция, так как я не могу ее протестировать, но код эта интересная ссылка заставляет работа.)

Ответ 3

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

Обратите внимание, что этот подход не зависит от модульной системы. Вместо того, чтобы полагаться на границы модулей, мы предоставляем явный список экземпляров, которые являются законными. Это означает, с одной стороны, что юридические экземпляры могут распространяться на несколько модулей или даже пакетов, а с другой стороны, что мы не можем предоставлять незаконные экземпляры даже в одном модуле.

Для этого ответа я предполагаю, что мы хотим закрыть следующий класс, чтобы он мог быть создан только для типов Int и Integer, но не для других типов:

 -- not yet closed
class Example a where
  method :: a -> a

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

{-# LANGUAGE TypeFamilies, EmptyDataDecls #-}

class Closed c where
  type Instance c a

Параметр c обозначает имя семейства типов, а параметр a - это индекс семейства типов. Семейный экземпляр c для a кодируется как Instance c a. Поскольку c также является параметром класса, все экземпляры семейства c должны быть указаны вместе в объявлении экземпляра одного класса.

Теперь мы используем эту структуру для определения семейства закрытых типов MemberOfExample для кодирования, что Int и Integer являются Ok, а все остальные типы не являются.

data MemberOfExample
data Ok

instance Closed MemberOfExample where
  type Instance MemberOfExample Int = Ok
  type Instance MemberOfExample Integer = Ok

Наконец, мы используем это замкнутое семейство типов в суперклассическом противопоставлении нашего Example.

class Instance MemberOfExample a ~ Ok => Example a where
  method :: a -> a

Мы можем определить допустимые экземпляры для Int и Integer, как обычно.

instance Example Int where
  method x = x + 1

instance Example Integer where
  method x = x + 1

Но мы не можем определить недопустимые экземпляры для других типов, кроме Int и Integer.

-- GHC error: Couldn't match type `Instance MemberOfExample Float' with `Ok'
instance Example Float where
  method x = x + 1

И мы также не можем расширять набор допустимых типов.

-- GHC error: Duplicate instance declarations
instance Closed MemberOfExample where
  type Instance MemberOfExample Float = Ok

-- GHC error: Associated type `Instance' must be inside a class instance
type instance Instance MemberOfExample Float = Ok

К сожалению, мы можем написать следующий фиктивный экземпляр:

-- Unfortunately accepted
instance Instance MemberOfExample Float ~ Ok => Example Float where
  method x = x + 1

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

-- Couldn't match type `Instance MemberOfExample Float' with `Ok'
test = method (pi :: Float)

Ответ 4

Вы можете реорганизовать typeclass в объявление данных (использовать синтаксис записи), который содержит все функции вашего класса. В любом случае фиксированный конечный список экземпляров звучит так, как будто вам не нужен класс.

Это, конечно, в основном то, что компилятор делает behibd сценами с вашим классом в любом случае.

Это позволит вам экспортировать список экземпляров как функции в ваш тип данных, и вы можете экспортировать их, но не конструкторы для типа данных. Аналогичным образом вы можете ограничить экспорт функций доступа и просто экспортировать необходимый интерфейс.

Это отлично работает, потому что типы данных не подпадают под допущение открытого мира, которое пересекает границу, по классам.

Иногда добавление системной сложности просто усложняет ситуацию.

Ответ 5

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

class (Elem t '[Int, Integer, Bool] ~ True) => Closed t where

type family Elem (t :: k) (ts :: [k]) :: Bool where
  Elem a '[] = False
  Elem a (a ': as) = True
  Elem a (b ': bs) = Elem a bs

instance Closed Int
instance Closed Integer
-- instance Closed Float -- ERROR