F#奇妙游(34):另外一种动态执行F#代码的方法

继续FSharp.Compiler.Service

人类最好的品质,就是不知足。不知足,才有进步的动力。

上面我们针对新的.NET,把前面James Randall (jamesdrandall.com)的技术线路中的代码更新了一下,实现了在文件中动态编译F#代码,然后执行。

今天准备把说好的输入F#代码作为输入,产生F#对象,然后存成mermaid实现一下,就感觉挺麻烦的,还得编译啊、载入啊……难道F#就这点本事?

于是我又翻开了FSharp.Compiler.Service的文档,发现还有一个模块,FSharp.Compiler.Interactive.Shell。居然还有这种好事!

FSharp.Compiler.Interactive.Shell

有了Shell,那F#能不能跟Shell进程深入交流呢?如果只是能通过管道来点输入、输出,那就跟我的想法差得比较远。一开始我就把另外一个进程的想法干掉了就是因为我对.NET平台不熟悉,以前C/C++就只能通过管道来跟另外一个进程交流,所以我就想着F#也是这样的。

现在看起来,就是直接把对象传回来?这样就可以直接用了。

FsiValue和FsiEvaluationSession

这个类的文档里面,有一个例子,就是把F#代码作为字符串,然后执行,然后把结果作为字符串返回。

看文档也很简单,我们直接去看灵魂————源代码。这里有一个小建议,看源代码首先别看fs文件,要看fsi文件。接口文件里面把内部的细节全部省略了,只给出我们要调用的部分,非常有用。

fsi

举个例子,我们这里就能从fsi.fsi(这两个fsi含义完全不同,前面那个是iteractive,后面那个是interface)中看到这个应该怎么用。

/// Represents an evaluated F# value
[<Class>]
type FsiValue =

    /// The value, as an object
    member ReflectionValue: obj

    /// The type of the value, from the point of view of the .NET type system
    member ReflectionType: Type

    /// The type of the value, from the point of view of the F# type system
    member FSharpType: FSharpType

/// Represents an evaluated F# value that is bound to an identifier
[<Sealed>]
type FsiBoundValue =

    /// The identifier of the value
    member Name: string

    /// The evaluated F# value
    member Value: FsiValue

首先是两个类型,一个类型是FsiValue,就代表一个值,用obj表示,其类型为ReflectionType(这个是从.NET类型系统来看的),另外一个是FSharpType,这是在F#中的类型。第二个就是FsiBoundValue,是对应的Shell中绑定的值,就是在FsiValue的基础上加一个绑定的字符串名字。

这两个类型就是我们与Shell交换值的核心内容。那么这个Shell本身呢?就是用一个FsiEvaluationSeesion来表示。

我们看一个F#的类型,首先可以从它的static成员看起,因为这个成员呢?实在对象还没创建之前就能调用的。这就两个方法,非常的清晰。

第一个方法就是创建一个进程,含义很清晰,配置、命令行,输入的管道、输出的管道(错误输出和普通输出),后面里那个搞不清楚是什么意思,但是是可选的,那就暂时不管他。

    /// <summary>Create an FsiEvaluationSession, reading from the given text input, writing to the given text output and error writers</summary>
    ///
    /// <param name="fsiConfig">The dynamic configuration of the evaluation session</param>
    /// <param name="argv">The command line arguments for the evaluation session</param>
    /// <param name="inReader">Read input from the given reader</param>
    /// <param name="errorWriter">Write errors to the given writer</param>
    /// <param name="outWriter">Write output to the given writer</param>
    /// <param name="collectible">Optionally make the dynamic assembly for the session collectible</param>
    /// <param name="legacyReferenceResolver">An optional resolver for legacy MSBuild references</param>
    static member Create:
        fsiConfig: FsiEvaluationSessionHostConfig *
        argv: string[] *
        inReader: TextReader *
        outWriter: TextWriter *
        errorWriter: TextWriter *
        ?collectible: bool *
        ?legacyReferenceResolver: LegacyReferenceResolver ->
            FsiEvaluationSession

