Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How should types be used in Haskell type classes?

I'm new to Haskell, and a little confused about how type classes work. Here's a simplified example of something I'm trying to do:

data ListOfInts = ListOfInts {value :: [Int]}
data ListOfDoubles = ListOfDoubles {value :: [Double]}

class Incrementable a where
    increment :: a -> a

instance Incrementable ListOfInts where
    increment ints = map (\x -> x + 1) ints

instance Incrementable ListOfDoubles where
    increment doubles = map (\x -> x + 1) doubles

(I realize that incrementing each element of a list can be done very simply, but this is just a simplified version of a more complex problem.)

The compiler tells me that I have multiple declarations of value. If I change the definitions of ListOfInts and ListOfDoubles as follows:

type ListOfInts = [Int]
type ListOfDoubles = [Double]

Then the compiler says "Illegal instance declaration for 'Incrementable ListOfInts'" (and similarly for ListOfDoubles. If I use newtype, e.g., newtype ListOfInts = ListOfInts [Int], then the compiler tells me "Couldn't match expected type 'ListOfInts' with actual type '[b0]'" (and similarly for ListOfDoubles.

My understanding of type classes is that they facilitate polymorphism, but I'm clearly missing something. In the first example above, does the compiler just see that the type parameter a refers to a record with a field called value and that it appears I'm trying to define increment for this type in multiple ways (rather than seeing two different types, one which has a field whose type of a list of Ints, and the other whose type is a list of Doubles)? And similarly for the other attempts?

Thanks in advance.

like image 702
Chris Avatar asked May 03 '13 09:05

Chris


People also ask

How are types used in Haskell?

In Haskell, every statement is considered as a mathematical expression and the category of this expression is called as a Type. You can say that "Type" is the data type of the expression used at compile time. To learn more about the Type, we will use the ":t" command.

How do you define a type class in Haskell?

A typeclass defines a set of methods that is shared across multiple types. For a type to belong to a typeclass, it needs to implement the methods of that typeclass. These implementations are ad-hoc: methods can have different implementations for different types.

How do you define data types in Haskell?

The Data Keyword and Constructors In general, we define a new data type by using the data keyword, followed by the name of the type we're defining. The type has to begin with a capital letter to distinguish it from normal expression names. To start defining our type, we must provide a constructor.

How does Haskell infer types?

Types are infered using a process generally called unification. Haskell belongs to the Hindley-Milner family, which is the unification algorithm it uses to determine the type of an expression. If unification fails, then the expression is a type error.


1 Answers

You're really seeing two separate problems, so I'll address them as such.

The first one is with the value field. Haskell records work in a slightly peculiar way: when you name a field, it is automatically added to the current scope as a function. Essentially, you can think of

data ListOfInts = ListOfInts {value :: [Int]}

as syntax sugar for:

data ListOfInts = ListOfInts [Int]

value :: ListOfInt -> [Int]
value (ListOfInts v) = v

So having two records with the same field name is just like having two different functions with the same name--they overlap. This is why your first error tells you that you've declared values multiple times.

The way to fix this would be to define your types without using the record syntax, as I did above:

data ListOfInts = ListOfInts [Int]
data ListOfDoubles = ListOfDoubles [Double]

When you used type instead of data, you simply created a type synonym rather than a new type. Using

type ListOfInts = [Int]

means that ListOfInts is the same as just [Int]. For various reasons, you can't use type synonyms in class instances by default. This makes sense--it would be very easy to make a mistake like trying to write an instance for [Int] as well as one for ListOfInts, which would break.

Using data to wrap a single type like [Int] or [Double] is the same as using newtype. However, newtype has the advantage that it carries no runtime overhead at all. So the best way to write these types would indeed be with newtype:

newtype ListOfInts = ListOfInts [Int]
newtype ListOfDoubles = ListOfDoubles [Double]

An important thing to note is that when you use data or newtype, you also have to "unwrap" the type if you want to get at its content. You can do this with pattern matching:

instance Incrementable ListOfInts where
  increment (ListOfInts ls) = ListOfInts (map (\ x -> x + 1) ls)

This unwraps the ListOfInts, maps a function over its contents and wraps it back up.

As long as you unwrap the value this way, your instances should work.

On a side note, you can write map (\ x -> x + 1) as map (+ 1), using something that is called an "operator section". All this means is that you implicitly create a lambda filling in whichever argument of the operator is missing. Most people find the map (+ 1) version easier to read because there is less unnecessary noise.

like image 180
Tikhon Jelvis Avatar answered Oct 21 '22 05:10

Tikhon Jelvis