Classes in Haskell #
In Haskell, classes define a type that has certain operations defined for it. Classes have similarities with interfaces
in languages like Java.
Basic Examples #
class BasicClass a where
method1 :: a -> a -> Bool
method2 :: a -> a
In the above snippet,
BasicClass
is a type class that accept a typea
. It has two methodsmethod1
andmethod2
, which have been defined but not implemented. To provide the implementation, we must create instances of the class.
To create an instance of BasicClass
for the type Int
:
instance BasicClass Int where
method1 x y = x == y
method x = x + 1
More Than One Types #
A type class can accept more than one type. These are often called multi-parameter type classes.
class Convertible a b where
convert :: a -> b
In this Convertible
class, there are two type parameters a
and b
. The convert
function converts an instance of type a
into another instance of type b
.
The instance of the class can be:
instance Convertible Int String where
-- The same `as convert x = show x`
-- Point-free style
convert = show
instance Convertible Double Int where
convert = floor
In point-free style (or tacit programming), instead of explicitly mentioning arguments, we construct and manipulate functions directly. This often results in cleaner, more readable code.
To use this multi-parameter type class:
main = do
print (convert (123 :: Int) :: String)
print (convert (123.5 :: Double) :: Int)
Type-directed Overloading #
The ability to use type annotations to guide type inference and disambiguate function calls is indeed a feature of Haskell and other statically-typed languages with sophisticated type systems, such as Scala and Rust.
This feature stems from the Hindley-Milner type system upon which Haskell’s type system is based. Hindley-Milner type systems are known for their strong, static typing rules and powerful type inference capabilities.
Here is an example using “type-directed return overloading” or “return-type overloading”. A Readable
type class that mimics the behavior of the Read
type class in a simplified way. The difference here will be that our readValue
function’s behavior will depend on the return type, not on the type of its argument.
Readable.hs
:
{-# LANGUAGE MultiParamTypeClasses, FlexibleInstances #-}
module Readable (Readable(..)) where
-- Define the type class
class Readable a where
readValue :: String -> a
-- Instance for Int
instance Readable Int where
readValue = read
-- Instance for Integer
instance Readable Integer where
readValue = toInteger . length
Main.hs
:
import Readable
main :: IO ()
main = do
putStrLn $ "Parsed Int: " ++ show (readValue "12345" :: Int)
putStrLn $ "String length as Integer: " ++ show (readValue "12345" :: Integer)
Note:
putStrLn ("Parsed Int: " ++ show (readValue "12345" :: Int))
is also valid. The moduleReadable (Readable(..))
where declaration at the top of a Haskell file defines a module namedReadable
. The items in parentheses after the module name (Readable(..)) specify what the module exports.