【问题标题】:Statically Typed Metaprogramming?静态类型元编程?
【发布时间】:2012-01-06 18:19:42
【问题描述】:

我一直在考虑将一些 Python 代码移植到 F# 或 Scala 等静态类型语言时会错过什么;库可以替换,简洁性相当,但我有很多python代码如下:

@specialclass
class Thing(object):
    @specialFunc
    def method1(arg1, arg2):
        ...
    @specialFunc
    def method2(arg3, arg4, arg5):
        ...

装饰器做了大量工作的地方:用带有状态的可调用对象替换方法,用额外的数据和属性扩充类,等等。虽然 Python 允许任何人随时随地进行动态猴子补丁元编程,但我发现基本上我所有的元编程都是在程序的一个单独的“阶段”中完成的。即:

load/compile .py files
transform using decorators
// maybe transform a few more times using decorators
execute code // no more transformations!

这些阶段基本上是完全不同的;我不在装饰器中运行任何应用程序级代码,也不在主应用程序代码中执行任何 ninja replace-class-with-other-class 或 replace-function-with-other-function。尽管该语言的“动态”特性表明我可以在任何我想要的地方这样做,但我从不在主应用程序代码中替换函数或重新定义类,因为它很快就会变得疯狂。

本质上,我是在开始运行代码之前对代码执行一次重新编译。

我所知道的静态类型语言中唯一类似的元编程是反射:即从字符串中获取函数/类,使用参数数组调用方法等。但是,这基本上将静态类型语言转换为动态类型语言,失去了所有类型安全(如果我错了,请纠正我?)。理想情况下,我认为,我会有以下内容:

load/parse application files 
load/compile transformer
transform application files using transformer
compile
execute code

本质上,您将使用普通编译器编译的任意代码来扩充编译过程,这将对主应用程序代码执行转换。关键是它本质上模拟了“加载、转换、执行”工作流程,同时严格保持类型安全。

如果应用程序代码出错,编译器会报错,如果转换器代码出错,编译器会报错,如果转换器代码编译但没有做正确的事情,要么会崩溃,要么编译后的步骤会报错最终类型不会相加。在任何情况下,您都不会通过使用反射进行动态调度来获得可能的运行时类型错误:它会在每一步都被静态检查。

所以我的问题是,这可能吗?它是否已经用我不知道的某种语言或框架完成了?理论上不可能吗?我对编译器或形式语言理论不是很熟悉,我知道它会使编译步骤变得完整并且不能保证终止,但在我看来,这是我需要匹配那种方便的代码 -我在保持静态类型检查的同时使用动态语言进行转换。

编辑:一个示例用例是一个完全通用的缓存装饰器。在 python 中它会是:

cacheDict = {}
def cache(func):
    @functools.wraps(func)
    def wrapped(*args, **kwargs):
        cachekey = hash((args, kwargs))
        if cachekey not in cacheDict.keys():
            cacheDict[cachekey] = func(*args, **kwargs)
        return cacheDict[cachekey]
    return wrapped


@cache
def expensivepurefunction(arg1, arg2):
    # do stuff
    return result

虽然高阶函数可以做一些这样的事情,或者objects-with-functions-inside 可以做一些这样的事情,AFAIK 它们不能被概括为与任何函数一起使用任意参数集并在保持类型的同时返回任意类型安全。我可以这样做:

public Thingy wrap(Object O){ //this probably won't compile, but you get the idea
    return (params Object[] args) => {
        //check cache
        return InvokeWithReflection(O, args)
    }
}

但是所有的转换都完全扼杀了类型安全。

编辑:这是一个简单的例子,其中函数签名没有改变。理想情况下,我正在寻找可以修改函数签名、更改输入参数或输出类型(a.l.a. 函数组合),同时仍保持类型检查。

【问题讨论】:

  • 有一个建议在 Scala 中添加宏,宏将解决你的大部分问题。 (scalamacros.org)
  • 目前还不是很清楚你想要实现什么样的代码转换;你能举几个具体的例子吗?另一方面,您可能对Template Haskell 感兴趣。
  • @DanBurton:我用一个例子更新了这个问题。我去看看Haskell模板!
  • 又是一个多阶段编程的例子。混合这个 + 强类型 + 一个集成这个概念的好编辑器会很棒..

标签: python scala f# metaprogramming


【解决方案1】:

非常有趣的问题。

