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
addThree = undefined

-- definition 2
addThree a b c = undefined

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
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 = undefined

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
addThree a b c = op a (op b c)
  where
    op :: Int -> Int -> Int
    op = undefined

Or, we could write a pointfree version of this function, with the undefined inline:

addThree = (undefined .) . undefined

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
addThree = undefined . undefined

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
addThree = op . op
  where
    op :: Int -> Int -> Int
    op = undefined

Now if we try to compile our program, we’ll get a useful error message:

Undefined.hs:4:12-13: error:
Couldn't match typeInt’ 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 typeInt -> 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.