Undefined
Undefined
Consider a function that takes 3 integers but hasn’t been defined:
addThree :: Int -> Int -> Int -> Int
There are several different ways that you could write a function like this. For example here are two possible definitions:
-- definition 1
= undefined
addThree
-- definition 2
= undefined addThree a b c
There are many other ways we could use undefined to write a version of
addThree
that type checks. Why are there so many different versions?
Hints
Click to reveal
Think about all of the ways that you can η-reduce (eta-reduce) your code when
the definition of the function is undefined
.
Click to reveal
You can also use undefined
multiple times.
Solution
Click to reveal
There are four obvious ways that we might write this function using undefined:
-- With all three arguments bound to variables
= undefined
addThree a b c
-- With the first two arguments bound to variables
= undefined
addThree a b
-- With the first argument bound to a variable
= undefined
addThree a
-- With no arguments bound to a variable
= undefined addThree
All of these implementations assume that we’re replacing the entire body of
addThree
with undefined
, but we can replace individual parts of the body as
well. For example, we might create a function called op
that represents some
binary operation that will eventually be defined as (+)
but for now we leave
it undefined
:
addThree :: Int -> Int -> Int -> Int
= op a (op b c)
addThree a b c where
op :: Int -> Int -> Int
= undefined op
Or, we could write a pointfree version of this function, with the undefined
inline:
= (undefined .) . undefined addThree
Be careful though! We can also use undefined
to write functions that compile,
but won’t really make sense if we try to define the undefined expressions. For
example, we could write:
addThree :: Int -> Int -> Int -> Int
= undefined . undefined addThree
Although this will compile, there isn’t a reasonable definition we could provide
for undefined
that would do what we want.
undefined
. We can address that by factoring the use of undefined
out into a
function and giving it an explicit type annotation, as we did in our earlier
example using op
:
addThree :: Int -> Int -> Int -> Int
= op . op
addThree where
op :: Int -> Int -> Int
= undefined op
Now if we try to compile our program, we’ll get a useful error message:
:4:12-13: error: …
Undefined.hsCouldn't match type ‘Int’ with ‘Int -> Int’
• Expected: Int -> Int -> Int -> Int
Actual: Int -> Int -> Int
In the first argument of ‘(.)’, namely ‘op’
• In the expression: op . op
In an equation for ‘addThree’:
addThree= op . op
where
op :: Int -> Int -> Int
= undefined
op |
:4:17-18: error: …
Undefined.hsCouldn't match type ‘Int -> Int’ with ‘Int’
• Expected: Int -> Int
Actual: Int -> Int -> Int
Probable cause: ‘op’ is applied to too few arguments
• In the second argument of ‘(.)’, namely ‘op’
In the expression: op . op
In an equation for ‘addThree’:
addThree= op . op
where
op :: Int -> Int -> Int
= undefined
op |
Compilation failed.
This gets to the heart of the question “why are there so many different ways to
define an expression using undefined”. Since undefined
can be used anywhere,
for an expression of any type, it’s extremely flexible. You can use undefined
almost anywhere, to fill in for almost anything, even things that wouldn’t ever
make sense with real code. That’s one of the drawbacks of this technique. When
you allow the compiler to infer the type of undefined, you may find that you’re
getting a false sense of security when your program compiles. It’s useful
frequently enough that you shouldn’t necessarily avoid it altogether, but beware
of the drawbacks.