Search code examples
purescript

Using an ADT's constrctor as a type in Purescript


Here's an example of an ADT from PureScript by Example

data Shape
  = Circle Point Number
  | Rectangle Point Number Number
  | Line Point Point
  | Text Point String

type Point =
  { x :: Number
  , y :: Number
  }

Does it make sense to want to do something like this?

boundingCircle :: Shape -> Circle
boundingBox :: Shape -> Rectangle

Right now, this isn't allowed, as Circle isn't a type. I could perhaps have Shape be a type class and Circle, Rectangle, ect have instances of Shape. Then I lose the semantics that a Shape is the union of exactly 4 constructors (pattern matching becomes messier).

Is treating ADT constructors as types themselves something really worth avoiding?


Solution

  • I could perhaps have Shape be a type class and Circle, Rectangle, ect have instances of Shape. Then I lose the semantics that a Shape is the union of exactly 4 constructors (pattern matching becomes messier).

    You can actually have both 4 separate types and easy pattern matching.

    First the individual types corresponding to the constructors in the original example:

    data Circle = Circle Point Number
    data Rectangle = Rectangle Point Number Number
    data Line = Line Point Point
    data Text = Text Point String
    

    And then the Shape type can be a simple "union" (or "sum") of the 4 individual types:

    data Shape = CircleShape Circle | RectangleShape Rectangle | LineShape Line | TextShape Text
    

    This setup would allow you to do both the things you seem to want. The functions you mention would have a form roughly like this:

    boundingCircle :: Shape -> Circle
    boundingCircle (CircleShape (Circle center radius)) = Circle (...) (...)
    boundingCircle (CircleRectangle (Rectangle corner length width)) = Circle (...) (...)
    

    and so on for the remaining cases, which also shows that you can pattern match on a Shape and know it is one of the four particular variants.

    It's not a "free win" though. Everything comes with pros and cons, and deciding how to design your datatypes isn't always straightforward. In particular, with the above, you will note that the pattern matching on a Shape, while still possible, has become significantly more verbose. Quite often you won't actually want to have things like Circle as their own types. Or you might be happy with the typeclass approach - while this stops you from pattern-matching, you could still have boundingCircle and so on as methods of that typeclass, or as other functions that can easily be derived from the typeclass methods.

    Indeed I would suggest that my favoured approach here might be exactly the typeclass one - if you're designing the typeclass, you can put in exactly the methods you need, and no more. But there's never any one "right answer" in these situations - you weigh the pros and cons and do what fits your use best.