Using Compound Types to Make Haskell Easier
Now that we know a bit more about Haskell’s type system, let’s get more familiar with some of the basic types. Like pretty much any other language, Haskell has integers, floating point numbers, characters, and strings. We can define a function that uses all of these basic types:
myFunction :: Int -> Float -> Char -> String -> String myFunction x y c str = (c : str) ++ “ “ ++ show x ++ show y myResult :: String myResult = myFunction 4 5.5 ‘a’ “bcd”
Notice also that “myFunction” has its own type, based on what arguments it takes, and what it returns. As we know from the previous article, applying the function to arguments results in the final return type of the function, which we see in “myResult”.
Tuples
But what if we want to return multiple things from a function? Say we want to return both a floating number (“myNum”) and a string (“myStr”) from “myFunction”:
myFunction :: Int -> Float -> Char -> String -> ??? myFunction x y c str = ??? Where myNum = x * y myStr = c : str
This is where tuples come in. Tuples allow us to combine multiple types into a single type. The type name of a tuple is denoted by a comma separated list of types within parentheses. Similarly, you build tuples with a comma separated list of the values. In this example, we would use a tuple of a Float and a String:
myFunction :: Int -> Float -> Char -> String -> (Float, String) myFunction x y c str = (myNum, myStr) Where nyNum = x + y myStr = c : str
Tuples can have as many types as you want, but many library functions only work up to tuples of 7 types. You can have the same type multiple times within a tuple as well. For instance, “(String, Int, Float)” and “(String, Int, Char, Float, Int)” are both valid types.
Most often though, two items are present within a tuple. In this case, you can use “fst” to access the first element of the tuple, and “snd” to access the second element.
addElementsOfTuple :: (Int, Int) -> Int addElementsOfTuple tup = fst tup + snd tup
Lists
The second special type we will discuss is the List. While a tuple can contain several elements , each with a different type, a List contains many elements, each having the same type. A list type is denoted by brackets around the original tuple type name. In code, you can create lists by putting brackets around a comma separated list of values.
listExample :: [Int] listExample = [1,2,3,4]
It is important to note, for people coming from other programming languages, that the list type in Haskell represents a linked list, not an array. It is quite efficient to separate the first element of a list from the rest, or to add an element to the front of a list. However, accessing an element in a list by index and appending two lists together are relatively inefficient operations. You can only effectively add to the back of a list using inefficient appending (++).
-- “:” is the concatenation operator for lists. It is efficient! Add1 :: [Int] -> [Int] add1 firstList = 1 : firstList -- This is efficient! getRest :: [Int] -> [Int] getRest (_ : rs) = rs -- This is relatively inefficient add1ToBack :: [Int] -> [Int] add1Toback l = l ++ [1] -- Indexing to the 6th element. Also relatively inefficient index :: [Int] -> Int index ls = ls !! 5
Notice above in the getRest example that when we receive the list as a parameter, we can pattern match on the structure of the list, allowing us to easier access certain parts of it. We’ll see this pattern matching behavior a lot as we dig deeper into the type system.
Lists are ubiquitous and important in Haskell. In fact, the String class is actually implemented as a list of characters! So if you ever see an error mentioning the type “[Char]”, understand it is talking about Strings! Accordingly, there are a number of library functions which help manipulating lists. We’ll talk about these more in a later article.
The last thing to mention about lists is the existence of infinite lists. Another concept for the future is Haskell’s system of lazy evaluation. It only evaluates the expressions that it absolutely needs to. It is illegal to bring a full infinite list into scope by, say, trying to print it. But you need not bring the full list into scope to take the first 4 elements of an infinite list. Here are two ways to make infinite lists in Haskell:
infiniteLists :: ([Int], [Int]) infiniteLists = (repeat 4, cycle [1,2,3]) -- This works fine! first4Elements :: [Int] first4Elements = take 4 (fst infiniteLists) -- This does not work! printInfiniteList :: IO () printInfiniteList = print (snd infiniteLists)
The first element in “infiniteLists” will be a list containing nothing but 4’s, and the second will be a list that cycles between its three elements, looking like “[1,2,3,1,2,3,1,2,3,...]”
Maybes
The last special type we will discuss is Maybes. Maybe is effectively a wrapper that can be put around any type. The wrapper encapsulates the possibility of failure.
There are two types of maybe values: Nothing and Just. If a Maybe value is Nothing, there is no other value attached to it. If it is Just, then it contains some inner value. You typically “unwrap” a Maybe value by pattern matching, either on a function argument or using a case statement. This allows you to customize the behavior in case something failed.
resultString :: Maybe Int -> String resultString Nothing = “It failed!” resultString (Just x) = “It succeeded! “ ++ (show x) resultString2 :: MaybeInt -> String resultString2 maybeX = case maybeX of Nothing -> “It failed!” Just x -> “It succeeded! “ ++ (show x)
The cool thing about all these types is that they are infinitely composable. You can have a list of lists, a tuple containing a Maybe and a List, or a Maybe List of Tuples of Ints, Floats, and Lists! So no matter what, remember these 3 fundamental Haskell data structures:
Tuples
Lists
Maybes