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

时间:2012-02-20 00:23:59

标签: 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"

我一直在想,这似乎是如此自然和合乎逻辑:

vmName = vm.name
hostName = host.name

但显然,这不起作用。我明白了。


问题

所以我的问题是。

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

  • 是否有一种干净的方式或类似于“范围解析运算符”的内容,如::.等,以便Haskell区分{{1}的数据类型(或任何其他没有唯一字段)属于并返回正确的结果?

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


作为旁注。

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


请您详细说明并告诉我它在现实世界中的表现如何?

5 个答案:

答案 0 :(得分:17)

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

  

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

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

  

但是元组很难处理,因为不可能提取复杂类型的单个部分

实际上,使用模式匹配可以非常简单。实施例

smallId :: VmInfo -> Bool
smallId (VmInfo { vmId = n }) = n < 10

至于如何在“现实世界”中完成,Haskell程序员往往非常依赖于知道每个字段在编译时的类型。如果希望字段类型发生变化,Haskell程序员会引入类型参数来携带不同的信息。实施例

data VmInfo a = VmInfo { vmId :: Int, vmName :: String, vmInfo :: a }

现在,您可以拥有VmInfo StringVmInfo DictionaryVmInfo Node或其他任何内容。

摘要:每个字段名称必须属于唯一类型,经验丰富的Haskell程序员使用静态类型系统而不是尝试解决它。你肯定想了解模式匹配。

答案 1 :(得分:10)

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

如果您真的想要可以对VmInfoHostInfo对象进行操作的函数,则需要一个类型类,例如

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
  ...

如果name machinemachine以及VmInfo,则HostInfo将有效。

答案 2 :(得分:5)

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

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

答案 3 :(得分:5)

镜头可以帮助您解决获取和设置数据结构元素时遇到的一些麻烦,特别是当它们嵌套时。如果你眯着眼睛,它们会给你一些外观,就像面向对象的访问器一样。

在此处详细了解Lens系列类型和功能:http://lens.github.io/tutorial.html

作为它们外观的示例,这是来自上述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)

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

答案 4 :(得分:4)

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

对于这样的情况,您应该只使用普通的代数数据类型:

data VMInfo = VMInfo String String String

(请注意,VMInfo 大写。)

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

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