在Haskell

时间:2017-09-21 15:58:59

标签: haskell types typeclass newtype

我是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中做这样的事情的最佳方法是什么?这是一个很好的设计选择,我在想什么是对/错?为什么?

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

4 个答案:

答案 0 :(得分: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)

是一个约束,表示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