StructuredQueries.jl - 一个通用的数据操作框架

2016 年 10 月 3 日 | David Gold

这篇文章描述了我今年夏天在Julia 实验室进行的工作,旨在开发StructuredQueries.jl,这是一个用于Julia的通用数据操作框架。

我们最初对这项工作的设想很大程度上受到Hadley Wickham的dplyr R包的启发,该包提供了对内存中的R表格数据结构和SQL数据库通用的数据操作动词,以及DataFramesMeta(由Tom Short发起),它提供了用于处理Julia DataFrame的元编程工具。

虽然通用的查询接口本身就是一个值得追求的目标(并且在其他地方也讨论过),但它对于解决Julia内存中表格数据结构特有的问题也可能很有用。我们将讨论查询接口如何为Julia表格数据结构开发面临的两个重要问题提供解决方案:列索引可空语义问题。因此,本文将描述我的工作进展,并讨论关于Julia中表格数据结构支持的更广泛的问题。我将为这些问题提供一些背景信息;读者可以随意跳过任何不感兴趣的细节。

回想一下,DataArrays.jl的主要缺点是它不允许类型可推断的索引。也就是说,DataArray中缺失值表示的方式——即使用一个标记NA::NAtype对象——意味着从Base.getindex(df::DataArray{T}, i)中可推断的最具体的返回类型是Union{T, NAtype}。这意味着,在Julia的编译器能够更好地处理小型Union类型之前,天真地索引到DataArray的代码将执行不必要的低效操作。

NullableArrays.jl 解决了这个缺点,方法是将类型T的缺失值和存在值都表示为类型Nullable{T}的对象。但是,此解决方案在其他方面存在局限性。首先,使用NullableArray并不能支持DataFrame的列索引中的类型推断。也就是说,即使DataFrame构建在NullableArray之上,Base.getindex(df::DataFrame, field::Symbol)的返回类型也不是可以直接推断的。将此第一个问题称为列索引问题。其次,NullableArrays引入了一些围绕Nullable类型的困难。将此第二个问题称为可空语义问题

列索引问题在有据可查。要了解困难之处,请考虑以下函数

function f(df::DataFrame)
    A = df[:A]
    x = zero(eltype(A))
    for i in eachindex(A)
        x += A[i]
    end
    return x
end

其中df[:A]df中检索名为:A的列。用户可能会合理地期望上述代码是惯用的Julia:工作是用一个包装在函数中的for循环编写的。但是,这段代码不会(提前)编译成高效的机器指令,因为df[:A]返回的对象的类型在静态分析期间无法推断。这是因为DataFrame类型无法将列的eltype传达给编译器。

可空语义问题在分散的一系列GitHub问题中有所描述(感兴趣的读者可以从这里这里开始)(以及至少一封邮件列表帖子)。据我所知,还没有给出自包含的处理方法(我并不一定声称现在就给出了)。这个问题有两个部分,我分别称之为“简单问题”和“困难问题”

  1. 给定f(x::T)的定义,f(x::Nullable{T})的语义应该是什么?

  2. 我们应该如何以足够通用和用户友好的方式实现这些语义?

在大多数情况下,“简单问题”的答案很清楚:如果x为null,则f(x::Nullable{T})应返回一个空的Nullable{U};如果x不为null,则返回Nullable(f(x.value))。关于如何选择类型参数U存在一个问题,但涉及Julia类型推断工具的解决方案似乎是正确的。(关于0.5风格的推导式以及关于空数组上map的返回类型的一个两个讨论,对这个问题都有影响。)我们将这些语义称为标准提升语义。值得注意的是,至少有一个可观的替代方案可以替代标准提升语义,至少在Nullable{Bool}参数上的二元运算符领域:三值逻辑。但是,是使用三值逻辑还是标准提升语义通常可以从程序的上下文和程序员的意图中明确看出。

另一方面,“困难问题”仍未解决。有许多可能的解决方案,很难知道如何权衡它们的成本和收益。

在描述了当前的查询接口之后,我们将回到列索引问题和可空语义的“困难问题”。在我们深入探讨之前,我想强调一下,这篇博文是一个状态更新,而不是发布公告(尽管StructuredQueries已注册,因此如果您愿意,可以试用它)。StructuredQueries(SQ)是一个正在进行中的工作,并且很可能在一段时间内都将如此。我希望说服读者,SQ仍然代表了Julia表格数据工具开发的一个有趣且有价值的方向。

查询框架

