Haskell: 如何将接口与实现分离
Haskell: how to separate interface from implementation
我知道在 Haskell 中有两种方法可以将接口规范与该接口的实现分开:
输入类,例如:
- 接口:
RandomGen
- 实现:
StdGen
条记录,例如:
问题 1:什么时候使用其中一种合适?
问题 2:还有哪些其他方法可以将 interface/impl 与 Haskell 分开?
问题 1 的答案非常简单:这两个选项是等价的——type classes 可以 "desugared" 只是数据类型。 http://www.haskellforall.com/2012/05/scrap-your-type-classes.html.
中描述了这个想法,并对其进行了论证。
问题 2 的答案是,这两种方法是将接口与实现分开的唯一方法。推理是这样的:
- 最终目标是以某种方式传递函数——这是因为在 Haskell 中没有其他方法可以实现任何东西而不是函数,所以为了传递实现,你需要传递函数(注意规范只是类型)
- 您可以传递单个函数或多个函数
- 要传递单个函数,您只需传递该函数,或者将该函数包裹在某些东西中以模仿类型 classes(即给您的界面命名(例如
CanFoo
)除了类型签名 (a -> Foo
)
- 要传递多个函数,只需在元组或记录中传递它们(就像我们的
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)
}
}
我知道在 Haskell 中有两种方法可以将接口规范与该接口的实现分开:
输入类,例如:
- 接口:
RandomGen
- 实现:
StdGen
- 接口:
条记录,例如:
问题 1:什么时候使用其中一种合适?
问题 2:还有哪些其他方法可以将 interface/impl 与 Haskell 分开?
问题 1 的答案非常简单:这两个选项是等价的——type classes 可以 "desugared" 只是数据类型。 http://www.haskellforall.com/2012/05/scrap-your-type-classes.html.
中描述了这个想法,并对其进行了论证。问题 2 的答案是,这两种方法是将接口与实现分开的唯一方法。推理是这样的:
- 最终目标是以某种方式传递函数——这是因为在 Haskell 中没有其他方法可以实现任何东西而不是函数,所以为了传递实现,你需要传递函数(注意规范只是类型)
- 您可以传递单个函数或多个函数
- 要传递单个函数,您只需传递该函数,或者将该函数包裹在某些东西中以模仿类型 classes(即给您的界面命名(例如
CanFoo
)除了类型签名 (a -> Foo
) - 要传递多个函数,只需在元组或记录中传递它们(就像我们的
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)
}
}