【问题标题】:Creating many similar newtypes/typeclass instances in Haskell在 Haskell 中创建许多类似的新类型/类型类实例
【发布时间】:2017-09-21 15:58:59
【问题描述】:

我是 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 定义为它的一个实例时,我意识到我希望能够使用类型“改变”函数f。明确一点:我想提供一个函数f :: Num a => a -> a -> a 并返回一个新类型Foo,它是Bar 的一个实例。假设我想这样做 5、10 次,唯一的区别是不同的功能 f。上面的代码我当然可以复制粘贴,不过不知道有没有别的办法呢?

在我看来,我把事情搞糊涂了。在 Haskell 中做这样的事情的最好方法是什么?这是一个很好的设计选择吗?我在想什么是对的/错的,为什么?

编辑:我意识到一个具体的例子可能有助于使问题更清楚(请注意,它可能看起来很复杂,我无法简化代码。上面的问题包含我认为的相同信息):我感兴趣的 typeclass 是来自库 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(最好不是在编译时?)并取回代数。这可能是一个糟糕的设计决策:如果是,为什么?

【问题讨论】:

  • 你能提供一个你打算如何使用它的例子吗?和/或可能提供第二个实例以明确冗余?更具描述性的名称也可能会有所帮助。 Foo 可以是Functor/Applicative 使用类似f <$> B a <*> B b 的东西
  • Bar 类型类看起来像 Semigroup。如果二元运算也有标识,则可能是Monoid。
  • 你能完全跳过 newtype 和 typeclass 并直接传递(+)(或任何其他你想要的函数)吗?
  • 我提供了一个我打算如何使用它的例子!我现在时间不多,稍后会尝试澄清更多,同时感谢所有回复!如果我可以跳过类型类、monoid 属性等,我将需要一些时间以有意义的方式回答。

标签: haskell types typeclass newtype


【解决方案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 和Foo f i 的类型级表示的证据。在main 中观察到,要使用这个“存在”,我们调用具有延续传递风格的函数,即

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

在函数中,foo 的行为就像它具有 Foo f0 Integer 类型,其中 f0 是专为此函数创建的全新类型。

它不会让我们 baz 和 Foos 来自不同的 fs 非常好,但不幸的是它不够聪明,无法让我们 baz 在一起 Foos 使用相同的函数使用对mkFoo 的不同调用,所以:

mkFoo (+) 5 $ \foo1 -> mkFoo (+) 5 $ \foo2 -> foo1 `baz` foo2  -- type error

【讨论】:

    【解决方案2】:

    这是对我的其他答案的补充,如果您的意图是解决实际问题而不是探索可能的问题,我实际上会建议的解决方案。它只是将类型类转换为“字典传递样式”,并且不使用任何花哨的扩展或任何东西。

    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】:

      这实际上与 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
      

      【讨论】:

        【解决方案4】:

        鉴于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)
        

        【讨论】:

          猜你喜欢
          • 2016-12-05
          • 2015-02-03
          • 1970-01-01
          • 2015-09-07
          • 2016-08-12
          • 1970-01-01
          • 2023-03-19
          • 1970-01-01
          • 2017-01-14
          相关资源
          最近更新 更多