Получение Eq и Show для ADT, содержащего поля, которые не могут иметь Eq или Show

Я хотел бы получить Eq и Show для ADT, содержащего несколько полей. Одним из них является функциональное поле. При выполнении Show я бы хотел, чтобы он отображал что-то фиктивное, например, например. "<function>"; при выполнении Eq я бы хотел, чтобы это поле игнорировалось. Как мне лучше всего сделать это без написания вручную полного экземпляра для Show и Eq?

Я не хочу оборачивать поле функции в newtype и писать для этого свои собственные Eq и Show - было бы слишком утомительно использовать такое.


person cheater    schedule 23.08.2020    source источник
comment
Ну... не делай этого. Если тип содержит функцию, его нельзя отобразить или сравнить на равенство, притворяться, что это не так, просто приведет к запутанным сюрпризам.   -  person leftaroundabout    schedule 23.08.2020
comment
@leftaroundabout, это вопрос личных предпочтений моей кодовой базы. Особенно Show не принесет никаких сюрпризов, это только для использования программистом во время интерактивного сеанса. Это не для сериализации.   -  person cheater    schedule 23.08.2020
comment
Достаточно справедливо, но если вы хотите играть по своим собственным правилам, вам также нужно будет написать свой собственный экземпляр. Не ожидайте, что сообщество/библиотеки/инструменты помогут вам с чем-то, что сообщество считает плохой идеей...   -  person leftaroundabout    schedule 23.08.2020
comment
@leftaroundabout извините, повествование об объективно плохих и хороших идеях, или что ваши вкусы представляют все сообщество, или что сообщество должно решать, что хорошо для меня, все это заходит слишком далеко для меня. Это превышение.   -  person cheater    schedule 23.08.2020


Ответы (3)


Обычно в таких случаях я делаю именно то, что вы говорите, что не хотите делать, а именно заключаю функцию в newtype и предоставляю для этого Show:

data T1
  { f :: X -> Y
  , xs :: [String]
  , ys :: [Bool]
  }
data T2
  { f :: OpaqueFunction X Y
  , xs :: [String]
  , ys :: [Bool]
  }
  deriving (Show)

newtype OpaqueFunction a b = OpaqueFunction (a -> b)

instance Show (OpaqueFunction a b) where
  show = const "<function>"

Если вы не хотите этого делать, вы можете вместо этого сделать функцию параметром типа и заменить его при Showing типе:

data T3' a
  { f :: a
  , xs :: [String]
  , ys :: [Bool]
  }
  deriving (Functor, Show)

