我最近考试不及格,主要是因为EDSL问题。我没有掌握这些概念所以我认为这就是我失败的原因。我认为我的老师解释是抽象让我理解,所以我想知道是否有人可以更清楚地解释它。
我想知道是否有人可以简要解释一下EDSL的组成部分是什么,以及它们的特征。在我们的课程中,我们经历了DSL的浅层和深层嵌入,并查看了DSL的以下构建块:
我认为构造函数和运行函数更加不言自明,因此我更有兴趣了解组合器派生或原始的原因。如果有人会解释其他概念,这不会有害。以下是我们讲座中的一个例子供参考。它是用于创建信号的DSL的浅层实现:
module Signal.Shallow
( Time
-- | the 'Signal' type is abstract
, Signal
-- * Smart constructors
, constS, timeS
-- * Combinators
, ($$), mapT
-- * Derived operation
, mapS
-- * Run function
, sample
) where
-- * Smart constructors
constS :: a -> Signal a
timeS :: Signal Time
-- * Combinators
($$) :: Signal (a -> b) -> Signal a -> Signal b
mapT :: (Time -> Time) -> Signal a -> Signal a
-- * Derived operation
mapS :: (a -> b) -> Signal a -> Signal b
-- * Run function
sample :: Signal a -> Time -> a
type Time = Double
newtype Signal a = Sig {unSig :: Time -> a}
-- | The constant signal.
constS x = Sig (const x)
-- | The time signal
timeS = Sig id
-- | Function application lifted to signals.
fs $$ xs = Sig (\t -> unSig fs t (unSig xs t))
答案 0 :(得分:4)
原始组合器是内置于DSL中的组合,以基本语言(即Haskell)定义。 DSL通常围绕抽象类型 -a类型构建,其实现对最终用户是隐藏的。它完全不透明。由语言呈现的原始组合器是需要知道抽象实际如何实现才能工作的组合。
另一方面,派生的组合器可以根据DSL中已有的其他组合器来实现。它不需要了解抽象类型的任何信息。换句话说,派生的组合子就是你自己写的 。这与Haskell本身的原始类型的想法非常相似。例如,您无法自己实施Int
或Int
等+
操作。这些需要编译器内置的东西才能工作,因为数字是专门处理的。另一方面,Bool
是不是原语;你可以把它写成一个库。
data Bool = True | False -- ... you can't do this for Int!
除了编译器实际上是你的Haskell库之外,DSL的“原始”和“派生”是相同的想法。
在您的示例中,Signal
是一种抽象类型。它是作为函数Time -> a
实现的,但该信息不从模块导出。将来,您(作为DSL的作者)可以自由更改Signal
的实现方式。 (而且,事实上,你真的想:这不是一个有效的表示,并且使用Double
时间是挑剔的。)
像$$
这样的函数很原始,因为它取决于知道Signal
是Time -> a
。当您更改Signal
的表示时,您将不得不重写$$
。此外,您图书馆的用户将无法自己实施$$
。
另一方面,mapS
是派生操作,因为它可以完全根据您导出的其他内容编写。它不需要知道关于Signal
的任何特殊内容,甚至可以由该库的一个用户编写。实现可能类似于:
mapS f signal = constS f $$ signal
请注意它如何使用constS
和$$
,但永远不会展开signal
。 如何解包信号的知识完全隐藏在这两个函数中。 mapS
是“派生的”,因为它只是在你的DSL中编写而不需要任何低于你的抽象级别的东西。当您更改Signal
的实施时,mapS
仍然会按原样运作:您只需要正确更新constS
和$$
,然后获得mapS
免费。
所以:原始组合器是直接构建到您的语言中并且需要了解其内部实现细节的组合器。派生组合器纯粹用您的语言编写,并不依赖于任何这些内部细节。它们只是便利功能,可以很容易地由您的图书馆的最终用户编写。