Creating Classes

A light introduction to newtype, classes, and instances.

This material motivated by Haskell Programming from first principles, by Allen and Moronuki. The Github user kingparra has notes that might be useful.

If you are using a compiler older than GHC 2021, you will need to add FlexibleInstances to your language. You need this line if you have problems with your Demo String (see below).

{-# LANGUAGE FlexibleInstances #-}

Newtype

You already know two ways to create a type:

type MyInt1 = Int
data MyInt2 = MyInt2 Int

There is a way that is similar to the second that is commonly used to rename a single type.

newtype MyInt3 = MyInt3 Int

A newtype renames a single type. It cannot hold more than one thing. It is used to be able to tell the difference between things that would otherwise look the same.

For example, if you wanted to use Double to represent both money and time, you could use newtypes to distinguish them.

newtype Money = Money Double
newtype Time = Time Double

This makes mistaking dollars for hours impossible inside your program.

Usually you want to use deriving (Show, Eq) just like in data.

Creating Classes I

A class declaration specifies the names and signatures of methods that have to be available for instances of that class.

The Demo class below just requires a function that turns takes in something and produces an Int.

class Demo a where
   numit :: a -> Int

In order to make String an instance of the Demo class, we need a way of getting a number out of any String. We could use the length.

instance Demo String where
   numit s = length s

Technically, less logical choices would also be allowed like always making the output number zero.

  1. Make a Posn into an instance of the Demo class by adding the coordinates.

     type Posn = (Int, Int)
    
  2. Make a SInt into an instance of Demo my multiplying the number by the length of the String.

     data SInt = SInt Int String
    

Creating Classes II

  1. Write a class declaration for StepTwo so that StepTwo a means that there is a method rev :: a -> a.

  2. Make Integer an instance of StepTwo where rev makes a number into its opposite.

  3. Make String into an instance of StepTwo by having rev reverse the incoming string.

  4. Write a class declaration for ProbThree so that ProbThree a means there is a combination method, comb :: a -> a -> a

  5. Make Double an instance of ProbThree by using multiplication to combine.

  6. Write a class declaration for Bracket that abstracts the pattern aITEMa, like "apple" becomes "DocappleDoc". The basex method (below) would give "Doc" in this case.

    Write code so that Bracket b means that there are two items available:

     basex :: b
     brack :: b -> String -> b
    
  7. (Danger, do at the end after all others.) Make String an instance of Bracket so that these tests pass:

     x :: String
     x = basex
     y = brack x "-WY-"
     checkBracket = [x  == "MrH", y == "MrH-WY-MrH" ]
    
  8. (Extreme Danger, do very last.) Make a newtype IntBrack that is just like an Int. Make IntBrack an instance of the Bracket class by choosing the number 42 as the basex and doing 42 + length word + 42 for the brack operation. Pay attention to types required by the signatures!

    Making the 42 in brack match value in basex is a little extra challenge.

Creating Classes III

  1. Create a newtype called AInt that is the same as an Int. Automatically add AInt to the Eq typeclass.

  2. Add AInt to the Show typeclass so that show (AInt 7) returns "I:7".

    main = do
        let x = AInt 7
        putStrLn $ show x
    
  3. Write a function g :: Int -> AInt that doubles the number given.

  4. Verify that g 5 == AInt 10 but g (g 5) is an error.

  5. Add AInt to the Ord typeclass by providing a (<=) method that compares the ints.

  6. Create a Scholar class so that Scholar a means there is method study :: a -> Int.

  7. Make AInt an instance of the Scholar class, where the result of studying is just 5 more than the integer in the AInt.

  8. Make String an instance of the Scholar class, where the result of studying is 5 times the length of the string.

Creating Classes IV

You can have functions that take in more than one item in your class.

  1. Make a Join class that has a method together :: a -> a -> a.

  2. Make [Int] an instance of the Join class in some logical way.

  3. Make Int an instance of the Join class by multiplying.

  4. Make AInt an instance of the Join class by adding.

Creating Classes V

In this section we declare and use the class Bizarre a, which has two methods

weird :: a
(<-->) :: Int -> a -> [a]
  1. Declare Bizarre.

  2. Make Int an instance of this class.

  3. Make String an instance of this class.

  4. A multipart question, in which you make a type called Boring and add it to the Bizarre typeclass.

    1. Make your own newtype Boring be an Int. Derive Show so you can see it.
    2. Write the signatures of the functions that you need to write.
    3. Now write them, by always choosing 1 and the list [2,3]. (Literally writing 1 is incorrect because it is the wrong type. Check your type signatures before you try to write a solution.)

    Testing your code:

        testBizarre = do
            print (weird :: Int)
            print $ (<-->) 5 (8::Int)
    
            print (weird :: String)
            print $ (<-->) 3 "Good"
    
            print (weird :: Boring)
            print $ (<-->) 5 (Boring 7)