Haskell 如果类实例可用,则使用专用实现
考虑以下情况:Haskell 如果类实例可用,则使用专用实现,haskell,types,typeclass,Haskell,Types,Typeclass,考虑以下情况: slow_func :: Eq a => [a] -> [a] fast_func :: Ord a => [a] -> [a] 我有两个函数,slow\u func和fast\u func。这些函数是同一抽象函数的不同实现(它们做相同的事情),但一个比另一个快。只有在可以订购类型a时,才能使用更快的实现。有没有一种方法可以构造一个函数,在可能的情况下充当fast\u func,否则就返回到slow\u func as_fast_as_possible
slow_func :: Eq a => [a] -> [a]
fast_func :: Ord a => [a] -> [a]
我有两个函数,slow\u func
和fast\u func
。这些函数是同一抽象函数的不同实现(它们做相同的事情),但一个比另一个快。只有在可以订购类型a
时,才能使用更快的实现。有没有一种方法可以构造一个函数,在可能的情况下充当fast\u func
,否则就返回到slow\u 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
希望其中一个实例在实例规范方面最专业化,忽略其上下文(而不是使用最严格的上下文,这正是我们在这里需要的)
有什么办法吗? < P>我会考虑两个选项: 重写规则 名义上,您可以在任何地方使用
slow\u 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
但如果我们添加(不做任何更改)
然后
重写规则有点难以依赖(内联只是其中一个障碍),但至少您可以确保使用slowFunc
运行的东西会继续工作(只是速度可能不够快),但肯定不会在缺少实例的问题中迷失方向。另一方面,您还应该确保slowFunc
和fastFunc
的行为实际上是相同的–在我的示例中,这实际上并没有给出!(但它可以很容易地进行相应的修改)
正如Alec在评论中强调的那样,您需要为想要快速生成的每种类型添加重写规则。好的方面是,这可以在代码完成后完成,并且在性能方面,在分析表明它很重要的地方完成
个别实例
这是一个可靠的解决方案:避免使用任何“一网打尽”的实例,而是为每种类型决定什么是合适的
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
事实证明你可以。说真的,我开始认为在哈斯克尔一切皆有可能。。。您可以使用最近公布的方法的结果。我使用的代码与作者编写的代码类似。我不确定我是否用了最好的方法,只是尝试应用建议方法的概念:
{-# 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
它根据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
更新
我已经更新了必需的实例。而不是
instance (c || Eq SlowWrapper) where resolve = \_ r -> r
现在是了
instance d => (Ord SlowWrapper || d) where resolve = \_ r -> r
我会说“不”。它将打破单独编译,和/或在实例搜索算法中引入回溯。在最坏的情况下,后者会使编译时间呈指数级增长。重叠实例已经是一个很糟糕的想法(IMO)。将默认定义
尽可能快\u func=fast\u func
,如果失败(因为类不是Eq
的实例),实现者可以将其定义为slow\u func
?留下一条评论说,slow_func
可以作为第二个选项使用?@Davislor我不是还需要两个实例声明,导致同样的问题吗?据我所知,唯一的变化是我可以保留一个实例声明为空。相关:类系统最初设计的使用方式是不声明两个多态实例,而是为每个类型创建一个单独的具体实例。整个类型类机制的设计使得关于选择哪个实例的决定不能基于哪些其他实例在范围内,以提高一致性(我们不希望导入您的Func
的两个模块根据导入的其他模块为同一类型选择不同的实例)。我可能只是直接使用slow_func
和fast_func
,而不尝试统一它们。重写规则方法很好,但值得一提的是,您仍然需要为每个专用签名添加一条规则;无论何时满足Ord
约束,都无法添加优化slowFunc=fastFunc
的重写规则。您仍然需要手动指示GHC可以立即看到这一事实的类型。我非常不喜欢重写规则的方法。很容易获得糟糕的性能,因为有些人不知道他们需要一个Ord
实例和一个重写规则来加快键入速度。它也不能扩展到更高级的类型。由于您必须手动包装值,您最好手动调用正确的函数。他只包装值,以摆脱Ord
,用于演示目的。@LukaHorvat我使用包装值仅用于演示目的。您可以使用这种不带包装器的方法。很难找到一种实现了Eq
但没有实现Ord
的标准数据类型来证明该解决方案确实有效。哦,你说得对,我还以为它是一种通用的新类型,比如FastWrapper a
。但我的评论仍然有效。您需要为要与函数一起使用的每种可能的类型编写一个实例,此时您最好为它们编写一个特殊的类。@LukaHorvat是的,不幸的是,这是约束联合方法的一个缺点-它需要一些样板代码。希望实例非常容易编写。你需要写一次
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
$ 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
instance (c || Eq SlowWrapper) where resolve = \_ r -> r
instance d => (Ord SlowWrapper || d) where resolve = \_ r -> r