This is a great question that will likely help lots of other beginners. Thanks for posting.
The error you’re encountering is because the record is wrapped in a Point type constructor, and you’re trying to access a field without unwrapping it first. The (Point p) in getX unwraps Point and stores the inner record in p.
There are pros and cons to wrapping records in another type constructor. One of the cons is that unwrapping is a bit tedious. Here’s the version without wrapping:
-- Not wrapped (notice `type` is used instead of `data`)
type Point =
{ x :: Number
, y :: Number
}
-- You probably wouldn't even write a function for this. Just use .x instead.
getX :: Point -> Number
getX p = p.x
The original for reference:
-- Wrapped (notice the additional `Point` constructor)
data Point = Point
{ x :: Number
, y :: Number
}
getX :: Point -> Number
getX (Point p) = p.x
For other readers following along, here’s the relevant section in the text:
Also wanted to briefly re-summarize some of the different ways to represent a Point record and motivations for each.
Type synonym:
type Point =
{ x :: Number
, y :: Number
}
The most convenient option. Start with this, unless you know you need one of the following options.
Newtype:
newtype Point = Point
{ x :: Number
, y :: Number
}
Provides some additional type safety and allows you to override default behaviors. For example, if you wanted a customized way to show points that’s different than records (this will make more sense in the type classes chapter).
ADT with a single constructor:
data Point = Point
{ x :: Number
, y :: Number
}
I actually don’t see any reason for doing this versus newtype. It offers nothing beyond what’s covered above for newtype. It also loses some of the nice wrap/unwrap derived conveniences (not discussed here).
So I’m wondering if this section of the book should be modified to eliminate the impractical data Point and replace with either:
newtype Point.
type Point in Ch5. Then in Ch6, follow-up on the type class motivations for newtype (teased in last line of newtype section) and ask for a Show instance for newtype Point in the first exercise. Edit: This is shaping up to be a nice simplification (PR).
(slight correction for any newcomers looking to copy-paste this code)
You have
newtype Point =
{ x :: Number
, y :: Number
}
and it would have to be
newtype Point = Point
{ x :: Number
, y :: Number
}
For any newcomers reading this, the newtype keyword is a drop-in replacement for the data keyword that offers some performance benefits and wrap/unwrap goodies that @milesfrain mentions, but is only allowed when you wrap just one thing. So
data MyConcreteType = MyConcreteConstructor MyConcreteThing
data MyGenericType genericThing = MyGenericConstructor genericThing
The one place I would use data for a data type with a single constructor is if I knew I would be adding more constructors later on. That way the compiler will stop me from using things like derive newtype instance or wrap/unwrap which only work with newtypes, saving me from having to redo them later. You’ll probably have to update lots of the places the data type is used when adding constructors anyway though, so perhaps it doesn’t make a huge difference.