关于 Scala 元编程的一些要点:

  • 在 scala 2.10 中将有 scala reflection 的发展

  • 在源到源转换(宏)中有工作,这是您正在寻找的东西:scalamacros.org

  • Java 有自省(通过反射 api)但不允许自我修改。但是,您可以使用工具来支持这一点(例如javassist)。从理论上讲,您可以在 Scala 中使用这些工具来实现内省之外的更多功能。

  • 根据我对您的开发过程的了解,您将域代码与装饰器(或交叉关注点,如果您愿意的话)分开,这样可以实现模块化和代码简单性。这可以很好地用于面向方面的编程,它允许这样做。对于 Java,有一个库 (aspectJ),但我怀疑它是否会与 Scala 一起运行。

【讨论】:

  • 听起来确实像 OP 正在做 AOP,基于他描述 python 装饰器用例的方式。我知道有用于 Java 和 .NET 的 AOP 工具,但如果有更多支持 AOP 的语言,我也不会感到惊讶。 .NET AOP 工具拦截编译阶段,对源代码进行必要的更改,然后让编译阶段运行 - 模仿 OP 讨论的确切流程。
【解决方案2】:

不知道为什么你这样做,很难知道这种方法在 Scala 还是 F# 中是正确的。但是暂时忽略这一点,至少在 Scala 中肯定可以实现,尽管不是在语言级别。

编译器插件可让您访问树并允许您对该树执行各种操作,所有操作均经过完全类型检查。

有一些 issues 在 Scala 编译器插件中生成合成方法 - 我很难知道这是否会给您带来问题。

可以通过创建一个编译器插件来解决这个问题,该插件生成源代码,然后在单独的通道中编译。例如,ScalaMock 就是这样工作的。

