“类型家族”与“数据家族”,简而言之?

时间:2014-01-01 16:05:38

标签: haskell type-families

我对如何在data familytype family之间进行选择感到困惑。 TypeFamilies上的wiki页面详细介绍了很多内容。偶尔它会非正式地将Haskell的data family称为散文中的“类型族”,但当然Haskell中也有type family

有一个简单的示例显示了两个版本的代码的显示位置,区别仅在于声明了data familytype family

-- BAD: f is too ambiguous, due to non-injectivity
-- type family F a

-- OK
data family F a 

f :: F a -> F a
f = undefined

g :: F Int -> F Int
g x = f x
此处的

typedata具有相同的含义,但type family版本无法进行类型检查,而data family版本则可以,因为{{1} }“创建新类型,因此是单射的”(维基页面说)。

我从这一切中得到的结论是“对于简单的案例尝试data family,如果它不够强大,请尝试data family”。这很好,但我想更好地理解它。是否有维恩图或决策树我可以遵循以区分何时使用哪个?

2 个答案:

答案 0 :(得分:55)

(将评论中的有用信息提升为答案。)

独立与班级声明

声明类型族和/或数据族的两种语法不同的方法,它们在语义上是等价的:

单机:

type family Foo
data family Bar

或作为类型类的一部分:

class C where
   type Foo 
   data Bar 

都声明了一个类型系列,但在类型类中,family部分隐含class部分,因此GHC / Haskell缩写声明。

“新类型”与“类型同义词”/“类型别名”

data family F创建一个新类型,类似于data F = ...创建新类型的方式。

type family F不会创建新类型,类似于type F = Bar Baz未创建新类型的方式(它只是为现有类型创建别名/同义词)。

type family

的非注入性示例

来自Data.MonoTraversable.Element的示例(稍加修改):

import Data.ByteString as S
import Data.ByteString.Lazy as L

-- Declare a family of type synonyms, called `Element` 
-- `Element` has kind `* -> *`; it takes one parameter, which we call `container`
type family Element container

-- ByteString is a container for Word8, so...
-- The Element of a `S.ByteString` is a `Word8`
type instance Element S.ByteString = Word8 

-- and the Element of a `L.ByteString` is also `Word8`
type instance Element L.ByteString = Word8  

在类型族中,等式Word8的右侧命名现有类型;左边的东西创建了新的同义词:Element S.ByteStringElement L.ByteString

拥有同义词意味着我们可以将Element Data.ByteStringWord8

互换
-- `w` is a Word8....
>let w = 0::Word8

-- ... and also an `Element L.ByteString`
>:t w :: Element L.ByteString
w :: Element L.ByteString :: Word8

-- ... and also an `Element S.ByteString`
>:t w :: Element S.ByteString
w :: Element S.ByteString :: Word8

-- but not an `Int`
>:t w :: Int
Couldn't match expected type `Int' with actual type `Word8'

这些类型的同义词是“非内射的”(“单向”),因此是不可逆的。

-- As before, `Word8` matches `Element L.ByteString` ...
>(0::Word8)::(Element L.ByteString)

-- .. but  GHC can't infer which `a` is the `Element` of (`L.ByteString` or `S.ByteString` ?):

>(w)::(Element a)
Couldn't match expected type `Element a'
            with actual type `Element a0'
NB: `Element' is a type function, and may not be injective
The type variable `a0' is ambiguous

更糟糕的是,GHC甚至无法解决非模棱两可的案件!:

type instance Element Int = Bool
> True::(Element a)
> NB: `Element' is a type function, and may not be injective

注意使用“可能不是”!我认为GHC是保守的,拒绝检查Element是否真的是单射的。 (也许是因为程序员可以稍后添加另一个type instance,导入预编译模块后,会增加歧义。

data family

的注入性示例

相反:在数据族中,每个右侧包含一个唯一的构造函数,因此定义是单射(“可逆”)方程。

-- Declare a list-like data family
data family XList a

-- Declare a list-like instance for Char
data instance XList Char = XCons Char (XList Char) | XNil

-- Declare a number-like instance for ()
data instance XList () = XListUnit Int

-- ERROR: "Multiple declarations of `XListUnit'"
data instance XList () = XListUnit Bool
-- (Note: GHCI accepts this; the new declaration just replaces the previous one.)

使用data family,查看右侧的构造函数名称(XConsXListUnit) 足以让类型推断器知道我们必须使用XList ()而不是XList Char。由于构造函数名称是唯一的,因此这些定义 是单射/可逆的。

如果type“只是”声明一个同义词,为什么它在语义上有用?

通常,type个同义词只是缩写,但type家庭同义词增加了力量:它们可以使一个简单的类型(种类*)成为“类型的同义词” * -> *应用于参数“:

type instance F A = B

使BF a匹配。例如,在Data.MonoTraversable中使用此选项可以创建Word8类型的简单类型Element a -> a匹配函数(上面定义了Element)。

例如,(有点傻),假设我们的版本const仅适用于“相关”类型:

> class Const a where constE :: (Element a) -> a -> (Element a)
> instance Const S.ByteString where constE = const

> constE (0::Word8) undefined
ERROR: Couldn't match expected type `Word8' with actual type `Element a0'

-- By providing a match `a = S.ByteString`, `Word8` matches `(Element S.ByteString)`
> constE (0::Word8) (undefined::S.ByteString)  
0

-- impossible, since `Char` cannot match `Element a` under our current definitions.
> constF 'x' undefined 

答案 1 :(得分:34)

我认为任何决策树或维恩图都不会存在,因为类型和数据系列的应用程序非常广泛。

一般来说,您已经突出了关键的设计差异,我同意您的意见,首先看看您是否可以逃脱data family

对我而言,关键点在于data family的每个实例都会创建一个新类型,这会大大限制功率,因为​​您不能做通常最自然的事情并使现有类型成为实例

例如,Haskell wiki page on "indexed types"上的GMapKey示例非常适合数据系列:

class GMapKey k where
  data GMap k :: * -> *
  empty       :: GMap k v
  lookup      :: k -> GMap k v -> Maybe v
  insert      :: k -> v -> GMap k v -> GMap k v

映射k的键类型是数据族的参数,实际映射类型是数据族(GMap k)的结果。作为GMapKey实例的用户,您可能很高兴GMap k类型对您来说是抽象的,只需通过类型类中的通用映射操作来操作它。

相比之下,同一维基页面上的Collects示例却恰恰相反:

class Collects ce where
  type Elem ce
  empty  :: ce
  insert :: Elem ce -> ce -> ce
  member :: Elem ce -> ce -> Bool
  toList :: ce -> [Elem ce]

参数类型是集合,结果类型是集合的元素。通常,用户将希望直接使用该类型上的常规操作来操作这些元素。例如,集合可能是IntSet,元素可能是Int。将Int包裹在其他类型中会非常不方便。

注意 - 这两个示例使用类型类,因此不需要family关键字,因为声明类型类中的类型意味着它必须是一个族。完全相同的考虑适用于独立家庭,但这只是一个抽象如何组织的问题。