StructuredQueries包提供了一个表示查询结构的框架,而不假设任何特定的对应语义。通过查询的结构,我们指的是调用的一系列特定的操作动词以及传递给这些动词的相应参数。通过查询的语义,我们指的是针对特定数据源执行具有特定结构的查询的实际行为。因此,查询语义既取决于查询的结构,也取决于执行查询的数据源的类型。我们将特定查询语义的实现称为集合机制

将查询结构的表示与集合机制分离有助于使当前的查询框架

这些期望是相互关联的。例如,集合机制的模块化允许后者在支持不同数据后端时重复使用,从而也支持通用性。

在本节中,我们将描述SQ如何表示查询结构。在接下来的章节中,我们将看到SQ的查询表示框架如何为上述列索引和可空语义问题提供解决方案。

要在SQ中表达查询,可以使用@query

@query qry

其中qry是遵循我们将在下文中描述的特定结构的Julia代码。qry根据我们称之为查询上下文的内容进行解析。通过上下文,我们指的是Julia代码的一般语义,它可能与标准Julia环境的语义不同。也就是说:虽然qry必须是有效的Julia语法,但代码不会像在@query宏之外执行那样运行。相反,出现在查询上下文中的代码(如qry)在运行之前会经过一系列转换。@query使用这些转换来生成qry结构的图形表示。@query qry调用返回一个Query对象,该对象包装了作为处理qry的结果生成的查询图。

我们上面说过,SQ根据其结构表示查询,但本身并不保证任何特定的语义。这允许包为给定的查询结构实现自己的语义。为了演示这种设计,我组合了(i)一个抽象表格数据类型AbstractTable;(ii)一个接口,用于支持针对我称之为列可索引类型T <: AbstractTable的集合机制;以及(iii)一个具体的表格数据类型Table <: AbstractTable,它满足列可索引接口,因此继承了一个集合机制来支持SQ查询。

以下行为模仿了人们期望从针对DataFrame的查询中获得的行为。将演示与Table一起使用而不是与DataFrame一起使用,其主要原因是易于实验。我可以比修改DataFrame类型和接口更容易地修改AbstractTable/Table类型和接口。事实上,这个项目已经变得与设计一个与Julia查询框架最兼容的内存中Julia表格数据类型一样重要,因为它与设计一个与内存中Julia表格数据类型兼容的查询框架一样重要。幸运的是,一旦我们确定此类支持应该位于何处,将Table的后端支持的实现移植到DataFrame的支持将非常简单。

让我们通过考虑使用鸢尾花数据集的示例来深入了解查询接口。(尽管TablesDemo.jl包仅用于演示,但它已注册,因此读者可以使用Pkg.add("TablesDemo.jl")轻松安装它并继续学习。)

julia> iris = Table(CSV.Source(joinpath(Pkg.dir("Tables"), "csv/iris.csv")))
Tables.Table
│ Row │ sepal_length │ sepal_width │ petal_length │ petal_width │ species  │
├─────┼──────────────┼─────────────┼──────────────┼─────────────┼──────────┤
│ 15.13.51.40.2"setosa" │
│ 24.93.01.40.2"setosa" │
│ 34.73.21.30.2"setosa" │
│ 44.63.11.50.2"setosa" │
│ 55.03.61.40.2"setosa" │
│ 65.43.91.70.4"setosa" │
│ 74.63.41.40.3"setosa" │
│ 85.03.41.50.2"setosa" │
│ 94.42.91.40.2"setosa" │
│ 104.93.11.50.1"setosa" │
⋮
with 140 more rows.

然后,我们可以使用@query来表达针对此数据集的查询——例如,根据sepal_length上的条件过滤行

julia> q = @query filter(iris, sepal_length > 5.0)
Query with Tables.Table source

这将生成一个Query{S}对象,其中S是数据源的类型

julia> typeof(q)
StructuredQueries.Query{Tables.Table}

传递给@query的查询的结构由一个操作动词(例如filter)组成,该动词依次将一个数据源(例如iris)作为其第一个参数,以及任意数量的查询参数(例如sepal_length > 5.0)作为其后续参数。这三个是查询的不同“部分”:(1)数据源(或简称“源”),(2)操作动词(或简称“动词”)和(3)查询参数。

