Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Polymorphic return type, interfaces, callbacks?

Tags:

haskell

Let's say Goo is my type class, which is often claimed to be the interface equivalent in languages like C++, Java or C#:

class Goo goo where ham :: goo -> String

data Hoo = Hoo
instance Goo Hoo where ham _ = "Hoo!"
                       mustard _ = "Oh oh."

data Yoo = Yoo
instance Goo Yoo where ham _ = "Yoo!"
                       mustard _ = "Whew"

But I cannot return a Goo:

paak :: (Goo goo) => String -> goo
paak g = (Yoo)

-- Could not deduce (goo ~ Yoo)
-- from the context (Goo goo)
--  bound by the type signature for paak :: Goo goo => String -> goo
--  at frob.hs:13:1-14
--  `goo' is a rigid type variable bound by
--        the type signature for paak :: Goo goo => String -> goo
--        at frob.hs:13:1
-- In the expression: (Yoo)
-- In an equation for `paak': paak g = (Yoo)

I found this enlightening statement, which explains why:

The type paak :: (Goo goo) => String -> goo does not mean that the function might return any Goo it wants. It means that the function will return whichever Goo the user wants.

(transliterated from sepp2k's answer here)

But then, how could I return or store something that satisfies the Goo constraints, but can be Hoo, Yoo, Moo, Boo or any other Goo?

Am I just entangled too much in own programming background, and need to think completely different, like resorting to C-like interfaces:

data WhewIamAGoo = WhewIamAGoo {
    ham' :: String
    mustard' :: String
}

paak :: String -> WhewIamAGoo
paak g = let yoo = Yoo 
         in WhewIamAGoo { ham' = ham yoo
                          mustard' = mustard ham
                        }

But that seems awkward.

In my specific case, I would like to use Goo like this:

let x = someGoo ....
in ham x ++ mustard x

I.e. the caller should not need to know about all the Yoos and whatnot.


edit: To clarify: I am looking for the way a Haskell programmer would go in such situation. How would you handle it in an idiomatic way?

like image 694
Sebastian Mach Avatar asked Dec 01 '22 21:12

Sebastian Mach


2 Answers

There are two ways of solving this problem that I consider idiomatic Haskell:

  1. Algebraic data type

    data Goo = Hoo | Yoo
    
    ham Hoo = "Hoo!"
    ham Yoo = "Yoo!"
    
    mustard Hoo = "Oh oh."
    mustard Yoo = "Whew"
    

    Pro: easy to add new operations
    Con: adding a new "type" potentially requires modifying many existing functions

  2. Record of supported operations

    data Goo = Goo { ham :: String, mustard :: String }
    
    hoo = Goo { ham = "Hoo!", mustard = "Oh oh." }
    yoo = Goo { ham = "Yoo!", mustard = "Whew" }
    

    Pro: easy to add new "types"
    Con: adding a new operation potentially requires modifying many existing functions

You can of course mix and match these. Once you get used to thinking about functions, data and composition rather than interfaces, implementations and inheritance, these are good enough in a majority of cases.

Type classes are designed for overloading. Using them to mimic object-oriented programming in Haskell is usually a mistake.

like image 136
hammar Avatar answered Dec 04 '22 01:12

hammar


Typeclasses are a bit like Java-style interfaces, but you don't really use them precisely the same way you use interfaces, so it's not a great way to learn them.

An interface is a type (because OO languages have subtypes, so other types can be subtypes of the interface, which is how you get anything done). All types are disjoint in Haskell, so a type class is not a type. It's a set of types (the instance declarations are where you declare what the members of the set are). Try to think of them this way. It makes the correct reading of type signatures much more natural (String -> a means "takes a String and returns a value of any type you want", and SomeTypeClass a => String -> a means "takes a String and returns a value of any type you want that is a member of SomeTypeClass").

Now you can't do what you want the way you want it, but I'm not sure why you need to do it the way you want it. Why can't paak just have the type String -> Yoo?

You say you're trying to do something like:

let x = someGoo ....
in ham x ++ mustard x

If someGoo ... is paak "dummy string", then x will be of type Yoo. But Yoo is a member of Goo, so you can call Goo methods like ham and mustard on it. If you later change paak to return a value in a different Goo type, then the compiler will tell you all the places that used any Yoo-specific functionality, and happily accept unchanged any places that called paak but then only used the Goo functionality.

Why do you need it to be typed "some unknown type which is a member of Goo"? Fundamentally, callers of paak don't operate on any type in Goo, they only operate on what paak actually returns, which is a Yoo.

You have some functions that operate on concrete types, which can call functions on those concrete types as well as functions that come from type classes of which the concrete type is a member. Or you have functions which operate on any type which is a member of some type class, in which case all you can call are functions that work on any type in the type class.

like image 45
Ben Avatar answered Dec 04 '22 02:12

Ben