Use a name other than print
for your image to avoid collisions with the Prelude's print
.
type Pixel = Int
type Row = [Pixel]
type PixelImage = [Row]
img :: PixelImage
img = [[208,152,240,29],[0,112,255,59],[76,185,0,152]]
This is an inefficient representation, but it will do for a learning exercise.
You could print a PixelImage
with the rows stacked on top of one another with a few imports at the top of your source and an I/O action:
import Control.Monad
import Data.List
import Text.Printf
printImage :: PixelImage -> IO ()
printImage img =
forM_ img $ putStrLn . intercalate " " . map (printf "%3d")
That may look intimidating, but everything there is familiar. The word order is just a little funny. For each Row
in the PixelImage
(forM_
, a lot like a for
loop in other languages) we print (with putStrLn
) the list of Pixel
values separated by two spaces (thanks to intercalate
) and left-padded with spaces to make uniform 3-character fields (printf
).
With the image from your question, we get
ghci> printImage img
208 152 240 29
0 112 255 59
76 185 0 152
Haskell lists are immutable: you cannot modify one in-place or destructively. Instead, think of it in terms of making a different list that's identical to the original except for the specified row.
modifyRow :: PixelImage -> (Row -> Row) -> Int -> PixelImage
modifyRow img f i = map go (zip [0..] img)
where go (j,r) | i == j = f r
| otherwise = r
This gives your function a chance to fire for each Row
in the PixelImage
. Say you want to zero out a particular row:
ghci> printImage $ modifyRow img (map $ const 0) 0
0 0 0 0
0 112 255 59
76 185 0 152
Reversing a row is
ghci> printImage $ modifyRow img reverse 0
29 240 152 208
0 112 255 59
76 185 0 152
In another language, you might write img[2] = [1,2,3,4]
, but in Haskell it's
ghci> modifyRow img (const [1..4]) 2
[[208,152,240,29],[0,112,255,59],[1,2,3,4]]
That usage isn't terribly evocative, so we can defined setRow
in terms of modifyRow
, a common technique in functional programming.
setRow :: PixelImage -> Row -> Int -> PixelImage
setRow img r i = modifyRow img (const r) i
Nicer:
ghci> printImage $ setRow img [4,3,2,1] 1
208 152 240 29
4 3 2 1
76 185 0 152
Maybe you want to scale the pixel values instead.
scaleRow :: (RealFrac a) => PixelImage -> a -> Int -> PixelImage
scaleRow img x i = modifyRow img f i
where f = let clamp z | z < 0 = 0
| z > 255 = 255
| otherwise = truncate z
in map (clamp . (x *) . fromIntegral)
For example:
ghci> printImage $ scaleRow img 0.5 1
208 152 240 29
0 56 127 29
76 185 0 152
Adding scaleImage
to apply a scaling factor to each Pixel
in a PixelImage
means a bit of refactoring to avoid repeating the same code in multiple places. We'd like to be able to use
scaleImage :: (RealFrac a) => a -> PixelImage -> PixelImage
scaleImage x = map $ scaleOneRow x
to get, say
ghci> printImage $ scaleImage 3 img
255 255 255 87
0 255 255 177
228 255 0 255
This means scaleOneRow
should be
scaleOneRow :: (RealFrac a) => a -> Row -> Row
scaleOneRow x = map (clamp . (x *) . fromIntegral)
which promotes clamp
to a toplevel function on Pixel
values.
clamp :: (RealFrac a) => a -> Pixel
clamp z | z < 0 = 0
| z > 255 = 255
| otherwise = truncate z
This in turn simplifies scaleRow
:
scaleRow :: (RealFrac a) => PixelImage -> a -> Int -> PixelImage
scaleRow img x i = modifyRow img (scaleOneRow x) i