查询的每个部分都会在其自己的上下文中评估代码。这些上下文中最重要的方面是名称解析。也就是说,名称的解析方式取决于它们出现在查询的哪个部分以及以何种身份出现

  1. 在数据源规范上下文中——例如,作为上面filter等动词的第一个参数——名称在@query调用的封闭作用域中进行评估。因此,上面用于定义q的查询中的iris精确地指代Main的顶层中名称绑定的Table对象。

  2. 操作动词的名称不会解析为对象,而只是表示如何构建查询的图形表示。(事实上,在接下来的内容中,在涉及filter子句的查询的执行中,永远不会调用这样的函数filter。)

  3. 在查询参数上下文中调用的函数的名称,例如sepal_length > 5.0中的>,在@query调用的封闭作用域中进行评估。

  4. 在查询参数上下文中作为函数调用参数出现的名称,例如sepal_length > 5.0中的sepal_length,不会解析为对象,而是被解析为数据源的“属性”(在本例中为iris)。当数据源是表格数据结构时,这些属性被视为列名,但这种行为只是特定查询语义的一个特性(见下文“路线图和开放性问题”部分)。作为给定函数调用参数传递的属性作为数据存储在图形查询表示中。

可以在@query上下文中将参数传递给动词。例如,上面的Query等价于以下代码生成的:

@query iris |> filter(sepal_length > 5.0)

在这种情况下,动词filter的第一个参数(sepal_length > 5.0)不是数据源规范(iris)(iris|>的第一个参数),而是一个查询参数(sepal_length > 5.0)。

Query对象表示由上述三个构建块组成的查询结构。要了解它是如何做到的,让我们看一下Query的内部结构。

julia> fieldnames(q)
2-element Array{Symbol,1}:
 :source
 :graph

第一个字段:source仅包含查询中指定的数据源——在本例中,是当指定查询时绑定到名称irisTable对象。第二个字段:graph包含查询结构的一个(不可否认,不太有趣的)图形表示。

julia> q.graph
FilterNode
  arguments:
      1)  sepal_length > 5.0
  inputs:
      1)  DataNode
            source:  unset source

传递给@query的原始qry表达式中的filter动词在图中由FilterNode对象表示,数据源由DataNode对象表示。FilterNodeDataNode都是抽象QueryNode类型的叶子子类型。FilterNode通过前者的:input字段连接到DataNode。通常,这些连接构成有向无环图。我们可以将此类图称为QueryNode图或查询图。

SQ目前开箱即用地识别以下动词——也就是说,它将它们正确地合并到QueryNode图中。

使用collect(q::Query)q具体化为一个具体的、结果集——因此称为“收集机制”。请注意,目前仅包括前四个动词支持可按列索引的接口——也就是说,可以针对可按列索引的数据源进行collect的动词:selectfiltergroupbysummarize。这就是这种支持目前的样子。

julia> q = @query iris |>
           filter(sepal_length > 5.0) |>
           groupby(species, log(petal_length) > .5) |>
           summarize(avg = mean(digamma(petal_width)))
Query with Tables.Table source

julia> q.graph
SummarizeNode
  arguments:
      1)  avg=mean(digamma(petal_width))
  inputs:
      1)  GroupbyNode
            arguments:
                1)  species
                2)  log(petal_length) > 0.5
            inputs:
                1)  FilterNode
                      arguments:
                          1)  sepal_length > 5.0
                      inputs:
                          1)  DataNode
                                source:  unset source


julia> collect(q)
Grouped Tables.Table
Groupings by:
    species
    log(petal_length) > 0.5 (with alias :pred_1)

Source: Tables.Table
│ Row │ species      │ pred_1 │ avg       │
├─────┼──────────────┼────────┼───────────┤
│ 1"virginica"true0.428644  │
│ 2"setosa"true   │ -3.17557  │
│ 3"versicolor"true   │ -0.136551 │
│ 4"setosa"false  │ -4.7391

我们希望在不久的将来支持其他动词。

再次强调,此收集机制由AbstractTables包提供,而不是StructuredQueries。如上所述,后者提供了一个表示查询结构的框架,而像AbstractTables这样的包(i)决定了针对特定后端执行具有特定结构的查询的含义,以及(ii)提供了(i)中行为的实现。

我们提供了一个便捷的宏@collect(qry),它等价于collect(@query(qry)),用于当希望在同一个命令中查询和收集时。

julia> @collect iris |>
           filter(erf(petal_length) / petal_length > log(sepal_width) / 1.5) |>
           summarize(sum = sum(ifelse(rand() > .5, sin(petal_width), 0.0)))
Tables.Table
│ Row │ sum       │
├─────┼───────────┤
│ 10.0998334

