Haskell: 如何将接口与实现分离

Haskell: how to separate interface from implementation

我知道在 Haskell 中有两种方法可以将接口规范与该接口的实现分开:

  1. 输入类,例如:

  2. 条记录,例如:

问题 1:什么时候使用其中一种合适?

问题 2:还有哪些其他方法可以将 interface/impl 与 Haskell 分开?

问题 1 的答案非常简单:这两个选项是等价的——type classes 可以 "desugared" 只是数据类型。 http://www.haskellforall.com/2012/05/scrap-your-type-classes.html.

中描述了这个想法,并对其进行了论证。

问题 2 的答案是,这两种方法是将接口与实现分开的唯一方法。推理是这样的:

  1. 最终目标是以某种方式传递函数——这是因为在 Haskell 中没有其他方法可以实现任何东西而不是函数,所以为了传递实现,你需要传递函数(注意规范只是类型)
  2. 您可以传递单个函数或多个函数
  3. 要传递单个函数,您只需传递该函数,或者将该函数包裹在某些东西中以模仿类型 classes(即给您的界面命名(例如 CanFoo)除了类型签名 (a -> Foo)
  4. 要传递多个函数,只需在元组或记录中传递它们(就像我们的 CanFoo 但有更多字段);请注意,在此上下文中,记录只是具有命名字段的命名元组类型。

— 显式或隐式传递函数(类型为 classes),如前所述,在概念上是一回事 [1] .


下面简单演示一下这两种方法是如何等效的:

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

如您所见,如果您使用记录,您的 "instances" 将不再自动查找,您需要将它们显式传递给需要它们的函数。

另请注意,在简单的情况下,记录方法可以简化为仅传递函数,因为传递 CanFoo { foo = \_ -> Foo } 与传递包装函数 \_ -> Foo 本身实际上是一样的。


[1]

事实上,在 Scala 中,这种概念等价性变得很明显,因为 Scala 中的类型 class 是根据类型(例如 trait CanFoo[T])、该类型的多个值进行编码的,和标记为 implicit 的该类型的函数参数,这将导致 Scala 在调用站点查找 CanFoo[Int] 类型的值。

// 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)
  }
}