I know of two ways to separate the specification of an interface from an implementation of that interface in Haskell:
type classes, e.g.:
RandomGen
StdGen
records, e.g.:
Network.Transport
Network.Transport.TCP
QUESTION 1: when is it appropriate to use one or the other?
QUESTION 2: what other ways are there to separate interface/impl in Haskell?
The answer to question 1 is quite simple: the two options are equivalent — type classes can be "desugared" to just data types. The idea has been described, and argued in favor of, in http://www.haskellforall.com/2012/05/scrap-your-type-classes.html.
The answer to question 2 is that these two are the only ways to separate interface from implementation. The reasoning would be something like this:
CanFoo
) in addition to just the type signature (a -> Foo
)CanFoo
but with more fields); note that a record is just a named tuple type with named fields, in this context.— Whether to pass around functions explicitly or implicitly (with type classes), is, as already demonstrated, one and the same thing conceptually [1].
Here's a simple demonstration of how the 2 methods are equivalent:
data Foo = Foo
-- using type classes
class CanFoo a where
foo :: a -> Foo
doFoo :: CanFoo a => a -> IO Foo
doFoo a = do
putStrLn "hello"
return $ foo a
instance CanFoo Int where
foo _ = Foo
main = doFoo 3
-- using explicit instance passing
data CanFoo' a = CanFoo' { foo :: a -> Foo }
doFoo' :: CanFoo' a -> a -> IO Foo
doFoo' cf a = do
putStrLn "hello"
return $ (foo cf) a
intCanFoo = CanFoo { foo = \_ -> Foo }
main' = doFoo' intCanFoo 3
As you can see, if you use records, your "instances" aren't looked up automatically anymore and instead you need to pass them explicitly to the functions that need them.
Note also that in the trivial case, the record method can be reduced to just passing around functions because passing around CanFoo { foo = \_ -> Foo }
is really the same as passing around the wrapped function \_ -> Foo
itself.
[1]
In fact, in Scala, this conceptual equivalence becomes obvious as a type class in Scala is encoded in terms of a type (e.g. trait CanFoo[T]
), a number of values of that type, and function parameters of that type that are marked to be implicit
, which will cause Scala to look up values of type CanFoo[Int]
at the call site.
// data Foo = Foo
case object Foo
// data CanFoo t = CanFoo { foo :: t -> Foo }
trait CanFoo[T] { def foo(x : T): Foo }
object CanFoo {
// intCanFoo = CanFoo { foo = \_ -> Foo }
implicit val intCanFoo = new CanFoo[Int] { def foo(_: Int) = Foo }
}
object MyApp {
// doFoo :: CanFoo Int -> Int -> IO ()
def doFoo(someInt: Int)(implicit ev : CanFoo[Int]] = {
println("hello")
ev.foo(someInt)
}
def main(args : List[String]) = {
doFoo(3)
}
}
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With