同样,请注意名称解析的模式:在查询参数上下文中调用的函数名称(例如erf)在@query调用的封闭作用域内进行评估,而此类函数的参数中的名称(例如petal_length)被视为数据源的属性(即iris)。

虚拟源

我们在上面看到了查询结构的三个部分:动词、源和查询参数。Query对象在QueryNode图中一起表示动词和查询参数,并分别包装数据源。这表明即使没有指定特定数据源,也应该能够使用@query生成查询图。可以通过使用虚拟源来精确地做到这一点,虚拟源本质上是占位符,稍后可以在调用collect时用特定数据源“填充”。要将源指示为虚拟源,只需在其前面加上:即可。例如

julia> q = @query select(:src, twice_sepal_length = 2 * sepal_length)
Query with dummy source src

julia> collect(q, src = iris)
Tables.Table
│ Row │ twice_sepal_length │
├─────┼────────────────────┤
│ 110.2               │
│ 29.8                │
│ 39.4                │
│ 49.2                │
│ 510.0               │
│ 610.8               │
│ 79.2                │
│ 810.0               │
│ 98.8                │
│ 109.8                │
⋮
with 140 more rows.

查询中虚拟源的名称(减去:)必须是传递给collect的关键字参数中的键。否则,该方法将失败。

julia> collect(q, tbl = iris)
ERROR: ArgumentError: Undefined source: tbl. Check spelling in query.
 in #collect#5(::Array{Any,1}, ::Function, ::StructuredQueries.Query{Symbol}) at /Users/David/.julia/v0.6/StructuredQueries/src/query/collect.jl:23
 in (::Base.#kw##collect)(::Array{Any,1}, ::Base.#collect, ::StructuredQueries.Query{Symbol}) at ./<missing>:0

这两个问题

现在我们已经了解了SQ查询框架本身包含的内容,我们可以讨论这样的框架如何帮助解决列索引和可空语义问题。

类型可推断性

回想一下,列索引问题在于类型推断无法检测到以下内容的返回类型:

function f(df::DataFrame)
    A = df[:A]
    x = zero(eltype(A))
    for i in eachindex(A)
        x += A[i]
    end
    return x
end

使上面的f易于类型推断的是将上面的A = df[:A]传递给执行循环的内部函数,例如

f_inner(A)
    x = zero(eltype(A))
    for i in 1:length(A)
        x += A[i]
    end
    return x
end

只要f_inner没有内联,类型推断将在f的主体调用f_inner的点“运行”,并且可以访问df[:A]eltype,因为后者作为参数传递给f_inner

当需要多列时,这种引入函数屏障的策略也适用。例如,假设我想生成一个新列C,其中C[i] = g(A[i], B[i])。以下解决方案是可进行类型推断的,因为压缩迭代器zip(A, B)的类型参数反映了ABeltype

function f(g, df)
    A, B = df[:A], df[:B]
    C = similar(A)
    f_inner!(C, g, zip(A, B))
    return DataFrame(C = C)
end

function f_inner!(C, g, itr) # bang because mutates C
    for (a, b) in itr
        C[i] = g(a, b)
    end
    return C
end

换句话说:如果打算遍历DataFrame某些列子集的行,则在某些时候必须存在一个函数屏障,通过该屏障传递一个参数,其签名反映相关列的eltype

上述操作可以针对可按列索引的表(例如Table对象)表示为

@query select(tbl, C = A * B)

支持针对例如Table源执行此查询的收集机制本质上遵循上面ff_inner的模式。也就是说,外部函数传递一个“标量内核”(此处为row -> row[1] * row[2]),该内核反映了A * B的结构,以及一个“行迭代器”(此处为zip(tbl[:A], tbl[:B]))给一个内部函数,该函数计算应用于迭代行迭代器返回的“行”的标量内核的值。(请注意,假定标量内核的参数是一个Tuple,其各个元素在“值表达式”(此处为A * B)的主体中假设命名属性(如AB)的位置,标量内核由此生成)。

标量内核以及有关从tblzip中提取哪一列的信息都存储在@query生成的QueryNode图中。生成此类图的大部分工作包括从qry表达式(此处为select(tbl, C = A * B))中提取此类信息并对其进行处理以生成(i)捕获转换形式(A * B)的lambda,(ii)命名结果列的SymbolC)和一个Vector{Symbol},该向量按在生成lambda期间遇到的顺序列出相关参数列名([:A, :B])。

