Search code examples
haskelltypeswrapper

Haskell: beginner question - how to wrap an existing type?


I want to create a fairly simple abstraction layer over the top of Linear.Matrix so that I can later swap out the implementation for other libraries like hmatrix.

The abstraction layer is only going to support basic operations like matrix construction, addition, multiplication, inverse, and a few other functions.

As a relative newcomer to Haskell I'm a bit confused about how one implements a wrapper of an existing type.

For example, I started with:

import Linear (M44, V4 (V4))

type MyMatrix44 = M44 (Double)
type MyRow4 = V4 (Double)

My understanding of the type keyword is that it creates a type alias that is largely ignored by the compiler. However when I query in GHCi:

λ> :t V4
V4 :: a -> a -> a -> a -> V4 a

But:

λ> :t MyRow4
<interactive>:1:1: error: Data constructor not in scope: MyRow4

Yet:

λ> :i MyRow4
type MyRow4 = V4 Double     -- Defined at <interactive>:10:1

So what is that telling me? That MyRow4 is not actually a type alias for V4 (Double)? Or does :t not actually show info for types unless they have a value constructor of the same name?

If I try and construct a value with my new type:

λ> MyRow4 1.0 2.0 3.0 4.0

<interactive>:15:1: error:
    Data constructor not in scope:
      MyRow4 :: Double -> Double -> Double -> Double -> t

So I need to wrap the value constructor too? I'm missing something fundamental here. Am I getting confused by the convention of type names often being the same name as value constructors?

I've read through Making Our Own Types and Typeclasses and I thought I understood the difference between types and value constructors, but I can't see how this translates into wrapping a type.

Where am I going wrong? Is there a good example of this kind of wrapper?

EDIT: rather than trying to alias the type, would I be better to create a wrapper that is composed of the underlying type, perhaps using a data structure?

Supplementary Questions

1: When a type is defined by:

data T = D1 Char | D2 Int

I understand that the "type constructor" is named T, but the terminology also talks about T being the "type". Are they one-and-the same, or two separate things that are (always?) called the same name?

2: In Linear.Matrix, this definition exists:

type M44 a = V4 (V4 a)

Does that mean that if I do :t v where v is a value, and get M44 Double that it’s the same type as if I get V4 (V4 Double) from :t with a different value? Are those values of the same type but the first is described using the type constructor, and the latter using the data constructor? I see this a lot with Linear.Matrix, where sometimes the type of the result is M44 a and other times it is V4 (V4 a), yet they seem to be the same thing.

3: "If you hide the data constructor from your interface (by not exporting it from the module in which you have defined it)" - if you name the data constructor the same thing as the type constructor, how does the export list differentiate between the two? A user of the module might want to pattern-match on the type, even if the data constructor is hidden, but omitting the name from the export list hides the type as well. One way to work around this is to use a different name for the data constructor, but that seems uncommon.


Solution

  • Let's start simple and say we have a concrete type T defined by

    data T = D1 Char | D2 Int
    

    Then D1 and D2 are data constructors; they let you construct values of type T. Data constructors have types. Indeed you can query these:

    > :t D1
    D1 :: Char -> T
    
    > :t D2
    D2 :: Int -> T
    

    What about T? T is a type constructor. Type constructors let you construct types. Type constructors do not have types themselves. Hence, a query for the type of T will be unsuccessful:

    > :t T
    
    <interactive>:1:1: error: Data constructor not in scope: T
    

    Here, when asking for a type, T is interpreted to be a data constructor (ask for the type of something that starts with an uppercase character and the system will look for a data constructor of that name).

    Instead, type constructors have kinds. You can think of kinds as "types of types". You can query these:

    > :k T
    T :: *
    

    So, the kind of T is *. * is the kind of proper types, i.e., types that can have values. For instance, we have Int :: *, Char :: *. But also Maybe :: * -> *, conveying that Maybe is a type constructor that expects a type argument of kind * in order to construct a proper type. That is, Maybe itself does not have values, but applying Maybe to a proper type as in Maybe Char and Maybe Int gives you a type that does have values.


    Now, say we want to wrap T in a wrapper type. There are a couple of options here. The most simple one is to create a type synonym:

    type W1 = T

    This tells the compiler that W1 is just another name for T. It does not create a new type and allows us to freely interchange T and W1. As an abstraction layer, this is not really useful as the abstraction would be leaky: for example, we can still use the data constructors of T to create and pattern match against W1-values, making it harder to later change the implementation of W1.

    data W2 = W2 T
    

    Here, we introduce a new type constructor W2 and a new data constructor W2. These are two different things that just happen to have the same name:

    > :k W2
    W2 :: *
    
    > :t W2
    W2 :: T -> W2
    

    If you hide the data constructor from your interface (by not exporting it from the module in which you have defined it), you have effectively hidden the implementation of W2. Of course, you have to export some helper functions instead, so that client code can still work with W2-values. For example to construct W2-values you could have

    w2 :: Int -> W2
    w2 n = W2 (D2 n)
    

    If you decide, later on to simplify the implementation of W2 and have it replaced by a concrete representation that does not have the ability to store a Char (as D1 gives us for T), you can simply do so by changing the definition of W2 and w2 to:

    data W2 = W2 Int
    
    w2 :: Int -> W2
    w2 n = W2 n
    

    Client code will be unaffected.


    Finally, by using data we have created a wrapper type that is not exactly isomorphic to the concrete representation. The data constructor W2 introduces a bit of overhead as it creates an indirection. For datatypes that have only a single data constructor that takes only a single argument, we can get rid of this overhead by using newtype:

    newtype W2 = W2 T
    

    (With this you loose the ability to tell apart W2 undefined from undefined, but typically, when just creating an wrapper for abstraction purposes, this is totally acceptable.)