Haskell:与自己的数据类型混淆。记录语法和唯一字段

Haskell:与自己的数据类型混淆。记录语法和唯一字段,haskell,Haskell,我刚刚发现了这一困惑,并希望确认它是什么。当然,除非我错过了什么 比如,我有这些数据声明: data VmInfo = VmInfo {name, index, id :: String} deriving (Show) data HostInfo = HostInfo {name, index, id :: String} deriving (Show) vm = VmInfo "vm1" "01" "74653" host = HostInfo "host1" "02" "98732"

我刚刚发现了这一困惑,并希望确认它是什么。当然,除非我错过了什么

比如,我有这些数据声明:

data VmInfo = VmInfo {name, index, id :: String} deriving (Show)
data HostInfo = HostInfo {name, index, id :: String} deriving (Show)

vm = VmInfo "vm1" "01" "74653"
host = HostInfo "host1" "02" "98732"
我一直认为,而且似乎如此自然和合乎逻辑的是:

vmName = vm.name
hostName = host.name
但这显然是行不通的。我知道了


问题 所以我的问题是

  • 使用记录语法创建数据类型时,是否必须确保所有字段都具有唯一的名称?如果是,为什么

  • 是否有一种干净的方法或类似于“范围解析运算符”的方法,如
    等,以便Haskell区分
    名称(或任何其他非唯一字段)所属的数据类型并返回正确的结果

  • 如果我有几个具有相同字段名的声明,那么正确的处理方法是什么


作为旁注。 通常,我需要返回与上面示例类似的数据类型。 首先,我以元组的形式返回它们(当时我认为这是正确的方法)。但是元组很难使用,因为它不可能像使用“!!”的列表那样简单地提取复杂类型的各个部分。接下来我想到了字典/散列。 当我尝试使用字典时,我想拥有自己的数据类型有什么意义呢? 播放/学习数据类型我遇到了导致我提出上述问题的事实。 因此,使用字典而不是自己的数据类型似乎更容易,因为我可以为不同的对象使用相同的字段



您能详细说明一下这一点并告诉我在现实世界中是如何做到的吗?

是的,同一模块中不能有两个字段名相同的记录。字段名作为函数添加到模块的作用域中,因此您可以使用
name vm
而不是
vm.name
。您可以在不同的模块中有两个具有相同字段名的记录,并导入其中一个限定为某个名称的模块,但这可能会很难处理

对于这种情况,您可能只需要使用普通代数数据类型:

data VMInfo = VMInfo String String String
(请注意,
VMInfo
必须大写。)

现在,您可以通过模式匹配访问
VMInfo
的字段:

myFunc (VMInfo name index id) = ... -- name, index and id are bound here

目前,命名字段是顶级函数,因此在一个作用域中只能有一个具有该名称的函数。有计划创建一个新的记录系统,允许在同一范围内的不同记录类型中具有相同名称的字段,但这仍处于设计阶段


目前,您可以使用唯一的字段名,或者在自己的模块中定义每个类型,并使用模块限定名。

Haskell记录语法有点老套,但记录名作为函数出现,并且该函数必须具有唯一的类型。因此,您可以在单个数据类型的构造函数之间共享记录字段名称,但不能在不同的数据类型之间共享

如果我有几个具有相同字段名的声明,那么正确的处理方法是什么

你不能。必须使用不同的字段名。如果希望从记录中选择重载名称,可以尝试使用类型类。但基本上,Haskell中的字段名并不像C或Pascal中那样工作。称之为“记录语法”可能是一个错误

但是元组很难使用,因为不可能提取复杂类型的各个部分

实际上,使用模式匹配很容易做到这一点。范例

smallId :: VmInfo -> Bool
smallId (VmInfo { vmId = n }) = n < 10
data VmInfo a = VmInfo { vmId :: Int, vmName :: String, vmInfo :: a }
现在您可以拥有
VmInfo字符串
VmInfo字典
VmInfo节点
,或者任何您想要的东西


小结:每个字段名必须属于唯一的类型,有经验的Haskell程序员使用静态类型系统,而不是试图绕过它。而且你肯定想学习模式匹配。

这不起作用的原因还有很多:小写的类型名和数据构造函数,使用
进行OO语言风格的成员访问。在Haskell中,这些成员访问函数实际上是自由函数,即
vmName=name vm
而不是
vmName=vm.name
,这就是为什么它们在不同的数据类型中不能有相同的名称

如果您确实希望函数能够同时在
VmInfo
HostInfo
对象上运行,则需要一个类型类,例如

class MachineInfo m where
  name :: m -> String
  index :: m -> String    -- why String anyway? Shouldn't this be an Int?
  id :: m -> String
并举例说明

instance MachineInfo VmInfo where
  name (VmInfo vmName _ _) = vmName
  index (VmInfo _ vmIndex _) = vmIndex
  ...
instance MachineInfo HostInfo where
  ...

然后,如果
machine
VmInfo
以及
HostInfo
name machine
将起作用,镜头可以帮助您减轻获取和设置数据结构元素的痛苦,尤其是当它们嵌套时。如果你眯着眼睛看的话,它们会给你一些东西,有点像面向对象的访问器

在此了解有关镜头系列类型和功能的更多信息:

作为它们外观的示例,这是上面github页面中Pong示例的一个片段:

data Pong = Pong
  { _ballPos :: Point
  , _ballSpeed :: Vector
  , _paddle1 :: Float
  , _paddle2 :: Float
  , _score :: (Int, Int)
  , _vectors :: [Vector]

  -- Since gloss doesn't cover this, we store the set of pressed keys
  , _keys :: Set Key
  }

-- Some nice lenses to go with it
makeLenses ''Pong
这使得镜头可以通过一些TemplateHaskell魔法访问成员,而无需下划线

稍后,有一个使用它们的示例:

-- Update the paddles
updatePaddles :: Float -> State Pong ()
updatePaddles time = do
  p <- get

  let paddleMovement = time * paddleSpeed
      keyPressed key = p^.keys.contains (SpecialKey key)

  -- Update the player's paddle based on keys
  when (keyPressed KeyUp) $ paddle1 += paddleMovement
  when (keyPressed KeyDown) $ paddle1 -= paddleMovement

  -- Calculate the optimal position
  let optimal = hitPos (p^.ballPos) (p^.ballSpeed)
      acc = accuracy p
      target = optimal * acc + (p^.ballPos._y) * (1 - acc)
      dist = target - p^.paddle2

  -- Move the CPU's paddle towards this optimal position as needed
  when (abs dist > paddleHeight/3) $
    case compare dist 0 of
      GT -> paddle2 += paddleMovement
      LT -> paddle2 -= paddleMovement
      _ -> return ()

  -- Make sure both paddles don't leave the playing area
  paddle1 %= clamp (paddleHeight/2)
  paddle2 %= clamp (paddleHeight/2)
——更新拨杆
updatePaddles::Float->State Pong()
updatePaddles time=do
p(高度/3)$
案例比较区0
燃气轮机->桨叶2+=桨叶运动
LT->桨叶2-=桨叶运动
_->返回()
--确保两个球拍都没有离开比赛场地
桨叶1%=夹持器(桨叶高度/2)
桨叶2%=夹持器(桨叶高度/2)

我建议在原始位置查看整个程序,并查看镜头材料的其余部分;即使你最终没有使用它们,它也非常有趣。

谢谢你。遗憾的是Haskell没有数据类型的范围解析操作符。对于普通的ADT,为其编写访问器函数似乎有点乏味