Сокращение кода за счет использования симметрии среди экземпляров классов нескольких типов - PullRequest
9 голосов
/ 26 августа 2011

Контекст

Я пишу модуль на Haskell, который представляет префиксы SI:

module Unit.SI.Prefix where

Каждый префикс SI имеет соответствующий тип данных:

data Kilo = Kilo deriving Show
data Mega = Mega deriving Show
data Giga = Giga deriving Show
data Tera = Tera deriving Show

-- remaining prefixes omitted for brevity

Проблема

Я хотел бы написать функцию, которая при применении с двумя префиксами SI определяет статически , какой из двух префиксов меньше . Например:

-- should compile:
test1 = let Kilo = smaller Kilo Giga in ()
test2 = let Kilo = smaller Giga Kilo in ()

-- should fail to compile:
test3 = let Giga = smaller Kilo Giga in ()
test4 = let Giga = smaller Giga Kilo in ()

Исходное решение

Вот решение, которое использует класс типов вместе с функциональной зависимостью:

{-# LANGUAGE FunctionalDependencies #-}                                                                                         
{-# LANGUAGE MultiParamTypeClasses #-}

class Smaller a b c | a b -> c where smaller :: a -> b -> c

instance Smaller Kilo Kilo Kilo where smaller Kilo Kilo = Kilo
instance Smaller Kilo Mega Kilo where smaller Kilo Mega = Kilo
instance Smaller Kilo Giga Kilo where smaller Kilo Giga = Kilo
instance Smaller Kilo Tera Kilo where smaller Kilo Tera = Kilo

instance Smaller Mega Kilo Kilo where smaller Mega Kilo = Kilo
instance Smaller Mega Mega Mega where smaller Mega Mega = Mega
instance Smaller Mega Giga Mega where smaller Mega Giga = Mega
instance Smaller Mega Tera Mega where smaller Mega Tera = Mega

instance Smaller Giga Kilo Kilo where smaller Giga Kilo = Kilo
instance Smaller Giga Mega Mega where smaller Giga Mega = Mega
instance Smaller Giga Giga Giga where smaller Giga Giga = Giga
instance Smaller Giga Tera Giga where smaller Giga Tera = Giga

instance Smaller Tera Kilo Kilo where smaller Tera Kilo = Kilo
instance Smaller Tera Mega Mega where smaller Tera Mega = Mega
instance Smaller Tera Giga Giga where smaller Tera Giga = Giga
instance Smaller Tera Tera Tera where smaller Tera Tera = Tera

Представленное выше решение, по-видимому, решает проблему правильно, однако оно имеет недостаток: число экземпляров классов типов равно квадратичное w.r.t. количество типов.

Вопрос

Есть ли способ уменьшить количество экземпляров классов типов до linear w.r.t. количество типов, возможно, используя симметрию?

Возможно, здесь уместнее использовать Template Haskell, и в этом случае смело предлагайте это как решение.

Спасибо!

Ответы [ 2 ]

7 голосов
/ 26 августа 2011

Вероятно, можно утверждать, что TH более уместен в подобных случаях.Тем не менее, я все равно сделаю это с типами.

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

Для рекурсивного решения мы сначала создаем натуральные числа и логические значения на уровне типа:

{-# LANGUAGE TypeOperators #-}
{-# LANGUAGE UndecidableInstances #-}
{-# LANGUAGE TypeFamilies #-}

data No = No deriving (Show)
data Yes = Yes deriving (Show)

newtype S nat = Succ nat deriving (Show)
data Z = Zero deriving (Show)

type Zero  = Z
type One   = S Zero
type Two   = S One
type Three = S Two

Немного простой арифметики:

type family Plus x y :: *
type instance Plus x Z = x
type instance Plus Z y = y
type instance Plus (S x) (S y) = S (S (Plus x y))

type family Times x y :: *
type instance Times x Z = Z
type instance Times x (S y) = Plus x (Times y x)

Предикат "меньше или равно" и простая условная функция:

type family IsLTE n m :: *
type instance IsLTE Z Z = Yes
type instance IsLTE (S m) Z = No
type instance IsLTE Z (S n) = Yes
type instance IsLTE (S m) (S n) = IsLTE m n

type family IfThenElse b t e :: *
type instance IfThenElse Yes t e = t
type instance IfThenElse No t e = e

И преобразования из префиксов SI в величину, которую они представляют:

type family Magnitude si :: *
type instance Magnitude Kilo = Three
type instance Magnitude Mega = Three `Times` Two
type instance Magnitude Giga = Three `Times` Three

... и т. Д.

Теперь, чтобы найти меньший префикс, вы можете сделать это:

type family Smaller x y :: *
type instance Smaller x y = IfThenElse (Magnitude x `IsLTE` Magnitude y) x y

Учитывая, что все здесь имеет взаимно однозначное соответствие между типоми единственный нулевой конструктор, населяющий его, это можно перевести на уровень термина, используя общий класс, подобный этому:

class TermProxy t where term :: t
instance TermProxy No where term = No
instance TermProxy Yes where term = Yes
{- More instances here... -}

smaller :: (TermProxy a, TermProxy b) => a -> b -> Smaller a b
smaller _ _ = term

Заполнение деталей по мере необходимости.


ДругойПодход включает использование функциональных зависимостей и перекрывающихся экземпляров для написания универсального экземпляра для заполнения пробелов - чтобы вы могли написать конкретные экземпляры для Kilo

Это углубляется в трактовку функциональных зависимостей как того, что они есть - языка программирования примитивной логики.Если вы когда-либо использовали Пролог, у вас должна быть грубая идея.В некотором смысле это хорошо, потому что вы можете позволить компилятору разобраться, основываясь на более декларативном подходе.С другой стороны, это также ужасно, потому что ...

  • Экземпляры выбираются без учета ограничений, только заголовок экземпляра.
  • Нет пути назад для поиска решения.
  • Чтобы выразить подобные вещи, вы должны включить UndecidableInstances из-за очень консервативных правил GHC о том, что, как он знает, прекратится;но тогда вы должны позаботиться о том, чтобы средство проверки типов не отправлялось в бесконечный цикл.Например, было бы очень легко сделать это случайно, учитывая такие экземпляры, как Smaller Kilo Kilo Kilo и что-то вроде (Smaller a s c, Smaller t b s) => Smaller a b c - подумайте, почему.

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


О, и для полноты, вот третий подход: На этот раз мы злоупотребляем дополнительными возможностями, которые дают нам перекрывающиеся экземпляры, чтобы напрямую реализовать рекурсивное решение, а не путем преобразования в натуральные числа и использования структурной рекурсии.

Во-первых, измените желаемый порядок как тип.-уровневый список:

data MIN = MIN deriving (Show)
data MAX = MAX deriving (Show)

infixr 0 :<
data a :< b = a :< b deriving (Show)

siPrefixOrd :: MIN :< Kilo :< Mega :< Giga :< Tera :< MAX
siPrefixOrd = MIN :< Kilo :< Mega :< Giga :< Tera :< MAX

Реализация предиката равенства для типов с использованием некоторых перекрывающихся махинаций:

class (TypeEq' () x y b) => TypeEq x y b where typeEq :: x -> y -> b
instance (TypeEq' () x y b) => TypeEq x y b where typeEq _ _ = term

class (TermProxy b) => TypeEq' q x y b | q x y -> b
instance (b ~ Yes) => TypeEq' () x x b 
instance (b ~ No) => TypeEq' q x y b 

Альтернативный класс "меньше чем", сдва простых случая:

class IsLTE a b o r | a b o -> r where
    isLTE :: a -> b -> o -> r

instance (IsLTE a b o r) => IsLTE a b (MIN :< o) r where
    isLTE a b (_ :< o) = isLTE a b o

instance (No ~ r) => IsLTE a b MAX r where
    isLTE _ _ MAX = No

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

instance ( TypeEq a x isA, TypeEq b x isB
         , IsLTE' a b isA isB o r
         ) => IsLTE a b (x :< o) r where
    isLTE a b (x :< o) = isLTE' a b (typeEq a x) (typeEq b x) o

class IsLTE' a b isA isB xs r | a b isA isB xs -> r where
    isLTE' :: a -> b -> isA -> isB -> xs -> r

instance (Yes ~ r) => IsLTE' a b Yes Yes xs r where isLTE' a b _ _ _ = Yes
instance (Yes ~ r) => IsLTE' a b Yes No xs r where isLTE' a b _ _ _ = Yes
instance (No ~ r) => IsLTE' a b No Yes xs r where isLTE' a b _ _ _ = No
instance (IsLTE a b xs r) => IsLTE' a b No No xs r where
    isLTE' a b _ _ xs = isLTE a b xs

По сути, это берет список уровня типа и два произвольных типа, затем идет по списку и возвращает Yes, если он находит первыйвведите, или No, если он найдет второй тип или достигнет конца списка.

Это на самом деле своего рода ошибка (вы можете понять, почему, если вы думаете о том, что происходит, если один или оба типа не отображаются ')в списке), а также склонен к сбою - прямая рекурсия, подобная этой, использует стек сокращения контекста в GHC, который очень мелок, поэтому его легко исчерпать и получить переполнение стека на уровне типов (ха-ха, да,шутка пишет сама) вместо того ответа, который вы хотели.

3 голосов
/ 26 августа 2011

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

На странице арифметики в Wiki Haskell есть несколько хороших примеров работы с натуральными числами уровня типа. Было бы неплохо начать.

Также обратите внимание, что на Hackage уже есть популярный пакет dimension для работы с единицами СИ аналогичным образом. Возможно, стоит посмотреть, как это реализовано в их коде.

...