如何定义模型参量?

背景

有一个文件model_skeleton.jl装模型外围架构modelSkeleton,一个文件model_content.jl装模型核心内容modelContent。

需求

要使用模型生成器modelBuilder把模型核心内容加载进模型外围架构里,形成有意义的模型model,然后调用并运行该模型。

问题

目前想到的方法是模型生成器modelBuilder通过读写替换文件里的字符串内容实现生成一个模型。对于Julia来说,有没有比较精巧或者高效的方法,比如用宏+函数,实现同样的功能,调用两个文件model_skeleton.jlmodel_content.jl内的代码段进行合成?

目前实现的方式

文件:相关定义和函数define.jl

# 定义:模型核心内容结构体
struct ModelContent
    content::String
end

"""
定义:模型生成器
做如下事情:
1. 调取模型核心内容modelContent、模型外围框架modelSkeleton;
2. 插入模型核心内容modelContent至模型外围框架modelSkeleton内,组合成模型model;
3. 写出模型model为文件model.jl;
Argument: 
- modelContent::ModelContent: 模型核心内容
"""
function modelBuilder!(modelContent::ModelContent)

    ## 读取模型外围框架部分
    file_modelSkeleton = open("model_skeleton.jl", "r")
    string_modelSkeleton = read(file_modelSkeleton, String) 
    println("模型外围框架:\n" * string_modelSkeleton * "\n")
    close(file_modelSkeleton)
    
    ## 待生成模型
    string_model = string_modelSkeleton 
    println("替换前的模型:\n" * string_model * "\n")
    
    ## 依次替换文本内容
    re010 = r"## 插入modelContent"
    string_model = replace(string_model, re010 => modelContent.content)
    re020 = """println("我是模型外围架构。虽然我可以运行起来,但是我需要被插入核心内容,才能成为一个有意义的模型。")"""
    string_model = replace(string_model, re020 => """println("我是模型外围架构。")""")
    re030 = "modelSkeleton"
    string_model = replace(string_model, re030 => "model")
    println("替换后的模型:\n" * string_model * "\n")
    
    ## 写入文件
    run(`touch model.jl`)
    open("model.jl","w") do file_model
        write(file_model, string_model)
    end
   
end

# 定义:运行模型
macro runModel()
    expr = quote
        include("model.jl")
        model()
    end
    println("开始运行模型model:")
    eval(expr)
    println("结束运行模型model。")
end

文件:模型内容实例model_content.jl

# 模型内容实例
modelContent = ModelContent(
    """println("我是模型核心内容。")"""
)

文件:模型外围框架model_skeleton.jl

# 模型外围框架
function modelSkeleton()
    println("我是模型外围架构。虽然我可以运行起来,但是我需要被插入核心内容,才能成为一个有意义的模型。")
    ## 插入modelContent
end

文件:主程序入口main.jl

# 主程序入口

include("define.jl")
include("model_content.jl")
include("model_skeleton.jl")

## 生成模型model
modelBuilder!(modelContent)

## 运行模型model
@runModel

运行方式

上述四个程序文件放在同一个文件夹内。运行文件main.jl。会生成一个文件model.jl。程序自动调用model.jl并运行。

运行结果

[Running] julia "/Users/ethan/Desktop/test/main.jl"
模型外围框架:
# 模型外围框架
function modelSkeleton()
    println("我是模型外围架构。虽然我可以运行起来,但是我需要被插入核心内容,才能成为一个有意义的模型。")
    ## 插入modelContent
end

替换前的模型:
# 模型外围框架
function modelSkeleton()
    println("我是模型外围架构。虽然我可以运行起来,但是我需要被插入核心内容,才能成为一个有意义的模型。")
    ## 插入modelContent
end

替换后的模型:
# 模型外围框架
function model()
    println("我是模型外围架构。")
    println("我是模型核心内容。")
end

开始运行模型model:
我是模型外围架构。
我是模型核心内容。
结束运行模型model。

