Haskell: как написать экземпляр `Monoid` для чего-то, что зависит от параметров

Я работаю над небольшой библиотекой для университета, которая выполняет целочисленные вычисления в циклической группе; Вещи как:

(3 (% 11)) + (10 (% 11))
--> (2 (% 11))

'Целые числа (% n)' явно образуют моноид при добавлении с '0 (% n)' в качестве элемента идентичности. Однако сложение имеет смысл только тогда, когда модуль двух добавляемых операндов одинаков: a (% n) + b (% n) имеет смысл, а a (% n) + b (% m) - нет.

Есть ли способ добиться этого с помощью системы типов Haskell? То же самое, конечно, справедливо и для mempty элемента идентичности: как можно построить 0 (% n)? Можно ли как-нибудь n сохранить в системе типов?

Или подобные структуры требуют использования зависимых типов?


person Qqwy    schedule 24.09.2016    source источник
comment
Кухонная мойка Haskell имеет достаточно зависимых типов, чтобы сохранить модуль как число уровня типа, а затем сделать добавление моноидом для каждого положительного модуля. Конечно, вам нужно убедиться, что вы сохраняете копию модуля на уровне значений, если вы хотите нормализовать представителей по делению.   -  person pigworker    schedule 24.09.2016
comment
Альтернативой зависимым типам для этой конкретной цели является пакет reflection. Вы будете работать в Reifies s Natural контексте, в котором новый тип вокруг Integer с s фантомом будет иметь все ожидаемые экземпляры. reify подбросит модуль упругости в воздух, а reflect вытащит модуль из воздуха.   -  person dfeuer    schedule 25.09.2016


Ответы (3)


Продолжая мой комментарий, вот первая трещина. Модуль определяется типом, но не каноническим выбором представителя: это делается просто вычислением, поэтому потребуется барьер абстракции. Также доступны типы ограниченных чисел, но они требуют немного больше работы.