这里的第一个参数就是配置,配置由另外一个静态函数提供。

    /// Get a configuration that uses the 'fsi' object (normally from FSharp.Compiler.Interactive.Settings.dll,
    /// an object from another DLL with identical characteristics) to provide an implementation of the configuration.
    /// The flag indicates if FSharp.Compiler.Interactive.Settings.dll  is referenced by default.
    static member GetDefaultConfiguration: fsiObj: obj * useFsiAuxLib: bool -> FsiEvaluationSessionHostConfig

    /// Get a configuration that uses the 'fsi' object (normally from FSharp.Compiler.Interactive.Settings.dll,
    /// an object from another DLL with identical characteristics) to provide an implementation of the configuration.
    /// FSharp.Compiler.Interactive.Settings.dll  is referenced by default.
    static member GetDefaultConfiguration: fsiObj: obj -> FsiEvaluationSessionHostConfig

    /// Get a configuration that uses a private inbuilt implementation of the 'fsi' object and does not
    /// implicitly reference FSharp.Compiler.Interactive.Settings.dll.
    static member GetDefaultConfiguration: unit -> FsiEvaluationSessionHostConfig

把静态的看完了,剩下就是主要的接口。

生命周期

第一个就是实现了IDisposable接口,那么在函数中用,可以写use xxx = FsiEvaluationSession.Create,否则还得自己调用Dispose()。其次就是能够中断执行的进程,Interrupt ()

/// Represents an F# Interactive evaluation session.
[<Class>]
type FsiEvaluationSession =

    interface IDisposable

    /// A host calls this to request an interrupt on the evaluation thread.
    member Interrupt: unit -> unit

核心功能一:值

下面就是核心的功能,把值传递给脚本进程,或者从脚本进程获得值。下面四个函数都是非常直观的,第一个是一个事件,绑定一个值就能获得这个时间;第二个和第三个函数就就是找到已经绑定的值;最后一个函数就是把当前程序中的值绑定到进程中。

    /// Event fires when a root-level value is bound to an identifier, e.g., via `let x = ...`.
    member ValueBound: IEvent<obj * Type * string>

    /// Gets the root-level values that are bound to an identifier
    member GetBoundValues: unit -> FsiBoundValue list

    /// Tries to find a root-level value that is bound to the given identifier
    member TryFindBoundValue: name: string -> FsiBoundValue option

    /// Creates a root-level value with the given name and .NET object.
    /// If the .NET object contains types from assemblies that are not referenced in the interactive session, it will try to implicitly resolve them by default configuration.
    /// Name must be a valid identifier.
    member AddBoundValue: name: string * value: obj -> unit

核心功能二:求值

