Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Return one of two types of same typeclass

I have the following type class

class MyClass c where
  aFunction :: c -> Bool

and two instances for two different data types

data MyDataType1 = MyDataType1

instance MyClass MyDataType1 where
  aFunction c = True

data MyDataType2 = MyDataType2

instance MyClass MyDataType2 where
  aFunction c = False

I want to write a function a function which takes two parameters of typeclass MyClass (which might be the same data type or might be different and returns one of them. I'm struggling to work out the type signature for this and I think I might be taking the wrong approach.

Would this be correct? If not what should I use instead?

chooseOne :: (MyClass a, MyClass b) => a -> b -> ?
chooseOne x y = if (aFunction x) then x else y
like image 649
Jim Jeffries Avatar asked May 11 '13 17:05

Jim Jeffries


4 Answers

Your return value could be of either type, so the compiler will complain unless you use the same type for both, giving

chooseOne :: (MyClass a, MyClass a) => a -> a -> a

which isn't what you mean.

To combine two potentially different types into one, you can use the Either data type:

data Either a b = Left a | Right b

so you would have

chooseOne :: (MyClass a, MyClass b) => a -> b -> Either a b
chooseOne x y = if (aFunction x) then Right x else Left y

But I'd rather write that

chooseOne :: (MyClass a, MyClass b) => a -> b -> Either a b
chooseOne x y | aFunction x = Right x 
              | otherwise   = Left y
like image 77
AndrewC Avatar answered Oct 17 '22 05:10

AndrewC


The function you're writing is impossible in Haskell---the return type must be fixed and known at compile time. Thus, to write something like what you're interested in you need an Either.

chooseOne :: (MyClass a, MyClass b) => a -> b -> Either a b
chooseOne x y = if (aFunction x) then Left x else Right y

Eventually, even in dynamic languages, you'd have to have some code which handles both the a and b types identically. This "eliminates" the Either and is embodied in the function Data.Either.either

either :: (a -> c) -> (b -> c) -> Either a b -> c
either f _ (Left a)  = f a
either _ g (Right b) = g b

For your particular case, since both a and b are instances of MyClass, it feels like we can make a slightly more convenient elimination function

eitherOfMyClass :: (MyClass a, MyClass b) => (a -> b) -> Either a a' -> b
eitherOfMyClass f (Left a)   = f a
eitherOfMyClass f (Right a') = f a'

But this actually won't type-check! If you look closely at the type you might be able to find the problem---the handler function we're passing in is specialized to a and thus cannot be applied to the Right side of your Either which is type b. We thus need to use forall, an extension enabled by LANGUAGE RankNTypes.

{-# LANGUAGE RankNTypes #-}

eitherOfMyClass :: (MyClass a, MyClass b) =>
                   (forall x. MyClass x => (x -> c)) -> Either a b -> c
eitherOfMyClass f (Left a)  = f a
eitherOfMyClass f (Right b) = f b

This ensures that whatever function f you pass in to eitherOfMyClass is truly general to any instance of MyClass and thus can be applied to both the a and the b in your Either.

like image 29
J. Abrahamson Avatar answered Oct 17 '22 05:10

J. Abrahamson


(See also “Haskell Antipattern: Existential Typeclass”.)

data MyType = MyType { aFunction :: Bool }

chooseOne :: MyType -> MyType -> MyType
chooseOne x y = if aFunction x then x else y

Caveat: your real MyClass might not be simple enough to make this work.

n.b. If you could write it as a class in an OOP language, then you can use this technique. OOP constructors translate to free-standing functions that return a MyType value.

  • All your class functions must take a class value as a parameter. So e.g. if your real class was Monoid, mempty :: Monoid a => a doesn't take a parameter so doesn't fit in this framework.
  • Your class functions can't take multiple parameters that must be of the same underlying type. Again e.g. if your real class was Monoid, mappend :: Monoid a => a -> a -> a takes two parameters, and they may be of any type that implements Monoid, but they must both be the same type that implements Monoid; mappend doesn't fit in this framework either.
like image 26
dave4420 Avatar answered Oct 17 '22 06:10

dave4420


You could always do it backwards: instead of returning type x or type y, you could accept two functions as input and execute one or the other depending on what you wanted to "return":

chooseOne :: (x -> z) -> (y -> z) -> x -> y -> z
chooseOne f1 f2 x y = if aFunction x then f1 x else f2 y

Notice that if you do chooseOne Left Right, you now have the Either-based solution that some of the other guys have suggested. You could also do something like chooseOne show show to return a String as your result.

Whether this approach is better or worse depends on why you actually want to build this class in the first place (i.e., what your program is trying to do)...

like image 1
MathematicalOrchid Avatar answered Oct 17 '22 06:10

MathematicalOrchid