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.