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
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.