请注意,这些数据(标量内核以及结果和参数字段)对于从原始查询参数(例如Expr对象:( C = A * B ))生成SQL代码不是必需的。因此,有人可能会争辩说,当可能能够在运行时调度collect时,计算此类数据并将其存储在QueryNode图中有点浪费,在Query{S}上,其中S是满足可按列索引接口的类型。这是一个很好的观点,但需要考虑两个因素。第一个是计算标量内核并从查询参数中提取结果和参数字段可能不会过分昂贵。第二个是运行时生成标量内核(i)涉及使用eval,应避免使用,并且(ii)可能需要大量工作才能将出现在要eval的表达式中的名称的模块信息重新合并到标量内核中。目前,最简单的方法是在宏扩展时生成标量内核,并让它们在QueryNode图中一起使用,即使后者要针对不需要此类数据的数据源(例如SQL连接)进行收集。

使用元编程来规避类型可推断性并不是一种新的策略。事实上,它是DataFramesMeta操作框架的基础。感兴趣的读者可以参考此处此处以了解更多关于这些努力的历史和动机。

可空语义的难题

回想一下,可空语义的难题涉及以“通用”方式实现给定的提升语义——也就是说,给定已定义的方法f(x::T)时,给定f(x::Nullable{T})的给定行为。

一个解决方案——也许是最明显的,并且我之前认可过——涉及将方法f(x::Nullable{T})定义为类似以下内容:

function f(x::Nullable{T})
    if isnull(x)
        return Nullable{U}()
    else
        return Nullable(f(x.value))
    end
end

对于具有n元参数的方法,有自然的类似物。此过程有点繁琐,但使用宏对其进行自动化并不困难,可以使用该宏对原始定义f(x::T)进行注释。将此方法称为“方法扩展提升”方法。

方法扩展提升方法非常灵活。但是,它确实面临一些困难。必须以某种方式决定哪些函数应该以这种方式提升,并且不清楚应该如何划定这条线(提升函数和未提升函数之间的线)。如果无法编辑函数的定义,则宏毫无用处;必须手动引入提升的变体。

还有一个问题。如果希望支持对具有“混合”签名的参数进行提升——即某些参数类型为Nullable而某些参数类型不为Nullable的签名——则必须扩展提升机制或为混合签名定义方法,例如+{T}(x, y::Nullable{T})。这最终可能导致大量方法。即使它们的定义可以通过元编程自动化,与方法扩散相关的编译成本也可能相当大(但我还没有测试过这一点)。

最后,存在NullableArrays.jl#148中描述的问题。我不会在这里重复整个论点。此问题的总结是:如果要依赖一组最少的提升运算符来支持用户定义函数的通用提升,则这些用户定义函数本质上必须放弃大部分多重调度。

与方法扩展提升相关的困难并非不可克服,但解决方案——即保留提升方法的存储库——需要不确定的维护和协调量。

实现标准提升语义的另一种方法是通过高阶函数——也就是说,在 Julia 0.5 中,高阶函数的性能很好。这样的函数——称之为lift——可能如下所示

function lift(f, x)
    if hasvalue(x)
        return Nullable(f(x))
    else
        U = Core.Inference.return_type(f, (typeof(x),))
        return Nullable{U}()
    end
end

