Topic: Simple IO, Graphics, and Drawing Shapes Date: Sep. 30, 2009 Number: 5 Examples: SimpleGraphics.hs, SierpinskiAlt.hs, Draw.hs Reading: Chap. 4 Announce - SA 3 due Fri. PS 1 is posted, due next Wednesday. Look at it before sections on Thursday. -- Haskell I/O I/O is the Achille's heel of functional languages. A major strength of functional languages is the substitution rule - you can replace a name by its value anywhere, or go the other way. If you call a function ten times with the same parameters you get the same answer 10 times. There are no side effects. If you have to evaluate two values it doesn't matter what order you evaluate them (as long as you have to evaluate them both). Unfortunately, I/O doesn't fit this very well. If you call putStr "Hello, world" once it is very different from calling putStr "Hello, world" ten times. Each time you read a character from a file you want to get a different character. And when printing two lines the order DOES matter. Therefore I/O functions are a bit different from normal functions. Haskell creates a special value called an "action". When you go to evaluate an expression whose value is an action, instead of just evaluating and using the result (which is the usual result of evaluating an expression) Haskell performs the action, whatever it is. Expressions that evaluate to actions are called "commands". The type of an Input/Output action is IO . (Will later see that IO is only one type of possible action.) Some commands just have side effects (like printing or drawing) and return nothing. They are void functions, in Java terminology. In Haskell, they have type IO (). The type () is called the unit type and has one value, also written (). The simplest command whose type is IO () is "return ()". It does nothing. That is sometimes very useful. Other commands return values - e.g. getLine reads and returns a line from the input. Its type is IO String (or IO [Char] - String is a type synonym for [Char]). The getChar command reads and returns a character, so has type IO Char. If x is an Integer, then "return x" has type IO Integer. -- Lazy Evaluation Commands reveal something that has previously been somewhat hidden - Haskell uses lazy evalution. You have seen that in Java: if (x != 0 && 1/x > 5) y = 1/x else y = x When evaluating "x != 0 && 1/x > 5" Java does not evaluate both inequalities and && them. It evaluates the first, and only evaluates the second if it needs it to determine the value of the &&. Thus we avoid a divide-by-zero error. Haskell evaluates EVERYTHING lazily. That is why when we say let message = putStrLn "Hello, world" nothing happens. Haskell can make the assignment of the command to the name without performing the command. When you then type message the expression has to be evaluated, so that is when it prints. Note that you can say: let first = putStrLn "First message" let second = putStrLn "Second message" let msglist = [first, second] No actions have been performed yet. Then: first second head msglist head (tail msglist) Normally typing an expression to the interpreter evaluates that expression and prints the result. For actions, what happens is the action is performed. All of these result in messages being printed. But: msglist gives an error. To understand this error, we need to know more about what the interpreter does. Each time you type an expression in the interpreter the interpreter evaluates it, calls a function "print". This function first calls "show" to convert the expression to a string (think "toString" in Java), and uses putStrLn to print it out. msglist evaluates to a list of commands of type IO (). Haskell doesn't have a version of show that converts something of type IO () to a string. It makes sense perform a command, and its value is the action. That action may be to print something, or to draw something, or to do nothing. But it doesn't make sense to convert that action to a string! Thus we get an error message. So what can we do? sequence_ msglist does what we want. Note that book says: sequence_ :: [IO a] -> IO () What is "IO a"? Will see in Chap. 5 that it is IO of an arbitrary type. Hudak got ahead of himself. What if I don't want to make a list of actions, but want to perform them one after the other? The "do" keyword is like where or let, but it lets you do a sequence of commands with last being IO , and the whole collection is of same type as the last command. That is because the "value" of the "do" is the value of the last command. The book claims all statements in a "do" must be of type IO (), but this is not true. However, for our uses of the "do" construct we will almost always have the last line be IO (), at least until late in the course. So do first; second performs each command IN ORDER! Important. The let and where don't say things are performed in order. (Using ";" to separate because I can't use separate lines in the interpreter.) Also, do :type first to discover that first is indeed of type IO (), and :type do first; second to discover that the whole command is of type IO (). What about doing input? There is a getLine method whose type is IO String. (Show this by using :t, and then by typing "getLine" into the interpreter, typing a string, and seeing that it prints the string.) Note that "show" of a string returns the string in quotes, so the value of the getLine command that is printed out is a string surrounded by quotes. To see that the value of a "do" is the value of the last command in the do, consider: do getLine; getLine When you run this the value printed is the value of the last line typed. The other value is thrown away. You might think that you can read a line by saying let line = getLine but nothing happens, and line is of type IO String. If I now type "line" the interpreter waits for me to type input line, then prints it as a string (in quotes). So how can I save the value read? Use "<-" line <- getLine Echos the same, but now line has value of the input string. Note that line is type String, not IO String. The "<-" converts a value of type IO into a value of type and binds the value to the name on the left side of the the "<-". It is actually a bit more complicated, but this simplification works for now. Note: We will learn more about this "<-" when we study the "IO modad". It is really "syntactic sugar" for a more complicated Mondad operation called "bind" that we will learn about later. The interpreter converts it to this other expressions before using it. Because of this "line <- getLine" is not considered an expression, and it has no type. (Use :t to demonstrate this.) So can do: do msg <- getLine; putStrLn ("You entered: " ++ msg) You can only use <- within a do, and not as the last thing. Last thing must be IO . Thus: do msg1 <- getLine; msg2 <- getLine invalid, but do msg1 <- getLine; msg2 <- getLine; putStrLn (msg1 ++ msg2) works fine. An additional point - can't do normal "=" within a "do". But can use a special version of let to bind a value that will last until the end of the do. So the following is possible: do let x = 5 let y = 2*x print (x + y) In fact, the whole top level of the interpreter is basically one big do statement! So need to use "let" when assign values in the interpreter. Show: let x = 4 let y = 2*x print (x + y) Also, show without (). Error because parenthesizes as (print x) + y (function application binds tighter than operator). -- Graphics commands Graphics are not quite as easy to understand as I/O commands. There is a type Graphic that is imported with SOE, but trying to print a Graphic expression doesn't work. Look at book's example for main0: main0 = runGraphics ( do w <- openWindow "My First Graphics Program" (300,300) drawInWindow w (text (100,200) "Hello Graphics World") k <- getKey w closeWindow w ) NOTE to self: To demo all of these function types, use :type to avoid writing it out! runGraphics :: IO () -> IO () is the thing that actually "does" graphics. Any graphic command must be performed inside of a call to runGraphics. (Of course, the command can be created outside of the call in a function or saved in a list, but must be performed inside the call.) openWindow :: String -> (Int, Int) -> IO Window String is the title and (Int, Int) gives size in pixels (width, height). Pops up a window and the <- assigns it to w. drawInWindow :: Window -> Graphic -> IO () text :: Point -> String -> Graphic where type Point = (Int, Int) is exported from graphics package. (0,0) is upper left corner of window, x positive to right, y positive DOWN. (Same as Java graphics.) Converts string to a Graphic whose lower left-hand corner is at Point. getKey :: Window -> IO Char (Book has a typo here, claims IO ()). Reads a character when you release the key. Here not because we care about k, but because we don't want the window to immediately close. closeWindow :: Window -> IO () closes the window. Note the "do" to allow sequencing. Note: still works without the "k <-". Book defines spaceClose. spaceClose :: Window -> IO () spaceClose w = do k <- getKey w if k == ' ' || k == '\x0' then closeWindow w else spaceClose w Gets a key. If key is space (or some funny character not in book!) then close the window. Else call self recursively. The functional way to write a While loop! (Really an Until loop.) This is called tail recursion. Last thing done is a recursive call - no need to do anything on return from call except return yourself. Compilers can easily convert tail recursion to loops, so tail recursion not costly. -- Drawing stuff other than text For drawing pictures, the graphics library provides: ellipse :: Point -> Point -> Graphic Points are two corners of the bounding box. shearEllipse :: Point -> Point -> Point -> Graphic Points define a parallelogram containing graphic. line :: Point -> Point -> Graphic polyline :: [Point] -> Graphic Draws lines, no close, no fill polygon :: [Point] -> Graphic Closes, filled polygon polyBezier :: [Point] -> Graphic Uses points as Bezier control points, draws smooth curve. Otherwise like polyline. Also: withColor :: Color -> Graphic -> Graphic Makes a graphic of the given color. So can do: pic1 = withColor Red (ellipse (150,150) (300,200)) pic2 = withColor Blue (polyline [(100,50),(200,50), (200,250),(100,250),(100,50)]) main2 = runGraphics ( do w <- openWindow "Some Graphics Figures" (300,300) drawInWindow w pic1 drawInWindow w pic2 spaceClose w ) Note that the assignments for pic1, pic2 done outside the runGraphics. drawInWindow converts the Graphic to a IO (), and draws it when performed. -- Sierpinski's triangle You did this in CS 5. First, look at fillTri: fillTri :: Window -> Int -> Int -> Int -> IO () fillTri w x y size = drawInWindow w (withColor Blue (polygon [(x,y),(x+size,y),(x,y-size),(x,y)])) First comment: don't need the (x,y) at end! Closes automatically. But makes it easy to convert to a polyline (where IS needed). Demonstrate. Second comment - it returns an IO (), because of the drawInWindow call. When called from within runGraphics it will actually draw it. The recursive definition is what you expect. The (x,y) is the lower left hand corner of the fractal being drawn (a right triangle shape), and the size is how long the legs are: minSize :: Int minSize = 8 sierpinskiTri :: Window -> Int -> Int -> Int -> IO () sierpinskiTri w x y size = if size <= minSize then fillTri w x y size else let size2 = size `div` 2 in do sierpinskiTri w x y size2 sierpinskiTri w x (y-size2) size2 sierpinskiTri w (x+size2) y size2 As usual, the runGraphics is at the top level. Note: use div as an infix operator. Can do this with any binary function if enclose in back-quotes `div`. We already saw can turn any binary operator into a function by enclosing in (), e.g. (+). General rule - function names use alphanumeric characters (starting with a letter), operator names use special symbols. In this context ' is considered alphanumeric. main3 = runGraphics ( do w <- openWindow "Sirpinski's Triangle" (400,400) sierpinskiTri w 50 300 256 spaceClose w ) To see an alternate way of handling things, instead of using "do" we can build a list of commands: sierpinskiTri :: Window -> Int -> Int -> Int -> [IO ()] sierpinskiTri w x y size = if size <= minSize then [fillTri w x y size] else let size2 = size `div` 2 in (sierpinskiTri w x y size2) ++ (sierpinskiTri w x (y-size2) size2) ++ (sierpinskiTri w (x+size2) y size2) and sequence_ them in main: main = runGraphics ( do w <- openWindow "Sierpinski's Triangle" (400,400) sequence_ (sierpinskiTri w 50 300 256) spaceClose w ) [GOT TO HERE in class, but will leave the notes logically grouped] -- Drawing Shapes This module imports the Shape module, and then draws the various shapes. Problem - the Graphics coordinate system is not a good match for our usual idea of a graphics plane - would normally have the origin at the center, use real coordinates, and have more than a pixel's width between successive coordinates. We also are used to the y-axis going in the other direction. Common trick in CS - when what you have is not what you want, write a module to make it better. So what do we need to do? 1) Have a better coordinate system. Call it "user coordinates", as opposed to "graphics coordinates". In the best of all worlds, could specify the x and y ranges on the graphics window. We will look at a module that does this later. Book takes a different approach: 1 unit in user coord. = 100 pixels. To do this, writes two conversion functions: inchToPixel :: Float -> Int inchToPixel x = round (100*x) pixelToInch :: Int -> Float pixelToInch n = intToFloat n / 100 intToFloat :: Int -> Float intToFloat n = fromInteger (toInteger n) Book has 2 exercises here, asking: Why not "100 * round x" in inchToPixel? (Only get values at 100-pixel intevals.) Why not intToFloat(n/100) in pixelToInch? (Can't divide two integers.) Defines some constants: xWin, yWin :: Int xWin = 600 yWin = 500 xWin2, yWin2 :: Int xWin2 = xWin `div` 2 yWin2 = yWin `div` 2 and a transformation function: trans :: Vertex -> Point trans (x,y) = ( xWin2 + inchToPixel x, yWin2 - inchToPixel y ) Why? Because the center of the coordinate system is at (xWin2, yWin2). Thus (0,0) should convert to this point, which it does. For x-axis positive x moves to left, negative to right; for y-axis positive y moves DOWN and negative y moves UP (graphics coord. are backwards in y from user coord. so subtract.) So pick a point. Say lower right corner. What are its user coordinates? (pixelToInch xWin2, -(pixelToInch yWin2)) Try doing computation by calculation, using substitution: trans (pixelToInch xWin2, -(pixelToInch yWin2)) => (xWin2 + inchToPixel (pixelToInch xWin2), yWin2 - inchToPixel (-(pixelToInch yWin2))) => (xWin2 + round (100 * (intToFloat xWin2/100)), ywin2 - round (100 * (intToFloat (-yWin2)/100))) => (xWin2 + xWin2, yWin2 + yWin2) => (xWin, yWin) Correct. That is encouraging! Book supplies transList: transList :: [Vertex] -> [Point] transList [] = [] transList (p:ps) = trans p : transList ps How can we write this as a higher-order function? transList list = map trans list So how do we draw all of our shapes? Decide where they should go on screen, transform them. shapeToGraphic :: Shape -> Graphic What about a rectangle? No built-in rectangle, so use a polygon. Center at (0,0). Then corners are all offset by half the side length in each direction: shapeToGraphic (Rectangle s1 s2) = let s12 = s1/2 s22 = s2/2 in polygon (transList [(-s12,-s22),(-s12,s22), (s12,s22),(s12,-s22)]) Center ellipse at (0,0). Then the lower left corner of the box is (-r1,-r2) and upper right is (r1,r2): shapeToGraphic (Ellipse r1 r2) = ellipse (trans (-r1,-r2)) (trans (r1,r2)) Decided to have right triangle in first quadrant with right angle at (0,0): shapeToGraphic (RtTriangle s1 s2) = polygon (transList [(0,0),(s1,0),(0,s2)]) Polygon is easy - translate all the vertices: shapeToGraphic (Polygon pts) = polygon (transList pts) So we can make a bunch of shapes: sh1,sh2,sh3,sh4 :: Shape sh1 = Rectangle 3 2 sh2 = Ellipse 1 1.5 sh3 = RtTriangle 3 2 sh4 = Polygon [(-2.5,2.5), (-1.5,2.0), (-1.1,0.2), (-1.7,-1.0), (-3.0,0)] and draw a couple, using "do" and drawInWindow: main0 = runGraphics ( do w <- openWindow "Drawing Shapes" (xWin,yWin) drawInWindow w (withColor Red (shapeToGraphic sh1)) drawInWindow w (withColor Blue (shapeToGraphic sh2)) spaceClose w ) Then decided to be more clever. Do something other than a separate drawInWindow for each shape. Need a way to pair a color with a shape: type ColoredShapes = [(Color,Shape)] shs :: ColoredShapes shs = [(Red,sh1),(Blue,sh2),(Yellow,sh3),(Magenta,sh4)] drawShapes :: Window -> ColoredShapes -> IO () drawShapes w [] = return () drawShapes w ((c,s):cs) = do drawInWindow w (withColor c (shapeToGraphic s)) drawShapes w cs NOTE - uses "return ()" to be able to do nothing on empty list. Alternate approach: How can we use map, sequence_ to achieve this? Will see more later. main1 = runGraphics ( do w <- openWindow "Drawing Shapes" (xWin,yWin) drawShapes w shs spaceClose w ) -- More higher-order functions Suppose I have a list of shapes: let shapeList = [sh1, sh2, sh3, sh4] and a list of colors: let colorList = [Red, Blue, Yellow, Magenta] Can I get shs from these easily? Yes: zipWith (,) colorList shapeList gives: [(Red,Rectangle 3.0 2.0),(Blue,Ellipse 1.0 1.5), (Yellow,RtTriangle 3.0 2.0), (Magenta,Polygon [(-2.5,2.5),(-1.5,2.0),(-1.1,0.2),(-1.7,-1.0), (-3.0,0.0)])] Comes up so often given the name zip.