Создание экземпляра класса для типа, основанного на другом классе, это экземпляр

#haskell #type-families #deriving #derivingvia

Вопрос:

У меня есть ситуация, когда у меня есть несколько похожих newtypes , которые все должны быть примерами Random Arbitrary , и много других вещей. Все они нуждаются в одинаковой пользовательской реализации функций randomR , random , и т.д. arbitrary Поэтому я поместил все эти реализации в класс.

Вот упрощенный пример, который просто справляется Random .

 {-# LANGUAGE ConstrainedClassMethods #-}
{-# LANGUAGE FlexibleContexts        #-}
{-# LANGUAGE ScopedTypeVariables     #-}
{-# LANGUAGE StandaloneDeriving      #-}
{-# LANGUAGE TypeFamilies            #-}

import qualified System.Random as SR

-- Numbers that are restricted to a narrower range
class Narrow t where
  type BaseType t

  -- Unsafe constructor for the instance type
  bless :: BaseType t -> t

  -- Safe constructor for the instance type
  narrow :: (Ord t, Bounded t) => BaseType t -> t
  narrow x | x' < (minBound :: t) = error "too small"
           | x' > (maxBound :: t) = error "too big"
           | otherwise     = x'
    where x' = bless x :: t

  -- Deconstructor for the instance type
  wide :: t -> BaseType t

  -- Random
  randomR
    :: (Ord t, Bounded t, SR.Random (BaseType t), SR.RandomGen g)
    => (t, t) -> g -> (t, g)
  randomR (a, b) g = (narrow x, g')
    where (x, g') = SR.randomR (wide a, wide b) g

  random
    :: (Ord t, Bounded t, SR.Random t, SR.RandomGen g)
    => g -> (t, g)
  random = SR.randomR (minBound, maxBound)
 

Вот пример одного из типов, которые мне нужны.

 -- | A number on the unit interval
newtype UIDouble = UIDouble Double
  deriving (Eq, Ord)

instance Bounded UIDouble where
  minBound = UIDouble 0
  maxBound = UIDouble 1

instance Narrow UIDouble where
  type BaseType UIDouble = Double
  bless = UIDouble
  wide (UIDouble x) = x
    
 

Я хочу, чтобы это был пример Random . В идеале я хотел бы написать что-то вроде:

 deriving ?strategy? instance SR.Random UIDouble
 

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

 instance SR.Random UIDouble where
  randomR = randomR
  random = random
 

Это не проблема сделать это для нескольких методов, но сделать это для Num , Fractional , RealFrac , Floating Serialize , и т. Д. для каждого из моих типов это немного утомительно.

Альтернатива, которую я исследовал, состоит в том, чтобы написать

 instance (Narrow t) => SR.Random t where
  randomR = randomR
  random = random
 

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

Комментарии:

1. «` Неразрешимые ситуации», которые, как я понимаю, плохи » — это распространенное заблуждение. Существует риск, что вы сделаете цикл компилятора, если ваши определения будут циклическими. (Твои-нет.) При условии, что компилятор завершит работу, объектный код будет типобезопасным и счастливым. (Если запускает циклы, это не из-за UndecidableInstances .)

Ответ №1:

Сначала вы определяете новый тип и даете ему нужный экземпляр раз и навсегда:

 newtype UseNarrow a = UN a
instance Narrow a => SR.Random (UseNarrow a) where
    randomR (UN lo, UN hi) g = (UN v, g) where v = randomR (lo, hi) g
    random g = (UN v, g) where v = random g
 

Затем во всех местах, где вы хотите использовать этот экземпляр, вы пишете:

 deriving via (UseNarrow UIDouble) instance SR.Random UIDouble
 

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

Для дальнейшего чтения обратитесь к DerivingVia Руководству пользователя GHC.

Комментарии:

1. Я думал, что для этого DerivingVia требуется указать тип, к которому UIDouble будет применено принуждение. Так не окажется ли minBound и maxBound в конечном итоге тем, что относится к принудительному типу?

2. @mhwombat я… так не думаю? Обратите внимание , что randomR в RHS определения метода в экземпляре, который я показываю здесь, вызывается тип a , а не UseNarrow a ! (На самом деле контекст экземпляра даже не требует достаточных ограничений, чтобы гарантировать существование типа minBound / maxBound for UseNarrow a .)

3. @mhwombat Вы говорите о типе via, который есть UseNarrow UIDouble . Даниэль использовал автономный вывод, но вы также можете написать его традиционно: newtype UIDouble .. deriving SR.Random via UseNarrow UIDouble .

4. Вы можете подумать о SR.Random (UseNarrow UIDouble) SR.Random UIDouble том, что словарь вводится в словарь принудительно

5. @Iceland_jack Ну, я использовал автономное получение, потому что вопрос использовал автономное получение…