这种定义可以自然地扩展到具有多个参数的方法。与方法扩展提升相比,这种方法的主要优势在于其通用性:只需要定义一个(两个、三个)高阶lift方法来支持所有一个(两个、n)参数函数的提升,而不是为每个这样的函数定义一个提升版本。请注意,只要hasvalue对非Nullable参数有一些通用的回退方法,这样的lift函数就可以涵盖标准和混合签名提升。(理想情况下,应确保代码针对类型为非Nullable的情况进行了优化;特别是,应确保删除了死分支——参见julia#18484。)我们将这种方法称为“高阶提升”方法。

因此,使用高阶提升方法,我们可以更好地避免方法激增和通用性问题,这很好。但是,现在我们要求用户在任何地方都调用lift。特别是,要将f(g(x))提升到Nullable参数x上,需要编写lift(f, lift(g, x))。在这种情况下,我们至少可以提供一个@lift宏,例如,遍历f(g(x))的AST,并将每个函数调用f(...)替换为lift(f, ...)的调用。这可能是合理的,但它仍然是支持缺失值的实现细节的人工产物,理想情况下,它不应该暴露给用户。

回想一下,当前的查询框架会提取查询参数的“值表达式”(例如,查询参数C = A * B中的B * C),并生成一个模拟前者结构的lambda(在这种情况下,row -> row[1] * row[2])。对该过程的一个提议修改(参见AbstractTables#2)是对值表达式(A * B)的AST进行修改,通过适当插入对lift的调用,例如

row -> lift(*, row[1], row[2])

虽然有一种更简单的方法可以实现标准提升语义,但这种方法(目前由列索引集合机制采用)不容易支持非标准提升语义,例如三值逻辑。

高阶提升方法并非没有其自身的缺点。最值得注意的是,非标准提升语义,例如三值逻辑,更难以实现,并且受到不适用于方法扩展提升方法的限制。这种困难的细节是另一篇博文的主题。问题的总结是:高阶提升(通过代码转换,例如在@query内)只能为在传递给@query的表达式中显式调用的方法提供非标准提升语义。也就是说,

@query filter(tbl, A | B)

可以通过高阶提升赋予,例如,三值逻辑语义,但

f(x, y) = x | y
@query filter(tbl, f(A, B))

不能。

哪种解决Nullable语义难题的方法更好?真的不清楚。目前,Julia统计社区正在尝试这两种解决方案。我希望时间和实验能够带来新的见解。

SQL 后端

上面我们已经看到(i)通用查询接口的实现如何为列索引和Nullable语义问题提供了一种解决方案,以及(ii)这些后来的解决方案如何以对所谓的可列索引内存中 Julia 表格数据结构通用的方式实现。但我们还没有说明该接口如何对除了内存中 Julia 对象之外的其他表格通用。特别是,我们希望上述框架也适用于 SQL 数据库连接。

Yeesian Ng在 SQ 的开发过程中提供了宝贵的反馈和想法,他还开始在一个名为SQLQuery的包中开发这样的扩展。我们正在努力将其与SQLQuery.jl#2中的结构化查询进一步集成,我们鼓励读者关注此项工作的更新。

路线图和未解决的问题

structuredQueries.jl#19中提供了一个通用路线图。我将简要描述一些我认为最紧迫/有趣的问题。

插值语法和实现都是重大的开放性问题。假设我想引用@query调用封闭作用域中的名称。一种简单的语法是在插值变量前加上$,如下所示

c = .5
q = @query filter(tbl, A > $c)

应该如何实现这一点?为了实现完全的通用性,我们希望能够从封闭作用域中“捕获”c并将其存储在q中。一种方法是将c包含在存储在q中的lambda () -> c的闭包中。但是,存在如何处理类型可推断性问题的问题。解决此问题可能需要或强烈建议某种“参数化查询”API,通过该 API,可以在查询参数上下文中将名称指定为一个参数,然后在@query调用后绑定该参数,例如,指定为collect的关键字参数或类似于bind!(q::Query[; kwargs...])的函数。

我们仍在决定查询上下文中的通用语法应该是什么样子。这个决定很大一部分涉及别名和相关功能应该如何工作。有关更多详细信息,请参见StructuredQueries.jl#21。此问题类似于插值语法问题,因为两者都涉及在不同查询上下文中的名称解析(例如,在数据源规范上下文中与在查询参数上下文中)。

最后,不仅collect而且图生成工具的可扩展性也是一个重要问题,我们希望在以后的文章中对此进行更多讨论。

如上所述,DataFramesMeta是通过元编程增强 Julia 中表格数据支持的一种开创性方法。在通用数据操作工具支持领域,另一个令人兴奋的(并且比目前讨论的包稍微成熟一些)的尝试是Query.jl,由David Anthoff开发。Query.jl 和 SQ 在目标方面非常相似,但在重要方面有所不同。对这些包的比较是另一篇博文的主题。

结论

以上文章描述了一项正在进行的工作。不仅是 StructuredQueries 包,还有 Julia 统计生态系统。虽然这个生态系统可能需要一段时间才能成熟,但我过去两年观察到的总体趋势令人鼓舞。还值得注意的是,上面描述的大部分内容如果没有 Julia 语言的发展将难以想象。特别是,高性能高阶函数和类型可推断映射都使我们能够探索以前由于确保类型可推断性所需的元编程量而变得困难的解决方案。看看在 Julia 0.6 及更高版本中改进后,我们还能想出什么,这将很有趣。

我非常感谢 John Myles White 对该项目的指导,感谢麻省理工学院的 Yeesian Ng 的合作,感谢 Viral Shah 和 Alan Edelman 安排这次机会,并感谢 Julia Central 及其他地方的许多其他人提供的帮助和见解。