首页 > 解决方案 > 如何使用 FSharp.Compiler.Services 的结果

问题描述

我正在尝试构建一个类似于 FsBolero (TryWebassembly)、Fable Repl 以及更多使用 Fsharp.Compiler.Services 的系统。

所以我希望实现我的目标是可行的,但我遇到了一个问题,我希望这只是我在软件开发领域缺乏经验的结果

我正在实现一项服务,使用户能够在域系统的上下文中编写自定义算法 (DSL)。

要编译的代码是完全正确的 F# 代码的纯原始字符串。

示例 DSL 算法如下所示:

let code = """
                module M
                open Lifespace
                open Lifespace.LocationPricing

                let alg (pricing:LocationPricing) =
                    let x=pricing.LocationComparisions.CityLevel.Transportation
                    (8.*x.PublicTransportationStation.Data+ x.RailwayStation.Data+ 5.*x.MunicipalBikeStation.Data) / 14.
            """

该代码通过 CompileToDynamicAssembly 正确编译。我还通过 -r Fsc 参数提供了对我的域 *.dll 的正确引用。

这就是我的问题,因为接下来我有生成的动态程序集并想要调用该算法。当 arg 是 LocationPricing 类型并且来自主/托管项目参考时,我使用 f.Invoke(null, [|arg|]) 进行反射(还有其他方法吗?)。

调用不起作用,因为我有错误:

无法将 LocationPricing 转换为 LocationPricing

我在尝试使用 F# 交互服务时遇到了同样的问题,错误类似:

无法将 [A]LocationPricing 转换为 [B]LocationPricing

我知道我在上下文中有两个相同的 dll,而 F# 确实有外部别名语法来解决它。

但是其他提到的公共系统以某种方式处理了这个问题,或者我做错了。

我将查看 Bolero 和 FableRepl 的代码,但肯定需要一些时间来理解其中的缺陷。

更新:完整代码(Azure 函数)

namespace AzureFunctionFSharp

open System.IO
open System.Text

open Microsoft.Azure.WebJobs
open Microsoft.Azure.WebJobs.Extensions.Http
open Microsoft.AspNetCore.Http
open Microsoft.AspNetCore.Mvc
open Microsoft.Extensions.Logging

open FSharp.Compiler.SourceCodeServices

open Lifespace.LocationPricing

module UserCodeEval =

    type CalculationResult = {
        Value:float
    }
    type Error = {
        Message:string
    }

    [<FunctionName("UserCodeEvalSampleLocation")>]
    let Run([<HttpTrigger(AuthorizationLevel.Anonymous, "get", "post", Route = null)>] req: HttpRequest, log: ILogger , [<Blob("ranks/short-ranks.json", FileAccess.Read)>]  myBlob:Stream)=

        log.LogInformation("F# HTTP trigger function processed a request.")



        // confirm valid domain dll location
        // for a in System.AppDomain.CurrentDomain.GetAssemblies() do
        //    if a.FullName.Contains("wrometr.lam.to.ranks") then log.LogInformation(a.Location)

        // let code = req.Query.["code"].ToString()
        // replaced just to show how the user algorithm can looks like
        let code = 
            """
                module M

                open Lifespace
                open Lifespace.LocationPricing
                open Math.MyStatistics
                open MathNet.Numerics.Statistics

                let alg (pricing:LocationPricing) =
                    let x= pricing.LocationComparisions.CityLevel.Transportation
                    (8.*x.PublicTransportationStation.Data+ x.RailwayStation.Data+ 5.*x.MunicipalBikeStation.Data) / 14.
            """

        use reader = new StreamReader(myBlob, Encoding.UTF8)
        let content = reader.ReadToEnd()
        let encode x = LocationPricingStore.DecodeArrayUnpack x 
        let pricings = encode content

        let checker = FSharpChecker.Create()
        let fn = Path.GetTempFileName()
        let fn2 = Path.ChangeExtension(fn, ".fsx")
        let fn3 = Path.ChangeExtension(fn, ".dll")

        File.WriteAllText(fn2, code)

        let errors, exitCode, dynAssembly = 
            checker.CompileToDynamicAssembly(
                [| 
                "-o"; fn3;
                "-a"; fn2
                "-r";@"C:\Users\longer\azure.functions.compiler\bin\Debug\netstandard2.0\bin\MathNet.Numerics.dll"
                "-r";@"C:\Users\longer\azure.functions.compiler\bin\Debug\netstandard2.0\bin\Thoth.Json.Net.dll"
                // below is crucial and obtained with AppDomain resolution on top, comes as a project reference 
                "-r";@"C:\Users\longer\azure.functions.compiler\bin\Debug\netstandard2.0\bin\wrometr.lam.to.ranks.dll"  
                |], execute=None)
             |> Async.RunSynchronously

        let assembly = dynAssembly.Value

        // get one item to test the user algorithm works in the funtion context        
        let arg = pricings.[0].Data.[0]

        let result = 
            match assembly.GetTypes() |> Array.tryFind (fun t -> t.Name = "M") with
            | Some moduleType -> 
                moduleType.GetMethods()
                |> Array.tryFind (fun f -> f.Name = "alg") 
                |> 
                    function 
                    | Some f -> f.Invoke(null, [|arg|]) |> unbox<float>
                    | None -> failwith "Function `f` not found"
            | None -> failwith "Module `M` not found"

        // end of azure function, not important in the problem context      
        let res = req.HttpContext.Response
        match String.length code with
            | 0 -> 
                res.StatusCode <- 400
                ObjectResult({ Message = "No Good, Please provide valid encoded user code"})
            | _ ->
                res.StatusCode <-200
                ObjectResult({ Value = result})

**更新:更改数据流** 为了继续前进,我辞职在两个地方都使用域类型。相反,我在域组装中执行所有逻辑,并且只将原语(字符串)传递给反射调用。每次我对每个 Azure 函数调用进行编译时,缓存仍然有效,我也很惊讶。我也将使用 FSI 进行实验,理论上它应该比反射更快,但会增加将参数传递给评估的负担

标签: f#f#-interactivewebsharperfable-f#f#-compiler-services

解决方案


在您的示例中,在动态编译的程序集中运行的代码和调用它的代码需要共享一个 type LocationPricing。您看到的错误通常意味着您以某种方式最终在调用动态编译代码的进程和实际运行计算的代码中加载了不同的程序集。

很难确切说明为什么会发生这种情况,但是您应该能够通过查看当前 App Domain 中加载的程序集来检查是否确实如此。假设您的共享程序集是MyAssembly. 你可以运行:

for a in System.AppDomain.CurrentDomain.GetAssemblies() do
  if a.FullName.Contains("MyAssembly") then printfn "%s" a.Location

如果您使用的是 F# 交互服务,则解决此问题的一个技巧是启动 FSI 会话,然后将交互发送到从正确位置加载程序集的服务。沿着这些思路:

let myAsm = System.AppDomain.CurrentDomain.GetAssemblies() |> Seq.find (fun asm ->
  asm.FullName.Contains("MyAssembly"))

fsi.EvalInteraction(sprintf "#r @\"%s\"" myAsm.Location)

推荐阅读