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

8
我知道在Haskell中有两种将接口的规范与其实现分离的方法:
  1. 类型类,例如:

  2. 记录,例如:

问题1:什么时候使用其中一种是合适的?
问题2:还有哪些其他方法可以在Haskell中分离接口/实现?

当您期望每种类型都有独特的实现,或者您想要按其类型标记实现,或者您只需要隐式实例解析的便利性时,请使用类型类。否则,请使用记录。但这只是我的观点...我认为至少第一部分非常基于个人看法(有些人几乎在任何情况下都是某种方法的死忠拥护者...)。 - user2407038
你可能会发现这个有用:https://dev59.com/qGQn5IYBdhLWcg3wGT4X - Sibi
类型类并不是为了将接口与规范分离。它们是为了引入上下文相关的重载符号。类型类作为模块的一个问题是,在类型类可见的范围内,所有实例都是可见的。 - Rodrigo Ribeiro
@user2407038,你的建议与下一个评论提供的链接中Gabriel所说的一致。谢谢。 - haroldcarr
@Sibi - 感谢你提供的链接。Gabriel 总是有很好的建议。 - haroldcarr
@RodrigoRibeiro :没错,但很多人都像我在 System.Random 的例子中展示的那样使用它们。 - haroldcarr
1个回答

4
问题1的答案很简单:这两个选项是等价的——类型类可以“解糖”为数据类型。这个想法已经在http://www.haskellforall.com/2012/05/scrap-your-type-classes.html中描述并被支持。
问题2的答案是,这两种方式都是将接口与实现分离的唯一方法。推理如下:
  1. 最终的目标是以某种方式传递函数——这是因为在Haskell中,没有其他实现任何东西的方法,除了函数,因此为了传递实现,您需要传递函数(请注意规范只是类型)
  2. 您可以传递单个函数或多个函数
  3. 要传递单个函数,只需直接传递该函数,或者使用类似于包装类型类的东西来传递该函数(即给您的接口命名(例如CanFoo),而不仅仅是类型签名(a -> Foo
  4. 要传递多个函数,只需在元组或记录中传递它们(像我们的CanFoo但带有更多字段);请注意,在这个上下文中,记录只是带有命名字段的命名元组类型。

—— 明确传递函数还是隐式传递函数(通过类型类),在概念上已经被证明是一样的[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

如您所见,如果使用记录(records),您的“实例”将不再自动查找,而是需要显式地将它们传递给需要它们的函数。
还要注意,在平凡的情况下,记录方法可以简化为仅传递函数,因为传递 CanFoo { foo = \_ -> Foo } 实际上与传递包装函数 \_ -> Foo 本身相同。
[1]
事实上,在Scala中,这种概念上的等价关系在Scala中被编码为类型类(type 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)
  }
}

我个人认为,类型类的示例比另一个示例容易理解得多。我不明白为什么有很多人建议不要使用它们。 - rubik
1
关于记录方法(如doFoo'),您可以启用RecordWildCards并编写doFoo' CanFoo'{..} a = do { ... ; return (foo a) }。这使得函数在语法上更加美观,即使记录字段是中缀二元函数也适用(例如data Num a = Num { (+) :: a -> a -> a })。 - user2407038

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接