This post was published 2 years 5 months 9 days ago and as such probably does not reflect my current opinions, knowledge or ability.Alright! So today I progressed a little further with my endeavours to learn Haskell, and I finally feel like things are coming together.
Previously, I’d worked my way through several tutorials. While I had remembered a lot of the small things they said, learned the syntax, written the couple of lines of code that were posed as example exercises, the moment I tried to take on any sizeable problem my brain just reverted back to splitting everything into objects.
(If you’re using Internet Explorer, you might need an SVG viewer to see the image above)
Last night I found this tutorial, which works through building a piece of software to create a park map of sorts as an SVG. I wouldn’t necessarily recommend it as the only or even first thing you read – there were areas where I had to refer to other guides, and felt I was spending a lot of time just figuring out why we were trying to achieve certain things rather than focussing on the Haskell that was behind them.
What’s good however is that the tutorial goes through a significant amount of code so you can see how everything comes together. It also lists good and bad points for each step – I found it beneficial to into some of the bad points and work to correct them.
After working through several of the tutorial, I felt ready to start working on a separate project: drawing bar graphs. Sure it’s primitive, and I think there are a lot of improvements that could be made, but I feel this really cleared up a lot of key concepts that just hadn’t quite “clicked” after my previous attempts.
There is a whole lot about the code that I’m worried about: areas that I know I can improve, areas that I think I will be able to improve as I learn more and, almost certainly, things I’m doing horribly wrong that would make experience programmers scream, without me even realising it. I’m hoping just dealing with writing to the .svg file in a more managed way (possibly with an existing XML library) will make a big difference to readability, as that’s where a lot of the mess seems to be.
Here’s the code (indentation will probably turn out a little dodgy):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 | -- Import from standard libraries import Data.List -- Define some data types type Point = (Float,Float) type Color = (Int,Int,Int) type LineColor = Color type FillColor = Color type Polygon = [Point] type Sample = (String,Float) -- Types of SVG Primitives and Elements data Shape = LinePoly LineColor Polygon | FilledPoly FillColor LineColor Polygon | Line LineColor (Point,Point) | Text Color Int Point String -- Name some colours rainbow@[red,green,blue,yellow,purple,teal,white,black] = [(255,0,0),(0,255,0),(0,0,255),(255,255,0),(255,0,255),(0,255,255),(255,255,255),(0,0,0)] -- Format coordinates for output writePoint :: Point -> String -- x,y writePoint (x,y) = (show x)++","++(show y)++" " writeLinePoint :: (Point,Point) -> String -- x1="" y1="" x2="" y2="" writeLinePoint ((x1,y1),(x2,y2)) = "x1=\""++(show x1)++"\" y1=\""++(show y1)++"\" x2=\""++(show x2)++"\" y2=\""++(show y2)++"\"" writeXY :: Point -> String -- x="" y="" writeXY (x,y) = "x=\""++(show x)++"\" y=\""++(show y)++"\"" -- Draw primitives and shapes writeShape :: Shape -> String writeShape (LinePoly (r,g,b) p) = "<polygon points=\""++(concatMap writePoint p)++"\" style=\"fill:transparent;stroke:rgb("++(show r)++","++(show g)++","++(show b)++");stroke-width:2\"/>\n" writeShape (FilledPoly (fr,fg,fb) (lr,lg,lb) p) = "<polygon points=\""++(concatMap writePoint p)++"\" style=\"fill:rgb("++(show fr)++","++(show fg)++","++(show fb)++");stroke:rgb("++(show lr)++","++(show lg)++","++(show lb)++");stroke-width:2\"/>\n" writeShape (Line (r,g,b) p) = "<line " ++ writeLinePoint p ++" style = \"stroke:rgb("++(show r)++","++(show g)++","++(show b)++");stroke-width:2\"/>\n" writeShape (Text (r,g,b) i p s) = "<text "++writeXY p++" font-family=\"Verdana\" font-size=\""++(show i)++"\" fill=\"rgb("++(show r)++","++(show g)++","++(show b)++")\">"++s++"</text>" -- Draw SVG header elements and contents writeShapes :: [Shape] -> String writeShapes p = "<svg xmlns=\"http://www.w3.org/2000/svg\">\n"++(concatMap writeShape p)++"</svg>" -- Draw and position graph bars and labels drawSample :: (Sample,Float) -> [Shape] drawSample ((s,x),i) = FilledPoly (round (x), round (x/2), round i * 10) black [(100, i * 60.0),(x + 100 , i * 60.0),(x + 100, i * 60.0 + 50),(100 ,i * 60.0 + 50)] : [Text black 16 (x + 120, i * 60 + 30) s] drawSamples :: [Sample] -> [Shape] drawSamples s = concatMap drawSample (zip s [1..]) -- Draw and position axis and titles drawAxis :: Float -> [String] -> [Shape] drawAxis x s = Line black ((100,50),(100,x * 60.0 + 60)) : Line black ((100, x * 60.0 + 60),(500, x * 60.0 + 60)) : Text black 28 (100, 40) (s !! 0) : Text black 20 (250, x * 60.0 + 100) (s !! 1) : [Text black 20 (20, x * 30.0 + 50) (s !! 2)] main = do -- Load graph data from file sample_text <- readFile "samples.txt" let file :: ([String],[Sample]) file = read sample_text -- Create the new image writeFile "graph.svg" $ writeShapes (drawSamples (snd file) ++ drawAxis (fromIntegral (length (snd file))) (fst file)) -- Output to show success putStr "Image graph.svg created. \n" |
Anyway, if you’ve read that and have any advice, feel free to be brutal and let me know what to change. As I said, there are a lot that I feel is still on the wrong tracks, but I wanted to get some code up here earlier rather than later in case anyone had feedback.
Hey,
I haven’t had a chance to look over the code very thoroughly, but I’ve got few suggestions from a quick run through of the code:
1: Look at creating instances of show for your defined data types. This will make your code much cleaner as anytime you print or call a function that uses show with one of those types the function will be called implicitly instead of needing to call the various write functions you have defined.
2: I would also create a polymorphic data type to represent your xml elements so they can be created programmatically. This way you could shorten those long string creations. (You can look for xml libraries on hackage, and use cabal to download them….let me know if you’re not familiar with cabal and I can point you to some sources)
3: Could you add the sample input so I could run the program and get a better understanding of its operation….I’ve never worked with svg files before.
4: This purely preference, but what I like to do is define which functions I’m using when importing a module.
Ex. import Data.List (sort,nub)
Though looking through your code it doesn’t appear that Data.List is required for any of the functions. Using this convention would make it easy to see scenarios like this where imports are extraneous.
5: Using tabs for formatting is Evil!
Keep in mind that these are merely suggestions and are no means hard and fast rules (except number 5
), and I would by no means consider myself a haskell expert, although I’m working on that. You are definitely off to a good start, keep up the practice!
Oh and one more thing, in the Object Mentor Blog, Bob Martin and Michael Feathers recently wrote several posts about writing clean code in functional languages. These are definitely worth checking out if your interested: http://blog.objectmentor.com/ (Maybe one day I’ll gather enough courage to start my own blog!)
-Bryan
Hey Bryan, thanks as ever for your awesome feedback.
1. I see what you mean there, will definitely do that.
2. Again, I think I understand so I’ll give that a go. I seem to have Cabal working correctly – I was hoping using someone else’s libraries would give me some more experience there.
3.
([ "What should I eat?", "Deliciousness", "Food" ], [ ("Cupcakes" , 400), ("Cookies", 300), ("Pie", 150), ("Ice-Cream", 250), ("Squid", 10) ])As you can possibly guess, I was reading in lists of values and labels and then realised I probably needed a header and axis labels as well – and ended up with this mess. I can think of better ways to do this but again, my aim was just to start by getting things working.
4. Good point. This came from working from the tutorial I linked to to start off – I realised I didn’t need the other modules but I wasn’t sure on that one, guess I probably should have checked :/
5. The “tabs” should be spaces (although I was using a different text editor from normal which wasn’t set up to replace tabs with spaces, so I may have made a mistake with that somewhere).
Its so strange being completely back at the beginning of learning to program again – I think I have far more sympathy for new first years coming to university now!
Thanks again for your help and for the link. It would be great if you started a blog – you clearly have a lot of great advice to share. I find time is an issue, and getting over the fact that I hate anything that I wrote more than a couple of weeks ago is hard, but it has certainly helped my programming come on a whole lot.
Hey,
So I got curious and looked up an xml library on hackage called HXT which thus far appears to be an excellent xml package. You can install it by doing ‘cabal install hxt’. It has to install a haskell curl library as a dependency which ended up being a problem for me. This was because it needed a particular package that I didn’t have installed, that included the file curl.h. I downloaded these packages and it remedied the problem for Ubuntu….hopefully it will help you:
http://packages.ubuntu.com/search?searchon=contents&keywords=curl.h&mode=exactfilename&suite=hardy&arch=any
Well, I went ahead and applied it to your example and replaced the meat of the xml creation with this:
—————————————
– Create XML Representation of SVG File Based On Shapes
createXml :: [Shape] -> IOStateArrow s XmlTree XmlTree
createXml shapes = mkelem “svg” [sattr "xmlns" "http://www.w3.org/2000/svg"] $ map createElement shapes
– Converts a shape into an xml element
createElement :: (ArrowXml a) => Shape -> a XmlTree XmlTree
createElement (LinePoly (r,g,b) p) = polygonElement (concatMap writePoint p) $ linePolyStyle r g b
createElement (FilledPoly (fr,fg,fb) (lr,lg,lb) p) = polygonElement (concatMap writePoint p) $ filledPolyStyle fr fg gb lr lg lb
createElement (Line (r,g,b) (p1,p2)) = lineElement p1 p2 $ lineStyle r g b
createElement (Text (r,g,b) i p s) = textElement p “Verdana” i (rgb r g b) s
– Creates a polygon xml element
polygonElement :: (ArrowXml a) => String -> String -> a XmlTree XmlTree
polygonElement points style = mkelem “polygon” [sattr "points" points, sattr "style" style] []
– Creates a line xml element
lineElement :: (ArrowXml a) => Point -> Point -> String -> a XmlTree XmlTree
lineElement (x1,y1) (x2,y2) style =
mkelem “line”
[ sattr "x1" (show x1), sattr "y1" (show y1), sattr "x2" (show x2), sattr "y2" (show y2), sattr "style" style] []
– creates a text xml element
textElement :: (ArrowXml a) => Point -> String -> Int -> String -> String -> a XmlTree XmlTree
textElement (x,y) fontFam fontSize fill text =
mkelem “text”
[ sattr "x" (show x), sattr "y" (show y), sattr "font-family" fontFam, sattr "font-size" (show fontSize), sattr "fill" fill ]
[ txt text ]
– Style attrubutes for each type of element
lineStyle r g b = “stroke:” ++ (rgb r g b) ++ “stroke-width:2″
filledPolyStyle fr fg fb lr lg lb = “fill:” ++ (rgb fr fg fb) ++ “stroke:” ++ (rgb lr lg lb) ++ “stroke-width:2″
linePolyStyle r g b = “fill:transparent;stroke:” ++ (rgb r g b) ++ “stroke-width:2″
– Creates rgb “thing” based on rgb int values
rgb :: Int -> Int -> Int -> String
rgb r g b = “rgb(“++(show r)++”,”++(show g)++”,”++(show b)++”);”
————————————
and replaced the writeFile with:
————————————-
– Create the xml representation
let svgXml = createXml (drawSamples (snd file) ++ drawAxis (fromIntegral (length (snd file))) (fst file))
– Use arrow to write the XML Representation to file
runX (root [] [svgXml] >>> writeDocument [(a_indent, v_1)] “graph2.svg”)
————————————–
The library is pretty simple to use. The primary function is ‘mkelem’ which takes a string name and two lists, a list of attribues and a lits of sub elements. Well thats my initial pass at using an xml library, I hope it helps you out….I gotta get back to work now
http://www.haskell.org/haskellwiki/HXT#Document_construction_examples
-Bryan
Sorry to post such a nightmare in a reply…it certainly didn’t look that way when I was writing it!
Hey, I was expecting to have another post up on the topic before now but since I haven’t I just wanted to let you know I haven’t given up!
I’ve been looking through the HXT documentation and examples here and there is a lot of new information for me to understand, but I’m working through it. As you may have seen, I’ve also been using C# for a one week game project – I’m seeing a difference in the way I look at programming already. Hopefully I’ll have something cool to share soon
Yea, from your recent posts it definitely seems as though your pretty busy. I know what you mean by programming differently…I was recently working on a Ruby program and found myself adding a foldr function to the Array class
. Well, if you find you need any help let me know!
- Bryan
Hazel, while you’re jamming out the C# for your game project, take a look at F#. Functional programming in .NET. It’s primarily architected by Simon Peyton Jones (The guy who did so much awesome work with GHC the Glasgow Haskell Compiler), and bares a lot of resemblance to Haskell. It will be one of the core languages in the 2010 release of Visual Studio, so very much worth taking a look at. Plus, you’ll have access to all the .NET libraries you’re use to.
-Alex
I’ve heard of F# and certainly been curious but have never really looked into it. I’ll definitely try and take some time to check it out.
Thanks!