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