设计权衡

哈德利·威克姆

莱昂内尔·亨利

magrittr可以通过许多不同的方式实现管道。本文档的目的是阐明每种方法的不同之处以及各种利弊。本文档主要针对magrittr开发人员(因此我们不要忘记重要的注意事项),但对于那些希望更好地理解管道或创建自己的管道以进行不同权衡的人来说,这将是他们感兴趣的

代码转换

对于如何转换基本R表达式中的管道,有三个主要选项。在这里,它们用x%>%foo()%>%bar():

我们将首先探索我们可能希望管道拥有的所需属性,然后看看这三种变体的作用。

所需属性

这些是我们可能希望管道拥有的特性,大致从最重要到最不重要依次排列。

通过稍微小心一点,可以对所有实现的refcounts有适当的可见性和中性影响,因此我们只考虑其他属性:

嵌套 渴望
(掩码)
渴望
(掩码-编号)
渴望
(词汇)
懒惰
(掩码)
懒惰
(掩码-编号)
懒惰
(词典编号)
多个占位符
懒惰的评估
坚持不懈
渴望解除束缚
渐进式堆栈
词汇效应
连续堆叠

设计决策的含义

一些属性直接反映了高级设计决策。

占位符绑定

嵌套管道不会将管道表达式分配给占位符。所有其他变体都执行此分配。这意味着,使用嵌套重写方法,除非管道表达式被多次粘贴,否则不可能有多个占位符。这将导致多个具有有害影响的评估:

样品(10)%>% 列表(., .)

#成为
列表(样品(10),样品(10))

在参数中指定占位符将保留嵌套和懒散。然而,这并不能正常工作,因为不能保证第一个参数会在第二个参数之前进行计算。

样品(10)%>% foo公司(., .)
foo公司(.<- 样品(10), .)

由于这些原因,嵌套管道不支持多个占位符。相比之下,所有其他变量都将管道表达式的结果指定给占位符。占位符绑定的创建方式有所不同(在掩码或当前环境中,使用编号符号或唯一符号,无论是懒散还是急切),但所有这些变体都允许使用多个占位符。

遮蔽环境

因为管道的局部变体是在掩码中计算的,所以它们没有词法效果或连续堆栈。这与当前环境中评估的词汇变体不同。

懒惰

与惰性变体不同,所有使用迭代求值实现管道的急切版本都不会通过惰性求值标准。

其次,没有惰性变量通过渐进堆栈标准。通过构造,延迟求值需要在求值开始之前将所有管道表达式推送到堆栈上。相反,所有急切的变体都有一个渐进堆栈。

编号占位符

所有使用带编号占位符的变体都无法立即解除管道值的绑定。这就是它们如何实现这些绑定的持久性。

三种实现

GNU R团队正在考虑使用解析时代码转换(就像->被转换为<-解析器)。

我们在magrittr中实施了三种方法:

这些方法具有互补的优点和缺点。

嵌套管道

`%|>%` <-马格里特::管道测试(_N)

多个占位符

嵌套管道不将表达式绑定到占位符,因此不支持多个占位符。

“foo” %|>% 列表(., .)
#>错误:无法使用多个占位符。

懒散的评价

因为它依赖于参数应用的通常规则,所以嵌套管道是惰性的。

{
  停止(“噢,不”)%|>% 尝试(沉默= 真的)
  “成功”
}
#>[1]“成功”

坚持不懈,渴望解放

管道表达式在每个函数的执行环境中绑定为承诺。只要承诺保持不变,此环境就会持续存在。评估承诺将放弃对可用于垃圾收集的环境的引用。

例如,这里有一个创建函数的函数工厂。构造的函数返回创建时提供的值:

工厂<- 功能(x)功能()x
fn公司<- 工厂(真的)
fn公司()
#>[1]正确

这不会导致嵌套管道出现任何问题:

fn公司<- 真的 %|>% 工厂()
fn公司()

渐进式堆栈

因为管道表达式是延迟求值的,所以在执行开始之前,整个管道都被推送到堆栈上。这将导致更复杂的回溯:

有错误的<- 功能()停止(“倾斜”)
(f)<- 功能(x) x个+ 1
<- 功能(x) x个+ 2
小时<- 功能(x) x个+ 

有错误的()%|>% (f)()%|>% ()%|>% 小时()
#>faulty()出错:倾斜

追溯()
#>7:停止(“倾斜”)
#>6:故障()
#>5:f(faulty())
#>4:g(f(faulty()))
#>3:h(g(f(faulty()))
#> 2: .管道处的外部2(magrittr_pipe)。R#181号
#>1:faulty()%|>%f()%|>%g()%>%h()

还要注意回溯中的表达式与实际代码的不同之处。这是因为管道的嵌套重写。

词汇效应

这是使用正常R评估规则的一个好处。副作用发生在正确的环境中:

foo公司<- 错误的
真的 %|>% 分配(“foo”, .)
foo公司
#>[1]正确

控制流具有正确的行为:

fn公司<- 功能() {
  真的 %|>% 返回()
  错误的
}
fn公司()
#>[1]正确

连续堆叠

因为求值发生在当前环境中,所以堆栈是连续的。让我们使用结构化回溯来检测错误,看看这意味着什么:

选项(错误=爱尔兰航空公司::中央)

回溯的树表示正确地表示了执行帧的层次结构:

foobar公司<- 功能(x) x个%|>% 夸克斯()
夸克斯<- 功能(x) x%|>% 停止()

“倾斜” %|>% foobar公司()
#>x%|>%stop()出错:倾斜

