Filling In Type Holes
Filling In Type Holes
Consider the following example code:
mapApply :: [a -> b] -> [a] -> [b]
mapApply toApply =
concatMap (\input -> map ($ input) toApply)
example :: [Int] -> String
example = mapApply undefined
where
letters :: [Char]
letters = ['a'..'z']
lookupLetter :: Int -> Char
lookupLetter n = letters !! n
offsets :: [Int -> Int]
offsets = [rot13, swap10, mixupVowels]
rot13 :: Int -> Int
rot13 n = (n + 13) `rem` 26
swap10 :: Int -> Int
swap10 n
| n <= 10 = n + 10
| n <= 20 = n - 10
| otherwise = n
mixupVowels :: Int -> Int
mixupVowels n =
case n of
0 -> 8
4 -> 14
8 -> 20
14 -> 0
20 -> 4
n' -> n'Try to fill in the value of undefined so that you get the following output:
λ example [5..15]
"spftqgurhvsuwtjxukyblzcmadnbeacfp"Use type holes to help you figure out the type of the value that you’ll need to use.
Hints
Click to reveal
The goal of this exercise is to understand how to use undefined and type holes
to work with code without necessarily needing to understand the implementation
of particular functions. In this exercise, focus on using the functions that
have already been defined for you by looking at their types. You should be able
to complete this exercise without working through the exact algorithm that’s
being used to transform the input into an output string.
Click to reveal
If you replace undefined with a type hole, the compiler will tell you what
type you should pass to mapApply.
Click to reveal
Replacing undefined with a type hole shows you that the argument to mapApply
should have the type [Int -> Char]. Although none of the where bindings have
precisely that type, notice that:
offsetsis a list of functions, although they returnIntlookupLetteris the only function that returns aChar
Click to reveal
If you’re still having trouble, try writing down some of what you know and
adding narrower type holes. In this case, let’s see if we can make any progress
by using map to transform offsets into the type of function we need to pass
into mapApply:
example = mapApply $ map _ offsets
FillingInTypeHoles.hs:8:26: error: …
• Found hole: _ :: (Int -> Int) -> Int -> Char
• In the first argument of ‘map’, namely ‘_’
In the second argument of ‘($)’, namely ‘map _ offsets’
In the expression: mapApply $ map _ offsets
• Relevant bindings include
lookupLetter :: Int -> Char
(bound at EffectiveHaskell/Exercises/Chapter3/FillingInTypeHoles.hs:14:5)
letters :: [Char]
(bound at EffectiveHaskell/Exercises/Chapter3/FillingInTypeHoles.hs:11:5)
offsets :: [Int -> Int]
(bound at EffectiveHaskell/Exercises/Chapter3/FillingInTypeHoles.hs:17:5)
rot13 :: Int -> Int
(bound at EffectiveHaskell/Exercises/Chapter3/FillingInTypeHoles.hs:20:5)
swap10 :: Int -> Int
(bound at EffectiveHaskell/Exercises/Chapter3/FillingInTypeHoles.hs:23:5)
mixupVowels :: Int -> Int
(bound at EffectiveHaskell/Exercises/Chapter3/FillingInTypeHoles.hs:29:5)
(Some bindings suppressed; use -fmax-relevant-binds=N or -fno-max-relevant-binds)
|
Compilation failed.Solution
Click to reveal
Although the solution to this exercise is fairly straightforward, but the process of finding the solution can help us learn how to work with types more effectively. Let’s take a look at the answer, then work through how we got there:
example = mapApply $ map (lookupLetter .) offsetsSo, how can we figure this out without resorting to working through the actual implementation details of the algorith? Let’s start at the beginning:
mapApply :: [a -> b] -> [a] -> [b]
mapApply toApply =
concatMap (\input -> map ($ input) toApply)
example :: [Int] -> String
example = mapApply undefined
where
letters :: [Char]
letters = ['a'..'z']
lookupLetter :: Int -> Char
lookupLetter n = letters !! n
offsets :: [Int -> Int]
offsets = [rot13, swap10, mixupVowels]
rot13 :: Int -> Int
rot13 n = (n + 13) `rem` 26
swap10 :: Int -> Int
swap10 n
| n <= 10 = n + 10
| n <= 20 = n - 10
| otherwise = n
mixupVowels :: Int -> Int
mixupVowels n =
case n of
0 -> 8
4 -> 14
8 -> 20
14 -> 0
20 -> 4
n' -> n'When we have a problem like this, the first thing we can do is to start figuring out what types we’re working with. You’ll sometimes hear this process being called “type tetris” because you’re trying to fit the types together.
mapApply is a good starting point for this problem. Although it’s written as a
polymorphic function, we know that example is returning a String. That tells
us that, in our call to mapApply we’re setting [b] to String. Since
String is the same as [Char], we can see that b must be Char. Let’s
re-write the type with this information:
mapApply :: [a -> Char] -> [a] -> [Char]We can follow the same process of elimination to figure out what type we’ll be
using for a, but we’re going to have to do a little guessing. In theory we
could ignore list of numbers we’re being passed in to example, and ignore all
of the functions defined in the where clause, but it seems unlikely that we’re
intended to ignore all of that and instead pass in some other type. Given the
evidence at hand, let’s take a guess and try using Int in place of a:
mapApply :: [Int -> Char] -> [Int] -> [Char]This seems like it could be reasonable. Let’s see what the compiler thinks. One
option would be to change the type of mapApply and see our program still
compiles. That approach would work for a small example like this, but in a
larger codebase we might not be able to change the type without breaking code
elsewhere, so let’s use a type hole at the call site instead:
example = mapApply _If we try to compile this, or load it up into ghci, we’ll see that the
compiler tells us that our guess was exactly right. The first argument to
mapApply should be [Int -> Char], meaning a must be Int and b must be
Char:
FillingInTypeHoles.hs:8:20: error: …
• Found hole: _ :: [Int -> Char]
• In the first argument of ‘mapApply’, namely ‘_’Great! So far, so good, but if we look at the functions we have available to us,
none of them have exactly the right type. The closest option that we have is
offsets, which has the type [Int -> Int].
We might not be able to pass offsets directly, but maybe we can do something
with it to make it fit the right shape. Let’s try asking the compiler. We’ll
once again use a type hole, but this time we’ll use the type hole to figure out
what we should apply offsets to so that we can get a function with the type we
need:
example = mapApply $ _ offsetsIn this version of our code, the type hole represents some function that’s
accepting offsets as an argument and returning a value of the correct
type. Let’s compile this version and see what the type of that function should
be:
FillingInTypeHoles.hs:9:22: error: …
• Found hole: _ :: [Int -> Int] -> [Int -> Char]
• In the second argument of ‘($)’, namely ‘_ offsets’This time the type hole wasn’t quite as helpful- it’s only restated what we
already knew. Still, we might be able to use this output to help us move
forward. The compiler has reminded us that we need something that can turn a
list of one type into a list of a different type. In other worse, we should try
using map:
example = mapApply $ map _ offsetsWe’ve still got a type hole, but we’re slowly narrowing in on the right code. Let’s ask the compiler for help again:
FillingInTypeHoles.hs:9:26: error: …
• Found hole: _ :: (Int -> Int) -> Int -> Char
• In the first argument of ‘map’, namely ‘_’
In the second argument of ‘($)’, namely ‘map _ offsets’
In the expression: mapApply $ map _ offsetsNow we’re making a little more progress. Our type hole is giving us some more
information about what we should pass to map. We need to pass in something
that can take a function with the type (Int -> Int) along with some particular
Int and return a Char. Looking all of our where bindings, lookupLetter
is the only function that returns a Char. The question now is, how should we
use lookupLetter? Let’s start by adding a new function to our where clause:
mapFunc :: (Int -> Int) -> Int -> Char
mapFunc f n = undefinedWe can try to use type holes here, but they aren’t like to give us a lot of new information since we have a good sense of what types we’re working with. We just need to figure out how to put them together.
One option would be to go completely minimal. We have an Int value, and we
need a Char. We already have a function that does that. Maybe we can ignore
the function we’re being passed and just call lookupLetter directly?
example :: [Int] -> String
example = mapApply $ map mapFunc offsets
where
mapFunc :: (Int -> Int) -> Int -> Char
mapFunc f n = lookupLetter n
-- ...Our program will compile and run this way, but as you might expect it gives us the wrong output:
λ example [5..15]
"fffggghhhiiijjjkkklllmmmnnnoooppp"We should probably use the function that’s being passed in. There’s only one way
that we can write this so that it will type check. We need to pass an Int into
f, and the only Int we have is n. f returns an Int, and the only thing
we can do with it is pass it to lookupLetters. Let’s give it a try:
example :: [Int] -> String
example = mapApply $ map mapFunc offsets
where
mapFunc :: (Int -> Int) -> Int -> Char
mapFunc f n = lookupLetter (f n)
-- ...Let’s give this a try:
λ example [5..15]
"spftqgurhvsuwtjxukyblzcmadnbeacfp"Success! Following the types, along with a little intuition, has let us finish
implementing our application without needing to delve too deeply into the
implementation details. Before we move on, let’s take a moment to do a little
refactoring. Our current version of mapFunc is simply passing the output of
one function into another. As you might recall, we can do that using function
composition. We can use eta reduction to remove the explicit n parameter at
the same time that we add function composition:
mapFunc f = lookupLetter . fNow that we’ve factored out n and we’re using function composition, you might
notice that we can eta reduce our function again to remove f:
mapFunc = (lookupLetter .)At this point, we may as well move the definition of mapFunc to it’s call
site. That leaves us with a final refactored version that looks like this:
mapApply :: [a -> b] -> [a] -> [b]
mapApply toApply =
concatMap (\input -> map ($ input) toApply)
example :: [Int] -> String
example = mapApply $ map (lookupLetter .) offsets
where
letters :: [Char]
letters = ['a'..'z']
lookupLetter :: Int -> Char
lookupLetter n = letters !! n
offsets :: [Int -> Int]
offsets = [rot13, swap10, mixupVowels]
rot13 :: Int -> Int
rot13 n = (n + 13) `rem` 26
swap10 :: Int -> Int
swap10 n
| n <= 10 = n + 10
| n <= 20 = n - 10
| otherwise = n
mixupVowels :: Int -> Int
mixupVowels n =
case n of
0 -> 8
4 -> 14
8 -> 20
14 -> 0
20 -> 4
n' -> n'