简体   繁体   中英

Polymorphic return type, interfaces, callbacks?

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

Ie the caller should not need to know about all the Yoo s 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?

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.

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.

First of all, it's generally not necessary! Your WhenIAmGoo approach is just fine; since Haskell is lazy it doesn't have any real drawbacks but is often much clearer.

But it's still possible:

{-# LANGUAGE RankNTypes              #-}

paak' :: String -> (forall goo . Goo goo => goo -> r) -> r
paak' g f = f Yoo

Looks complicated?

To understand the issue you need to know how Haskell's Hindley-Milner -based type system works quite fundamentally different from what eg C++ and Java do. In these languages, as you seem to know, polymorphism is basically a kind of limited dynamic typing: if you pass an object with an "interface type", you actually rather pass a wrapper around an object, which knows how the interface methods are implemented in it.

In Haskell, it's different. A polymorphic signature, explicitly written, looks like this:

paak :: { forall goo . (Goo goo) } => {String -> goo}

which means, there is actually a completely seperate extra argument to the function, the "dictionary argument". That's what's used to access the interface. And since this is indeed an argument passed to the function, the function obviously doesn't get to choose it.

To pass the dictionary out of the function, you need to employ such evil tricks as I did above: you don't directly return a polymorphic result, but rather ask the caller "how are you going to use it? But mind, I can't tell what concrete type you're going to get..." Ie, you require them to give you themselves a polymorphic function, into which you can then insert the concrete type of your choice.

Such a function can be used this way:

myHamAndMustard = paak' arg (\myGoo -> ham myGoo ++ mustard myGoo )

which isn't exactly nice. Again, the usually better way is to have just have a transparent non-polymorphic container for all the possible outputs. Very often, that's still not optimal; you probably have approached you entire problem from too much of an OO angle.

Based on the information provided so far, the C-style interface (== record with functions) seems to be the way to go.

However, to make it sweeter to use, add a smart constructor and make AnyGoo an instance of Goo :

data AnyGoo = AnyGoo {
    ham' :: String 
}
instance Goo AnyGoo where
    ham = ham' 


anyGoo :: (Goo goo) => goo -> AnyGoo 
anyGoo goo = AnyGoo { ham' = ham goo }

then you can uniformly call ham for all Goo :

> let x = anyGoo Hoo
> let y = anyGoo Yoo
> ham x
"Hoo!"
> ham y
"Yoo!"

paak would then return an AnyGoo instead of a Goo :

paak :: String -> AnyGoo
paak _ = anyGoo Yoo

But then, you (I) would be passing a certain type around again, thus could better return hammar's suggestion.

I'd like to support answer @phresnel but I'd like to add some general thoughts.

What you should understand is that with that signature paak :: (Goo goo) => String -> goo you are trying to control your future evaluation with type system. Types exists only in compile time like in C#, C++ and other OOP biased languages. To get types represented differently in run-time such languages uses tables of virtual functions etc. In Haskell you should do the same thing and wrap it in something.

data AnyGoo = forall a . (Goo a) => AnyGoo a

paak :: String -> AnyGoo
paak g = AnyGoo Yoo

In this case compiler (with help of ExistentialQuantification and other stuff) have multiple constructors (like with multiple constructors of classes that implements one interface) for AnyGoo which is open for any type that have instance within Goo typeclass.

But in this case its enough to use data value (like virtual functions).

data Goo = Goo { ham :: String }

-- ham :: Goo -> String
yoo = Goo { ham = "Yoo!" }
paak :: String -> AnyGoo
paak g = Goo yoo

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM