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 anyGoo
it wants. It means that the function will return whicheverGoo
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:
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
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.