R# 语言介绍
R# 语言是一种类 R 语言,在 .NET 环境下实现。
引言
多年来,我一直使用 VB.NET 语言进行科学计算工作,我很好奇是否有办法对我的 VB.NET 库进行脚本化。在学校学习 R 语言后,我想知道我是否能够将 R 语言及其向量化编程特性与我的 VB.NET 库原生结合起来。于是,这个想法催生了 R#
语言。
R# 语言的诞生源于将向量化编程语言特性带入 .NET 平台的想法。有一些向量化编程语言,如 MATLAB 语言、S 语言和 R 语言,它们都作为语言原型候选者,用于设计我的新语言。在语言特性研究和进行一些背景调查工作后,R 语言被选为 .NET 平台上的新向量化编程语言原型,因此这种新的向量化编程语言被命名为 R#
,因为这种新语言是从 R 语言派生的一种方言。
如果您对 R# 语言感兴趣,这里有一些可能对学习 R/R# 语言有用的资源链接:
- R# 语言源代码仓库:https://github.com/rsharp-lang/R-sharp
- 我关于 R# 库/包的博客文章(大部分是中文):https://stack.xieguigang.me/tag/rsharp/
- R 语言学习:https://www.r-bloggers.com/
- 数据科学学习:https://medium.com/towards-data-science
R# 解释器的设计
它是如何工作的?
R# 语言目前是一种解释型编程语言,其解释器包含四个模块:
Interpreter
:包含 R# 语言解释器的源代码,以及所有表达式类模型的定义。Language
:包含从输入脚本文本中解析语言标记所需的代码,以及用于根据语言标记序列和语言上下文创建相应 R# 表达式对象的语法解析器。Runtime
:包含导入外部 .NET 函数所需的代码以及运行 R# 表达式评估的运行时环境定义。此文件夹还包含一些用于操作数据集的原始 R# 函数,例如 lapply、sapply、list、which 等。System
:包含运行时配置、第三方包加载器以及用于构建自己的 R# 包的工具的代码。
通过结合这 4 个模块中的代码,我们可以创建一个工作流程来运行 R# 脚本,将 R# 脚本与我们 .NET 库中已有的函数进行互操作,并评估 R# 表达式以生成 .NET 对象。
工作流程:运行 R# 代码
下面是一个工作流程图,用于说明如何运行 R# 代码输入。
- R# 环境初始化:在 R# 系统初始化的最开始,将调用 R# 系统的代码模块来
- 加载配置文件,
- 初始化全局环境,
- 挂接 R# 基础包中所有的 .NET API 函数,
- 然后加载启动包并初始化运行时环境。
最后,R# 就准备好运行我们的脚本代码了。
- 输入的脚本文本随后将由语言命名空间中定义的扫描器对象解析为 R# 语言标记。语言标记序列是扫描器通过其
char
遍历操作输出的。生成标记序列中的语言标记顺序是 R# 解释器中语法分析模块用于创建语法树的语法上下文信息。在根据标记序列构建出语法树模型后,脚本文本就被解析为 R# 程序:一组表达式模型。 - R# 语言的表达式模型是基于给定评估上下文产生结果值的最基本模型,因此我们可以将 R# 表达式模型抽象为一个基类对象。
Namespace Interpreter.ExecuteEngine
''' <summary>
''' An expression object model in R# language interpreter
''' </summary>
Public MustInherit Class Expression
''' <summary>
''' Evaluate the R# expression for get its runtime value result.
''' </summary>
''' <pa ram name="envier"></param>
''' <returns></returns>
Public MustOverride Function Evaluate(envir As Environment) As Object
End Class
End Namespace
VisualBasic 中的代码演示
R#
语言解释器最初是用 VB.NET 语言编写的,因此 R# 语言与 .NET 运行时完全兼容。这意味着您可以将 R# 环境嵌入到您的 .NET 应用程序中,这将使您能够对您的 .NET 库进行脚本化。这是一个在 github 上关于在 VB.NET 应用程序中运行 R# 脚本文件的完整示例代码:“RunRScriptFile”。
首先,我们需要有一个运行时配置文件来运行 R# 语言解释器运行时的初始化工作流程。运行时配置文件是一个 XML 文件,如果它在给定文件位置丢失,则可以自动生成。
Dim R As RInterpreter = RInterpreter.FromEnvironmentConfiguration(
configs:="/path/to/config.xml"
)
如果某个外部第三方 R# 库 DLL 文件不在应用程序目录或库文件夹中,那么您应该通过运行时配置来设置 DLL 目录文件夹路径:
If Not SetDllDirectory.StringEmpty Then
Call R.globalEnvir.options.setOption("SetDllDirectory", SetDllDirectory)
End If
在运行给定的 R# 脚本文件之前加载一些启动包。
' Call R.LoadLibrary("base")
' Call R.LoadLibrary("utils")
' Call R.LoadLibrary("grDevices")
' Call R.LoadLibrary("stats")
For Each pkgName As String In startupsLoading
Call R.LoadLibrary(
packageName:=pkgName,
silent:=silent,
ignoreMissingStartupPackages:=ignoreMissingStartupPackages
)
Next
最后,我们可以通过从 R# 解释器导出的 Source
函数来运行脚本代码。
result = R.Source(filepath)
如果您只想评估脚本文本,而不期望从文本文件中运行代码,那么您可以尝试从 R# 解释器引擎导出的 Evaluate
函数。
' Run script by invoke method
Call R.Evaluate("
# test script
let word as string = ['world', 'R# user', 'GCModeller user'];
let echo as function(words) {
print( `Hello ${ words }!` );
}
echo(word);
")
R# 与 LINQ 对比
如前所述,R# 语言是一种向量化编程语言。因此,R# 编程中的许多操作都是向量化的,这意味着我们可以在一个表达式中执行多次相同的操作。
尽管 .NET 平台中的 LINQ 语言特性为所有 .NET 语言提供了一些类似向量化编程的语言特性,但与 R/R# 语言相比,它仍然有些不方便。这里有一些例子:
1. 算术运算
在这里,我们可以通过 LINQ 进行一些简单的数学运算,如加法、减法、乘法和除法。
{1,2,3,4,5}.Select(Function(xi) xi + 5).ToArray
在 R# 语言中进行完全相同的数学运算会更简单。
[1, 2, 3, 4, 5] + 5;
# [1] 6 7 8 9 10
以下是在 R# 环境中支持的运算符:
operator | description | 示例 | 与 VB 对比 |
+ | 加法 | a + b | a + b |
- | 减法 | a - b | a - b |
* | 乘法 | a * b | a * b |
/ | 除法 | a / b | a / b |
\ | 整数除法 | a \ b | a \ b |
% | 修改 | a % b | a Mod b |
! | not | !a | Not a |
== | equals | a == b | a = b |
!= | 不等于 | a != b | a <> b |
&& | 和 | a && b | a AndAlso b |
|| | 或 | a || b | a OrElse b |
类似于 | 字符串模式匹配 | a like $"\d+" | a Like "*.jpg" |
in | contains | a in b | b.ContainsKey(a) |
2. 数学函数
与 .NET LINQ 相比,使用数学函数在 R# 语言中也超级优雅和简单。
log10([10, 100, 1000, 10000, 100000]);
.NET LINQ
{10, 100, 1000, 10000, 100000}.Select(AddressOf Math.Log10).ToArray()
3. LINQ 函数
尽管大多数 R# 脚本代码都可以向量化,但在 R# 脚本中处理复杂复合数据集时,仍然需要一些循环式操作。虽然 R# 语言中有 for
循环或 while
循环,但大多数情况下不推荐在 R# 编程中使用这种循环代码。与原始 R 语言一样,apply
系列函数可用于此目的。
R# 语言中的 sapply
或 lapply
函数是一种类似 LINQ 的函数,可用于处理复杂数据集合。
sapply
表示序列应用,它等同于 LINQ 中的Select
函数。sapply
函数接受 R# 语言中的集合数据,然后生成新的向量数据。lapply
表示列表应用,它等同于 LINQ 中的ToDictionary
函数。lapply
函数的工作方式与sapply
函数类似,接受 R# 语言中的集合数据,但生成一个新的键值对列表数据。
以下是一个关于在 R# 语言中使用 sapply
和 lapply
函数以及相应的 LINQ 对比代码的示例:
[1,2,3,4,5] |> sapply(xi -> xi + 5);
# [1] 6 7 8 9 10
{1,2,3,4,5}.Select(Function(xi) xi + 5).ToArray()
然后,如果您想过滤掉输入数据集合中一些不需要的数据,您可以使用 .NET LINQ 中的 Where
函数。与 LINQ 的工作方式相同,R# 语言在数据处理管道中也有一个数据过滤器。LINQ 函数 Where
的条件过滤器等同于 R# 中的 which
函数,这是一个例子:
' filter data in .NET LINQ by Where
{1,2,3,4,5}
.Where(Function(x) x > 3)
.ToArray()
# filter data in R# language by which
[1,2,3,4,5]
|> which(x -> x > 3)
;
# another conditional filter syntax in original R language style
x = [1,2,3,4,5];
x[which(x > 3)];
# more simple way:
x[x > 3];
R# 与 VisualBasic 对比
除了 R# 语言的向量化编程特性是其与 VisualBasic.NET 语言最大的区别外,还有许多其他语言特性可以区分 R# 语言和 VisualBasic.NET 语言。
1. 声明新函数
函数是我们程序的基本模块,我们可以通过一些逻辑组合函数来构建复杂的应用程序。通过函数,我们可以重用代码,使我们的程序模块化和标准化。在 R# 语言中声明新函数非常灵活。
根据文档所述,R 函数在 R 语言中也是一种数据类型。因此,我们可以使用 VisualBasic 的符号声明风格创建 R# 函数,例如:
# formal style
const add5 as function(xi) {
return(xi + 5);
}
# or replace the as with equal sign
# this will makes the R# code more typescript style:
const add5 = function(xi) {
return(xi + 5);
}
在 R# 函数声明的正式风格中,符号名称是函数名,as
部分的表达式表示我们声明的目标符号的类型是函数,而函数闭包体是符号实例值。
也许正式风格包含太多书写 R# 代码的词语,所以您也可以用 lambda 风格书写 R# 函数:
# syntax sugar borrowed from julia language
const f(x) = x + 5;
# syntax sugar from the original R language
const add5 = function(xi) xi + 5;
请注意:我们在脚本中声明的所有 R# 函数都是向量化的,所以大多数时候我们不需要在函数中使用额外的 for
循环或 while
循环。
const f(x) = x + 5;
f([1,2,3,4,5]);
# [1] 6 7 8 9 10
2. Lambda 函数 & 函数式编程
R# 语言也是一种函数式编程语言,因此在 R# 中将函数作为另一个函数的参数值也非常容易。通过上面学习的 sapply
函数的相同示例,我们可以演示如何在 R# 语言中进行函数式编程:
const add5 = function(xi) {
return(xi + 5);
}
sapply([1,2,3,4,5], add5);
sapply([1,2,3,4,5], function(x) {
x + 5;
});
也许,上面演示代码中的书写仍然有点冗长。因此,lambda
函数被引入到 R# 语言中,以简化 R# 中的函数式编程代码。
sapply([1,2,3,4,5], x -> x + 5);
3. 管道与扩展函数对比
.NET 中有一个很棒的语言编程特性,称为扩展方法:通过在 VisualBasic.NET 语言中用 ExtensionAttribute
标记目标静态函数,我们可以使目标函数调用呈现类似对象实例方法的形式。利用扩展方法,我们可以在 .NET 中链接函数调用并构建数据管道。
与原始 R 语言相比,R# 语言引入了一个管道运算符。管道运算符将使所有 R# 函数能够以管道化的方式自然调用。例如:
const add5 = function(x) {
return(x + 5);
}
[1,2,3,4,5]
|> add5()
# we even can pipeline the anonymous function
# in R# language
|> (function(x) {
return(x ^ 2);
})
;
4. 基于表达式与基于语句
VisualBasic 语言是一种基于语句的语言,这意味着大多数 VisualBasic 代码不会产生值,除非 VB 语句表达式是一个函数调用。与 VisualBasic 语言不同,R# 编程语言是基于表达式的,这意味着所有 R# 代码都可以产生值。这里有一个足够清晰地展示两种语言之间差异的例子:
Dim x As Double
If test1 Then
x = 1
Else
x = -1
End If
正如您在上面看到的代码所示,由于 VB 代码是基于语句的,If 块无法产生值,所以我们需要在两个语句中为变量 x
赋值。不同的是,R# 语言是基于表达式的,所以我们可以直接从这样的 if 分支代码中获取结果值。
const x as double = {
if (test1) {
1;
} else {
-1;
}
}
R# 语言中的数据集
R# 语言中有四种原始数据类型,R# 语言中的所有原始类型都是一种原子向量。
R# 原始类型 | VisualBasic.NET | 注意 |
num | Single, Double | Single 将转换为 Double |
int | Short, Integer, Long | Short, Integer 将转换为 Long |
原始 | 字节型 | 值范围在 [0,255] |
chr | Char, String | 来自 VisualBasic.NET 的 Char 和 String 在 R# 运行时被统一为字符,而 Char 是一种特殊的 string :其 nchar 值等于 1 。 |
logi | 布尔值 | 除了 TRUE 和 FALSE 之外,R# 中的逻辑值字面量也可以是 true, false, yes, no。 |
any | 对象 | .NET 对象在 R# 语言中,任何类型的 .NET 对象也是一种**伪**原始类型。 |
基于这些原始类型,我们就可以在 R# 语言中组合更复杂的数据类型。
键值对列表
R# 语言中的 list 类型是一种类似于 VisualBasic 中的 Structure
的数据类型。list 类型非常灵活:您可以在值槽中存储任何类型的数据,但列表中的键名必须是字符类型。您可以使用 list 函数创建列表,例如:
list(a = 1, b = 2, flag = [TRUE, FALSE], c = "Hello world!")
# List of 4
# $ a : int 1
# $ b : int 2
# $ flag : logical [1:2] TRUE FALSE
# $ c : chr "Hello world!"
而不是 list 函数,R# 语言中引入了一种更具语法糖特性的语言特性:JSON 字面量。
# json literal in R# language will also produce a list object
{
a: 1,
b: 2,
flag: [TRUE, FALSE],
c: "Hello world!"
}
# List of 4
# $ a : int 1
# $ b : int 2
# $ flag : logical [1:2] TRUE FALSE
# $ c : chr "Hello world!"
作为参考,对于 R# 键值对列表中的槽值,如果我们知道名称,可以使用 $
运算符;如果我们不知道槽名,可以使用 [[xxx
]] 索引器语法。例如:
const x = list(a = 1, b = 2, flag = [TRUE, FALSE], c = "Hello world!");
# TRUE, FALSE
x$flag
for(name in names(x)) {
# the code we demonstrate at here is kind of
# reflection liked code in .NET
print(x[[name]]);
}
dataframe
R# 语言中的 dataframe
类型是一种二维表。R# dataframe
中的每一列都是一种原子向量数据。您可以将 R# 语言中的 dataframe
视为一种特殊的键值对列表对象。dataframe
中列之间的数据类型可以是变化的。
可以通过 data.frame
函数创建 dataframe
对象。
data.frame(a = 1, b = 2, c = "Hello world!", flag = [TRUE, FALSE]);
# a b c flag
# ----------------------------------------------------
# <mode> <integer> <integer> <string> <boolean>
# [1, ] 1 2 "Hello world!" TRUE
# [2, ] 1 2 "Hello world!" FALSE
或者可以通过 as.data.frame
函数将 dataframe
从列表数据对象转换过来。
as.data.frame(list(a = 1, b = 2, c = "Hello world!", flag = [TRUE, FALSE]));
# a b c flag
# ----------------------------------------------------
# <mode> <integer> <integer> <string> <boolean>
# [1, ] 1 2 "Hello world!" TRUE
# [2, ] 1 2 "Hello world!" FALSE
键值列表和 dataframe
对象之间的区别是:列表中的值可以是任何类型的数据,而 dataframe
中的值应该是原子向量。列表和 dataframe
之间关于向量数据的一个更明显的区别是向量大小:列表中的所有向量大小可以是变化的,但 dataframe
中每列的向量大小应该是 1 个元素或 n 个元素,其中 n 个元素必须等于数据框的行数。这是一个关于创建不同大小向量的 dataframe
的错误示例。
data.frame(a = 1, b = [1,2,3], f = [TRUE, FALSE]);
# Error in <globalEnvironment> -> data.frame
# 1. arguments imply differing number of rows
# 2. a: 1
# 3. b: 3
# 4. f: 2
#
# R# source: Call "data.frame"("a" <- 1, "b" <- [1, 2, 3], "f" <- [True, False])
#
# base.R#_interop::.data.frame at REnv.dll:line <unknown>
# SMRUCC/R#.global.<globalEnvironment> at <globalEnvironment>:line n/a
基于原子向量、列表和数据框数据类型,我们有足够的组件来创建 R# 脚本来解决特定的科学问题。
在 R# 中访问任何 .NET 对象
除了 R# 向量、列表和数据框之外,R# 语言中还有另一种数据类型:原生的 .NET 对象。是的,我们可以直接将 R# 代码与 .NET 代码进行互操作。为了访问给定 .NET 对象实例的数据属性,R# 语言引入了 PowerShell 语言中的 .NET 对象属性引用语法,例如,在 VisualBasic 中有一个类定义:
Class metadata
Public Property name As String
Public Property features As Double()
End Class
然后,我们可以从上面显示的类对象中读取 name 属性值。
# this syntax just works for get property
# set property value is not yet supported.
x = new metadata(name = "My Name", features = [1,2,3,4,5]);
[x]::name;
# if the property value is an array of the
# primitive type in R# language, then it will
# be treated as a atomic vector!
[x]::features + 5;
# [1] 6 7 8 9 10
太神奇了!
R# 语言中的数据可视化
除了创建 R# 语言使我们的 .NET 库可脚本化之外,创建 R# 语言的另一个目的是我们可以以简单的方式检查我们的数据。为了检查我们的 dataset
,我们可以在 R# 语言中使用 str
或 print
函数。更令人兴奋的是,我们可以在 R# 环境中直接绘制数据,以可视化的方式检查数据。
在学习 R# 中的图表绘制之前,我们应该学习如何在 R# 语言中保存图形图像。目前 R# 环境中有两种图形驱动程序:
bitmap
函数用于光栅图像。wmf
函数用于创建窗口元文件图像。svg
函数用于矢量图像。pdf
函数用于将 pdf 文件用作图形画布(目前工作不佳)。
与原始 R 语言一样,我们在任何数据绘图之前都应该创建一个图形设备,然后编写代码来绘制数据。在代码绘制图形后,我们应该使用 dev.off()
函数关闭图形设备驱动程序,并将所有数据刷新到由 bitmap 或 svg 图形驱动程序函数打开的目标文件中。
我们通常可以通过以下 R# 代码模式将图形绘制到给定的图像文件中:
# for vector image, just simply change the bitmap function to svg function
# svg(file = "/path/to/image.svg");
bimap(file = "/path/to/image.png");
# code for charting plot
plot(...);
dev.off();
现在我们已经知道了如何在 R# 语言中创建图像文件,接下来我们将学习如何在 R# 环境中绘制数据。R# 基础环境中已经定义了一些原始的图表绘制功能,您可以直接在 R# 脚本中使用它们,而无需安装任何其他第三方库。例如散点图:
# read scatter point data from a given table file
# and then assign to tuple variables
[x, y, cluster] = read.csv("./scatter.csv", row.names = NULL);
# umap scatter with class colors
bitmap(file = "./scatter.png") {
plot(x, y,
padding = "padding:200px 400px 200px 250px;",
class = cluster,
title = "UMAP 2D Scatter",
x.lab = "dimension 1",
y.lab = "dimension 2",
legend.block = 13,
colorSet = "paper",
grid.fill = "transparent",
size = [2600, 1600]
);
};
在 R# 环境中绘制数据非常简单,是的,我们只需要绘制数据!R# 环境中的原始数据绘制函数使事情变得简单,但不够灵活:如果我们想进行更多的绘图样式调整,我们没有太多的参数来修改我们的绘图。因此,我们引入了一个为 R# 环境编写的图形库:ggplot
包。
R# 的 ggplot
ggplot
包是一个类似于 R 语言 ggplot2
包的图形语法库,用于 R# 语言编程。R# 语言是为 .NET 运行时设计的另一种科学计算语言,R# 是从 R 语言发展而来的。R 语言中有一个著名的图形库叫做 ggplot2
,所以保持一致,R# 语言也开发了一个叫做 ggplot
的图形库。
通过使用 ggplot
包,我们可以以更便捷、更灵活的方式在 .NET 环境中进行数据图表绘制。例如,通过 ggplot
在 R# 中进行统计绘图:
ggplot(myeloma, aes(x = "molecular_group", y = "DEPDC1"))
+ geom_boxplot(width = 0.65)
+ geom_jitter(width = 0.3)
# Add horizontal line at base mean
+ geom_hline(yintercept = mean(myeloma$DEPDC1), linetype="dash",
line.width = 6, color = "red")
+ ggtitle("DEPDC1 ~ molecular_group")
+ ylab("DEPDC1")
+ xlab("")
+ scale_y_continuous(labels = "F0")
# Add global annova p-value
+ stat_compare_means(method = "anova", label.y = 1600)
# Pairwise comparison against all
+ stat_compare_means(label = "p.signif", method = "t.test",
ref.group = ".all.", hide.ns = TRUE)
+ theme(
axis.text.x = element_text(angle = 45),
plot.title = element_text(family = "Cambria Math", size = 16)
)
;
R# 的 ggraph
在 .NET 环境中进行网络图数据可视化并不容易。R# 的 ggplot
包也提供了一个包模块,可用于简单地进行网络图数据可视化,这个包叫做 ggraph
。
如前所述,使用 .NET 环境中的 ggplot
包进行数据可视化非常简单且灵活。我们只需组合 ggraph
和 ggplot
,然后就可以编写优雅的代码进行网络图数据可视化。
ggplot(g, padding = "padding: 50px 300px 50px 50px;")
+ geom_node_convexHull(aes(class = "group"),
alpha = 0,
stroke.width = 0,
spline = 0,
scale = 1.25
)
+ geom_edge_link(color = "black", width = [1,6])
+ geom_node_point(aes(
size = ggraph::map("degree", [12, 50]),
fill = ggraph::map("group", "paper"),
shape = ggraph::map("shape", pathway = "circle", metabolite = "Diamond")
)
)
+ geom_node_text(aes(size = ggraph::map("degree", [4, 9]),
color = "gray"), iteration = -5)
+ layout_springforce(
stiffness = 30000,
repulsion = 100.0,
damping = 0.9,
iterations = 10000,
time_step = 0.0001
)
+ theme(legend.text = element_text(
family = "Bookman Old Style",
size = 4
))
;
ggplot
和 ggraph
R# 包的开发受到了 R 语言 ggplot2
包的启发,因此许多函数用法可以参考 ggplot2
包。这里是 ggplot2
包手册,可能有助于在 R# .NET 环境中使用 ggplot
图表函数。