Undefined
Undefined
Consider a function that takes 3 integers but hasn’t been defined:
addThree :: Int -> Int -> Int -> IntThere are several different ways that you could write a function like this. For example here are two possible definitions:
-- definition 1
addThree = undefined
-- definition 2
addThree a b c = undefinedThere 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
addThree a b c = undefined
-- With the first two arguments bound to variables
addThree a b = undefined
-- With the first argument bound to a variable
addThree a = undefined
-- With no arguments bound to a variable
addThree = undefinedAll 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
addThree a b c = op a (op b c)
where
op :: Int -> Int -> Int
op = undefinedOr, we could write a pointfree version of this function, with the undefined inline:
addThree = (undefined .) . undefinedBe 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
addThree = undefined . undefinedAlthough 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
addThree = op . op
where
op :: Int -> Int -> Int
op = undefinedNow if we try to compile our program, we’ll get a useful error message:
Undefined.hs:4:12-13: error: …
• Couldn'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
op = undefined
|
Undefined.hs:4:17-18: error: …
• Couldn'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
op = undefined
|
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.