views:

98

answers:

2

Hi, In order to grasp better typeclasses (starting pretty much form scratch) I had a go at modelling 2-D shapes with area calculations, like this:

module TwoDShapes where

class TwoDShape s where
    area :: s -> Float

data Circle = Circle Float deriving Show
aCircle radius | radius < 0 = error "circle radius must be non-negative"
               | otherwise  = Circle radius
instance TwoDShape Circle where
    area (Circle radius) = pi * radius * radius

data Ellipse = Ellipse Float Float deriving Show
anEllipse axis_a axis_b | axis_a < 0 || axis_b < 0 = error "ellipse axis length must be non-negative"
                        | otherwise                = Ellipse axis_a axis_b
instance TwoDShape Ellipse where         
    area (Ellipse axis_a axis_b) = pi * axis_a * axis_b

And so on for other kinds of shape.

This is fine but it occurred to me to try this:

module TwoDShapes  where

class TwoDShape s where
    area :: s -> Float

data TwoDShapeParams = TwoDShapeParams Float Float Float deriving Show

instance TwoDShape TwoDShapeParams where
    area (TwoDShapeParams length_a length_b constant) = foldl (*) 1 [length_a, length_b, constant]

aCircle radius | radius < 0 = error "circle radius must be non-negative"
               | otherwise  = TwoDShapeParams radius radius pi

anEllipse axis_a axis_b | axis_a < 0 || axis_b < 0 = error "ellipse axis length must be non-negative"
                        | otherwise                = TwoDShapeParams axis_a axis_b pi

etc. which is also fine. With the goal of information hiding I change the module declaration to look like this:

module TwoDShapes (TwoDShape, area, aCircle, anEllipse, aRectangle, aTriangle)

and slightly to my surprise this 1) works and 2) in ghci aCircle evaluates to TwoDShapeParams 1.0 1.0 3.1415927 which is true but I don't understand how the type TwoDShapeParams is visible outside the module. I'm not sure what I was expecting, but not this.

What I'd really like is for the typeclass, it's method and the “smart constructors” to be visible outside the module and nothing else. Can that be done?

+1  A: 

If you see *TwoDShapes in the ghci prompt, it can access everything in the module:

http://www.haskell.org/ghc/docs/6.12.1/html/users_guide/interactive-evaluation.html

The new prompt is *Main, which indicates that we are typing expressions in the context of the top-level of the Main module. Everything that is in scope at the top-level in the module Main we just loaded is also in scope at the prompt (probably including Prelude, as long as Main doesn't explicitly hide it).

The syntax *module indicates that it is the full top-level scope of module that is contributing to the scope for expressions typed at the prompt. Without the *, just the exports of the module are visible.

Try loading TwoDShapes without the * and check what the type of aCircle is.

Alexey Romanov
+6  A: 

Although the representation of TwoDShapes is hidden, you have derived a Show instance for it, which allows an arbitrary value of type TwoDShapes to be converted to a String, so this is the source of the information leakage. A truly abstract type should not define a Show instance, or indeed a Data instance which similarly exposes information about the representation. It's fine to have a way to convert your type to a String, as long as the String is independent of the representation (see the Show instances of Data.Map.Map and Data.Array.Array for good examples of this).

Note that the module system is doing its job: you still can't refer to the TwoDShapes constructor outside of the module that defines it.

Simon Marlow
What I am surprised at (and I don't have a Haskell compiler installed at this computer, so can't check what happens) is that `aCircle` can be exported at all when the non-exported type `TwoDShapeParams` (not its value constructor) appears in its type.
Alexey Romanov
You wrote `module TwoDShapes (TwoDShape, ...` which exported the *type* `TwoDShape`, but not its respresentation. However, even if you hadn't exported the type, you would still be able to export a function whose type mentioned `TwoDShape`, you just wouldn't be able to refer to `TwoDShape` anywhere else. The Haskell module system controls the visibility of identifiers, and that's all it does.
Simon Marlow
I am not the author of the question :)
Alexey Romanov
sorry! hope the explanation suffices anyway.
Simon Marlow
Aha. Hadn't appreciated that by default Show produces a string containing an expression which should evaluate to the value being shown. In which case, of course it does what I see.I looked but did not understand the instance in Data.Map.Map but I did figure this out:instance Show TwoDShapeParams where showsPrec d s = showString $ printf "a shape with bounding box side lengths %f %f" (first s) (second s) where first (TwoDShapeParams a b k) = a second (TwoDShapeParams a b k) = b PS: how come you guys can get code to appear in a tt font in comments and I can't?
keithb
When you say that "A truly abstract type should not define a [...] Data instance" do you mean that I should not have my `data TwoDShapeParams` even though it is private to the module?
keithb
No, it refers to the `Data` typeclass (http://www.haskell.org/ghc/docs/6.12.1/html/libraries/base-4.2.0.0/Data-Data.html#t%3AData), I believe.
Alexey Romanov