Вот в чем сводится моя проблема: у меня есть класс, и я хочу, чтобы каждый Floating и каждый Integral создавали его экземпляры, но по-разному. В минимальном примере кода:
class MyClass a where
func :: a -> Bool
instance (Floating a) => MyClass a where
func _ = True
instance (Integral b) => MyClass b where
func _ = False
Это само по себе вызывает ошибку: «Дубликаты объявлений экземпляров». Насколько мне известно, основная причина этого заключается в том, что тип, реализующий как плавающий, так и интегральный, приведет к неопределенному поведению, поэтому Haskell не принимает ограничения во внимание.
Начнем с того, что такие прагмы, как {-# INCOHERENT #-}, не помогают.
Есть несколько трюков, которые можно проделать с семействами типов. Я видел этот подход в других местах:
{-# LANGUAGE DataKinds #-}
{-# LANGUAGE TypeFamilies #-}
{-# LANGUAGE UndecidableInstances #-}
import Data.Data (Proxy(..))
-- My class, for which I want two classes of instances
class MyClass a where
func :: a -> Bool
-- A type family providing a value depending on the type variable
data FromFloatingOrIntegralResult = FromFloating | FromIntegral
type family FromFloatingOrIntegral a :: FromFloatingOrIntegralResult where
FromFloatingOrIntegral (Floating a) = 'FromFloating
FromFloatingOrIntegral (Integral a) = 'FromIntegral
-- A class implementation with a proxy to differentiate between the two classes
class MyClassImpl (r :: FromFloatingOrIntegralResult) a where
funcImpl :: Proxy r -> a -> Bool
-- Implementing the previous instances
instance (Floating a) => MyClassImpl FromFloating a where
funcImpl _ floating = True
instance (Integral b) => MyClassImpl FromIntegral b where
funcImpl _ integral = False
Теперь мне нужно сделать MyClassImpl экземпляром MyClass.
instance (MyClassImpl FromFloating a) => MyClass a where
func = funcImpl (Proxy :: Proxy FromFloating)
instance (MyClassImpl FromIntegral a) => MyClass a where
func = funcImpl (Proxy :: Proxy FromIntegral)
Это приводит к той же проблеме, с которой я начал. Каким-то образом мне нужно одновременно создать экземпляры FromFloating и FromIntegral.
instance (MyClassImpl (FromFloatingOrIntegral a) a) => MyClass a where
func = funcImpl (Proxy :: Proxy (FromFloatingOrIntegral a))
Здесь «a» имеет своего рода «ограничение» вместо типа.
instance (MyClassImpl (FromFloatingOrIntegral b) a) => MyClass a where
func = funcImpl (Proxy :: Proxy (FromFloatingOrIntegral b))
someRandomFunction :: (MyClass a) => a -> Bool
someRandomFunction a = func a
Второй аргумент можно оставить неоднозначным с помощью расширения AllowAmbigousTypes, но тогда компилятор будет жаловаться на неоднозначную переменную.
class MyClassImpl (r :: FromFloatingOrIntegralResult) a | a -> r where
funcImpl :: Proxy r -> a -> Bool
Добавление функциональных зависимостей лишь приводит к появлению начальной проблемы при создании двух экземпляров.
Проблема, по-видимому, связана с тем, что в
type family FromFloatingOrIntegral a :: FromFloatingOrIntegralResult where
FromFloatingOrIntegral (Floating a) = 'FromFloating
FromFloatingOrIntegral (Integral a) = 'FromIntegral
FromFloatingOrIntegral ожидает получить значение типа Constraint. Но, похоже, не существует способа регулярно применять ограничения к семейству типов.
Это невыполнимая задача, с которой я столкнулся, или есть какое-то языковое расширение или метод, который мог бы мне помочь?
«У меня есть класс, и я хочу, чтобы каждый Floating и каждый Integral создавал его экземпляр» — мой совет — отказаться от этой цели и вместо этого хотеть, чтобы каждый экземпляр, который вы используете, создавал его экземпляр. Эту цель гораздо легче достичь; просто напишите примеры.





Есть некоторые вещи, которые вы можете сделать в этом отношении до такой степени, что они могут быть полезными, а могут и не быть полезными, но на самом деле это не совсем предполагаемое использование системы классов типов для написания экземпляров типа instance SomeConstraint t => SomeClass t. Вы собираетесь писать экземпляры для типов, а не для всех других классов.
Вопрос не только в том, что делать в зоне пересечения двух ограничений. Он создан для того, чтобы попытаться сохранить свойство: всякий раз, когда компилятор пытается решить ограничение для типа в любом месте программы (даже в независимых пакетах библиотек, которые не импортируют друг друга напрямую, а используются вместе в одной программе), он всегда будет выберите тот же самый экземпляр, даже если существуют другие экземпляры, которые не видны во время компиляции этого модуля. На это свойство полагаются основные библиотеки, такие как Data.Map; если бы когда-либо было возможно разрешить два разных экземпляра Ord для данного типа в разных частях одной и той же программы, то API Map пришлось бы проектировать совершенно по-другому.
Таким образом, компилятор никогда не сможет, например, зафиксировать экземпляр instance (Integral b) => MyClass b для некоторого типа T просто потому, что он не видит ни одного instance Floating T прямо сейчас; такой экземпляр может существовать где-то еще. Вместо этого конструкция такова, что единственный возможный экземпляр должен быть идентифицируем, просто глядя на заголовки видимых экземпляров, игнорируя ограничения. Если этот единственный экземпляр имеет ограничения, то их также придется решить впоследствии, но их нельзя использовать как часть идентификации единственного возможного экземпляра.
Есть способы снять некоторые ограничения (например, использование перекрывающихся или бессвязных прагм), но когда вы это делаете, вы фундаментально работаете против системы; это возлагает на вас ответственность за избежание всех тонких проблем, для предотвращения которых предназначена обычная система. Поэтому я бы вообще не рекомендовал пробовать эти функции тем, кто еще не является экспертом в использовании классов типов «обычными» способами.
В целом работать с системой гораздо проще. Вместо того, чтобы добавлять всю сложность дополнительных классов реализации и семейств типов, чтобы вы могли правильно маршрутизировать типы к вашим универсальным экземплярам, вы можете просто очень легко объявить экземпляр для каждого отдельного типа. Используя такие вещи, как DerivingVia, вы обычно можете свести это к однострочному объявлению экземпляра для каждого типа; не так уж много встроенных типов Integral или Floating, поэтому нетрудно получить меньше шаблонов, чем гораздо более сложная установка, которая пытается получить экземпляры всего класса (и все это очень просто и механично; вы мог бы легко сгенерировать его, если хотите, но обычно его тоже недостаточно, чтобы оно того стоило). Например:
{-# LANGUAGE DerivingVia #-}
class MyClass a where
func :: a -> Bool
-- Declare a simple newtype for each of the whole-class instances you wanted.
-- Don't worry, you never actually have to use these types.
newtype FloatingMyClass a = FloatingMyClass a
newtype IntegralMyClass a = IntegralMyClass a
-- Write the whole-class instances you wanted but for the *newtypes* instead
-- of actually covering every type. You can write the code basically exactly
-- the same way you wanted; no additional complexity with type families etc,
-- only a boring newtype wrapper.
instance Floating a => MyClass (FloatingMyClass a) where
func _ = True
instance Integral a => MyClass (IntegralMyClass a) where
func _ = False
-- Now for each type, write a one liner declaring which of the newtype
-- instances should be used to used to produce the instance for the actual
-- type.
deriving via IntegralMyClass Int instance MyClass Int
deriving via IntegralMyClass Integer instance MyClass Integer
deriving via FloatingMyClass Float instance MyClass Float
deriving via FloatingMyClass Double instance MyClass Double
По сути, новые типы с прикрепленными к ним предполагаемыми экземплярами становятся именами «экземпляров шаблонов». Хотя вам по-прежнему придется объявлять экземпляр для каждого типа отдельно, вам не обязательно писать реализацию для каждого из них; вы можете просто назвать «шаблон», который хотите использовать.
Большим преимуществом этой схемы является то, что когда вы обнаружите, что MyClass будет полезен для типа, который не определяется только тем, Integral или Fractional, ничто не мешает вам написать конкретный экземпляр для этого типа вместо deriving via одного из экземпляры нового типа. С вашей первоначальной идеей вы были бы заблокированы в этот момент.
Или, конечно, если вы знаете, что намерены func всегда определяться по тому, является ли тип Integral или Floating, можно привести аргумент, что вам даже не нужен класс:
funcF :: Floating a => a -> Bool
funcF _ = True
funcI :: Integral a => a -> Bool
funcI _ = False
Это требует дифференциации имен и выбора одного на каждом месте использования, но компилятор сообщит вам, если вы ошиблись, и потребуется довольно много использований, прежде чем время, которое вы потратите на ввод короткого суффикса, превысит время, которое вы тратите на ввод короткого суффикса. потратил бы на установку более причудливых методов. Это не кажется «умным», но это легко и быстро. Иногда лучше заставить компилятор выбирать между вариантами, просто давая им разные имена и сообщая об этом компилятору, вместо того, чтобы использовать класс и пытаться убедиться, что компилятор всегда может понять, что вы имели в виду, когда использовали перегруженное имя.
«Основная причина этого заключается в том, что тип, который реализует как плавающий, так и интегральный, приведет к неопределенному поведению» - да. Итак, какое поведение вы хотите для таких типов?