r.sendecky
r.sendecky

Reputation: 10363

Haskell: Confusion with own data types. Record syntax and unique fields

I just uncovered this confusion and would like a confirmation that it is what it is. Unless, of course, I am just missing something.

Say, I have these data declarations:

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"

What I always thought and what seems to be so natural and logical is this:

vmName = vm.name
hostName = host.name

But this, obviously, does not work. I got this.


Questions

So my questions are.


As a side note.

In general, I need to return data types similar to the above example. First I returned them as tuples (seemed to me the correct way at the time). But tuples are hard to work with as it is impossible to extract individual parts of a complex type as easy as with the lists using "!!". So next thing I thought of the dictionaries/hashes. When I tried using dictionaries I thought what is the point of having own data types then? Playing/learning data types I encountered the fact that led me to the above question. So it looks like it is easier for me to use dictionaries instead of own data types as I can use the same fields for different objects.


Can you please elaborate on this and tell me how it is done in real world?

Upvotes: 16

Views: 6289

Answers (5)

Levi Pearson
Levi Pearson

Reputation: 4984

Lenses can help take some of the pain out of dealing with getting and setting data structure elements, especially when they get nested. They give you something that looks, if you squint, kind of like object-oriented accessors.

Learn more about the Lens family of types and functions here: http://lens.github.io/tutorial.html

As an example for what they look like, this is a snippet from the Pong example found at the above github page:

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

That makes lenses to access the members without the underscores via some TemplateHaskell magic.

Later on, there's an example of using them:

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

I recommend checking out the whole program in its original location and looking through the rest of the lens material; it's very interesting even if you don't end up using them.

Upvotes: 5

leftaroundabout
leftaroundabout

Reputation: 120711

There are more reasons why this doesn't work: lowercase typenames and data constructors, OO-language-style member access with .. In Haskell, those member access functions actually are free functions, i.e. vmName = name vm rather than vmName = vm.name, that's why they can't have same names in different data types.

If you really want functions that can operate on both VmInfo and HostInfo objects, you need a type class, such as

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

and make instances

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

Then name machine will work if machine is a VmInfo as well as if it's a HostInfo.

Upvotes: 11

Norman Ramsey
Norman Ramsey

Reputation: 202505

Haskell record syntax is a bit of a hack, but the record name emerges as a function, and that function has to have a unique type. So you can share record-field names among constructors of a single datatype but not among distinct datatypes.

What is the correct way to deal with this if I have several declarations with the same field names?

You can't. You have to use distinct field names. If you want an overloaded name to select from a record, you can try using a type class. But basically, field names in Haskell don't work the way they do in say, C or Pascal. Calling it "record syntax" might have been a mistake.

But tuples are hard to work with as it is impossible to extract individual parts of a complex type

Actually, this can be quite easy using pattern matching. Example

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

As to how this is done in the "real world", Haskell programmers tend to rely heavily on knowing what type each field is at compile time. If you want the type of a field to vary, a Haskell programmer introduces a type parameter to carry varying information. Example

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

Now you can have VmInfo String, VmInfo Dictionary, VmInfo Node, or whatever you want.

Summary: each field name must belong to a unique type, and experienced Haskell programmers work with the static type system instead of trying to work around it. And you definitely want to learn about pattern matching.

Upvotes: 20

Daniel Fischer
Daniel Fischer

Reputation: 183888

Currently, the named fields are top-level functions, so in one scope there can only be one function with that name. There are plans to create a new record system that would allow having fields of the same name in different record types in the same scope, but that's still in the design phase.

For the time being, you can make do with unique field names, or define each type in its own module and use the module-qualified name.

Upvotes: 5

Tikhon Jelvis
Tikhon Jelvis

Reputation: 68152

Yes, you cannot have two records in the same module with the same field names. The field names are added to the module's scope as functions, so you would use name vm rather than vm.name. You could have two records with the same field names in different modules and import one of the modules qualified as some name, but this is probably awkward to work with.

For a case like this, you should probably just use a normal algebraic data type:

data VMInfo = VMInfo String String String

(Note that the VMInfo has to be capitalized.)

Now you can access the fields of VMInfo by pattern matching:

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

Upvotes: 4

Related Questions