newtype T3 = T3 (T3' (X -> Y))

data Opaque = Opaque

instance Show Opaque where
  show = const "..."

instance Show T3 where
  show (T3 t) = show (Opaque <$ t)

Или я рефакторинг своего типа данных, чтобы получить Show только для тех частей, которые я хочу сделать Showспособными по умолчанию, и переопределить другие части:

data T4 = T4
  { f :: X -> Y
  , xys :: T4'     -- Move the other fields into another type.
  }

instance Show T4 where
  show (T4 f xys) = "T4 <function> " <> show xys

data T4' = T4'
  { xs :: [String]
  , ys :: [Bool]
  }
  deriving (Show)  -- Derive ‘Show’ for the showable fields.

Или, если у меня небольшой тип, я буду использовать newtype вместо data и выводить Show через что-то вроде OpaqueFunction:

{-# LANGUAGE DerivingVia #-}

newtype T5 = T5 (X -> Y, [String], [Bool])
  deriving (Show) via (OpaqueFunction X Y, [String], [Bool])

Вы можете использовать пакет iso-deriving, чтобы сделать это для data типов, использующих линзы, если вам небезразличны сохранение имен полей/аксессоров записей.

Что касается Eq (или Ord), не очень хорошая идея иметь экземпляр, который приравнивает значения, которые можно различить каким-то образом, поскольку один код будет рассматривать их как идентичные, а другой — нет, и теперь вы вынуждены заботиться о стабильности: в некоторых случаях, когда у меня есть a == b, я должен выбрать a или b? Вот почему заменяемость является законом для Eq: forall x y f. (x == y) ==> (f x == f y), если f является "общедоступной" функцией, которая поддерживает инварианты типа x и y (хотя числа с плавающей запятой также нарушают это). Лучшим выбором является что-то вроде T4 выше, имея равенство только для частей типа, которые могут удовлетворять законам, или явно используя сравнение по модулю какой-либо функции на сайтах использования, например, comparing someField.

person Jon Purdy    schedule 23.08.2020
comment
Это довольно круто, особенно T5. Можно ли использовать это в ADT, которые не используют расширения GADT? Я предполагаю, что T5 не полиморфен? (это то, что я хочу, тбх) - person cheater; 23.08.2020
comment
@cheater: Да, если я правильно вас понял, вы можете использовать DerivingVia для полиморфных типов и даже многие GADT с StandaloneDeriving. Стратегия получения via просто требует Coercible экземпляров, которые автоматически выводятся для newtype, которые репрезентативно равны. В «ролевой» системе, которая разрешает приведения, есть некоторые недостатки, когда компилятор не может доказать безопасность приведения, поэтому иногда вам может потребоваться настроить ваши типы или использовать QuantifiedConstraints, чтобы говорить о типах более высокого порядка, например. forall a b. Coercible a b => Coercible (f a) (f b). - person Jon Purdy; 23.08.2020
comment
Я только что понял, что забыл, что мне нужно несколько конструкторов для этого типа. Так что он не поместится внутри newtype. Как бы вы сделали что-то вроде T5 для АТД с двумя конструкторами и без параметров типа? Не будете ли вы так добры, чтобы добавить свой ответ? - person cheater; 24.08.2020
comment
@cheater: Если вы хотите использовать DerivingVia напрямую, вам нужно использовать что-то вроде newtype X = X (Either Y Z) deriving (…) via (Either Y' Z') или iso-deriving с типом data. По крайней мере, я позабочусь о добавлении примера последнего. Я надеюсь, что в будущем мы сможем вывести больше вещей структурно для data типов. На данный момент вам придется построить приличное количество механизмов самостоятельно с помощью Generic с помощью методов, подобных тем, которые описаны в Зеркало-зеркало: отражение и кодирование через. - person Jon Purdy; 24.08.2020
comment
Написание материала для data звучит очень болезненно - я думаю, что ваше предложение newtype ... Either может быть лучше на данный момент. Я надеюсь, что в ближайшем будущем по крайней мере основные типы data будут поддерживаться лучше. - person cheater; 25.08.2020
comment
Я принял ваш ответ прямо сейчас как лучший, и я надеюсь, что если data поддержка материализуется, вы обновите ответ :) - person cheater; 25.08.2020

Один из способов получить правильные экземпляры Eq и Show — вместо жесткого кодирования поля функции сделать его параметром типа и предоставить функцию, которая просто «стирает» это поле. То есть, если у вас есть

data Foo = Foo
  { fooI :: Int
  , fooF :: Int -> Int }

вы меняете его на

data Foo' f = Foo
  { _fooI :: Int
  , _fooF :: f }
 deriving (Eq, Show)
type Foo = Foo' (Int -> Int)

eraseFn :: Foo -> Foo' ()
eraseFn foo = foo{ fooF = () }

Тогда Foo по-прежнему не будет Eq- или Showable (что, в конце концов, не должно быть), но чтобы сделать отображаемое значение Foo, вы можете просто обернуть его в eraseFn.

person leftaroundabout    schedule 23.08.2020
comment
Это интересно. Думаю, когда у вас есть значение с функцией внутри, вы можете просто выполнить myVal { _fooF = () }, чтобы получить версию для печати. Проблема в том, что это подразумевает, что Foo должен быть полиморфным в f, тогда как на самом деле этого не должно быть, он должен принимать только один тип. - person cheater; 23.08.2020
comment
Ну, это не обязательно должно быть «публично полиморфным» — вы можете экспортировать только тип Foo, но не Foo', тогда это выглядит так, как будто поле функции жестко запрограммировано. - person leftaroundabout; 24.08.2020

Модуль Text.Show.Functions в base предоставляет экземпляр show для функций, отображающих <function>. Чтобы использовать его, просто:

import Text.Show.Functions

Он просто определяет экземпляр примерно так:

instance Show (a -> b) where
  show _ = "<function>"

Точно так же вы можете определить свой собственный экземпляр Eq:

import Text.Show.Functions

instance Eq (a -> b) where
  -- all functions are equal...
  -- ...though some are more equal than others
  _ == _ = True

data Foo = Foo Int Double (Int -> Int) deriving (Show, Eq)

main = do
  print $ Foo 1 2.0 (+1)
  print $ Foo 1 2.0 (+1) == Foo 1 2.0 (+2)  -- is True

Это будет потерянный экземпляр, поэтому вы получите предупреждение с -Wall.

Очевидно, что эти экземпляры будут применяться ко всем функциям. Вы можете написать экземпляры для более специализированного типа функции (например, только для Int -> String, если это тип поля функции в вашем типе данных), но нет возможности одновременно (1) использовать встроенные Eq и Show производные механизмы для получения экземпляров для вашего типа данных, (2) не вводить оболочку newtype для поля функции (или какой-либо полиморфизм другого типа, как указано в других ответах), и (3) только экземпляры функции применяются к полю функции вашего тип данных, а не другие значения функции того же типа.

Если вы действительно хотите ограничить применимость экземпляров пользовательских функций без оболочки newtype, вам, вероятно, потребуется создать собственное решение на основе дженериков, что не имеет особого смысла, если вы не хотите делать это для большого количества типов данных. . Если вы пойдете по этому пути, то модули Generics.Deriving.Show и Generics.Deriving.Eq в generic-deriving предоставят шаблоны для этих экземпляров, которые можно модифицировать для специальной обработки функций, что позволит вам получить экземпляры для каждого типа данных, используя некоторые экземпляры-заглушки, например:

instance Show Foo where showsPrec = myGenericShowsPrec
instance Eq Foo where (==) = myGenericEquality
person K. A. Buhr    schedule 23.08.2020
comment
Проблема с модулями «необязательных экземпляров» заключается в том, что они являются вирусными, и любой нижестоящий также получит сомнительное благословение от того, что этот экземпляр находится в области действия. (Ура, коммунизм... свиньи бы обрадовались.) - person leftaroundabout; 23.08.2020
comment
на самом деле это довольно хороший трюк, когда я хочу что-то отладить, я просто добавляю производное Show к нужному типу и добавляю импорт вверху. Отличный материал! Не уверен насчет аргумента виральности, но при такой отладке нет необходимости хранить этот код. - person cheater; 23.08.2020