5
\$\begingroup\$

I've been slowly learning Haskell over the past few months. I made a small module to graph functions using Gloss, and I would like feedback on how idiomatic it is (looking for ways to simplify using standard functions and to make it more pointfree).

Graph.hs

module Graph (graph) where

import Graphics.Gloss

graph :: (Float -> Float) -> (Float, Float, Float, Float) -> Float -> Picture
graph f (l, r, b, t) dx = pictures $ map Line visible
    where
        points :: [(Float, Float)]
        points = [(x, f x) | x <- [l,l+dx..r]]

        pairs :: [[(Float, Float)]]
        pairs = zipWith (\x y -> [x,y]) points $ tail points

        visible :: [[(Float, Float)]]
        visible = filter (all (\(_,y) -> b <= y && t >= y)) $ pairs

Main.hs

import Graphics.Gloss

import Graph

main :: IO ()
main = display FullScreen white . scale 20 20 . pictures $ [
        color blue $ graph f (-10, 10, -10, 10) 0.001,
        color black $ Line [(0, -10), (0, 10)],
        color black $ Line [(-10, 0), (10, 0)]
    ]
    where
        f :: Float -> Float
        f x = 1 / (x - 1)

Output Output

\$\endgroup\$
3
\$\begingroup\$

This looks great. Honestly, there isn't anything I'd change. You could make it more pointfree in some parts, but that's not going be more readable or maintainable.

Magic numbers

The only part I'd change are the static dimensions in main. Those are fine in a toy program, but a proper one will need some kind of configuration, so make sure that you use proper values from the beginning:

main :: IO ()
main = display FullScreen white . scale width height . pictures $ [
        color blue $ graph f (l, r, b, t) 0.001,
        color black $ Line [(origin, b), (origin, t)],
        color black $ Line [(l, origin), (r, origin)]
    ]
    where
        f :: Float -> Float
        f x = 1 / (x - 1)

        -- easy configurable:
        (l, r, b, t) = (-10, 10, -10, 10)
        width  = 20
        height = 20
        origin =  0

That way you can also exchange the values with proper command line interpretation

main :: IO ()
main = do
    (l, r, b, t) <- getDimensions
    let width = r - l
    let height = t - b
    let origin = ...
    display FullScreen white . scale width height . pictures $ [
...

After all, no magic numbers is a good practice in both imperative and functional languages.

Next, I'd introduce GraphBound as a type synonym, just to make graph's type signature easier on the eye:

-- | Graph boundaries, given in (left, right, bottom, top) order
type GraphBound = (Float, Float, Float, Float)

graph :: (Float -> Float) -> GraphBound -> Float -> Picture
graph f (l, r, b, t) dx = pictures $ map Line visible
    ...

You might even exchange GraphBound with a proper data type later which checks does not export its constructor to make sure that you don't end up with left = 20 and right = -10:

makeGraph :: Float -> Float -> Float -> Float -> Maybe GraphBound

However, that's an overkill, so let's not focus on that for too long.

List comprehensions vs. point-free

Now let's get back to your original query. Is it possible to make graph more point-free?

Sure:

graph :: (Float -> Float) -> (Float, Float, Float, Float) -> Float -> Picture
graph f (l, r, b, t) = pictures . map Line 
                     . filter (all (\(_,y) -> b <= y && t >= y)) 
                     . (tail >>= flip (zipWith (\x y -> [x, y]))) 
                     . map (\x -> (x, f x)) 
                     . flip (enumFromThenTo l) r . (l+)

The point dx is completely gone from graph. However, the function is now unreadable. We went from a perfectly understandable function to a highly complex one. It gets a lot more readable if we use some helpers, but at that point we're almost back to your original function:

graph :: (Float -> Float) -> (Float, Float, Float, Float) -> Float -> Picture
graph f (l, r, b, t) = pictures . map Line . filter inGraph
                     . segments . points . ranged
   where
     inGraph (_,y) = fall (\(_,y) -> b <= y && t >= y)
     segments ps   = zipWith (\x y -> [x, y]) ps $ tail ps
     ...

That's not better than your original version, because your original version is already very good to begin with. The only change I could envision is a list comprehension in visible, but that's a matter of preference:

graph :: (Float -> Float) -> (Float, Float, Float, Float) -> Float -> Picture
graph f (l, r, b, t) dx = pictures lines
    where
        points = [(x, f x) | x <- [l,l+dx..r]]
        pairs  = zipWith (\x y -> [x,y]) points $ tail points

        lines = [Line segment | segment <- pairs,  all (\(_,y) -> b <= y && t >= y)) segment]

But you're the judge on which variant you want to use.

Other remarks

Thank you for using type signatures. Keep in mind that it's uncommon to use them in local bindings (where), as the outer function's signature should fix all types already. Inner type signatures can be a hassle if you change your outer type signature later, but they're sometimes necessary.

\$\endgroup\$
-4
\$\begingroup\$

You should simplify using standard functions and make it more pointfree.

import Data.List.Split

graph :: (Float -> Float) -> (Float, Float, Float, Float) -> Float -> Picture
graph f (l, r, b, t) dx = pictures $ map Line
  $ wordsBy (\(_,y) -> b > y || t < y) [(x, f x) | x <- [l,l+dx..r]]
\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.