Treating Strings like Lists

We've spent the last month studying some of the intricacies of the different string types in Haskell. For this last article, I wanted to have a little bit of fun and consider how we might apply some of the ideas we learned from "list" functions back in January to these different string types.

The naive way to use these functions would be to transform your "Text" or "ByteString" back into a "String", run the list-based function, and then convert back. But it should hopefully be obvious now that that isn't such a great idea!

But these other string types still kind of seem like lists, so we should want to apply those functions. And because of this the authors for those packages included versions of the most common list based functions specifically geared for these types. For example, Text and ByteStrings have their own versions of functions like "map", "filter" and "fold".

{-# LANGUAGE OverloadedStrings #-}

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

T.map :: (Char -> Char) -> T.Text -> T.Text
T.filter :: (Char -> Bool) -> T.Text -> T.Text
T.foldl :: (a -> Char -> a) -> a -> T.Text -> a

B.map :: (Word8 -> Word8) -> B.ByteString -> B.ByteString
B.filter :: (Word8 -> Bool) -> B.ByteString -> B.ByteString
B.foldl :: (a -> Word8 -> a) -> a -> B.ByteString -> a

Notice how for the ByteString, instead of Char being the underlying value, it's Word8. Unfortunately, this makes it a little harder to do character-specific manipulations, but we can still try!

>> T.filter isLower "Hello World"
"ello orld"
>> B.map (+1) "abcde"
"bcdef"

The numeric property underlying ByteStrings means you can do some interesting mathematical things with them. For example, you can build a simple "Caesar Cypher" like so, if you assume that your input is all lower-case or spaces. The number 97 indicates the offset for the letter 'a' as a Word8:

caesarCypher :: Int -> B.ByteString -> B.ByteString
caesarCypher shift = B.map shiftChar
  where
    shiftChar :: Word8 -> Word8
    shiftChar 32 = 32 -- Space
    shiftChar x = (((x - 97) + shift) `mod` 26) + 97

...

>> caesarCypher "hello there"
"mjqqt ymjwj"

Even more complicated functions like intercalate and transpose can be found in these libraries!

>> let bytestrings = ["One", "Two", "Three", "Four", "Five"] :: [B.ByteString]
>> B.transpose bytestrings
["OTTFF", "nwhoi", "eoruv", "ere", "e"]
>> let texts = ["Hello", "my", "friend"] :: [T.Text]
>> T.intercalate ", " texts
"Hello, my, friend"

So even if your program is using more advanced string types instead of the basic [Char], you can still get all the functional benefits of Haskell's many different ways of manipulating lists of values!

This is the end of our exploration of string types for now. But we'll be back with a new topic for March. So subscribe to our newsletter and keep coming back here if you want to stay up to date with the blog!

Previous
Previous

Using Either as a Monad

Next
Next

Fusion Powered Strings!