Использование экзистенциалов для поднятия значений в типы без снижения производительности - PullRequest
5 голосов
/ 05 февраля 2020

Я пытаюсь сделать компоновку сгибов по последовательности (сродни библиотеке 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, но это выглядит не элегантно. Есть ли лучший способ?

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...