Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Haskell: how to separate interface from implementation

I know of two ways to separate the specification of an interface from an implementation of that interface in Haskell:

  1. type classes, e.g.:

    • interface: RandomGen
    • impl: StdGen
  2. records, e.g.:

    • interface: Network.Transport
    • impl: 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?

like image 852
haroldcarr Avatar asked Dec 19 '15 16:12

haroldcarr


1 Answers

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:

  1. ultimately the goal is to pass around functions somehow — this is because there is no other way in Haskell to implement anything whatsoever than as functions, so in order to pass around implementations, you need to pass around functions (note that specifications are just types)
  2. you can either pass around a single function or several functions
  3. to pass around a single function, you just pass that function around, or that function wrapped in something to mimick type classes (i.e. to give your interface a name (e.g. CanFoo) in addition to just the type signature (a -> Foo)
  4. to pass around multiple functions, you just pass them around inside a tuple or a record (like our 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)
  }
}
like image 65
Erik Kaplun Avatar answered Nov 25 '22 21:11

Erik Kaplun