hgiesel
hgiesel

Reputation: 5648

Redundancy regarding product types and tuples in Haskell

In Haskell you have product types and you have tuples.

You use tuples if you don't want to associate a dedicated type with the value, and you can use product types if you wish to do so.

However I feel there is redundancy in the notation of product types

data Foo = Foo (String, Int, Char)
data Bar = Bar String Int Char

Why are there both kinds of notations? Is there any case where you would prefer one the other?

I guess you can't use record notation when using tuples, but that's just a convenience problem. Another thing might be the notion of order in tuples, as opposed to product types, but I think that's just due to the naming of the functions fst and snd.

Upvotes: 3

Views: 664

Answers (3)

Benjamin Hodgson
Benjamin Hodgson

Reputation: 44603

@chi's answer is about the technical differences in terms of Haskell's evaluation model. I hope to give you some insight into the philosophy of this sort of typed programming.

In category theory we generally work with objects "up to isomorphism". Your Bar is of course isomorphic to (String, Int, Char), so from a categorical perspective they're the same thing.

bar_tuple :: Iso' Bar (String, Int, Char)
bar_tuple = iso to from
    where to (Bar s i c) = (s, i, c)
          from (s, i, c) = Bar s i c

In some sense tuples are a Platonic form of product type, in that they have no meaning beyond being a collection of disparate values. All the other product types can be mapped to and from a plain old tuple.

So why not use tuples everywhere, when all Haskell types ultimately boil down to a sum of products? It's about communication. As Martin Fowler says,

Any fool can write code that a computer can understand. Good programmers write code that humans can understand.

Names are important! Writing down a custom product type like

data Customer = Customer { name :: String, address :: String }

imbues the type Customer with meaning to the person reading the code, unlike (String, String) which just means "two strings".

Custom types are particularly useful when you want to enforce invariants by hiding the representation of your data and using smart constructors:

newtype NonEmpty a = NonEmpty [a]

nonEmpty :: [a] -> Maybe (NonEmpty a)
nonEmpty [] = Nothing
nonEmpty xs = Just (NonEmpty xs)

Now, if you don't export the NonEmpty constructor, you can force people to go through the nonEmpty smart constructor. If someone hands you a NonEmpty value you may safely assume that it has at least one element.

You can of course represent Customer as a tuple under the hood and expose evocatively-named field accessors,

newtype Customer = Bar (String, String)
name, address :: Customer -> String
name (Customer (n, a)) = n
address (Customer (n, a)) = a

but this doesn't really buy you much, except that it's now cheaper to convert Customer to a tuple (if, say, you're writing performance-sensitive code that works with a tuple-oriented API).

If your code is intended to solve a particular problem - which of course is the whole point of writing code - it pays to not just solve the problem, but make it look like you've solved it too. Someone - maybe you in a couple of years - is going to have to read this code and understand it with no a priori knowledge of how it works. Custom types are a very important communication tool in this regard.

Upvotes: 7

chi
chi

Reputation: 116139

The type

data Foo = Foo (String, Int, Char)

represents a double-lifted tuple. It values comprise

undefined
Foo undefined
Foo (undefined, undefined, undefined)
etc.

This is usually troublesome. Because of this, it's rare to see such definitions in actual code. We either have plain data types

data Foo = Foo String Int Char

or newtypes

newtype Foo = Foo (String, Int, Char)

The newtype can be just as inconvenient to use, but at least it does not double-lift the tuple: undefined and Foo undefined are now equal values.

The newtype also provides zero-cost conversion between a plain tuple and Foo, in both directions.

You can see such newtypes in use e.g. when the programmer needs a different instance for some type class, than the one already associated with the tuple. Or, perhaps, it is used in a "smart constructor" idiom.

Upvotes: 4

jakubdaniel
jakubdaniel

Reputation: 2223

I would not expect the pattern used in Foo to be frequent. There is slight difference in what the constructor acts like: Foo :: (String, Int, Char) -> Foo as opposed to Bar :: String -> Int -> Char -> Bar. Then Foo undefined and Foo (undefined, ..., ...) are strictly speaking different things, whereas you miss one level of undefinedness in Bar.

Upvotes: 0

Related Questions