Brad Phillips
Brad Phillips

Reputation: 33

Haskell compile time checking of smart constructors

I'm learning Haskell, running through the lectures: http://www.cis.upenn.edu/~cis194/spring13/

I've got:

module HanoiDisk(HanoiDisk, hanoiDisk) where
import Control.Exception
data HanoiDisk = HanoiDisk' Integer deriving (Show)
hanoiDisk :: Integer -> HanoiDisk 
hanoiDisk n = assert (n >= 1) $ HanoiDisk' n

This works, but if i have:

main = do 
  print(show (hanoiDisk (-3))

I only get an error during run-time and not at compile-time.

I'm pretty keen to understand how to eliminate run-time exceptions entirely.

Can anyone provide an alternative approach?

Thanks

Upvotes: 2

Views: 407

Answers (2)

Kartik Sabharwal
Kartik Sabharwal

Reputation: 503

From what I understand, you want a way to "fail nicely" when someone applies the function hanoiDisk to an argument that's less than 1.

As a commenter stated, doing that at compile time is outside the scope of basic Haskell and you shouldn't need it in your day-to-day code!

You can definitely "fail nicely" by using the Either a b datatype.

The idea is that if you have a function hanoiDisk :: Integer -> HanoiDisk that takes an Integer and is supposed to return a HanoiDisk value if the input is "good" and an error value of some sort when the input is "bad", you can encode that using alternate constructors.

The constructors for the Either a b datatype are Left a and Right b where an error output would be of the form Left a and a good output would be of the form Right b. Let's rewrite your function using this.

hanoiDisk :: Integer -> Either String HanoiDisk 
hanoiDisk n = if n >= 1 
              then Right (HanoiDisk' n)
              else Left "a hanoi disk must be least 1"

(Probably) More Appropriate Answer

Let's discuss the simpler problem of constructing numbers that must be nonnegative (as opposed to positive) in a way that's acceptable to the compiler.

I think the problem is tied to the way numbers are parsed by the compiler. Any time you use the symbols '0', '1', '2', '3', '4', ..., '9' to represent digits in your program the language parser expects the end result to conform to a type like Int, Double, etc. and so when you use these symbols you open yourself up to the possibility that someone might prepend a '-' to the sequence of digits and turn your nonnegative number into a negative one.

Let's make a new module called Natural which will allow us to create positive numbers. In it, we define "aliases" for the symbols '0',...,'1' using the first two letters of each symbol's name (eg. tw for '2'). Since humans write natural numbers using the decimal system, we create a data type called Natural that takes two arguments - the first digit of the number we're representing and then a list of subsequent digits. Finally, we selectively export functions from the module to prohibit "misuse" by users.

module Natural (ze,on,tw,th,fo,fi,si,se,ei,ni,Natural(..)) where

newtype Digit = Digit Int

ze = Digit 0
on = Digit 1
tw = Digit 2
th = Digit 3
fo = Digit 4
fi = Digit 5
si = Digit 6
se = Digit 7
ei = Digit 8
ni = Digit 9

data Natural = Nat Digit [Digit]

As an example, the natural number 312 would be represented as Nat th [on,tw].

Any module importing Natural would only have access to the functions that we export, so attempts to use anything else to define a value of type Natural would result in compile errors. Furthermore, since we didn't export the Digit constructor there's no way for importers to define their own values for the Digit type.

I'm leaving out definitions of the instances for Num, Integral, Eq, Ord, etc. because I don't think they would add more to my explanation.

Upvotes: 2

Will Ness
Will Ness

Reputation: 71065

Haskell checks types when compiling a code, not values. To make types depend on values is the job of "dependent types". It is an advanced topic.

The other way to achieve this is to make your hanoiDisk work not with Integers, but with some "PositiveInteger" type which can not possibly be negative (or 0 as well..?). It is a more basic approach.

There will be nothing to assert -- it should be impossible for you to even write down a negative value with this type. You'll have to make this type an instance of Num, Eq, Ord, and Show (maybe Enum as well).

The usual way is to define

data Nat = Z | S Nat 
           deriving (Eq, Show)

Upvotes: 3

Related Questions