PureScript by example: Chapter Pattern Matching: Exercise: doubleScaleAndCenter

Hi, this is a solution for the Line case:

doubleScaleAndCenter line@(Line (Point s) (Point e)) =
  Line
    (Point { x: (s.x - cx) * 2.0, y: (s.y - cy) * 2.0 })
    (Point { x: (e.x - cx) * 2.0, y: (e.y - cy) * 2.0 })
  where
    c = getCenter line
    cx = getX c
    cy = getY c

If I use c directly, it does not work:

doubleScaleAndCenter line@(Line (Point s) (Point e)) =
  Line
    (Point { x: (s.x - c.x) * 2.0, y: (s.y - c.y) * 2.0 })
    (Point { x: (e.x - c.x) * 2.0, y: (e.y - c.y) * 2.0 })
  where
    c = getCenter line

I get

  Could not match type
                 
    { x :: Number
    | t0         
    }            
                 
  with type
         
    Point

But getX (Point p) = p.x, why can’t they be substituted?

2 Likes

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:

1 Like

I realized the reference solution for centerShape is not as clean as it could be, so here’s a PR to fix that. https://github.com/purescript-contrib/purescript-book/pull/253


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:

  1. newtype Point.
  2. 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).
2 Likes

(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

can be replaced by

newtype MyConcreteType = MyConcreteConstructor MyConcreteThing
newtype MyGenericType genericThing = MyGenericConstructor genericThing

But you could not use newtype for things like

data ZeroThings = EmptyConstructor
data TwoThings = MyConstructor Thing1 Thing2
data TwoConstructors 
  = Constructor1 Thing1 
  | Constructor2 Thing2

Good find. I must have assumed the compiler is good enough to catch errors I make in forum posts too. Edited the original.

1 Like

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.

2 Likes