Я пытаюсь сделать компоновку сгибов по последовательности (сродни библиотеке foldl), но с большим количеством вещей, сделанных на уровне типов, в надежде, что компилятору будет легче вставлять и отсеивать вещи CSE (и это надежды не беспочвенны, основываясь на некоторых экспериментах).
Для этого у меня есть класс типов по типу (это действительно упрощенный пример):
class Statistic a where
type StateOf a = k | k -> a
type ResultOf a = k | k -> a
extract :: StateOf a -> ResultOf a
computeStep :: StateOf a -> Double -> StateOf a
...
У меня также есть несколько базовые экземпляры
data Stats = Min | Max | Avg | Stddev
instance Statistic 'Min where ...
instance Statistic 'Max where ...
...
, а также объединяющий экземпляр
data a ::: b = a ::: b
instance (Statistic a, Statistic b) => Statistic (a '::: b) where
type StateOf (a '::: b) = StateOf a ::: StateOf b
type ResultOf (a '::: b) = ResultOf a ::: ResultOf b
extract (a ::: b) = extract a ::: extract b
computeStep = ...
и что-то, что действительно выполняет свою работу:
run :: Statistic s => [Double] -> ResultOf s
run = ...
Если я сделаю что-то вроде run @('Avg '::: 'Stddev)
, результирующая производительность кода примерно такая же, как и для рукописного аналога, поэтому он выглядит многообещающе (и также выглядит лучше, чем foldl, оправдывая усилия - на самом деле run @('Stddev '::: 'Stddev)
выглядит так же быстро, как одиночный stddev!).
Но что, если я захочу написать приложение командной строки и позволить пользователю выбирать статистику, которую он хочет вычислить? Мне нужно будет перевести термины (проанализированные параметры) в тип (экземпляр Statistic
)!
Это не так сложно, и этот код выполняет свою работу (и мне очень нравится, что gh c допускает аннотации типов в шаблонах):
data SomeStats where
MkSomeStats :: (Statistic s, Show (ResultOf s)) => proxy s -> SomeStats
run' :: SomeStats -> [Double] -> String
run' (MkSomeStats (_ :: proxy s)) input = show $ run @s input
promoteStat :: Stats -> SomeStats
promoteStat Min = MkSomeStats (Proxy :: Proxy 'Min)
promoteStat Max = MkSomeStats (Proxy :: Proxy 'Max)
...
promoteStats :: [Stats] -> SomeStats -- uh oh, I should've used NonEmpty
promoteStats [s] = promoteStat s
promoteStats (s:ss) =
case (promoteStat s, promoteStats ss) of
(MkSomeStats (_ :: proxy1 s), MkSomeStats (_ :: proxy2 ss)) -> MkSomeStats (Proxy :: Proxy (s '::: ss))
Результат даже работает правильно, но производительность получающегося кода ужасна, на порядок или два хуже, чем вариант с приложением прямого типа без каких-либо экзистенциалы. Это на самом деле имеет смысл - теперь компилятор не имеет возможности вставлять что-то, и ему просто нужно нести словарь для класса Statistic
, даже для объединяющих экземпляров Statistic
!
На данный момент Я на самом деле думаю о генерации обработчика опций с использованием Template Haskell, который будет просто генерировать ветки 2^|Stats| - 1
case
, но это выглядит не элегантно. Есть ли лучший способ?