继续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含义完全不同,前面那个是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文件,打开命名空间,并且有两个函数来求值表达式,一个函数运行有副作用的语句。整个核心就是evalExpression
和evalInteraction
两个方法。
在构造过程中,设置进程的输入输出管道,并用--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
简直就是很完美。
总结
- FSharp.Compiler.Service提供了丰富的建立进程的工具;
- 可以通过FsiEvaluationSession来完成值的交互和表达式求值;
- 这个方法感觉上调用起来比编译成Assembly要轻量一些;
- 两个方法的比较可以在应用中再考虑。