Topic: Representing Shapes Date: Sep. 28, 2009 Number: 4 Examples: Shape.hs, lhs2hs.hs Reading: SOE Chap. 2, begin Chap 3 Announce - SA 2 due Wednesday -- Do the wordLength example from last class notes. -- Modules A module is a way of bundling a group of related functions so that they can be used in (imported into) other programs. In particular, it gives a way of controlling which functions, user-defined types, etc. are visible outside of the module. SOE introduces a Shape module to represent various shapes: Rectangles, ellipses, right triangles, and polygons. The SOE/src code is in Literate Haskell, which allows text to be written around the code. The suffix is ".lhs" rather than ".hs". -- The lines of code to be compiled start with ">". -- Lines beginning with "<" are in the text, but not necessarily executable. -- lines beginning with "|" are also in the text, but are often just expressions or code fragments. I have written a program lhs2hs that can be used to turn .lhs files into .hs files. It is linked as an example program. Use it now; we may look at how it works later. Let's look at the Shape module. The first thing is the header: module Shape ( Shape (Rectangle, Ellipse, RtTriangle, Polygon), Radius, Side, Vertex, square, circle, distBetween, area ) where This gives the module name (file should have SAME name), which must be capitalized. Rule - all module names and type names are capitalized! If just say "module Foo where" everything in the module is exported. Otherwise only listed things are visible in programs that import the module. Above we export a data type (Shape), its constructors (in parentheses), types, and functions. -- Type declarations create a new name (type synonym) for an existing type. Examples: type Radius = Float type Side = Float type Vertex = (Float,Float) Two reasons for doing this: 1) Documentation - can choose names that explain how the data is used rather than what its internal representation is. 2) Flexibility. If want to change the underlying data structure (e.g. use Double instead of Float), we can do it in one place, and it will work correctly everywhere. Note: Standard Prelude defines: type String = [Char] so can use String as a type. -- Data type declarations create something new data Shape = Rectangle Side Side | Ellipse Radius Radius | RtTriangle Side Side | Polygon [Vertex] deriving Show What do we have? First, keyword data. Then the name of the data type, which must be capitalized. Finally, a list of constructors. The constructor names must be capitalized, to distinguish them from normal functions. Note that we get some polymorphism this way - a Shape variable or parmeter could be any of Rectangle, Ellipse, RtTriangle, and Polygon. Each constructor has its own set of data that is used to define it and is stored. So a Rectangle has two Sides. (Could have said "Rectangle Float Float". This clearer.) Ellipse has two radii. Polygon has list of Vertices, each a pair of Floats. "deriving Show" says to automatically create a "show"function, which is equivalent to the Java "toString" function. This function is needed for the print function to print the data type. We will see more about "deriving" later. Note that a Polygon has coordinates, so is located in the plane. However, the other three have only size information, not location information. Seems strange, if we want to draw them. We will see later how SOE handles this. Define one or more of each, show what prints when you print it. See that the constructor is part of the data stored. Note that we can define functions for other shapes in terms of the shapes we have: square s = Rectangle s s circle r = Ellipse r r However, these are regular functions that call constructors, not constructors themselves. Still only 4 different types of Shapes to deal with. Define a square and circle, show what prints. So next we can create functions that have data types as parameters. First, we give type signature for the function area: area :: Shape -> Float Sort of like OO programming, but while DATA is saved in the data type, the FUNCTIONS aren't. They are free-standing, but need to be defined for all of the constructors. We have "dispatch on constructor type", but done via pattern matching. Given a Shape variable, we can safely call area on it no matter which type of shape it is. The area function behaves differently for different contructors (and the associated data). Look at the easy ones first: area (Rectangle s1 s2) = s1*s2 area (RtTriangle s1 s2) = s1*s2/2 area (Ellipse r1 r2) = pi*r1*r2 Note that we are pattern-matching on the constructor and the associated data. Actually, not so different from what we did before. [] and (:) are constructors for the List type, with "syntactic sugar". How about a Polygon? Not so easy. First approach: recursive decomposition. Cut off an "ear". (Draw, show get triangle and smaller polygon.) So what is left? Computer the area of the triangle, add to area of smaller polygon. For convex polygon, any three consecutive vertices form an ear, so: WRITE: area(Polygon (v1 : v2 : v3 : vs)) = triArea v1 v2 v3 + area (Polygon (v1 : v3 : vs)) -- Left out v2 area(Polygon _) = 0 -- < 3 vertices must have 0 area. This is fine, but it is kind of awkward. We take apart and rebuild polgons at each step. All start with vertex v1. So if do this recursively: (Draw a few levels, show that we eventually get a triangle for each edge, with 1 as the vertex opposite the edge.) Book takes advantage of this: area (Polygon (v1:vs)) = polyArea vs where polyArea :: [Vertex] -> Float polyArea (v2:v3:vs') = triArea v1 v2 v3 + polyArea (v3:vs') polyArea _ = 0 Note polyArea is a locally-define function. Not visible outside of area. But that is not the main reason for not doing it separately. Note that the call to triArea uses v1. But v1 is not a parameter to polyArea. Where does it come from? Everything in the "where" construct can see all of area's parameters. So by including it inside the area definition we save passing v1 everywhere. polyArea gets a chain of vertices, and takes off successive edges, pairs them with v1. What about triArea? Heron's formula, known to the Greeks (but not many people these days): triArea :: Vertex -> Vertex -> Vertex -> Float triArea v1 v2 v3 = let a = distBetween v1 v2 b = distBetween v2 v3 c = distBetween v3 v1 s = 0.5*(a+b+c) in sqrt (s*(s-a)*(s-b)*(s-c)) distBetween :: Vertex -> Vertex -> Float distBetween (x1,y1) (x2,y2) = sqrt ((x1-x2)^2 + (y1-y2)^2) Go through idea of saving partial calculations - s to avoid recalculating, a, b, c to avoid long formulas. What would this look like as a where clause? Problem with this - only works for convex polygons. Actually for star-shaped from v1 (means that whole polyon interior if visible from v1 without crossing edges). How do more generally? Exercise 2.5. Use trapezoids. Show how it works, using horizontal line through min y value. See SA 2 for more details.