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

asked Oct 20, 2019 at 7:08
\$\endgroup\$
0

2 Answers 2

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.

answered Oct 22, 2019 at 15:14
\$\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]]
answered Oct 20, 2019 at 12:54
\$\endgroup\$
0

Your Answer

Draft saved
Draft discarded

Sign up or log in

Sign up using Google
Sign up using Email and Password

Post as a guest

Required, but never shown

Post as a guest

Required, but never shown

By clicking "Post Your Answer", you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.