第二个核心功能就是各种在进程中运行对象,大概包含Interaction和Expression两个类型。第一个就是那些有副作用的语句,例如导入一个dll,载入一个文件,打印一个值这些;Expression就是求值一个表达式。相对来说也挺直观的。

    /// Execute the code as if it had been entered as one or more interactions, with an
    /// implicit termination at the end of the input. Stop on first error, discarding the rest
    /// of the input. Errors are sent to the output writer, a 'true' return value indicates there
    /// were no errors overall. Execution is performed on the 'Run()' thread.
    ///
    /// Due to a current limitation, it is not fully thread-safe to run this operation concurrently with evaluation triggered
    /// by input from 'stdin'.
    member EvalInteraction: code: string * ?cancellationToken: CancellationToken -> unit

    /// Execute the code as if it had been entered as one or more interactions, with an
    /// implicit termination at the end of the input. Stop on first error, discarding the rest
    /// of the input. Errors are sent to the output writer, a 'true' return value indicates there
    /// were no errors overall. Execution is performed on the 'Run()' thread.
    ///
    /// Due to a current limitation, it is not fully thread-safe to run this operation concurrently with evaluation triggered
    /// by input from 'stdin'.
    /// The scriptFileName parameter is used to report errors including this file name.
    member EvalInteraction: code: string * scriptFileName: string * ?cancellationToken: CancellationToken -> unit

    /// Execute the code as if it had been entered as one or more interactions, with an
    /// implicit termination at the end of the input. Stop on first error, discarding the rest
    /// of the input. Errors and warnings are collected apart from any exception arising from execution
    /// which is returned via a Choice. Execution is performed on the 'Run()' thread.
    ///
    /// Due to a current limitation, it is not fully thread-safe to run this operation concurrently with evaluation triggered
    /// by input from 'stdin'.
    member EvalInteractionNonThrowing:
        code: string * ?cancellationToken: CancellationToken -> Choice<FsiValue option, exn> * FSharpDiagnostic[]

    /// Execute the code as if it had been entered as one or more interactions, with an
    /// implicit termination at the end of the input. Stop on first error, discarding the rest
    /// of the input. Errors and warnings are collected apart from any exception arising from execution
    /// which is returned via a Choice. Execution is performed on the 'Run()' thread.
    ///
    /// Due to a current limitation, it is not fully thread-safe to run this operation concurrently with evaluation triggered
    /// by input from 'stdin'.
    /// The scriptFileName parameter is used to report errors including this file name.
    member EvalInteractionNonThrowing:
        code: string * scriptFileName: string * ?cancellationToken: CancellationToken ->
            Choice<FsiValue option, exn> * FSharpDiagnostic[]

    /// Execute the given script. Stop on first error, discarding the rest
    /// of the script. Errors are sent to the output writer, a 'true' return value indicates there
    /// were no errors overall. Execution is performed on the 'Run()' thread.
    ///
    /// Due to a current limitation, it is not fully thread-safe to run this operation concurrently with evaluation triggered
    /// by input from 'stdin'.
    member EvalScript: filePath: string -> unit

    /// Execute the given script. Stop on first error, discarding the rest
    /// of the script. Errors and warnings are collected apart from any exception arising from execution
    /// which is returned via a Choice. Execution is performed on the 'Run()' thread.
    ///
    /// Due to a current limitation, it is not fully thread-safe to run this operation concurrently with evaluation triggered
    /// by input from 'stdin'.
    member EvalScriptNonThrowing: filePath: string -> Choice<unit, exn> * FSharpDiagnostic[]

    /// Execute the code as if it had been entered as one or more interactions, with an
    /// implicit termination at the end of the input. Stop on first error, discarding the rest
    /// of the input. Errors are sent to the output writer. Parsing is performed on the current thread, and execution is performed
    /// synchronously on the 'main' thread.
    ///
    /// Due to a current limitation, it is not fully thread-safe to run this operation concurrently with evaluation triggered
    /// by input from 'stdin'.
    member EvalExpression: code: string -> FsiValue option

    /// Execute the code as if it had been entered as one or more interactions, with an
    /// implicit termination at the end of the input. Stop on first error, discarding the rest
    /// of the input. Errors are sent to the output writer. Parsing is performed on the current thread, and execution is performed
    /// synchronously on the 'main' thread.
    ///
    /// Due to a current limitation, it is not fully thread-safe to run this operation concurrently with evaluation triggered
    /// by input from 'stdin'.
    /// The scriptFileName parameter is used to report errors including this file name.
    member EvalExpression: code: string * scriptFileName: string -> FsiValue option

    /// Execute the code as if it had been entered as one or more interactions, with an
    /// implicit termination at the end of the input. Stop on first error, discarding the rest
    /// of the input. Errors and warnings are collected apart from any exception arising from execution
    /// which is returned via a Choice. Parsing is performed on the current thread, and execution is performed
    /// synchronously on the 'main' thread.
    ///
    /// Due to a current limitation, it is not fully thread-safe to run this operation concurrently with evaluation triggered
    /// by input from 'stdin'.
    member EvalExpressionNonThrowing: code: string -> Choice<FsiValue option, exn> * FSharpDiagnostic[]

    /// Execute the code as if it had been entered as one or more interactions, with an
    /// implicit termination at the end of the input. Stop on first error, discarding the rest
    /// of the input. Errors and warnings are collected apart from any exception arising from execution
    /// which is returned via a Choice. Parsing is performed on the current thread, and execution is performed
    /// synchronously on the 'main' thread.
    ///
    /// Due to a current limitation, it is not fully thread-safe to run this operation concurrently with evaluation triggered
    /// by input from 'stdin'.
    /// The scriptFileName parameter is used to report errors including this file name.
    member EvalExpressionNonThrowing:
        code: string * scriptFileName: string -> Choice<FsiValue option, exn> * FSharpDiagnostic[]

