我是Haskell的初学者,并试图了解类型类和类型。我有以下示例(代表我正在研究的代数中的一个真正问题),其中我定义了一个只包装Num实例的类型,以及一个定义二进制操作baz
的类型类。
newtype Foo i = B i deriving (Eq, Ord, Show)
class Bar k where
baz :: k -> k -> k
instance Num k => Bar (Foo k) where
baz (B a) (B b) = B (f a b)
f :: Num a => a -> a -> a
f a b = a + b
当将Bar
定义为此实例时,我意识到我希望能够“变化”#34;具有类型的函数f
。要明确:我想提供一个函数f :: Num a => a -> a -> a
并返回一个新类型Foo
,它是Bar
的一个实例。假设我想要做5次,10次,唯一的区别是不同的函数f
。我当然可以复制并粘贴上面的代码,但我想知道是否还有其他方法?
在我看来,我很困惑。在Haskell中做这样的事情的最佳方法是什么?这是一个很好的设计选择,我在想什么是对/错?为什么?
编辑:我意识到一个具体的例子可能有助于使问题更清楚(请注意,它可能看起来很复杂,我无法简化代码而不是这个。上面的问题包含我认为的相同信息):我感兴趣的类型类是来自库HaskellForMaths的Algebra k v
:
class Algebra k b where
unit :: k -> Vect k b
mult :: Vect k (Tensor b b) -> Vect k b
此处k
是一个字段(数学结构,如实数或复数),而v
是向量空间中的基础选择。我想用它这样的东西
newtype Basis i = T i deriving (Eq, Ord, Show)
type Expression k = Vect k (Basis Int)
instance Algebra k Basis where
unit x = x *> return I
mult = linear mult'
where mult' (T x ,T y) = comm x y
where comm a b = sum $ map (\c -> structure a b c *> t c) [0..n]
t :: Int -> Expression k
t a = return (T a)
然后根据我的喜好改变地图structure
。这里类型T
只是编写抽象基础元素T 1, T 2, ...
的一种便捷方式。我想要这样做的原因是代数的标准数学定义,其结构常量(这里:structure
)。总结一下:我希望能够改变函数f
(最好不要在编译时?)并取回代数。这可能是一个糟糕的设计决定:如果是,为什么?
答案 0 :(得分:1)
您可以使用reflection。这是一种相当先进的技术,可能有更好的方法来解决您的问题,但您已经说明了这种方式,这似乎是您正在寻找的。 p>
{-# LANGUAGE FlexibleContexts, RankNTypes, ScopedTypeVariables, UndecidableInstances #-}
import Data.Reflection
import Data.Proxy
class Bar k where
baz :: k -> k -> k
newtype Foo f i = B i -- f is a type level representation of your function
deriving (Eq, Ord, Show)
instance (Num k, Reifies f (k -> k -> k)) => Bar (Foo f k) where
baz (B a) (B b) = B (reflect (Proxy :: Proxy f) a b)
mkFoo :: forall i r. (i -> i -> i) -> i
-> (forall f. Reifies f (i -> i -> i) => Foo f i -> r) -> r
mkFoo f x c = reify f (\(p :: Proxy f) -> c (B x :: Foo f i))
main = do
mkFoo (+) 5 $ \foo1 -> do
print $ foo1 `baz` B 5 -- 10
mkFoo (*) 5 $ \foo2 -> do
print $ foo2 `baz` B 5 -- 25
print $ foo1 `baz` foo2 -- type error
这里有很多事情,所以有几点说明。
Reifies f (k -> k -> k)
是一个约束,表示f
是类型k -> k -> k
的函数的类型级表示。当我们reflect (Proxy :: Proxy f)
(一种将类型f
传递给reflect
的奇特方式,因为显式类型应用程序直到最近才被允许),我们将函数本身退出。
现在来到mkFoo
mkFoo :: forall i r. (i -> i -> i) -> i
-> (forall f. Reifies f (i -> i -> i) => Foo f i -> r) -> r
第一个forall
适用于ScopedTypeVariables
,因此我们可以参考函数体内的类型变量。第二个是真正的rank-2 type,
(forall f. Reifies f (i -> i -> i) => Foo f i -> r) -> r
并且它是存在类型的常见编码,因为Haskell没有第一类存在性。您可以将此类型视为
exists f. ( Reifies f (i -> i -> i) , Foo f i )
或某些此类返回类型 f
以及f
是函数i -> i -> i
的类型级表示形式的证据,以及{ {1}}。在Foo f i
中观察使用此"存在性",我们用连续传递样式调用该函数,即
main
在该功能中,mkFoo (+) 5 $ \foo -> -- what to do with foo
的行为类似于foo
类型,其中Foo f0 Integer
是专为此功能制作的全新类型。
很不错,它让我们f0
来自不同的baz
Foo
,但不幸的是,它不够聪明允许我们使用对f
的不同调用,使用相同的函数baz
Foo
mkFoo
{/ 1}},所以:
mkFoo (+) 5 $ \foo1 -> mkFoo (+) 5 $ \foo2 -> foo1 `baz` foo2 -- type error
答案 1 :(得分:1)
这实际上与luqui的答案没有什么不同,但是我们不是在运行时使用f
定义从幻像类型reify
到具体函数的映射,而是在编译时执行此操作。这使代码更简单,更容易使用。
{-# LANGUAGE MultiParamTypeClasses #-}
{-# LANGUAGE FlexibleInstances #-}
class Bar k where
baz :: k -> k -> k
-- Foo has now another phantom type variable, that we use to pick the
-- desired f.
newtype Foo f k = B k
-- | GetF is used to retrieve a function for a given type label.
class GetF f k where
appF :: Foo f k -> Foo f k -> Foo f k
-- Now we can make an instance for Bar if we have an instance for GetF
instance GetF f k => Bar (Foo f k) where
baz x y = appF x y
-- = Usage example
-- | Add is just a label. We never use it at value level.
data Add
instance Num k => GetF Add k where
appF (B x) (B y) = B (x + y)
example :: Foo Add Int
example = B 1 `baz` B 2 -- = B 3
答案 2 :(得分:1)
这是对我的另一个答案的补充,如果你的意图是解决实际问题而不是探索可行的问题,我实际建议的解决方案。它只是将类型类转换为“字典传递样式”,并且不使用任何花哨的扩展或任何东西。
data Bar k = Bar { baz :: k -> k -> k }
newtype Foo i = B i
fooBar :: (i -> i -> i) -> Bar (Foo i)
fooBar f = Bar { baz = \(B x) (B y) -> B (f x y) }
然后当你有一个使用它的函数时,传递一个Bar
字典:
doThingsWithFoos :: Bar (Foo Int) -> Foo Int -> Foo Int -> Foo Int
doThingsWithFoos bar a b = baz bar a (baz bar a b)
使用起来有点冗长,但这种解决方案非常灵活。字典完全是一流的,因此,例如,您可以开始对字典本身进行更高级别的操作:
transportBar :: (a -> b) -> (b -> a) -> Bar a -> Bar b
transportBar f finv bar = Bar { baz = \x y -> f (baz bar (finv x) (finv y)) }
sumBar :: (Num a) => Bar a -> Bar a -> Bar a
sumBar bar1 bar2 = Bar { baz = \x y -> baz bar1 x y + baz bar2 x y }
这两种转换都是使用类型类的主要痛苦。
答案 3 :(得分:0)
鉴于\-{2}
仅取决于f
,您可以定义另一个类,这样可以打包这样的函数:
k