I want to build a list of different things which have one property in common, namely, they could be turned into string. The object-oriented approach is straightforward: defi
You can store partially applied functions in the list.
Suppose we are building a ray-tracer with different shape that you can intersect.
data Sphere = ...
data Triangle = ...
data Ray = ...
data IntersectionResult = ...
class Intersect t where
intersect :: t -> Ray -> Maybe IntersectionResult
instance Intersect Sphere where ...
instance Intersect Triangle where ...
Now, we can partially apply the intersect
to get a list of Ray -> Maybe IntersectionResult
such as:
myList :: [(Ray -> Maybe IntersectionResult)]
myList = [intersect sphere, intersect triangle, ...]
Now, if you want to get all the intersections, you can write:
map ($ ray) myList -- or map (\f -> f ray) myList
This can be extended a bit to handle an interface with multiples functions, for example, if you want to be able to get something of a shape :
class ShapeWithSomething t where
getSomething :: t -> OtherParam -> Float
data ShapeIntersectAndSomething = ShapeIntersectAndSomething {
intersect :: Ray -> Maybe IntersectionResult,
getSomething :: OtherParam -> Float}
Something I don't know is the overhead of this approach. We need to store the pointer to the function and the pointer to the shape and this for each function of the interface, which is a lot compared to the shared vtable usually used in OO language. I don't have any idea if GHC is able to optimize this.
My answer is fundamentally the same as ErikR's: the type that best embodies your requirements is [String]
. But I'll go a bit more into the logic that I believe justifies this answer. The key is in this quote from the question:
[...] things which have one property in common, namely, they could be turned into string.
Let's call this type Stringable
. But now the key observation is this:
Stringable
is isomorphic to String
!That is, if your statement above is the whole specification of the Stringable
type, then there is a pair functions with these signatures:
toString :: Stringable -> String
toStringable :: String -> Stringable
...such that the two functions are inverses. When two types are isomorphic, any program that uses either of the types can be rewritten in terms of the other without any change to its semantics. So Stringable
doesn't let you do anything that String
doesn't let you do already!
In more concrete terms, the point is that this refactoring is guaranteed to work no matter what:
Stringable
and stick that into a [Stringable]
, turn the object into a String
and stick that into a [String]
.Stringable
by applying toString
to it, you can now eliminate the call to toString
. Note that this argument generalizes to types more complex than Stringable
, with many "methods". So for example, the type of "things that you can turn into either a String
or an Int
" is isomorphic to (String, Int)
. The type of "things that you can either turn into a String
or combine them with a Foo
to produce a Bar
" is isomorphic to (String, Foo -> Bar)
. And so on. Basically, this logic leads to the "record of methods" encoding that other answers have brought up.
I think the lesson to draw from this is the following: you need a specification richer than just "can be turned into a string" in order to justify using any of the mechanisms you brought up. So for example, if we add the requirement that Stringable
values can be downcast to the original type, an existential type now perhaps becomes justifiable:
{-# LANGUAGE GADTs #-}
import Data.Typeable
data Showable = Showable
Showable :: (Show a, Typeable a) => a -> Stringable
downcast :: Typeable a => Showable -> Maybe a
downcast (Showable a) = cast a
This Showable
type is not isomorphic to String
, because the Typeable
constraint allows us to implement the downcast
function that allows us to distinguish between different Showable
s that produce the same string. A richer version of this idea can be seen in this "shape example" Gist.