首页 文章

在Haskell中创建许多类似的newtypes / typeclass实例

提问于
浏览
2

我是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 是这个的一个实例时,我意识到我希望能够"vary"与该类型的函数 f . 要明确:我想提供一个函数 f :: Num a => a -> a -> a 并返回一个新类型 Foo ,它是 Bar 的一个实例 . 说我想做5次,10次,唯一不同的是不同的功能 f . 我当然可以复制并粘贴上面的代码,但我想知道是否还有其他方法?

在我看来,我很困惑 . 在Haskell中做这样的事情的最佳方法是什么?这是一个很好的设计选择,我在想什么是对/错?为什么?

编辑:我意识到一个具体的例子可能有助于使问题更清楚(请注意,它可能看起来很复杂,我不能简化代码而不是这个 . 上面的问题包含我认为的相同信息):我是类型类感兴趣的是 Algebra k v 来自图书馆HaskellForMaths

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)

然后根据我的喜好改变 Map structure . 这里 T 类型只是编写抽象基础元素 T 1, T 2, ... 的便捷方式 . 我想要这样做的原因是代数的结构常数的标准数学定义(这里: structure ) . 总结一下:我希望能够改变函数 f (最好不要在编译时?)并取回代数 . 这可能是一个糟糕的设计决定:如果是这样,为什么?

4 回答

  • 1

    你可以使用reflection . 这是一种相当先进的技术,可能有更好的方法来解决您的问题,但您已经说明了它的方式,这似乎是您正在寻找的 .

    {-# 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)
    

    是一个约束,表示 fk -> 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 的类型级表示和 Foo f i 的证据 . 在 main 中观察要使用这个"existential",我们用连续传递样式调用该函数,即

    mkFoo (+) 5 $ \foo -> -- what to do with foo
    

    在函数中, foo 的行为类似于 Foo f0 Integer 类型,其中 f0 是专为此函数制作的全新类型 .

    它让我们 baz 来自不同的 f s,但遗憾的是它不够聪明,不能让我们 baz 一起 Foo 用相同的函数使用不同的 mkFoo 调用,所以:

    mkFoo (+) 5 $ \foo1 -> mkFoo (+) 5 $ \foo2 -> foo1 `baz` foo2  -- type error
    
  • 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 }
    

    这两种转换都是使用类型类的主要痛苦 .

  • 1

    这实际上与luqui的答案没有什么不同,而是使用 reify 而不是在运行时定义从幻像类型 f 到具体函数的映射我们在编译时这样做 . 这使代码更简单,更容易使用 .

    {-# 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
    
  • 0

    鉴于 f 仅取决于 k ,您可以定义另一个类,它将打包这样的函数,如下所示:

    newtype Foo i = B i deriving (Eq, Ord, Show)
    
    class PreBar k where
      preBar :: k -> k -> k
    
    class Bar k where
      baz :: k -> k -> k
    
    instance (Num k, PreBar k) => Bar (Foo k) where
        baz (B a) (B b) = B (preBar a b)
    

相关问题