Chapter 3. SOE

If I found the exercises in chapter 2 a little hard, the fact that there
are only two of them in this chapter should have been a warning…

Amusingly, Hudak suggests that the module SOEGraphics will not
evolve, as it is designed for the textbook, however, as it happens, it
has changed its API in a significant way – by being renamed to
Graphics.SOE.

The chapter starts explaining “actions” and how we'll do IO with them.
The explanation is clear, and doesn't mention the magic and terrifying
word “Monad” once.

We learn how to do “Hello World”!  He briefly mentions commands like
readFile, getLine, writeFile, but we're
concentrating on graphics here, so not going to go into that much
detail.

The example of creating putCharList by sequence_ing a
group of putChar actions is very odd and intriguing.

And then we move onto a graphics example!  The first time a do
expression is seen, and in my edition the code listing spans pages 40
and 41, with the opening “do“ being just before the page break,
so it's easy to get the alignment wrong.

(Although, to be fair, he does mention alignment the page before and the
rules are the same as the “let“ and “where“ clauses
we've already used).

ex 3.1 Defined putStr and getLine “recursively”, ie. not as a sequence_ of mapped actions.

The hint says to use “return“ like the preceding discussion, which
appears to be very brief – though I said the IO discussion was clear,
I'm not sure it really explains what "return" does in the detail
necessary to understand this exercise.

With a bit of playing putStr seems to be definable something like this:

 > putStr2 :: String -> IO () > putStr2 [] = return () > putStr2 (s:ss) = do putChar s >                     putStr2 ss

You can do return () :: IO () instead, as per his hint, but it
doesn't seem to be necessary.

Though that seems to work, it's interesting observing myself trying to
get to that point, basically flailing around at random till I find
something that ghci will compile.  This was even more evident
for the getLine example.  I'm trying to create an auxiliary
function _getLine2 which will do the looping, and I'm getting errors
like

 Couldn't match `[Char]' against `a -> IO a'

Which reminds me that I should be thinking this through rather
than trying to work it out intuitively.   Without the auxiliary function
I can get as far as:

 >    getLine2 :: IO String >    getLine2 = do x <- getChar >                  if x == '\n' then return "" :: IO String >                               else  ...

but at that point I really want to get the char returned from getChar
and then recurse into getLine2 again, concatenating the result.  But
that means running yet more IO commands, and I'm unsure as to how to do
that in Haskell syntax.  I tried:

 >    getLine2 :: IO String >    getLine2 =  >        do x <- getChar >           if x == '\n' then return "" :: IO String  >                        else y <- getLine2; return (x : y) :: IO String

which complains with the helpful “Parse error” (Haskell error reporting
is surprisingly underwhelming.  Say what you like about my main
language, Perl (and, yes, most people do…) but it has surprisingly
helpful and useful error messages compared to anything else I've seen.)

Adding a do:

 >    getLine2 :: IO String >    getLine2 =  >        do x <- getChar >           if x == '\n' then return "" :: IO String  >                        else do y <- getLine2; return (x : y) :: IO String 

works though.  (I was sure I'd tried this before).  As I said, until I
understand this, this kind of flailing around adding stuff at random is
probably the order of the day, and I don't think that the discussion in
the book about this topic was really enough to be able to realistically
do that much else.

Ex 3.2.  Draw a snowflake fractal!

On the face of it, with the good examples on drawing a triangle, this
exercise ought to be easy in comparison!

OK, first of all, there is the maths fun of how to work out where the
triangle points go.  I started messing about with a bit of pythagoras,
and ended up deciding that I'm too stupid to work this out, so let's
just work out some likely looking constants to multiply “s“ by,
and we should be ok.  (But I will at some point look up the maths to do
this properly – I was guessing that with a bit of pythagoras and some
equations I'd be able to get there, but I appear to have forgotten
everything I studied 12 years ago and just can't do it)

At this point haskell bites me in the arse with Integers.
fromInteger takes an Integer, whereas polygon takes Ints.  This
is obviously time to kick cute small fluffy creatures.

Sneaking ahead a few pages to chapter 4, we see the definition

 >    intToFloat :: Int -> Float >    intToFloat n = fromInteger (toInteger n)

Which helps.  (quicksilver on #haskelll pointed out that there is a
function fromIntegral).  Eventually, I'm able to create the
snowflake generator!  To add the withColor, I end up passing a list,
threading it through (in the way that I'm beginning to understand that
Monads will help me with when I understand them…), and using a bit of
cleverness with a repeated lazy list:

 > main >   = let colors = cycle [White,Blue,Yellow,Red,Green] >     in runGraphics ( >         do w <- openWindow "Snowflake" (800,900) >            star w colors (350,350) 280 >            spaceClose w >         )

I started off with concat . repeat, but quicksilver pointed me
at cycle, which to be honest is more readable.

 >    star :: Window -> [Color] -> (Int,Int) -> Int -> IO () >    star w (c:cs) (x,y) s >        = let t1 = triangle (x,y)   s >              t2 = triangle (x,y) (-s) >              points = t1 ++ t2 >              aux (x',y') = star w cs (x',y') (s `div` 3) >          in do drawInWindow w (withColor c $ polygon t1) >                drawInWindow w (withColor c $ polygon t2) >                if s > 2 then sequence_ (map aux points) >                         else return ()

Again, quicksilver suggests mapM_ instead of sequence_ .
map
.

Update: Though I used the definition of intToFloat in the
original version of the triangle routine, (multiplying likely looking
constants by the side argument, because I didn't get the maths to draw a
proper equilateral triangle) I now implemented regularPoly
so I just used that.  The small matter that the Shape constructors take
Float while we're using Int is why I ended up using truncate:

 > triangle (x,y) s >  = let poly2list (Polygon p) =  >        map (\(x1,y1)->(x + truncate x1, y + truncate y1)) p >    in poly2list (regularPoly 3 (fromIntegral s))

This actually works!  I think the Graphics.SOE library may be both slow
and buggy though due to:

  • It's observably slow.  I'd expect the whole fractal to be drawn
    near instantly.  Or at any rate faster than the very leisurely rendering
    I'm getting.
  • sometimes rendering just stops until an IO even happens.  If I move
    the mouse pointer over the window then the rendering picks up.
    ghci regularly moans “thread blocked indefinitely”.
  • If I necessitate even a partial redraw of the window (for example
    if another window obscured a section of this one), then on redraw the
    whole fractal is drawn again (slowly), and sometimes twice.

Is this how Graphics.SOE works?  If so, I'd say it isn't
particularly impressive, though to be fair it is a sample library for
pedagogical purposes.  Of course, it's likely that the problem lies with
my implementation.  Comments welcome!

And here's the snowflake!

Screenshot of final snowflake fractal from SOE chapter 3