Войдите, {-# LANGUAGE KitchenSink #-}. Я имею в виду (на самом деле не так уж и плохо)

{-# LANGUAGE DataKinds, GADTs, KindSignatures, FlexibleInstances #-}

и давайте приступим.

Во-первых, просто рефлекторно, я ввожу хасохистские натуральные числа:

data Nat = Z | S Nat              -- type-level numbers
data Natty :: Nat -> * where      -- value-level representation of Nat
  Zy :: Natty Z
  Sy :: Natty n -> Natty (S n)
class NATTY n where               -- value-level representability
  natty :: Natty n
instance NATTY Z where
  natty = Zy
instance NATTY n => NATTY (S n) where
  natty = Sy natty

На мой взгляд, это именно то, что вы делаете, когда хотите объявить тип данных, а затем разрешить другим типам зависеть от его значений. Библиотека синглтонов Ричарда Айзенберга автоматизирует конструкцию.

(Если в этом примере используются числа для индексации векторов, некоторые люди указывают, что векторы () также могут служить одиночными числами для Nat. Они, конечно, технически правильны, но ошибочны. Когда мы думаем о Natty и NATTY как о систематических сгенерированные из Nat, это право, которое мы можем использовать или не использовать по своему усмотрению, а не лишнее, которое нужно оправдать. В этом примере не используются векторы, и было бы неправильно вводить векторы только для того, чтобы иметь синглтоны для Nat.)

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

int :: Nat -> Integer
int Z = 0
int (S n) = 1 + int n

instance Show Nat where
  show = show . int

nat :: Natty n -> Nat
nat Zy = Z
nat (Sy n) = S (nat n)

instance Show (Natty n) where
  show = show . nat

Теперь мы готовы объявить Mod.

data Mod :: Nat -> * where
  (:%) :: Integer -> Natty n -> Mod (S n)

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

remainder :: Natty n   -- predecessor of modulus
          -> Integer   -- any representative
          -> Integer   -- canonical representative
  -- if candidate negative, add the modulus
remainder n x | x < 0 = remainder n (int (nat (Sy n)) + x)
  -- otherwise get dividing
remainder n x = go (Sy n) x x where
  go :: Natty m  -- divisor countdown (initially the modulus)
     -> Integer  -- our current guess at the representative
     -> Integer  -- dividend countdown
     -> Integer  -- the canonical representative
    -- when we run out of dividend the guessed representative is canonical
  go _      c 0 = c
    -- when we run out of divisor but not dividend,
    --   the current dividend countdown is a better guess at the rep,
    --   but perhaps still too big, so start again, counting down
    --   from the modulus (conveniently still in scope)
  go Zy     _ y = go (Sy n) y y
    -- otherwise, decrement both countdowns
  go (Sy m) c y = go m c (y - 1)

Теперь мы можем сделать умный конструктор.

rep :: NATTY n                 -- we pluck the modulus rep from thin air
    => Integer -> Mod (S n)    -- when we see the modulus we want
rep x = remainder n x :% n where n = natty

И тогда экземпляр Monoid просто:

instance NATTY n => Monoid (Mod (S n)) where
  mempty                    = rep 0
  mappend (x :% _) (y :% _) = rep (x + y)

Я добавил еще кое-что:

instance Show (Mod n) where
  show (x :% n) = concat ["(", show (remainder n x), " :% ", show (Sy n), ")"]
instance Eq (Mod n) where
  (x :% n) == (y :% _) = remainder n x == remainder n y

С небольшим удобством ...

type Four = S (S (S (S Z)))

мы получили

> foldMap rep [1..5] :: Mod Four
(3 :% 4)

Так что да, вам нужны зависимые типы, но Haskell достаточно зависимо типизирован.

person pigworker    schedule 24.09.2016
comment
@Qqwy, вы можете избежать здесь FlexibleInstances, если хотите, используя class Mon n where monempty :: Mod n; monappend :: Mod n -> Mod n -> Mod n с экземплярами для 'Z и 'S n, а затем instance Mon n => Monoid (Mod n) where .... - person dfeuer; 25.09.2016

Это тот же ответ, что и @pigworker, но написанный менее болезненным (более эффективным и приятным синтаксисом) способом.

{-# LANGUAGE DataKinds, KindSignatures, ScopedTypeVariables #-}
module Mod(Mod) where
import Data.Proxy
import GHC.TypeLits

data Mod (n :: Nat) = Mod Integer

instance (KnownNat n) => Show (Mod n) where
    showsPrec p (Mod i) = showParen (p > 0) $
      showsPrec 0 i . showString " :% " . showsPrec 0 (natVal (Proxy :: Proxy n))

instance Eq (Mod n) where
    Mod x == Mod y = x == y

instance forall n . (KnownNat n) => Num (Mod n) where
    Mod x + Mod y = Mod $ (x + y) `mod` natVal (Proxy :: Proxy n)
    Mod x - Mod y = Mod $ (x - y) `mod` natVal (Proxy :: Proxy n)
    Mod x * Mod y = Mod $ (x * y) `mod` natVal (Proxy :: Proxy n)
    fromInteger i = Mod $ i `mod` natVal (Proxy :: Proxy n)
    abs x = x
    signum x = if x == 0 then 0 else 1

instance (KnownNat n) => Monoid (Mod n) where
    mempty = 0
    mappend = (+)

instance Ord (Mod n) where
    Mod x `compare` Mod y = x `compare` y

instance (KnownNat n) => Real (Mod n) where
    toRational (Mod n) = toRational n

instance (KnownNat n) => Enum (Mod n) where
    fromEnum = fromIntegral
    toEnum = fromIntegral

instance (KnownNat n) => Integral (Mod n) where
    quotRem (Mod x) (Mod y) = (Mod q, Mod r) where (q, r) = quotRem x y
    toInteger (Mod i) = i

И мы получаем

> foldMap fromInteger [1..5] :: Mod 4
3 :% 4
> toInteger (88 * 23 :: Mod 17)
1
> (3 :: Mod 4) == 7
True

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

person augustss    schedule 25.09.2016
comment
Правда, мне не пришлось делать столько унарных арифметических действий, сколько я использовал в своем ответе (я отказался от принудительного использования диапазона целых чисел, потому что очень спешил). Материал Natty NATTY, который я вижу как результат перевода, который выполняет мой препроцессор ... Между тем, помните, что арифметика Mod 0, и имейте в виду, что деление немного интереснее: 1/3 = 11 (mod 16). - person pigworker; 27.09.2016
comment
Я не стал делать деление, обратное умножению. Это слишком много работы. :) - person augustss; 27.09.2016
comment
Кроме того, я считаю, что мой quotRem подчиняется закону, установленному в документации Haskell. :) - person augustss; 27.09.2016

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

>>> import Data.Modular
>>> 10 * 11 :: ℤ/7
5
person shang    schedule 26.09.2016
comment
Очень хорошо! Теперь мне не нужно делать посылку. - person augustss; 26.09.2016