Ambiguous type variable 'blah' in the constraint... how to fix?

593 views Asked by At

I'm trying to write a simple ray-tracer in Haskell. I wanted to define a typeclass representing the various kinds of surfaces available, with a function to determine where a ray intersects them:

{-# LANGUAGE RankNTypes #-}

data Vector = Vector Double Double Double
data Ray = Ray Vector Vector

class Surface s where
  intersections :: s -> Ray -> [Vector]

-- Obviously there would be some concrete surface implementations here...

data Renderable = Renderable
  { surface    :: (Surface s) => s
  , otherStuff :: Int
  }

getRenderableIntersections :: Renderable -> Ray -> [Vector]
getRenderableIntersections re ra = intersections (surface re) ra

However this gives me the error:

Ambiguous type variable 's' in the constraint:
  'Surface'
    arising from a use of 'surface'

(The actual code is more complex but I've tried to distill it to something simpler, while keeping the gist of what I'm trying to achieve).

How do I fix this? Or alternatively, given that I come from a standard OO background, what am I fundamentally doing wrong?

2

There are 2 answers

0
sclv On BEST ANSWER

Please don't use existential types for this! You could, but there would be no point.

From a functional standpoint you can drop this typeclass notion of Surface entirely. A Surface is something that maps a Ray to a list of Vectors, no? So:

type Surface = Ray -> [Vector]

data Renderable = Renderable
 { surface    :: Surface
 , otherStuff :: Int
}

Now if you really want, you can have a ToSurface typeclass essentially as you gave:

class ToSurface a where
     toSurface :: a -> Surface

But that's just for convenience and ad-hoc polymorphism. Nothing in your model requires it.

In general, there are a very few use cases for existentials, but at least 90% of the time you can substitute an existential with the functions it represents and obtain something cleaner and easier to reason about.

Also, even though it may be a tad too much for you to take in, and the issues don't exactly match, you might find useful some of Conal's writing on denotational design: http://conal.net/blog/posts/thoughts-on-semantics-for-3d-graphics/

5
R. Martinho Fernandes On

In your getRenderableIntersections function you call surface. There is no way for the interpreter to figure out what instance of the class Surface you want to use. If you have two such instances:

instance Surface SurfaceA where
  -- ...
instance Surface SurfaceB where
  -- ...

How can the interpreter determine the type of surface?

The way you defined Renderable means there is a function surface :: Surface s => Renderable -> s.

Try creating an instance Surface SurfaceA and asking the following type query (given a simple constructor SurfaceA):

> :t surface (Renderable SurfaceA 0) -- What's the type of the expression?

So, what type is this expression? I bet you're expecting SurfaceA. Wrong. Take the type of surface. It takes a Renderable argument and we're passing it a Renderable argument. What is left after that? Surface s => s. That's the type of that expression. We still don't know what type does s represent.

If you want the type to be SurfaceA you need to change your code so it becomes something like surface :: Surface s => Renderable s -> s. This way what s is can be determined, because it is the same s used in Renderable.

EDIT: As suggested by @mokus, you could also try the ExistentialTypes extension. It allows "hiding" away type parameters on the right side of a type declaration.

data Renderable = forall s. Surface s => Renderable
  { surface    :: s
  , otherStuff :: Int
  }

The HaskellWiki page I linked to above even has an example very similar to what you want to do.

EDIT: (By @stusmith) - For the record, I'm including code below which compiles based on these suggestions here. However I've accepted the answer which I think shows a better way of approaching things.

{-# LANGUAGE ExistentialQuantification #-}

data Vector = Vector Double Double Double
data Ray = Ray Vector Vector

class Surface_ s where
  intersections :: s -> Ray -> [Vector]

data Surface = forall s. Surface_ s => Surface s

instance Surface_ Surface where
  intersections (Surface s) ra = intersections s ra

data Renderable = Renderable
  { surface :: Surface
  }

getRenderableIntersections :: Renderable -> Ray -> [Vector]
getRenderableIntersections re ra = intersections (surface re) ra