Haskell multiparameter class (templated type class)

Viewed 83

I'ld like to implement a general Gaussian function for scalars and vectors. In Ada or C++ I simply would choose template for this, but in Haskell it's a little bit confusing.

I would start by defining a class that can apply Gaussian operators, like combining or calculating the probability:

class Gaussian g where
  (!*) :: g -> g -> g
  prob :: g -> a -> Float -- Here, I want a to be depending on g

data Gaussian1D = Gaussian1D Float Float
data Gaussian2D = Gaussian2D (Linear.V2 Float) Linear.V2(LinearV2 Float)

, and I'ld like to have something like:

instance Gaussian Gaussian1D where
  prob :: Gaussian1D -> Float -> Float

instance Gaussian Gaussian2D where
  prob :: Gaussian2D -> Linear.V2 Float -> Float

But I'm not able to implement this in a nice way. Would this somehow be possible with a multiparameter class without digging into the field of template-haskell, eg:

class Gaussian g a where
  (!*) :: g a -> g a -> g a
  prob :: g a -> a -> Float

? At the moment, I'm failing to realize this scenario when I do domething like:

data Gaussian1D = Gaussian1D Float Float

instance Gaussian Gaussian1D Float where

with error: Expected kind ‘* -> *’, but ‘Gaussian1D’ has kind ‘*’ (btw, I don't understand why this error occurs)

Thx

4 Answers

This seems like a textbook use-case for Functional dependencies, a GHC-only language feature that allows multi-parameter type classes where one parameter uniquely determines the other:

{-# LANGUAGE FunctionalDependencies #-}

class Guassian g a | g -> a where
  (!*) :: g -> g -> g
  prob :: g -> a -> Float

instance Gaussian Gaussian1D Float where
  -- ...

If you write g a, for example in:

(!*) :: g a -> g a -> g a

It means that g takes a type parameter. That should for example work if Gaussian1D was defined as:

data Gaussian1D a = Gaussian1D a a

But that is not the case here. You can make a typeclass with two type parameters:

{-# LANGUAGE AllowAmbiguousTypes, MultiParamTypeClasses #-}

class Guassian g a where
  (!*) :: g -> g -> g
  prob :: g -> a -> Float

But this thus means that you can make an instance for any g, a combination. This thus means that a does not depend on g, or at least not directly. You thus can implement a Gaussian Gaussian1D Double and Gaussian Gaussian1D Float.

You can however make use of functional dependencies [wiki] where the type a is determined by the type g. This thus means that no two as can be used for the same g:

{-# LANGUAGE AllowAmbiguousTypes, FunctionalDependencies, MultiParamTypeClasses #-}

class Guassian g a | g -> a where
  (!*) :: g -> g -> g
  prob :: g -> a -> Float

Others have mentioned functional dependencies. Another option is to use a type family to make a depend on g:

class Gaussian g where
  type Scalar g
  (!*) :: g -> g -> g
  prob :: g -> Scalar g -> Float


instance Gaussian Gaussian1D where
  type Scalar Gaussian1D = Float
  prob :: Gaussian1D -> Float -> Float
  prob x y = ...

Since you are familiar with C++ templates, this is vaguely similar to

class Gaussian1D {
   // ...
   using Scalar = float;
};

so that later on you can use typename T::Scalar in a templated context to refer to the associated type. In Haskell you do that using Scalar t instead.

The correspondence is not perfect, of course. For one, C++ will only fully type check your templated code at instantiation time, while Haskell does that immediately, on-use.

A good reason to have a multiparameter class is to be able to switch the base numeric type from Float to Double at will.

We can start by defining a scalar Gaussian distribution type like this, with f as the floating-point type argument:

{-#  LANGUAGE  FlexibleInstances      #-}
{-#  LANGUAGE  MultiParamTypeClasses  #-}

data GaussianScalar f = GaussianScalar { mean :: f , variance :: f } 

We do not put any type constraints here. Of course, the f data type is expected to be some Floating type. But the consensus in the Haskell community has been for a long time that type constraints are best given at individual function level on an as-needed basis.

Next, we define two mathematical utility functions which will come handy later to populate our instance:

gaussianConvol :: Floating f =>
                  (GaussianScalar f) -> (GaussianScalar f) -> (GaussianScalar f)
gaussianConvol (GaussianScalar m1 v1) (GaussianScalar m2 v2) =
    GaussianScalar (m1+m2) (v1+v2)

gaussianDensity :: Floating f => (GaussianScalar f) -> f -> f
gaussianDensity gd x  =  let  m = mean gd
                              v = variance gd
                              σ = sqrt v
                              r = (x-m) / σ
                              n = 1/(σ * sqrt(2*pi))
                         in  n * exp (-(r*r)/2)

We now define our Gaussian class with two type parameters, the first one being the distribution object and the second one our base numeric type:

class  Gaussian g f  where 
     (!*) :: Floating f => g f -> g f -> g f  -- addition/convolution
     prob :: Floating f => g f -> f -> f      -- probability density

We can now define a Gaussian class instance for our scalar Gaussian distribution data type, using the auxiliary mathematical functions defined above:

instance (Floating f)  =>  Gaussian  GaussianScalar f
  where 
    (!*)  =  gaussianConvol 
    prob  =  gaussianDensity

Note: one has to be very careful with syntax. The compiler rejects instance (Floating f) => Gaussian (GaussianScalar f), as it is a class taking two type parameters.

Sample client code:

main = do
    let  gd1 = GaussianScalar 0.0 1.0
         x   = 2.0 :: Double
         y   = prob gd1 x     -- class function
    putStrLn $ "y = " ++ (show y)
Related