Как использовать результаты 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. Я также предоставил правильную ссылку на свой домен *.dll через параметр -r Fsc.

И здесь возникают мои проблемы, поскольку у меня есть сгенерированная динамическая сборка, и я хочу вызвать этот алгоритм. Я делаю это с отражением (есть ли другой способ?) с помощью f.Invoke(null, [|arg|]), когда arg имеет тип LocationPricing и исходит из ссылки на основной/хостинговый проект.

Invoke не работает, потому что у меня ошибка:

Невозможно преобразовать 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, теоретически это должно быть быстрее, чем отражение, но с дополнительной нагрузкой для передачи параметров в оценки.


person Pawel Stadnicki    schedule 13.09.2019    source источник


Ответы (1)


В вашем примере код, который выполняется внутри вашей динамически скомпилированной сборки, и код, вызывающий его, должны иметь общий тип LocationPricing. Ошибка, которую вы видите, обычно означает, что вы каким-то образом оказались с другой сборкой, загруженной в процесс, который вызывает динамически скомпилированный код, и код, фактически выполняющий вычисления.

Трудно точно сказать, почему это произошло, но вы сможете проверить, так ли это на самом деле, просмотрев сборки, загруженные в текущий домен приложения. Скажите, что ваша общая сборка — 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)
person Tomas Petricek    schedule 15.09.2019
comment
Предлагаемая сборка через AppDomain подтвердила, что местоположения одинаковы. Это полное сообщение об ошибке: System.Private.CoreLib: Исключение при выполнении функции: UserCodeEvalSampleLocation. System.Private.CoreLib: объект типа «Lifespace.LocationPricing+LocationPricing» не может быть преобразован в тип «Lifespace.LocationPricing+LocationPricing». В исходном вопросе я также предоставил полный код, который я делаю. - person Pawel Stadnicki; 16.09.2019