First of all, make sure you read and understand "Haskell Antipattern: Existential Typeclass". Your example code is more complex than it needs to be.
Basically, you're asking how to perform the equivalent of a downcast in Haskell—cast a value from a supertype to a subtype. This sort of operation can intrinsically fail, so the type is something like Element -> Maybe E1
.
The first question to ask here is: do you really need to? There are two complementary alternatives to this. First: you can formulate your "supertype" in such a way that it only ever has a finite, fixed number of "subtypes." Then you implement your type just as a union:
data Element = E1 String | E2 Int
And every time you want to use an Element
you pattern match and presto, you have the case-specific data:
processElement :: Element -> whatever
processElement (E1 str) = ...
processElement (E2 i) = ...
The downsides to this approach are that:
- Your union type can only have a fixed set of subcases.
- Every time you add a subcase you will have to modify all the existing operations to add an extra matching case for it.
The upsides are:
- By enumerating all the subcases in your type, you can use the compiler to tell you when you've missed one.
- Adding a new operation is easy, and doesn't require you to modify any existing code.
The second way you can go is to reformulate the type as an "interface". By this I mean your type is now going to be modeled as a record type, each of whose fields constitutes a "method":
data Element = Element { say :: String }
-- A "constructor" for your first subcase
makeE1 :: String -> Element
makeE1 str = Element str
-- A "constructor" for your second subcase
makeE2 :: Int -> Element
makeE2 i = Element (show i)
This has the upside that you now can have as many subcases as you want, and you can easily add them without modifying existing operations. It has these two downsides:
- If you need to add new operations, you will have to add a "method" (field) to the
Element
type, and modify every existing function that constructs an Element
.
- Consumers of the
Element
type can never tell which subcase they're dealing with, or get information specific to this subcase. E.g., a consumer can't tell a particular Element
started was constructed with makeE2
, much less extract the Int
that such an Element
encapsulates.
(Note that your example with existentials is equivalent to this "interface" approach, and shares the same advantages and limitations. It's just needlessly verbose.)
But if you really insist on having the equivalent of a downcast, there is a third alternative: use the Data.Dynamic
module. A Dynamic
value is an immutable container that holds a single value of any type that instantiates the Typeable
class (which GHC can derive for you). Example:
data E1 = E1 String deriving Typeable
data E2 = E2 Int deriving Typeable
newtype Element = Element Dynamic
makeE1 :: String -> Element
makeE1 str = Element (toDyn (E1 str))
makeE2 :: Int -> Element
makeE2 i = Element (toDyn (E2 i))
-- Cast an Element to E1
toE1 :: Element -> Maybe E1
toE1 (Element dyn) = fromDynamic dyn
-- Cast an Element to E2
toE2 :: Element -> Maybe E2
toE2 (Element dyn) = fromDynamic dyn
-- Cast an Element to whichever type the context expects
fromElement :: Typeable a => Element -> Maybe a
fromElement (Element dyn) = fromDynamic dyn
This is the closest solution to the OOP downcasting operation. The downside to this is that downcasts are inherently not type safe. Let's go back to the case where, some months later, you need to add an E3
subcase to your code. Well, the problem now is you have a lot of functions sprinkled throughout the code that are testing whether an Element
is an E1
or an E2
, which were written before E3
ever existed. How many of these functions will break when you add this third subcase? Good luck, because the compiler has no way of helping you!
Note that this three-alternative scenario I've described also exists in OOP, with these three alternatives:
- The OOP counterpart to union type is the Visitor pattern, which is meant to make it easy to add new operations to a type without having to modify its subclasses. (Well, relatively easy. The Visitor pattern is hella verbose.)
- The OOP counterpart to the "interface" solution is to code 100% to an interface (or abstract class). This means not only that you use an interface—it also means that your client code never "peeks under the interface" to see what the actual implementation classes are; it relies entirely on the interface methods and their contracts.
- The OOP counterpart to the
Dynamic
solution is to use downcasting. It has the same downsides as I explained above—somebody can come in and add a new subclass, and code that "peeks" at the runtime subtype may not be ready to handle this.
So to the broader question of how to change from OOP thinking to Haskell thinking, I think this comparison provides a good starting point. OOP and Haskell provide all three alternatives. OOP makes #3 very easy, but that basically gives you rope to hang yourself with; #2 is what many OOP gurus would recommend you do, and it can be achieved if you are disciplined; but #1 in OOP gets very verbose. Haskell makes #1 easiest; #2 is not much harder to implement, but requires more careful forethought ("am I providing the correct operations for all users of this type?"); #3 is the one that's a bit verbose and against the grain of the language.