65.9K
CodeProject 正在变化。 阅读更多。
Home

R# 语言介绍

starIconstarIconstarIconstarIconstarIcon

5.00/5 (6投票s)

2022 年 8 月 3 日

CPOL

15分钟阅读

viewsIcon

8712

downloadIcon

63

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# 解释器的设计

它是如何工作的?

R# 语言目前是一种解释型编程语言,其解释器包含四个模块:

  • Interpreter:包含 R# 语言解释器的源代码,以及所有表达式类模型的定义。
  • Language:包含从输入脚本文本中解析语言标记所需的代码,以及用于根据语言标记序列和语言上下文创建相应 R# 表达式对象的语法解析器。
  • Runtime:包含导入外部 .NET 函数所需的代码以及运行 R# 表达式评估的运行时环境定义。此文件夹还包含一些用于操作数据集的原始 R# 函数,例如 lapply、sapply、list、which 等。
  • System:包含运行时配置、第三方包加载器以及用于构建自己的 R# 包的工具的代码。

通过结合这 4 个模块中的代码,我们可以创建一个工作流程来运行 R# 脚本,将 R# 脚本与我们 .NET 库中已有的函数进行互操作,并评估 R# 表达式以生成 .NET 对象。

工作流程:运行 R# 代码

下面是一个工作流程图,用于说明如何运行 R# 代码输入。

  1. R# 环境初始化:在 R# 系统初始化的最开始,将调用 R# 系统的代码模块来
    1. 加载配置文件,
    2. 初始化全局环境,
    3. 挂接 R# 基础包中所有的 .NET API 函数,
    4. 然后加载启动包并初始化运行时环境。

    最后,R# 就准备好运行我们的脚本代码了。

  2. 输入的脚本文本随后将由语言命名空间中定义的扫描器对象解析为 R# 语言标记。语言标记序列是扫描器通过其 char 遍历操作输出的。生成标记序列中的语言标记顺序是 R# 解释器中语法分析模块用于创建语法树的语法上下文信息。在根据标记序列构建出语法树模型后,脚本文本就被解析为 R# 程序:一组表达式模型。
  3. 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# 语言中的 sapplylapply 函数是一种类似 LINQ 的函数,可用于处理复杂数据集合。

  • sapply 表示序列应用,它等同于 LINQ 中的 Select 函数。sapply 函数接受 R# 语言中的集合数据,然后生成新的向量数据。
  • lapply 表示列表应用,它等同于 LINQ 中的 ToDictionary 函数。lapply 函数的工作方式与 sapply 函数类似,接受 R# 语言中的集合数据,但生成一个新的键值对列表数据。

以下是一个关于在 R# 语言中使用 sapplylapply 函数以及相应的 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 的 CharString 在 R# 运行时被统一为字符,而 Char 是一种特殊的 string:其 nchar 值等于 1
logi 布尔值 除了 TRUEFALSE 之外,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# 语言中使用 strprint 函数。更令人兴奋的是,我们可以在 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 包进行数据可视化非常简单且灵活。我们只需组合 ggraphggplot,然后就可以编写优雅的代码进行网络图数据可视化。

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
))
;

ggplotggraph R# 包的开发受到了 R 语言 ggplot2 包的启发,因此许多函数用法可以参考 ggplot2 包。这里是 ggplot2 包手册,可能有助于在 R# .NET 环境中使用 ggplot 图表函数。

© . All rights reserved.