静态类型元编程?

时间:2011-11-26 04:55:51

标签: python scala f# metaprogramming

我一直在考虑将一些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

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

如果应用程序代码被borked编译器会抱怨,如果变换器代码被borked编译器会抱怨,如果变换器代码编译但是没有做正确的事情,它会崩溃或编译后的步骤会抱怨最后的类型不会加起来。在任何情况下,您都不会通过使用反射来进行动态调度来获得运行时类型错误:它将在每一步都进行静态检查。

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

编辑:一个示例用例是一个完全通用的缓存装饰器。在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

虽然高阶函数可以做其中的一些或者带有函数的对象可以做到这一点,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.函数组合),同时仍然保持类型检查。

5 个答案:

答案 0 :(得分:10)

非常有趣的问题。

关于Scala中元编程的一些观点:

  • 在scala 2.10中,scala reflection

  • 会有所发展
  • 源代码转换(宏)是您需要的工作:scalamacros.org

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

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

答案 1 :(得分:5)

  

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

有很多方法可以在静态类型编程语言中实现相同的效果。

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

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

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

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

答案 2 :(得分:4)

在不知道为什么你这样做的情况下,很难知道这种方法在Scala或F#中是否正确。但是现在忽略这一点,至少可以在Scala中实现,尽管不是在语言层面。

编译器插件允许您访问树,并允许您对该树执行各种操作,所有这些操作都完全受到类型检查。

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

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

答案 3 :(得分:2)

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

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

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

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

根据我们的经验,操纵复杂的语言需要复杂的编译器支持和推理;这是70年来构建编译器的 规范课程。所有上述工具都无法访问符号表和各种流量分析;毕竟,程序的一部分如何运作取决于在远程部分采取的行动,因此信息流是基本的。 [正如另一个答案的评论中所述,你可以使用这些工具实现符号表/流分析;我的观点是他们没有给你这么做的特别支持,这些都是困难的任务,更糟糕的是在具有复杂类型系统和控制流的现代语言上]。

我们的DMS Software Reengineering Toolkit是一个提供上述所有设施(Life After Parsing)的PTS,但需要付出一定代价才能将其配置为您的特定语言或DSL,我们会尝试通过提供这些设施来改善这些设施off-the-shelf for mainstream languages 3}}。 [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提供的任何流动分析机械。

答案 4 :(得分:0)

  

理想情况下,我正在寻找的可以修改函数签名,更改输入参数或输出类型(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结合起来。

  3. 有关尝试改进当前在SparkR中的完成方式,请参阅https://spark-summit.org/east-2015/functionality-and-performance-improvement-of-sparkr-and-its-application/。另请参阅https://github.com/onetapbeyond/renjin-spark-executor以获得简单的集成方法。

    在解决方案方面,我们可以使用Renjin(基于Java的解释器)作为运行时引擎,但使用StrategoXT Metaborg来解析R并生成强类型的Scala API(就像你描述的那样)。

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