ConstraintKinds 在一个超级简单的例子中解释
ConstraintKinds explained on a super simple example
什么是 Constraint kind?
为什么有人会使用它(在实践中)?
它有什么用?
能否给出一个简单的代码示例来说明前两个问题的答案?
为什么在 this 代码中使用它?
好吧,我会提到它允许你做的两件实用的事情:
- 通过类型class约束对类型进行参数化
- 编写类型 classes 允许它们的实例指定它们需要的约束。
也许最好用一个例子来说明这一点。 classic Haskell 缺点之一是您不能为类型参数强加 class 约束的类型创建 Functor
实例;例如,containers
库中的 Set
class 需要对其元素进行 Ord
约束。原因是在 "vanilla" Haskell 中,您必须对 class 本身进行约束:
class OrdFunctor f where
fmap :: Ord b => (a -> b) -> f a -> f b
...但是此 class 仅适用于特别需要 Ord
约束的类型。不是通用的解决方案!
那么,如果我们可以采用 class 定义并抽象出 Ord
约束,允许各个实例说明它们需要什么约束呢?那么,ConstraintKinds
加上 TypeFamilies
允许:
{-# LANGUAGE ConstraintKinds, TypeFamilies, FlexibleInstances #-}
import Prelude hiding (Functor(..))
import GHC.Exts (Constraint)
import Data.Set (Set)
import qualified Data.Set as Set
-- | A 'Functor' over types that satisfy some constraint.
class Functor f where
-- | The constraint on the allowed element types. Each
-- instance gets to choose for itself what this is.
type Allowed f :: * -> Constraint
fmap :: Allowed f b => (a -> b) -> f a -> f b
instance Functor Set where
-- | 'Set' gets to pick 'Ord' as the constraint.
type Allowed Set = Ord
fmap = Set.map
instance Functor [] where
-- | And `[]` can pick a different constraint than `Set` does.
type Allowed [] = NoConstraint
fmap = map
-- | A dummy class that means "no constraint."
class NoConstraint a where
-- | All types are trivially instances of 'NoConstraint'.
instance NoConstraint a where
(请注意,这并不是将 Functor
实例化为 Set
的唯一障碍;参见 this discussion. Also, 。)
不过,这种解决方案目前还没有被普遍采用,因为 ConstraintKinds
或多或少仍是一项新功能。
ConstraintKinds
的另一个用途是通过 class 约束或 class 对类型进行参数化。我将重现 this Haskell "Shape Example" code that I wrote:
{-# LANGUAGE GADTs, ConstraintKinds, KindSignatures, DeriveDataTypeable #-}
{-# LANGUAGE TypeOperators, ScopedTypeVariables, FlexibleInstances #-}
module Shape where
import Control.Applicative ((<$>), (<|>))
import Data.Maybe (mapMaybe)
import Data.Typeable
import GHC.Exts (Constraint)
-- | Generic, reflective, heterogeneous container for instances
-- of a type class.
data Object (constraint :: * -> Constraint) where
Obj :: (Typeable a, constraint a) => a -> Object constraint
deriving Typeable
-- | Downcast an 'Object' to any type that satisfies the relevant
-- constraints.
downcast :: forall a constraint. (Typeable a, constraint a) =>
Object constraint -> Maybe a
downcast (Obj (value :: b)) =
case eqT :: Maybe (a :~: b) of
Just Refl -> Just value
Nothing -> Nothing
这里Object
类型的参数是class类型(种类* -> Constraint
),所以你可以有像Object Shape
这样的类型,其中Shape
是 class:
class Shape shape where
getArea :: shape -> Double
-- Note how the 'Object' type is parametrized by 'Shape', a class
-- constraint. That's the sort of thing ConstraintKinds enables.
instance Shape (Object Shape) where
getArea (Obj o) = getArea o
Object
类型的作用是两个特征的组合:
- 一种存在类型(由
GADTs
启用),它允许我们将不同类型的值存储在相同的 Object
类型中。
ConstraintKinds
,这使我们能够让 Object
类型的用户指定约束,而不是将 Object
硬编码到某些特定的 class 约束集他们想要作为 Object
类型的参数。
现在我们不仅可以制作 Shape
个实例的异构列表:
data Circle = Circle { radius :: Double }
deriving Typeable
instance Shape Circle where
getArea (Circle radius) = pi * radius^2
data Rectangle = Rectangle { height :: Double, width :: Double }
deriving Typeable
instance Shape Rectangle where
getArea (Rectangle height width) = height * width
exampleData :: [Object Shape]
exampleData = [Obj (Circle 1.5), Obj (Rectangle 2 3)]
...但是由于 Object
中的 Typeable
约束,我们可以 向下转换 :如果我们正确猜测 [=29 中包含的类型=],我们可以恢复原来的类型:
-- | For each 'Shape' in the list, try to cast it to a Circle. If we
-- succeed, then pass the result to a monomorphic function that
-- demands a 'Circle'. Evaluates to:
--
-- >>> example
-- ["A Circle of radius 1.5","A Shape with area 6.0"]
example :: [String]
example = mapMaybe step exampleData
where step shape = describeCircle <$> (downcast shape)
<|> Just (describeShape shape)
describeCircle :: Circle -> String
describeCircle (Circle radius) = "A Circle of radius " ++ show radius
describeShape :: Shape a => a -> String
describeShape shape = "A Shape with area " ++ show (getArea shape)
什么是 Constraint kind?
为什么有人会使用它(在实践中)?
它有什么用?
能否给出一个简单的代码示例来说明前两个问题的答案?
为什么在 this 代码中使用它?
好吧,我会提到它允许你做的两件实用的事情:
- 通过类型class约束对类型进行参数化
- 编写类型 classes 允许它们的实例指定它们需要的约束。
也许最好用一个例子来说明这一点。 classic Haskell 缺点之一是您不能为类型参数强加 class 约束的类型创建 Functor
实例;例如,containers
库中的 Set
class 需要对其元素进行 Ord
约束。原因是在 "vanilla" Haskell 中,您必须对 class 本身进行约束:
class OrdFunctor f where
fmap :: Ord b => (a -> b) -> f a -> f b
...但是此 class 仅适用于特别需要 Ord
约束的类型。不是通用的解决方案!
那么,如果我们可以采用 class 定义并抽象出 Ord
约束,允许各个实例说明它们需要什么约束呢?那么,ConstraintKinds
加上 TypeFamilies
允许:
{-# LANGUAGE ConstraintKinds, TypeFamilies, FlexibleInstances #-}
import Prelude hiding (Functor(..))
import GHC.Exts (Constraint)
import Data.Set (Set)
import qualified Data.Set as Set
-- | A 'Functor' over types that satisfy some constraint.
class Functor f where
-- | The constraint on the allowed element types. Each
-- instance gets to choose for itself what this is.
type Allowed f :: * -> Constraint
fmap :: Allowed f b => (a -> b) -> f a -> f b
instance Functor Set where
-- | 'Set' gets to pick 'Ord' as the constraint.
type Allowed Set = Ord
fmap = Set.map
instance Functor [] where
-- | And `[]` can pick a different constraint than `Set` does.
type Allowed [] = NoConstraint
fmap = map
-- | A dummy class that means "no constraint."
class NoConstraint a where
-- | All types are trivially instances of 'NoConstraint'.
instance NoConstraint a where
(请注意,这并不是将 Functor
实例化为 Set
的唯一障碍;参见 this discussion. Also,
不过,这种解决方案目前还没有被普遍采用,因为 ConstraintKinds
或多或少仍是一项新功能。
ConstraintKinds
的另一个用途是通过 class 约束或 class 对类型进行参数化。我将重现 this Haskell "Shape Example" code that I wrote:
{-# LANGUAGE GADTs, ConstraintKinds, KindSignatures, DeriveDataTypeable #-}
{-# LANGUAGE TypeOperators, ScopedTypeVariables, FlexibleInstances #-}
module Shape where
import Control.Applicative ((<$>), (<|>))
import Data.Maybe (mapMaybe)
import Data.Typeable
import GHC.Exts (Constraint)
-- | Generic, reflective, heterogeneous container for instances
-- of a type class.
data Object (constraint :: * -> Constraint) where
Obj :: (Typeable a, constraint a) => a -> Object constraint
deriving Typeable
-- | Downcast an 'Object' to any type that satisfies the relevant
-- constraints.
downcast :: forall a constraint. (Typeable a, constraint a) =>
Object constraint -> Maybe a
downcast (Obj (value :: b)) =
case eqT :: Maybe (a :~: b) of
Just Refl -> Just value
Nothing -> Nothing
这里Object
类型的参数是class类型(种类* -> Constraint
),所以你可以有像Object Shape
这样的类型,其中Shape
是 class:
class Shape shape where
getArea :: shape -> Double
-- Note how the 'Object' type is parametrized by 'Shape', a class
-- constraint. That's the sort of thing ConstraintKinds enables.
instance Shape (Object Shape) where
getArea (Obj o) = getArea o
Object
类型的作用是两个特征的组合:
- 一种存在类型(由
GADTs
启用),它允许我们将不同类型的值存储在相同的Object
类型中。 ConstraintKinds
,这使我们能够让Object
类型的用户指定约束,而不是将Object
硬编码到某些特定的 class 约束集他们想要作为Object
类型的参数。
现在我们不仅可以制作 Shape
个实例的异构列表:
data Circle = Circle { radius :: Double }
deriving Typeable
instance Shape Circle where
getArea (Circle radius) = pi * radius^2
data Rectangle = Rectangle { height :: Double, width :: Double }
deriving Typeable
instance Shape Rectangle where
getArea (Rectangle height width) = height * width
exampleData :: [Object Shape]
exampleData = [Obj (Circle 1.5), Obj (Rectangle 2 3)]
...但是由于 Object
中的 Typeable
约束,我们可以 向下转换 :如果我们正确猜测 [=29 中包含的类型=],我们可以恢复原来的类型:
-- | For each 'Shape' in the list, try to cast it to a Circle. If we
-- succeed, then pass the result to a monomorphic function that
-- demands a 'Circle'. Evaluates to:
--
-- >>> example
-- ["A Circle of radius 1.5","A Shape with area 6.0"]
example :: [String]
example = mapMaybe step exampleData
where step shape = describeCircle <$> (downcast shape)
<|> Just (describeShape shape)
describeCircle :: Circle -> String
describeCircle (Circle radius) = "A Circle of radius " ++ show radius
describeShape :: Shape a => a -> String
describeShape shape = "A Shape with area " ++ show (getArea shape)