展开Haskell数据类型

展开Haskell数据类型,haskell,types,Haskell,Types,是否可以用新值扩展数据类型 例如: 汇编如下: data Axes2D = X | Y data Axes3D = Axes2D | Z 但是,以下几点: data Axes2D = X | Y deriving (Show, Eq) data Axes3D = Axes2D | Z deriving (Show, Eq) type Point2D = (Int, Int) type Point3D = (Int, Int, Int) move_along_axis_2D :: Point

是否可以用新值扩展数据类型

例如: 汇编如下:

data Axes2D = X | Y
data Axes3D = Axes2D | Z
但是,以下几点:

data Axes2D = X | Y deriving (Show, Eq)
data Axes3D = Axes2D | Z deriving (Show, Eq)

type Point2D = (Int, Int)
type Point3D = (Int, Int, Int)

move_along_axis_2D :: Point2D -> Axes2D -> Int -> Point2D
move_along_axis_2D (x, y) axis move | axis == X = (x + move, y)
                                    | otherwise = (x, y + move)

move_along_axis_3D :: Point3D -> Axes3D -> Int -> Point3D
move_along_axis_3D (x, y, z) axis move | axis == X = (x + move, y, z)
                                       | axis == y = (x, y + move, z)
                                       | otherwise = (x, y, z + move) 
给出以下编译错误(
沿\u轴移动\u 3D
注释掉不会给出错误):

因此,是否可以将
Axes2D
类型的
X
Y
以及
Axes3D
类型的

如果可能的话:我做错了什么?否则:为什么不可能?

不可能。注意,在

data Axes2D = X | Y
data Axes3D = Axes2D | Z
Axes3D
类型中的
Axes2D
是一个不带参数的值构造函数,因此
Axes3D
有两个构造函数,
Axes2D
Z

不同类型的值构造函数不能具有相同的名称(在相同的范围内),因为这将使类型推断不可能。你会怎么做

foo X = True
foo _ = False

作为一种类型你有吗?(与参数化类型有点不同,所有
可能a
都有同名的值构造函数,这是可行的。但这是因为
可能
接受一个类型参数,并且名称仅在使用相同(一元)类型构造函数构造的类型之间共享。它不适用于空类型构造函数。)

与丹尼尔·菲舍尔(Daniel Fischer)所说的一起,进一步说明为什么这是不可能的:你想要的子类型的问题不仅仅是命名模糊;一般来说,它们使类型推断更加困难。由于这个原因,我认为Scala的类型推断比Haskell的更受限制和局部性

但是,您可以使用类型类系统对这类事情进行建模:

class (Eq t) => HasAxes2D t where
  axisX :: t
  axisY :: t

class (HasAxes2D t) => HasAxes3D t where
  axisZ :: t

data Axes2D = X | Y deriving (Eq, Show)
data Axes3D = TwoD Axes2D | Z deriving (Eq, Show)

instance HasAxes2D Axes2D where
  axisX = X
  axisY = Y

instance HasAxes2D Axes3D where
  axisX = TwoD X
  axisY = TwoD Y

instance HasAxes3D Axes3D where
  axisZ = Z
然后,您可以使用防护装置对这些值进行“模式匹配”:

displayAxis :: (HasAxes2D t) => t -> String
displayAxis axis
  | axis == axisX = "X"
  | axis == axisY = "Y"
  | otherwise = "Unknown"
这与子类型化有许多相同的缺点:使用
axisX
axisY
axisZ
将有变得模棱两可的趋势,需要类型注释,而这些注释不符合练习的要点。与使用具体类型相比,使用这些类型类约束编写类型签名也有点难看

还有另一个缺点:对于具体类型,当您使用
Axes2D
编写函数时,一旦处理了
X
Y
,您就知道已经涵盖了所有可能的值。使用类型类解决方案,没有任何东西可以阻止您将
Z
传递给一个需要
HasAxes2D
实例的函数。您真正想要的是将关系反过来,这样您可以将
X
Y
传递给需要三维轴的函数,但不能将
Z
传递给需要二维轴的函数。我认为没有任何方法可以用Haskell的类型类系统正确地建模


这种技术有时很有用——例如,将一个OOP库(如GUI工具包)绑定到Haskell——但一般来说,更自然的做法是使用具体的类型,并明确支持OOP术语中的调用,即在构造函数中显式包装“子类型”。处理构造函数包装/展开通常不太麻烦,而且更灵活。

可以使用广义代数数据类型。我们可以使用具有类型约束的数据构造函数创建泛型(GADT)类型。然后我们可以定义指定完整类型的专用类型(类型别名),从而限制允许使用哪些构造函数

{-# LANGUAGE GADTs #-}

data Zero
data Succ a

data Axis a where
  X :: Axis (Succ a)
  Y :: Axis (Succ (Succ a))
  Z :: Axis (Succ (Succ (Succ a)))

type Axis2D = Axis (Succ (Succ Zero))
type Axis3D = Axis (Succ (Succ (Succ Zero)))
现在,保证只将
X
Y
传递到定义为接受
Axis2D
参数的函数中。构造函数
Z
无法匹配
Axis2D
的类型

不幸的是,GADT不支持自动
派生
,因此您需要提供自己的实例,例如:

instance Show (Axis a) where
  show X = "X"
  show Y = "Y"
  show Z = "Z"
instance Eq (Axis a) where
  X == X = True
  Y == Y = True
  Z == Z = True
  _ == _ = False

啊,
Axes3D
中的
Axes2D
是一个值构造函数,而另一个
Axes2D
是一个类型构造函数。所以这些是不同的,我说的对吗?foo会有type
a->Bool
?首先正确,类型构造函数和值构造函数甚至在不同的命名空间中。由于没有同时出现类型构造函数和值构造函数的地方,因此可以根据名称所表示的语法来确定,因此值构造函数与类型构造函数具有相同的名称没有问题。第二个错误,因为只有极少数类型具有值构造函数
X
。因此,如果可能的话,foo的类型是(由语法组成)
foo::(HasConstructor X a)=>a->foo
@Xochipilli:No;无法判断任何类型的未知值是否为
X
;那会打破的。它必须类似于
foo::(a[has constructor]X)=>a->Bool
(事实上,
(a[has constructor]X)
基本上对应于我答案的
(hasaxes2da)
,除了我将
X
Y
捆绑到同一个类型类中之外,如果将
Axis2D
Axis3D
放在单独的模块中,两种数据类型对于某些构造函数可能具有相同的名称。但是,如果您希望同时使用这两个模块,则必须导入它们,以解决歧义。
instance Show (Axis a) where
  show X = "X"
  show Y = "Y"
  show Z = "Z"
instance Eq (Axis a) where
  X == X = True
  Y == Y = True
  Z == Z = True
  _ == _ = False