Haskell, как создать экземпляр всех элементов двух классов?

Вот в чем сводится моя проблема: у меня есть класс, и я хочу, чтобы каждый 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. Но, похоже, не существует способа регулярно применять ограничения к семейству типов.

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

«Основная причина этого заключается в том, что тип, который реализует как плавающий, так и интегральный, приведет к неопределенному поведению» - да. Итак, какое поведение вы хотите для таких типов?

Bergi 13.06.2024 02:01

«У меня есть класс, и я хочу, чтобы каждый Floating и каждый Integral создавал его экземпляр» — мой совет — отказаться от этой цели и вместо этого хотеть, чтобы каждый экземпляр, который вы используете, создавал его экземпляр. Эту цель гораздо легче достичь; просто напишите примеры.

Daniel Wagner 13.06.2024 19:00
Стоит ли изучать PHP в 2026-2027 годах?
Стоит ли изучать PHP в 2026-2027 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в...
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Приемы CSS-макетирования - floats и Flexbox
Приемы CSS-макетирования - floats и Flexbox
Здравствуйте, друзья-студенты! Готовы совершенствовать свои навыки веб-дизайна? Сегодня в нашем путешествии мы рассмотрим приемы CSS-верстки - в...
Тестирование функциональных ngrx-эффектов в Angular 16 с помощью Jest
В системе управления состояниями ngrx, совместимой с Angular 16, появились функциональные эффекты. Это здорово и делает код определенно легче для...
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
3
2
84
1
Перейти к ответу Данный вопрос помечен как решенный

Ответы 1

Ответ принят как подходящий

Есть некоторые вещи, которые вы можете сделать в этом отношении до такой степени, что они могут быть полезными, а могут и не быть полезными, но на самом деле это не совсем предполагаемое использование системы классов типов для написания экземпляров типа 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

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

Другие вопросы по теме