I'm currently playing around with ADTs in Haskell and try to build an ADT Figure
:
data Figure = Rect { x :: Integer, y :: Integer, width :: Integer, height :: Integer}
| Circle { x :: Integer, y :: Integer, radius :: Integer}
| CombiFigure Figure Figure
deriving (Eq, Show, Read)
Now I came across the question how to implement a function that should not accept every Figure
, but e.g. only a Circle
.
Do I already have a bad design? Or is there some best-practice how to do this?
As example, think about a diameter function. All that came to my mind (I'm a complete beginner in Haskell) are the following two options, using undefined
or Maybe
:
1:
diameter :: Figure -> Integer
diameter (Circle _ _ r) = 2 * r
diameter _ = undefined
2:
diameter :: Figure -> Maybe Integer
diameter (Circle _ _ r) = Just (2 * r)
diameter _ = Nothing
Are there more preferable ways on how to accomplish that? Thanks!
You are correct that there is something not right here. The best way of thinking about it would be to start at the function diameter
and decide what it's type should ideally be. You would likely come up with
diameter :: Circle -> Integer
diameter (Circle _ _ r) = 2 * r
because diameters are only defined for circles.
This means that you will have to augment your data structure by splitting out Circle (and Rect too):
data Figure = RectFigure Rect
| CircleFigure Circle
| CombiFigure Figure Figure
deriving (Eq, Show, Read)
data Rect = Rect { rectX :: Integer, rectY :: Integer, rectWidth :: Integer, height :: Integer}
deriving (Eq, Show, Read)
data Circle = Circle { circleX :: Integer, circleY :: Integer, circleRadius :: Integer}
deriving (Eq, Show, Read)
which is nice because it is now more flexible: you can write functions that don't care what Figure
they are applied to, and you can write functions that are defined on specific Figure
s.
Now, if we are in a higher-up function and have a reference to a Figure
and we want to compute its diameter
if it's a CircleFigure
, then you can use pattern matching to do this.
Note: using undefined
or exceptions (in pure code) is likely a code smell. It could probably be solved by rethinking your types. If you have to indicate failure, then use Maybe
/Either
.
Your type definition by itself (i.e data Figure = ...
) is introducing partial functions. e.g. even though width
is of type width :: Figure -> Integer
it can only work on Rect
values:
\> width $ Rect 1 2 3 4
3
\> width $ Circle 1 2 3
*** Exception: No match in record selector width
so, you already have defined functions which can work on one figure but not another (similar to diameter
function in the question).
That said, a 3rd solution would be to define Circle
, Rectangle
etc, as separate types; then, define a Figure
type class which defines the common interface of these types.
class Figure a where
area, perimeter :: a -> Double
instance Figure Circle where
area = ...
perimeter = ...
Additionally each type may have their own exclusive functions. Or, you may add more interfaces, (i.e. type classes) which cover some but not all the figure types.
An advantage of type classes is that they are easier to extend; e.g. if one wants to add, say, a Triangle
type later on, he can opt-in any type class which applies to a triangle, and define an instance only for those type classes.
Whereas in the data Figure = ...
approach, you need to find every function which can take a Figure
as argument and make sure it will handle a Triangle
as well. If you are shipping a library then you do not have access to all these functions.
>> for the reference, there was a similar recent discussion of data declaration vs type classes on haskell cafe mailing list.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With