Использовать специализированную реализацию, если экземпляр класса доступен

Рассмотрим следующую ситуацию:

slow_func :: Eq a  => [a] -> [a]
fast_func :: Ord a => [a] -> [a]

У меня есть две функции, slow_func и fast_func. Эти функции являются различными реализациями одной и той же абстрактной функции (они выполняют одно и то же), но одна работает быстрее другой. Более быстрая реализация доступна только в том случае, если можно заказать тип a. Есть ли способ построить функцию, которая действует как fast_func когда это возможно, и в slow_func случае возвращается к slow_func?

as_fast_as_possible_func :: Eq a => [a] -> [a]

Я уже пробовал следующее:

{-# LANGUAGE OverlappingInstances  #-}

class Func a where
    as_fast_as_possible_func :: [a] -> [a]

instance Ord a => Func a where
    as_fast_as_possible_func = fast_func

instance Eq a => Func a where
    as_fast_as_possible_func = slow_func

К сожалению, это не компилируется, генерируя следующую ошибку:

Duplicate instance declarations:
  instance Ord a => Func a
    -- Defined at [...]
  instance Eq a => Func a
    -- Defined at [...]

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

Есть ли способ сделать это?

Ответы

Ответ 1

Оказалось, что вы можете. Серьезно, я начинаю думать, что все возможно в Haskell... Вы можете использовать результаты недавно анонсированного подхода constraint-unions. Я использую код, похожий на тот, который был написан @leftaroundabout. Не уверен, что я сделал это наилучшим образом, просто попытался применить концепции предлагаемого подхода:

{-# OPTIONS_GHC -Wall -Wno-name-shadowing #-}

{-# LANGUAGE AllowAmbiguousTypes        #-}
{-# LANGUAGE ConstraintKinds            #-}
{-# LANGUAGE FlexibleContexts           #-}
{-# LANGUAGE FlexibleInstances          #-}
{-# LANGUAGE GeneralizedNewtypeDeriving #-}
{-# LANGUAGE MultiParamTypeClasses      #-}
{-# LANGUAGE RankNTypes                 #-}
{-# LANGUAGE ScopedTypeVariables        #-}
{-# LANGUAGE TypeApplications           #-}
{-# LANGUAGE TypeOperators              #-}

module Main where

import           Data.List (group, nub, sort)

infixr 2 ||
class  c || d where
    resolve :: (c => r) -> (d => r) -> r

slowFunc :: Eq a => [a] -> [a]
slowFunc = nub

fastFunc :: Ord a => [a] -> [a]
fastFunc = map head . group . sort

as_fast_as_possible_func :: forall a. (Ord a || Eq a) => [a] -> [a]
as_fast_as_possible_func = resolve @(Ord a) @(Eq a) fastFunc slowFunc

newtype SlowWrapper = Slow Int deriving (Show, Num, Eq)
newtype FastWrapper = Fast Int deriving (Show, Num, Eq, Ord)

instance      (Ord FastWrapper || d) where resolve = \r _ -> r
instance d => (Ord SlowWrapper || d) where resolve = \_ r -> r

main :: IO ()
main = print . sum . as_fast_as_possible_func $ (Fast . round) 
                                             <$> [sin x * n | x<-[0..n]]
  where n = 20000

Ключевая часть здесь as_fast_as_possible_func:

as_fast_as_possible_func :: forall a. (Ord a || Eq a) => [a] -> [a]
as_fast_as_possible_func = resolve @(Ord a) @(Eq a) fastFunc slowFunc

Он использует соответствующую функцию в зависимости от того, является ли a Ord или Eq. Я положил Ord на первое место, потому что все, что есть Ord, автоматически Eq, и правила проверки столбца могут не запускаться (хотя я не тестировал эту функцию при смене ограничений). Если вы используете Slow здесь (Fast . round) вместо Fast, вы можете наблюдать значительно более медленные результаты:

$ time ./Nub  # With `Slow` 
Slow 166822

real    0m0.971s
user    0m0.960s
sys     0m0.008s


$ time ./Nub  # With `Fast` 
Fast 166822

real    0m0.038s
user    0m0.036s
sys     0m0.000s

UPDATE

Я обновил требуемые экземпляры. Вместо

instance (c || Eq SlowWrapper)  where resolve = \_ r -> r

Теперь это

instance d => (Ord SlowWrapper || d) where resolve = \_ r -> r

Спасибо @rampion за объяснение!

Ответ 2

Я бы рассмотрел два варианта:

Переписать правила

Вы можете номинально использовать slow_func везде, но пусть переписывать правила, когда это возможно, оптимизировать. Например,

import Data.List

slowFunc :: Eq a => [a] -> [a]
slowFunc = nub

fastFunc :: Ord a => [a] -> [a]
fastFunc = map head . group . sort

main = print . sum . slowFunc $ round <$> [sin x * n | x<-[0..n]]
 where n = 100000

является медленным (duh):

$ ghc -O2 Nub.hs && time ./Nub
[1 of 1] Compiling Main             ( Nub.hs, Nub.o )
Linking Nub ...
-3670322

real    0m51.875s
user    0m51.867s
sys 0m0.004s

но если мы добавим (не изменяя ничего)

{-# NOINLINE slowFunc #-}
{-# RULES "slowFunc/Integer" slowFunc = fastFunc :: [Integer] -> [Integer] #-}

затем

$ ghc -O2 Nub.hs && time ./Nub
[1 of 1] Compiling Main             ( Nub.hs, Nub.o )
Linking Nub ...
-3670322

real    0m0.250s
user    0m0.245s
sys 0m0.004s

Переписать правила немного сложно, чтобы полагаться (вложение - это только одна вещь, которая может мешать), но по крайней мере вы можете быть уверены, что что-то, что работает с slowFunc, будет работать (возможно, не так быстро), но определенно не потеряется в некоторых проблемах с отсутствующим экземпляром. С другой стороны, вы также должны быть уверены, что slowFunc и fastFunc действительно ведут себя одинаково - в моем примере это фактически не дано! (Но его можно легко изменить соответствующим образом).

Как подчеркивает Алек в комментариях, вам нужно будет добавить правило перезаписи для каждого отдельного типа, который вы хотите сделать быстро. Хорошо, что это можно сделать после завершения кода и именно там, где профилирование указывает, что это важно, с точки зрения производительности.

Индивидуальные экземпляры

Это надежное решение: воздерживаться от любых экземпляров catch-all и вместо этого выбирать для каждого типа подходящее.

instance Func Int where
    as_fast_as_possible_func = fast_func
instance Func Double where
    as_fast_as_possible_func = fast_func
...

instance Func (Complex Double) where
    as_fast_as_possible_func = slow_func

Вы можете сохранить несколько повторяющихся строк, сделав более распространенную версию по умолчанию:

{-# LANGUAGE DefaultInstances #-}

class Func a where
  as_fast_as_possible_func :: [a] -> [a]
  default as_fast_as_possible_func :: Ord a => [a] -> [a]
  as_fast_as_possible_func = fast_func

instance Func Int
instance Func Double
...

instance Func (Complex Double) where
    as_fast_as_possible_func = slow_func