辅助功能

除了操作值和求值表达式(语句)之外,还有一些辅助的功能,第一个是获得代码补全的;第二个就是把对象格式化为字符串。

    /// A host calls this to get the completions for a long identifier, e.g. in the console
    ///
    /// Due to a current limitation, it is not fully thread-safe to run this operation concurrently with evaluation triggered
    /// by input from 'stdin'.
    member GetCompletions: longIdent: string -> seq<string>\

    /// Format a value to a string using the current PrintDepth, PrintLength etc settings provided by the active fsi configuration object
    member FormatValue: reflectionValue: obj * reflectionType: Type -> string

例子

有了前面的这些工具,我们很容易就实现一个Eval模块。

module Eval =
    let private fsiSession () =
        let sbOut = StringBuilder()
        let sbErr = StringBuilder()
        let inStream = new StringReader("")
        let outStream = new StringWriter(sbOut)
        let errStream = new StringWriter(sbErr)

        // Build command line arguments & start FSI session
        let argv = [|"--noninteractive"|]

        let fsiConfig = FsiEvaluationSession.GetDefaultConfiguration()
        FsiEvaluationSession.Create(fsiConfig, argv, inStream, outStream, errStream)
    
    let private fsi = fsiSession ()
        
    /// Evaluate expression & return the result
    let evalExpression text =
        match fsi.EvalExpression(text) with
        | Some value -> value.ReflectionValue
        | None -> None

    let eval<'T> text =
        match fsi.EvalExpression(text) with
        | Some value -> value.ReflectionValue |> unbox<'T>
        | None -> failwith $"{text} is not valid"
    
    let evalInteraction text =
        fsi.EvalInteractionNonThrowing text
    
    let loadDll fn =
        evalInteraction $"#r @\"{fn}\""
    
    let openName name =
        evalInteraction $"open {name}"

这个模块会内部建立一个交互进程,然后可以载入dll文件,打开命名空间,并且有两个函数来求值表达式,一个函数运行有副作用的语句。整个核心就是evalExpressionevalInteraction两个方法。

在构造过程中,设置进程的输入输出管道,并用--noninteractive来关掉交互的console。

使用的例子如下。

Eval.loadDll @"D:\PARA\0Projects\HPLW\ADC.NET\Algorithm.ADC\bin\Debug\net7.0\Algorithm.ADC.dll"
Eval.openName "Algorithm.ADC.ComplexSystem"

let cmds = """
series [ para [ subs "接受命令1"; subs "接受命令2" ]
         subs "命令分配"             
         para [ subs "执行单元1"
                subs "执行单元2"
                subs "执行单元3" ]
         subs "执行确认"
         subs "执行评估" ]
"""

printfn "%A" (Eval.evalExpression(cmds))

printfn "%A" (Eval.evalExpression($"{cmds} |> toMermaid"))

let sys = Eval.eval<ComplexSystem>(cmds)

printfn "%s" sys.mermaid

接受命令1
接受命令2
命令分配
执行单元1
执行单元2
执行单元3
执行确认
执行评估

简直就是很完美。

总结

  1. FSharp.Compiler.Service提供了丰富的建立进程的工具;
  2. 可以通过FsiEvaluationSession来完成值的交互和表达式求值;
  3. 这个方法感觉上调用起来比编译成Assembly要轻量一些;
  4. 两个方法的比较可以在应用中再考虑。
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

大福是小强

除非你钱多烧得慌……

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值