How do typeclasses and modules interact?
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
开发者_如何学Go 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?
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.
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.
精彩评论