Тип данных для конечных полей в Haskell?

Я пытаюсь немного изучить Haskell, написав небольшой набор функций для вычислений над конечными полями (Галуа). Несколько лет назад я написал первую версию подобной библиотеки для системы компьютерной алгебры GNU Maxima ( см. Здесь) и подумал, что попробую то же самое с Haskell.

Тем не менее, я все путаюсь с типами данных. Для конечного поля вам понадобится базовое простое число q (характеристика поля) и многочлен p(x), который неприводим по модулю q. Если p(x) имеет степень n, то порядок поля равен q^n, и все его элементы являются полиномами (по модулю q) со степенью n-1 или меньше.

Мы можем представить многочлены в виде списков их коэффициентов, так что элементы поля являются просто списками (или векторами, если вы предпочитаете) элементов Z_q и длины n. Сложение выполняется по компонентам по модулю q, а умножение выполняется по модулю p(x).

Я рассчитываю, смогу ли я получить тип данных, и сложение будет отсортировано, остальное будет просто. Моя первая попытка такая:

import Data.List

data GF = GF {characteristic::Int
             ,power::Int
             ,poly::[Int]
             ,irreducible::[Int]
             } deriving(Eq, Show)

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

Тогда у меня была функция сложения:

addGF :: GF -> GF -> GF
addGF x y = GF q n zp p
  where
    q = characteristic x
    n = power x
    zp = zipWith (\i j -> rem (i+j) q) xp yp
      where
        xp = poly x
        yp = poly y
    p = irreducible x

Это работает, но не элегантно, и я уверен, что очень "не-Хаскель-иш". Частично проблема в том, что я не знаю, как отделить определение (или тип) поля Галуа от его элементов.

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

Поэтому я предполагаю, что мой вопрос таков: как определить универсальный тип поля Галуа таким образом, чтобы операции над его элементами были максимально естественными?

Я прочитал несколько страниц об определении типов данных, классов и т. Д., И я не сомневаюсь, что одна из них содержит решение моей проблемы. Но чем больше я читаю, тем больше смущаюсь. Все, чего я хочу, это чтобы кто-то мягко, но твердо указал мне правильное направление. Спасибо!

2 ответа

Я не думаю, что ваш GF тип уродлив или неверен. Основная проблема, которую я вижу, заключается в том, что addGF не гарантирует, что элементы действительно могут быть добавлены. Вместо этого вы можете сделать:

addGF :: GF -> GF -> Maybe GF
addGF x y -- the pipes below are called "guards", a multi-way `if` syntax
  | q == characteristic y && n == power y && p == irreducible y = Just $ GF q n zp p
  | otherwise = Nothing
  where
    q = characteristic x
    n = power x
    zp = zipWith (\i j -> rem (i+j) q) xp yp
      where
        xp = poly x
        yp = poly y
    p = irreducible x

Возможно, было бы более эргономично и полезно (но не принципиально другое решение) отделить понятие поля от его элементов, например так:

-- these names are probably not appropriate
data Field  
  = Field { characteristic::Int
          , power::Int
          , irreducible::[Int]
          } deriving(Eq, Show)

-- formerly `GF`:
data FieldElement
  = FieldElement 
          { field::Field
          , poly::[Int]
          } deriving(Eq, Show)

Тогда в охраннике выше, вам просто нужно сделать, например,

...
| field x == field y = Just $ ...

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

Если вы знаете, что будете работать с определенными полями с параметрами, известными во время компиляции, то вы можете позволить средству проверки типов принудительно применить инвариант в addGF для тебя. Один из способов будет выглядеть так:

-- see `Data.Proxy` for info about this idiom
class SomeField f where
   characteristic :: proxy f -> Int
   power :: proxy f -> Int
   irreducible :: proxy f -> [Int]

-- A field element is just the polynomial, tagged with its field using the `f` type parameter
-- you may want to not expose the internals of `GF` but instead expose a 
-- makeGF function which enforces whatever invariants should hold between the parameters
-- of a field and the polynomial of its element.
newtype GF f = GF { poly :: [Int] }

-- `addGF` is no longer partial; the type system enforces that both arguments are elements of the same field
addGF :: (SomeField f)=> GF f -> GF f -> GF f
addGF x@(GF xp) (GF yp) = GF $ zipWith (\i j -> rem (i+j) q) xp yp
  where q = characteristic x

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

Это достаточно легко поднять characteristic а также power в систему типов в современном Haskell (GHC>=7,8), то есть

{-# LANGUAGE TypeOperators, DataKinds #-}
{-# LANGUAGE FlexibleContexts, TypeFamilies, UndecidableInstances #-}
{-# LANGUAGE StandaloneDeriving #-}

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

import GHC.TypeLits
import Data.Modular

data GF χ -- characteristic
        n -- power
   = GF { irreducible :: [ℤ/χ]
        , poly :: [ℤ/χ]
        }

Это уже дает вам бесплатно, что любые добавления на полиномах будут по модулю χ,

Вы могли бы также выразить, что всегда есть n + 1 коэффициенты:

import qualified Data.Vector.Fixed as Fix
import qualified Data.Vector.Fixed.Boxed as Fix

data GF χ n
   = GF { irreducible :: Fix.Vec (n+1) (ℤ/χ)
        , poly :: Fix.Vec (n+1) (ℤ/χ)
        }
deriving instance (KnownNat χ, Fix.Arity (n+1)) => Show (GF χ n)

addGF :: (KnownNat χ, Fix.Arity (n+1))
           => GF χ n -> GF χ n -> GF χ n
addGF (GF irr xp) (GF irr' yp)
 | irr==irr'  = GF irr $ Fix.zipWith (+) xp yp
 | otherwise  = error "Cannot add elements of finite fields with different irreducible polynomials!"

main = print (GF irr (Fix.fromList [0,0,1]) `addGF` GF irr (Fix.fromList [0,1,1])
               :: GF 2 2)
 where irr = Fix.fromList [1,1,1]

Результат:

GF {irreducible = fromList [1,1,1], poly = fromList [0,1,0]}

Все еще уродливо, что мы должны проверять во время выполнения неприводимый многочлен. Хотя в принципе можно было бы поднять это и до уровня шрифта, я не уверен, что это действительно сработает; мы уже настаиваем на том, насколько хорошо Haskell может использоваться в качестве языка с зависимой типизацией. Возможно, было бы достаточно выбрать для каждой характеристики и степени только один раз неприводимый многочлен, который всегда будет использоваться?

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