【问题标题】:Haskell: how to separate interface from implementationHaskell:如何将接口与实现分离
【发布时间】:2015-12-20 16:04:18
【问题描述】:

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

  1. 类型类,例如:

  2. 记录,例如:

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

问题 2:在 Haskell 中还有哪些其他方法可以分离接口/实现?

【问题讨论】:

  • 当你期望每种类型都有一个唯一的实现,或者你想通过它们的类型来标记实现,或者你只是需要隐式实例解析的便利,使用类型类。否则,使用记录。但这只是我的观点……我认为至少第 1 部分是非常基于观点的(有些人在几乎所有情况下都是顽固地主张一个胜过另一个……)。
  • 您可能会发现这很有用:stackoverflow.com/questions/17100036/…
  • 类型类不用于与规范分离的接口。它们用于引入上下文相关的重载符号。类型类作为模块的一个问题是所有实例在类型类可见的范围内都是可见的。
  • @user2407038 您的建议遵循 Gabriel 在下一条评论提供的链接中所说的内容。谢谢。
  • @Sibi - 感谢您的链接。加布里埃尔总是有很好的建议。

标签: haskell interface polymorphism typeclass


【解决方案1】:

问题 1 的答案非常简单:这两个选项是等价的——类型类可以“脱糖”为数据类型。这个想法已在http://www.haskellforall.com/2012/05/scrap-your-type-classes.html 中进行了描述和论证。


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

  1. 最终的目标是以某种方式传递函数——这是因为在 Haskell 中除了函数之外没有其他方法可以实现任何东西,所以为了传递实现,你需要传递函数(注意规范是只是类型)
  2. 您可以传递单个函数或多个函数
  3. 要传递单个函数,您只需传递该函数,或者将该函数包装在某些东西中以模仿类型类(即,除了类型签名(@ 987654323@)
  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

如您所见,如果您使用记录,您的“实例”将不再自动查找,而是需要将它们显式传递给需要它们的函数。

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


[1]

事实上,在 Scala 中,这种概念上的等价性变得很明显,因为 Scala 中的类型类是根据类型(例如 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)
  }
}

【讨论】:

  • 我个人发现 typeclass 示例比其他示例更容易理解。我不明白为什么很多人反对他们。
  • 至于记录方法(与doFoo'一样)您可以启用RecordWildCards并写入doFoo' CanFoo'{..} a = do { ... ; return (foo a) }。这使得函数在语法上更好,如果记录字段是中缀二进制函数(即data Num a = Num { (+) :: a -> a -> a }),这甚至可以工作
猜你喜欢
  • 2020-07-03
  • 1970-01-01
  • 1970-01-01
  • 2016-08-09
  • 2012-06-02
  • 1970-01-01
  • 2016-09-05
  • 2016-06-28
  • 1970-01-01
相关资源
最近更新 更多