Контекст

Я пишу модуль 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. количество типов, возможно, за счет симметрии?

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

Благодарность!

9
jsk 26 Авг 2011 в 20:12

2 ответа

Лучший ответ

Вероятно, можно было бы возразить, что 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

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

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

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


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

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

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, который очень мелкий, поэтому его легко исчерпать и получить переполнение стека на уровне типа (ха-ха, да, шутка записывается сама) вместо нужного ответа.

7
C. A. McCann 26 Авг 2011 в 17:30

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

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

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

3
Rotsor 26 Авг 2011 в 18:30