Контекст
Я пишу модуль 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, и в этом случае не стесняйтесь предлагать это в качестве решения.
Благодарность!
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, вы должны иметь приблизительное представление. В некотором смысле это хорошо, потому что вы можете позволить компилятору разобраться во всем на основе более декларативного подхода. С другой стороны, это ужасно, потому что ... Fundeps и перекрывающиеся экземпляры строго более мощные, чем семейства типов, но в целом их неудобно использовать и кажутся несколько неуместными по сравнению с более функциональным, рекурсивным стилем, который использует последний. Да, и для полноты картины, вот третий подход: на этот раз мы злоупотребляем дополнительными возможностями, которые дает нам перекрывающиеся экземпляры, чтобы реализовать рекурсивное решение напрямую, а не путем преобразования в натуральные числа и использования структурной рекурсии. Во-первых, уточните желаемый порядок в виде списка на уровне типов: Реализуйте предикат равенства для типов , используя некоторые перекрывающиеся махинации: Альтернативный класс "меньше" с двумя простыми случаями: И затем рекурсивный случай с вспомогательным классом, используемым для отсрочки рекурсивного шага на основе анализа случая логического значения уровня типа: По сути, он принимает список на уровне типов и два произвольных типа, затем просматривает список и возвращает На самом деле это своего рода ошибка (вы можете понять, почему, если подумать о том, что происходит, если одного или обоих типов нет в списке), а также склонность к сбоям - такая прямая рекурсия использует стек сокращения контекста в GHC, который очень мелкий, поэтому его легко исчерпать и получить переполнение стека на уровне типа (ха-ха, да, шутка записывается сама) вместо нужного ответа.UndecidableInstances
из-за очень консервативных правил GHC относительно того, что, как он знает, будет прекращено; но тогда вы должны позаботиться о том, чтобы средство проверки типов не попало в бесконечный цикл. Например, это было бы очень легко сделать случайно, учитывая такие примеры, как Smaller Kilo Kilo Kilo
и что-то вроде (Smaller a s c, Smaller t b s) => Smaller a b c
- подумайте, почему.
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
, если он находит второй тип или достигает конца. списка.
Вы можете сопоставить свои типы с натуральными числами уровня типа, а затем выполнять сравнения, используя их. Это должно сделать его линейным, поскольку вам нужно указать только один экземпляр для каждого типа, сопоставляя его с соответствующим числом.
На странице арифметики типов в Haskell Wiki есть несколько хороших примеров работы с натуральными числами на уровне типов. Это было бы хорошее место для начала.
Также обратите внимание, что уже существует популярный пакет Dimensional на Hackage для работы с единицами SI аналогичным образом. . Возможно, стоит посмотреть, как это реализовано в их коде.
Похожие вопросы
Новые вопросы
haskell
Haskell - это функциональный язык программирования, отличающийся строгой статической типизацией, отложенной оценкой, обширной поддержкой параллелизма и параллелизма и уникальными возможностями абстракции.