1
votes

I'm trying to learn Haskell and I thought I would try and replicate the Linux 'xxd' utility. However I am stuck on the right-hand column (which displays ASCII or a blank for non-printing characters).

As a reminder, typical xxd output looks like this:

00000000: 4927 6d20 7472 7969 6e67 2074 6f20 6c65  I'm trying to le
00000010: 6172 6e20 4861 736b 656c 6c2e 2041 7320  arn Haskell. As 
...

I also want to take advantage of the Unicode "Control Pictures" block to display little symbols for the control codes 0..31 rather than dots or placeholders. So I have a helper function that converts a Word8 to a Char and at the same time replaces the control characters with the equivalent from the Control Pictures block.

http://www.unicode-symbol.com/block/Control_Pictures.html

Constraints: Eventually the program will read a file from disk so I am expecting to read in a ByteString or lazy ByteString. Also I want to use Data.Text to hold the output rather than String.

Ideally: I would like to avoid converting the ByteString to something else wholesale e.g. [Word8] because - ultimately - I need to learn how to work with it, not around it.

My problem is that I can't get map to work for me. Neither B.map nor T.map will work because they expect a function ([a] -> [a]). Prelude.map looks more promising as it expects ([a] -> [b]) but I can't get it to work with the imported types. So I had a go at defining my own map (just to try and find something that would work which I could then replace with a built-in function when I had a better understanding) but that isn't working either.

The non-functioning code I have is as follows

import qualified Data.ByteString as B
import qualified Data.Text as T
import Data.Word
import Data.Char

{- Make some normally undisplayable bytes into displayable chars -}
displayableChar :: Word8 -> Char
displayableChar w
  | i < 32  = chr (0x2400 + i)  -- 0x2420 control codes
  | i < 33  = chr 0x2423        -- 0x2423 trough for space
  | i < 127 = chr i
  | i < 128 = chr 0x2421        -- 0x2421 del
  | otherwise = ' '
  where i = fromIntegral w


mymap :: (Word8 -> Char) -> B.ByteString -> [Char]
mymap f bstr
  | bstr == B.empty = []
  | otherwise       = f x : map f xs
  where
    x = B.head bstr
    xs = B.tail bstr


test_data = B.pack [1..250]

Advice and suggestions welcome please on what is the 'right' way to apply displayableChar to each byte in the ByeString and get Text out.

1

1 Answers

1
votes

You wrote map instead of mymap in your function definition:

mymap :: (Word8 -> Char) -> B.ByteString -> [Char]
mymap f bstr
  | bstr == B.empty = []
  | otherwise       = f x : mymap f xs   -- <- here
  where
    x = B.head bstr
    xs = B.tail bstr

Another way is to use B.unwrap to get a list of Word8 so you can then apply map which works on lists:

mymap :: (Word8 -> Char) -> ByteString -> [Char]
mymap f bs = map f (B.unwrap bs)