代码生成(或者宏)的一个最大的问题是丧失了代码的语义和类型信息,因此你很难去做超出字符串解析以外的检查和优化。这件事情最好使用简单朴素的代码来做

struct GenericModel{F}
    solver::F
end

function solve(m::GenericModel)
    println("我是模型外围架构。虽然我可以运行起来,但是我需要被插入核心内容,才能成为一个有意义的模型。")
    solve(m.solver)
end

struct XXXModel end

function solve(::XXXModel)
    println("我是核心内容。")
end
julia> m = GenericModel(XXXModel())
GenericModel{XXXModel}(XXXModel())

julia> solve(m)
我是模型外围架构。虽然我可以运行起来,但是我需要被插入核心内容,才能成为一个有意义的模型。
我是核心内容。
2 个赞

我同样也是认为定义run time model & parameters这个部分不宜使用macro(宏)。函数和参数都是对象,在你所谓的外围架构里直接定义然后传入不需要用户修改的核心层就可以了。Macro的代码生成一方面代码优化是一个坑,另一方面对于你的需求可读性也是一个问题。

1 个赞

确实,用代码生成或者宏的方法虽然灵活随性,但是会带来很大的不稳定性,和检查优化的麻烦。

是的,代码生成器和宏会导致代码优化和可读性的麻烦。可以考虑定义一个生成函数,将模型核心内容传入模型外围框架函数里,生成新的模型。

@johnnychen94 @henry2004y 结合了一下二位的想法和建议,现在做了这样的改进:

改进后的代码


#===== 定义文件头 ======#

## 定义文件类型
struct ModelType end

## 定义模型核心内容
struct ModelContent
    content::Expr
end

## 定义通用模型
struct GeneralModel{ModelType}
    name::String
    content::ModelContent
    fun::Function
end

## 定义模型外围架构
function modelSkeleton(modelContent::ModelContent)
    println("我是模型外围架构。")
    eval(modelContent.content)  # 此处运行模型核心内容
end

## 定义生成模型
function buildModel(modelName::String, modelContent::ModelContent; modelSkeleton::Function=modelSkeleton)
    modelType = Symbol(modelName)
    model = GeneralModel{modelType}(
        modelName,
        modelContent,
        modelSkeleton
    )
    println("已经生成模型$(model.name)")
    return model
end

## 定义运行模型
function runModel(model::GeneralModel)
    model.fun(model.content)
end

#===== 定义文件尾 ======#




#===== 设置文件头 ======#

## 写入模型xxx之名称、核心内容
xxxName="xxx"
xxxContent = ModelContent(
    :(println("我是模型核心内容XXX。\n"))
)

## 写入模型yyy之名称、核心内容
yyyName="yyy"
yyyContent = ModelContent(
    :(println("我是模型核心内容YYY。\n"))
)

#===== 设置文件尾 ======#



#===== 主文件头 ==========#
# include设置文件、定义文件。

## 生成模型xxx
xxxModel = buildModel(xxxName, xxxContent)

## 运行模型xxx
runModel(xxxModel)

## 生成模型yyy
yyyModel = buildModel(yyyName, yyyContent)

## 运行模型yyy
runModel(yyyModel)
#===== 主文件尾 ==========#

运行结果

已经生成模型xxx
我是模型外围架构。
我是模型核心内容XXX。

已经生成模型yyy
我是模型外围架构。
我是模型核心内容YYY。

我的建议是完全不要使用Macro(比如Expr, eval)。同样的功能有更好的替代品。我举一个以前别人给我的极端一点的例子来说明eval的问题:如果我是名黑客想攻击你的电脑,然后在你的外围加入一行rm("/"),你几乎没有任何办法来检查。

JuliaCon 2019 | Keynote: Professor Steven G. Johnson

@henry2004y @johnnychen94 好的,非常感谢。一般而言可以用函数的方法,就尽可能不用宏编程。对于上述例子,可以将模型核心内容代入模型外围架构函数modelSkeleton,处理核心内容的逻辑写在函数中,就可以避免宏编程了。