Search code examples
loopshaskellside-effects

haskell : how to draw some pixels (side effects) using gtk and the function map


I am trying to learn gtk2hs, an API wich allows haskell programs to use windows, menus, toolbars and graphs.

I want to draw the Mandelbrot set, so I began to write the stuff, but I am stuck at the end, namely when I have to use side effects to draw each point of the Mandelbrot set.

The datas are : I have a canvas (a drawing area) of 300px * 200px, and a function mandelbrot :: Float -> Float -> Bool, whose output is true if the point is in the mandelbrot set, and false otherwise.

the work to achieve is : for each pixel (width : 0 to 300, height : 0 to 200), transform the coordinates to a range of [-2..2]*[-2..2], call the mandelbrot function, and if the result is true, call a function of the package Cairo which draws the point. (The function is C.rectangle a b 1 1.)

My try:

example :: Double -> Double -> C.Render ()
example width height = do
  setSourceRGB 0 0 0
  setLineWidth 1

  let 
    affiche a b = do 
      if (mandelbrot a b) then 
        C.rectangle a b 1 1
        return()
      return ()

    colonnes = [0..299]
    lignes = [0..199]
  in 
    map (\t -> t/300*4-2) colonnes
    map (\t -> t/200*4-2) lignes
    map affiche (zip (colonnes,lignes))
    stroke            -- it displays the changes on the screen

It triggers the error: - at the second return:

Error: Parse error: return

thanks

EDIT: Thanks a lot for your answer. I greatly improved my program, but I still have errors.

here is the latest version:

affiche :: Double -> Double -> Render()
affiche a b = when (mandelbrot a b) $ C.rectangle a b 1 1

colonnes = [ t/300.0*4.0-2.0 | t<-[0.0..299.0] ]
lignes = [ t/200.0*4.0-2.0 | t<-[0.0..199.0] ]


example :: Double -> Double -> C.Render ()
example width height = do
    setSourceRGB 0 0 0
    setLineWidth 1

    mapM_ (\ (a, b) -> affiche a b) (zip (colonnes,lignes))
    stroke

and the error is:

(all 2 at the line with "mapM_"):

*   Couldn't match type ‘[(a0, b0)]’ with ‘(Double, Double)’
  Expected type: [b0] -> (Double, Double)
    Actual type: [b0] -> [(a0, b0)]
  In the second argument of ‘mapM_’, namely
    ‘(zip (colonnes, lignes))’
  In a stmt of a 'do' block:
    mapM_ (\ (a, b) -> affiche a b) (zip (colonnes, lignes))


*  Couldn't match expected type ‘[a0]’
              with actual type ‘([Double], [Double])’
  In the first argument of ‘zip’, namely ‘(colonnes, lignes)’
  In the second argument of ‘mapM_’, namely
    ‘(zip (colonnes, lignes))’
  In a stmt of a 'do' block:
    mapM_ (\ (a, b) -> affiche a b) (zip (colonnes, lignes))

I also have an other question : please confirm me that if a function returns the type "Render()" then all the statements inside it should return a value of this type. thanks


Solution

  • The if construct in Haskell doesn't work like in other languages: it's actually the equivalent to the ternary conditional operator that's ? : in C-like languages. That means, if you use if ... then, you must aways also have an else branch.

    The reason: Haskell isn't imperative; you don't write out what should be done but what the result should be. In an imperative language, you can do nothing, but in Haskell you always need to specify some result.

    Now, a monadic do block of course is basically an imperative embedded language. Here, you can just specify that a local result should be the no-op action, to achieve that nothing is done at that point:

        affiche a b = do 
          if mandelbrot a b then 
            C.rectangle a b 1 1
            return ()   -- note: `return` isn't needed here
           else
            return ()
          return ()  -- nor here
    

    A shorter way of writing this would be with the when combinator, which is basically if-then-else with a return () in the else-branch:

        affiche a b = when (mandelbrot a b) $ C.rectangle a b 1 1
    

    There's another problem with your code:

      in 
        map (\t -> t/300*4-2) colonnes      -- number list
        map (\t -> t/200*4-2) lignes        -- number list
        map affiche (zip (colonnes,lignes)) -- list of `Render ()` actions
        stroke                              -- `Render ()` action
    

    There, you just write out a couple of expressions of completely different types. You can't do that! What is Haskell supposed to do with these expressions?

    Well, evidently you want to execute a sequence of actions. Hence you need do again!

      in do
        ...
    

    But these lists you get from map (\t -> t/300*4-2) colonnes are not actions at all. You can't execute them, just evaluate them. In this case, apparently you want colonnes to be the result of mapping that function over the list [0..299]. Well, then why don't you specify this right away?

        colonnes = map (\t -> t/300*4-2) [0..299]
        lignes = map (\t -> t/200*4-2) [0..199]
    

    or, why not as a list comprehension

        colonnes = [ t/300*4-2 | t<-[0..299] ]
        lignes = [ t/200*4-2) t<-[0..199] ]
    

    Finally, you need to map affiche over the lists. That's in fact a “monadic action map”. The function for this is mapM_, not map.

        colonnes = [ t/300*4-2 | t<-[0..299] ]
        lignes = [ t/200*4-2) t<-[0..199] ]
      in
        mapM_ affiche $ zip colonnes lignes
        stroke
    

    Almost there, but not quite. If the signature of affiche would take a tuple of two numbers (you get such tuples from zip), then this would work. However, affiche a b = do means the function is curried (as is customary in Haskell, in particularly also for zip!). You can easily undo that though

        mapM_ (uncurry affiche) $ zip colonnes lignes
    

    but in this specific case I would actually recommend instead defining

        affiche :: (Float,Float) -> C.Render ()
        affiche (a,b) = when (mandelbrot a b) $ C.rectangle a b 1 1
    

    because a and b really belong together, forming a single coordinate specification.

    There's one more problem: you've used Float numbers here. Well... it doesn't really make sense to do that, in Haskell. At any rate, rectangle needs Double as arguments, so either switch both affiche and mandelbrot to also take Double, or convert a and b before passing to rectangle:

        affiche :: (Float,Float) -> C.Render ()
        affiche (a,b) = when (mandelbrot a b)
                      $ C.rectangle (realToFrac a) (realToFrac b) 1 1
    

    Oh, and yet another thing: I don't think zip does the right thing here. But... figure this out yourself...