【讨论】:

    【解决方案3】:

    所以我的问题是,这可能吗?

    在静态类型的编程语言中有很多方法可以达到同样的效果。

    您基本上已经描述了在执行程序之前对程序进行一些术语重写的过程。这种功能可能以 Lisp 宏的形式最为人所知,但一些静态类型语言也有宏系统,最著名的是 OCaml 的可用于扩展语言的 camlp4 宏系统。

    更一般地说,您正在描述一种形式的语言可扩展性。有许多替代方案,不同的语言提供不同的技术。有关更多信息,请参阅我的博客文章 Extensibility in Functional Programming。请注意,这些语言中的许多都是研究项目,因此其动机是添加新功能而不一定是好的功能,因此它们很少改进其他地方发明的好的功能。

    包括标准 ML、OCaml 和 F# 在内的 ML(元语言)系列语言专为元编程而设计。因此,它们倾向于对词法分析、解析、重写、解释和编译提供出色的支持。然而,F# 是这个家族中最遥远的成员,缺乏像 OCaml 这样的语言可以从中受益的成熟工具(例如,camlp4、ocamllex、dypgen、menhir 等)。 F# 确实有 fslex、fsyacc 和一个名为 FParsec 的受 Haskell 启发的解析器组合库的部分实现。

    您可能会发现,使用更传统的元编程形式(尤其是 DSL 或 EDSL)可以更好地解决您面临的问题(您尚未描述)。

    【讨论】:

      【解决方案4】:

      您可能对source-to-source program transformation systems (PTS) 感兴趣。

      此类工具解析源代码,生成 AST,然后允许对代码进行任意分析和/或转换,最终从修改后的 AST 重新生成源代码。

      一些工具通过程序界面提供解析、树构建和 AST 导航,例如ANTLR。许多更现代的动态语言(Python、Scala 等)已经构建了一些自托管解析器库,甚至 Java(编译器插件)和 C#(开放编译器)也正在追赶这个想法。

      但大多数这些工具只提供对 AST 的程序访问。具有表面语法重写的系统允许您使用具有被操作语言的语法的模式来表达“如果您看到 this 将其更改为 that”。其中包括Stratego/XTTXL

      根据我们的经验,操作复杂的语言需要复杂的编译器支持和推理;这是 70 年来人们构建编译器的经典课程。上述所有工具都无法访问符号表和各种流分析;毕竟,程序的某一部分如何运作,取决于远程部分采取的行动,因此信息流是基础。 [正如 cmets 在另一个答案中所述,您可以使用这些工具实现符号表/流分析;我的观点是,它们没有为此提供特别支持,而且这些任务很艰巨,对于具有复杂类型系统和控制流的现代语言来说更糟]。

      我们的DMS Software Reengineering Toolkit 是一个提供上述所有设施 (Life After Parsing) 的 PTS,但需要付出一定成本才能将其配置为您的特定语言或 DSL,我们试图通过提供这些 off-the-shelf for mainstream languages 来改善这一点。 [DMS 为构建/管理符号表、控制和数据流提供了明确的基础设施;这已用于为 Java 1.8 和完整的 C++14 实现这些机制]。

      DMS 还被用于定义meta-AOP,这些工具使人们能够为任意语言构建 AOP 系统并应用类似于 AOP 的操作。

      在任何情况下,只要您直接或间接地修改 AST,您就无法保证“类型安全”。您只能通过编写不会破坏它的transformation rules 来获得它。为此,您需要一个定理证明器来检查每个修改(或此类修改的组合)是否没有破坏类型安全,这几乎超出了现有技术。但是,您可以谨慎编写规则,并获得非常有用的系统。

      您可以在使用 DMS 的 example that defines and manipulates algebra and calculus 中看到一个 DSL 规范和使用表面语法源到源重写规则进行操作的示例,该规则保留了语义。我注意到这个例子很容易理解。特别是,它没有展示任何 DMS 提供的流动分析机制。

      【讨论】:

        【解决方案5】:

        理想情况下,我正在寻找可以修改函数签名、更改输入参数或输出类型(a.l.a. 函数组合),同时仍保持类型检查。

        我同样需要在类型安全的世界中提供 R API。通过这种方式,我们可以将来自 R 的大量科学代码带入 Scala 的(类型)安全世界。

        基本原理

        1. 使通过 Specs2 记录 API 的业务领域方面成为可能(参见 https://etorreborre.github.io/specs2/guide/SPECS2-3.0/org.specs2.guide.UserGuide.html;由 Scala 代码生成)。想想反向应用的领域驱动设计。

        2. 采用面向语言的方法来应对 SparkR 面临的挑战,尝试将 Spark 与 R 相结合。

        请参阅https://spark-summit.org/east-2015/functionality-and-performance-improvement-of-sparkr-and-its-application/ 以尝试改进目前在 SparkR 中的工作方式。另请参阅 https://github.com/onetapbeyond/renjin-spark-executor 了解简单的集成方式。

        在解决这个问题方面,我们可以使用 Renjin(基于 Java 的解释器)作为运行时引擎,但使用 StrategoXT Metaborg 解析 R 并生成强类型的 Scala API(如您所描述的)。

        StrategoTX (http://www.metaborg.org/en/latest/) 是我所知道的最强大的 DSL 开发平台。允许使用允许组合语言的解析技术组合/嵌入语言(更长的故事)。

        【讨论】:

        • 艾拉,我再读一遍,如果我仍然读,我会后悔两次。急于将应用创意摆在桌面上。但我确实了解语义与语法,并且对语义设计有一个很好的想法,对您的工作深表敬意。 Stratego XT 是一个成熟的程序转换系统,因此也可以检查语义。将它与您的工具进行比较会很棒。如果所有工具都是开源的,那就更是如此。 :-)
        • Stratego 是开源的;它没有 DMS 的能力。我们认为这是因为 Stratego 是出于(良好的)学术原因而构建的,但没有资源(例如,资金)来以尽可能具有商业效益的方式开发它。 DMS 是由商业应用程序产生的资金建立的。 (参见semanticdesigns.com/SuccessStories/index.html,尤其是关于陶氏化学的部分)我相信,如果 DMS​​ 是开源的,它就不会像现在这样有效。 YMMV,但我已经打赌了。
        • 关于战略:“语义也可以检查”。是的,理论上是可以的; Stratego 是一个广义的 Post 系统 == 图灵机,可以说可以计算任何东西。这与您想要使用 Post 系统/图灵机计算任何东西不同。当您向我展示使用 Stratego 计算 C++14 的准确数据流时,我会更加注意。 (DMS 确实如此)。
        • Stratego 一直是一个令人印象深刻的工具,它展示了纯重写和干净集成的可能性。是的,我对编写语法(继承自 SDF)的能力印象深刻。是的,我知道您可以让 Stratego 计算任何东西。我已经看到了让 Stratego 通过重写来计算程序事实的各种方法。尚不清楚这些方法是否可以扩展,而经典的编译器算法却可以。我知道 Dryad,但这个参考资料并没有提供任何有用的信息。这实际上用于 Java 1.8 吗? (我在 Dryad 上找到了技术论文)
        • DMS 在 60 秒内对 500K SLOC Java 源代码系统进行名称解析。我很悲观,通过以这种规模重写树来进行名称解析会做得很好。与迭代流分析器相反,这主要是一个线性时间过程。
        猜你喜欢
        • 2012-01-13
        • 2011-02-09
        • 1970-01-01
        • 1970-01-01
        • 2015-04-03
        • 1970-01-01
        • 2014-06-26
        • 2010-12-02
        • 1970-01-01
        相关资源
        最近更新 更多