爱尔兰航空公司::last_trace(上次跟踪)()
#><错误/rlang_error>
#>倾斜
#>回溯:
#>     █
#>  1. ├─“倾斜”%|>%foobar()
#>  2. └─全局::foobar(“倾斜”)
#>  3.   ├─x%|>%quux()
#>  4.   └─全局::quux(x)
#>  5.     └─x%|>%stop()

急切的词法管道

`%!>%` <-马格里特::管道管理器现有

多个占位符

管道表达式被急切地指定给占位符。这样就可以多次使用占位符,而不会导致多次计算。

“foo” %!>% 列表(., .)
#> [[1]]
#>[1]“foo”(foo)
#>
#> [[2]]
#>[1]“foo”

懒惰的评估

任务要求对每个步骤进行积极评估。

{
  停止(“噢,不”)%!>% 尝试(沉默= 真的)
  “成功”
}
#>停止时出错(“哦,不”)%!>%尝试(silent=TRUE):哦,不

坚持不懈:

因为我们正在更新.在每个步骤中,管道表达式都不是持久的。当管道表达式没有立即求值时,这会产生微妙的影响。

使用急切管道,如果我们试图调用管道中间的构造函数,我们会得到与工厂函数相当混淆的结果。在下面的代码段中,占位符.绑定到构造的函数本身而不是初始值真的,在调用函数时:

fn公司<- 真的 %!>% 工厂()%!>%{ .() }
fn公司()
#>函数()x
#><字节码:0x11d1b6758>
#><环境:0x11da6d628>

还有,因为我们正在绑定.在当前环境中,一旦管道返回,我们需要对其进行清理。此时,占位符不再存在:

fn公司<- 真的 %!>% 工厂()
fn公司()
#>fn()中出错:对象“.”找不到

或者它已重置为以前的值(如果有):

.<- “错误”
fn公司<- 真的 %!>% 工厂()
fn公司()
#>[1]“错误”

渴望解除绑定:

这是在每个步骤更新占位符值的另一面。可以立即收集以前的中间值。

累进堆栈:

由于管道表达式在到达时会逐个求值,因此当抛出错误时,只有管道的相关部分位于堆栈上:

有错误的<- 功能()停止(“倾斜”)
(f)<- 功能(x) x个+ 1
<- 功能(x) x+ 2
小时<- 功能(x) x个+ 

有错误的()%!>% (f)()%!>% ()%!>% 小时()
#>faulty()出错:倾斜

追溯()
#>4:停止(“倾斜”)
#>3:故障()
#> 2: .管道处的外部2(magrittr_pipe)。R#163号
#>1:faulty()%!>%f()%!>%g()%!>%h()

词汇效果和连续叠加:

在当前环境中而不是在面具中进行评估会产生正确的副作用:

foo公司<- 错误的
不适用 %!>%{食品<- 真的; . }
#>[1]不适用

foo公司
#>[1]正确
fn公司<- 功能() {
  真的 %!>% 返回()

  错误的
}
fn公司()
#>[1]正确

惰性掩蔽管

`%?>%` <-马格里特::管道_蓝色_掩蔽

多个占位符

管道表达式以惰性方式指定给占位符。这样就可以多次使用占位符,而不会导致多次计算。

“foo” %?>% 列表(., .)
#> [[1]]
#>[1]“foo”
#>
#> [[2]]
#>[1]“foo”

懒惰的评估

参数赋值为延迟分配()并懒散地评估:

{
  停止(“噢,不”)%?>% 尝试(沉默= 真的)
  “成功”
}
#>[1]“成功”

坚持不懈:

惰性屏蔽管道对每个管道表达式使用一个屏蔽环境。这允许持久化中间值和无序求值。工厂功能按预期工作,例如:

fn公司<- 真的 %?>% 工厂()
fn公司()
#>[1]正确

渴望解除绑定:

因为我们为每个管道表达式使用一个掩码环境,所以只要不再需要中间值,就可以立即收集它们。

累进堆栈:

使用惰性管道,在计算之前将整个管道推送到堆栈上。

有错误的<- 功能()停止(“倾斜”)
(f)<- 功能(x) x个+ 1
<- 功能(x) x个+ 2
小时<- 功能(x) x个+ 

有错误的()%?>% (f)()%?>% ()%?>% 小时()
#>faulty()出错:倾斜

追溯()
#>7:停止(“倾斜”)
#>6:故障()
#>5:f(.)
#>4:克(.)
#>3:h(.)
#> 2: .管道处的外部2(magrittr_pipe)。R#174号
#>1:故障()%?>%f()%?>%g()%?>%h()

然而,请注意,由于占位符的存在,回溯过程比嵌套管道方法更加整洁。

词汇效应

惰性管道在掩码中求值。这会导致词汇副作用在不正确的环境中发生。

foo公司<- 错误的
真的 %?>% 分配(“foo”, .)
foo公司
#>[1]错误

堆栈敏感函数,如返回()函数无法找到正确的帧环境:

fn公司<- 功能() {
  真的 %?>% 返回()
  错误的
}
fn公司()
#>[1]错误

连续烟囱

屏蔽环境导致堆栈树不连续:

foobar公司<- 功能(x) x个%?>% 夸克斯()
夸克斯<- 功能(x) x个%?>% 停止()

“倾斜” %?>% foobar公司()
#>x%?>%中的错误stop():倾斜

爱尔兰航空公司::last_trace(上次跟踪)()
#><错误/rlang_error>
#>倾斜
#>回溯:
#>     █
#>  1. ├─“倾斜”%?>%foobar()
#>  2. ├─全局::foobar(.)
#>  3. │ └─x%?>%quux()
#>  4. └─全局::quux(.)
#>  5.   └─x%?>%停止()