Learning Haskell: Characters and Strings

posted 2 years ago

Diving Into Types

Types are a way of categorizing values.

There are several basic types.

There are those for numbers: integers, fractional values, etc.

There are booleans, specifically True or False.

Then, there are the types that we are talking about in this article: Char or character and String.

A String is a list of characters.

You can check the type of a value with the :type or :t command:

Prelude> :type 'r'
'r' :: Char

Notice that the character is in single quotes. This is a requirement for a character.

Char includes alphabetic characters, Unicode, symbols, etc.

As mentioned in a previous article, :: is read as "this has the type". In this case, 'r' has the type Char.

Let's try a string of text:

Prelude> :type "Hello!"
"Hello!" :: [Char]

String is a type synonym for a list of characters.

The square brackets around Char are syntactic sugar for a list.

Strings are wrapped in double quotes.

Printing Simple Strings

Printing "Hello!":

Prelude> print "Hello!"
"Hello!"

You can print different types using the print function.

These are prints specific to String:

Prelude> putStrLn "Hello!"
Hello!
Prelude> putStr "Hello!"
Hello!Prelude> 

putStr and putStrLn are similar to print, but they print the String without double quotes.

This is because they have a different type than print does.

Enter the following into strings.hs:

module Strings where

stringPrinter :: IO ()
stringPrinter = putStrLn "Hello!"

Load it into GHCi:

Prelude> :load strings.hs
[1 of 1] Compiling Strings          ( strings.hs, interpreted )
Ok, one module loaded.
*Strings> stringPrinter
Hello!
*Strings> 

stringPrinter has the type IO ().

IO stands for input/output.

Printing to the screen requires that the output of a module be wrapped in the IO type.

In the REPL, it implicitly understands and implements IO without you having to specify it.

Begin another file called strings2.hs:

module Strings2 where

main :: IO ()
main = do
  putStrLn "Let's count!"
  putStr "zero, one"
  putStr "two, three, and"
  putStrLn " four!"
*Strings2> :load strings2.hs
[1 of 1] Compiling Strings2         ( strings2.hs, interpreted )
Ok, one module loaded.
*Strings2> main
Let's count!
zero, one, two, three, and four!

The do syntax allows for sequencing actions.

It is most commonly used to sequence the actions that make up your program.

The Ln in putStrLn indicates that it starts a new line.

Concatenating Strings

Concatenating means to link together.

The result of concatenating two strings is a combined version.

Make a new file called concatenating.hs:

module Concatenating where

greeting :: String
greeting = "Hello" ++ " Haskell"

hello :: String
hello = "Hello"

haskell :: String
haskell = "Haskell"

main :: IO ()
main = do
  putStrLn greeting
  putStrLn combined
  where
    combined = concat [hello, " ", haskell]
*Concatenating> main
Hello Haskell
Hello Haskell
  1. We defined three values: greeting, hello, and haskell.
  2. We specified the type for each of these
  3. We concatenated the strings with (++) and concat.

Concatenation Functions

(++) is an infix operator. When we need to refer to an infix operator in an position that is not infix, we put parenthesis around it.

concat is a normal function. Parentheses aren't necessary.

Let's take a look at the types for (++) and concat:

++     has the type [a] -> [a] -> [a]
concat has the type [[a]] -> [a]

If you're using GHC 7.10 or higher, the type signature is Foldable t => t [a]. For our current purposes, just read it as being [[a]].

concat says that it takes a list of lists as input and will return a list.

The result from concat is flattened.

Prelude> concat [[0, 1, 2], [3], [4], [5, 6]]
[0,1,2,3,4,5,6]
Prelude> concat ["I", " like ", "turtles"]
"I like turtles"

Let's break down the (++) type:

(++) :: [a] -> [a] -> [a]
--      [1]    [2]    [3]
  1. Takes an argument of type [a]. This type is a list of elements of some type a. The function does not know what a is. a will be made known as some point in the life-cycle.
  2. Another argument of type [a]. A list of elements of some type a. Because the variables are the same a == a, they must remain the same.
  3. Return the result of type [a].

The type variable a is polymorphic because it can represent various types.

Again, a must equal a:

Prelude> "hello" ++ " haskell" -- same types
"hello haskell"
Prelude> "hello" + [1] -- different types, will not work

<interactive>:17:1: error:
    • No instance for (Num [Char]) arising from a use of ‘+’
    • In the expression: "hello" + [1]
      In an equation for ‘it’: it = "hello" + [1]

<interactive>:17:12: error:
    • No instance for (Num Char) arising from the literal ‘1’
    • In the expression: 1
      In the second argument of ‘(+)’, namely ‘[1]’
      In the expression: "hello" + [1]

Top-level Definitions Versus Local Definitions

Top-level declarations are not nested within anything else, and they are in scope throughout the whole module.

Local declarations are nested within some other expression and are not visible outside that expression.

module TopOrLocal where

topLevelFunction :: Integer -> Integer
topLevelFunction x =
  x + y + topLevelNumber
  where
    y = 5

topLevelNumber :: Integer
topLevelNumber = 5
*TopOrLocal> topLevelFunction 5
15

You can access topLevelFunction and topLevelNumber anywhere in the module. You could also import them from another module.

y is effectively invisible outside of topLevelFunction; you cannot access it anywhere else in the module.

More List Functions

Since a String is a list, we can use standard list operations on it:

-- The (:) operator builds a list
Prelude> 'C' : "layton"
"Clayton"
-- head returns the first element of a list
Prelude> head "Clayton"
'C'
-- tail returns the list with the head chopped off
Prelude> tail "Clayton"
"layton"
-- take returns the specified number of elements from a list starting from the left side
Prelude> take 2 "Clayton"
"Cl"
-- drop returns the remainder of the list after the specified numbers of elements has been dropped
Prelude> drop 4 "Clayton"
"ton"
-- The infix operator, (!!), returns the element at the specified index (indices start from 0)
Prelude> "Clayton" !! 0 -- same as: (!!) "Clayton" 0
'C'

Sources

Haskell List Functions - https://hackage.haskell.org/package/base-4.15.0.0/docs/Data-List.html

Haskell Book - https://haskellbook.com/