Gabriel Gonzalez recently wrote an article entitled Advice for Haskell beginners and I was mildly surprised to see that out of four tips, one was:
Avoid typeclass abuse
On second thought, though, this is important advice. In retrospect, my inclination to use typeclasses in inappropriate ways was a big problem in my earlier Haskell days. I made this mistake a lot:
I thought Java interfaces mapped to Haskell typeclasses.
But really, more often they map to Haskell
Julie Moronuki and I have written fairly extensively about the extent to which interfaces and typeclasses can be compared. (Summary of our conclusion: they share a related conceptual purpose but are not directly analogous.) Here I want to share a quick example of how they do map to records, by translating a Java example into Haskell.
To pick a simple and arbitrary example, I went to the Oracle's Java
documentation page entitled What Is an Interface?. They give the
following Bicycle
interface:
interface Bicycle {
void changeCadence(int newValue);
void changeGear(int newValue);
void speedUp(int increment);
void applyBrakes(int decrement);
}
And then a class called AcmeBicycle
, which implements Bicycle
and also has
an additional method called printStates
.
class AcmeBicycle implements Bicycle {
int cadence = 0;
int speed = 0;
int gear = 1;
void changeCadence(int newValue) {
cadence = newValue;
}
void changeGear(int newValue) {
gear = newValue;
}
void speedUp(int increment) {
speed = speed + increment;
}
void applyBrakes(int decrement) {
speed = speed - decrement;
}
void printStates() {
System.out.println("cadence:" +
cadence + " speed:" +
speed + " gear:" + gear);
}
}
So now let's turn that into Haskell. I'm going to translate as directly as
possible. Using mutable references for the three fields of AcmeBicycle
is
unusual for Haskell, but for the sake of comparison we'll do it anyway using
three IORef
s.
The preliminaries: Let's enable RecordWildCards
because we'll be using records
a lot, and import the module containing IORef
.
{-# LANGUAGE RecordWildCards #-}
import Data.IORef
Now we'll jump straight to the punchline: Here's how the Bicycle
Java
interface is represented as a Haskell record. Each of the four methods in the
interface corresponds to a field in the record.
data Bicycle =
Bicycle
{ changeCadence :: Int -> IO ()
, changeGear :: Int -> IO ()
, speedUp :: Int -> IO ()
, applyBrakes :: Int -> IO ()
}
The AcmeBicycle
Java class also translates into a Haskell record. Each of
the three fields in the Java interface corresponds to a field in the record.
data AcmeBicycle =
AcmeBicycle
{ cadence :: IORef Int
, speed :: IORef Int
, gear :: IORef Int
}
The implicit constructor in the Java class becomes an explicit IO
action in
Haskell.
newAcmeBicycle :: IO AcmeBicycle
newAcmeBicycle =
do
cadence <- newIORef 0
speed <- newIORef 0
gear <- newIORef 1
return AcmeBicycle{..}
The printStates
Java class method becomes an ordinary top-level Haskell
function.
printStates :: AcmeBicycle -> IO ()
printStates AcmeBicycle{..} =
do
c <- readIORef cadence
s <- readIORef speed
g <- readIORef gear
putStrLn ("cadence:" ++ show c ++
" speed:" ++ show s ++
" gear:" ++ show g)
In the Java example, AcmeBicycle
is a subtype of Bicycle
, which in Java
parlance means that every AcmeBicycle
value is also a Bicycle
value. Thus
if x
is an AcmeBicycle
and f
is a function that accepts a Bicycle
argument, we can write the Java expression f(x)
.
We don't have that kind of polymorphism in our Haskell translation, but we
really don't need it; we just need a function that converts AcmeBicycle
to
Bicycle
.
acmeToBicycle :: AcmeBicycle -> Bicycle
acmeToBicycle AcmeBicycle{..} =
Bicycle
{ changeCadence = writeIORef cadence
, changeGear = writeIORef gear
, speedUp = \x -> modifyIORef' speed (\s -> s + x)
, applyBrakes = \x -> modifyIORef' speed (\s -> s - x)
}
Thus if x
is an AcmeBicycle
and f
is a function that accepts a Bicycle
argument, we can write the Haskell expression f (acmeToBicycle x)
. There is
one extra function call involved, but in exchange we eliminate the conceptual
complexity of Java subtyping or Haskell overloading, and we can program entirely
in simple monomorphic functions and data types.