Nano Hash - криптовалюты, майнинг, программирование

Могут ли функции F# быть специализированными во время выполнения?

Предположим, у меня есть функция F#, которая должна работать как локально, так и удаленно. Я хочу создать прокси функции, и пусть этот прокси решает, где запускать функцию, чтобы удаленный вызов был полностью невидим для внешнего мира. Если он будет работать локально, прокси просто вернет саму функцию, например:

let proxy_local f = f

Если нет, функция должна собрать все свои аргументы, затем отправить их по соединению и получить результат обратно. Вот тут-то и возникает сложность: мне нужно точно знать, какую функцию пользователь надеется передать через прокси, а также знать аргументы этой функции, чтобы я мог собрать их перед отправкой по сети.

Насколько я знаю, я не могу проверить сам аргумент функции, так как во время выполнения это будет какой-то неизвестный подкласс FSharpFunc, который собирает аргументы и вызывает с ними функцию. Чтобы обойти это, я думаю, мне нужно использовать цитату (кстати, есть ли лучший способ сделать эту часть?):

let getMethodInfo = function
    | Call (_, mi, _) -> [], mi
    | Lambdas (vs, Call(_, mi, _)) -> List.map (fun (v: Var list) -> (List.head v).Type) vs, mi
    | _ -> failwith "Not a function"

let proxy_remote (f: Expr) =
    let (argTypes, methodInfo) = getMethodInfo f
    … ? // What to return?

let proxy f = if isLocal then proxy_local f else proxy_remote f

getMethodInfo выше не будет работать для методов с аргументами кортежа, и нам также потребуется изменить proxy_local, но давайте пока оставим это в стороне. Проблема заключается в возвращаемом значении proxy_remote. Это должна быть функцией с теми же аргументами, что и у оригинала, но она должна отправлять их по сети в конце. Что-то вроде этого:

let callRemote (method: MethodInfo) a b c … = doHttpConnection()

Но аргументы должны быть введены. Это настоящая проблема: поскольку вызовы функций F# транслируются в подклассы FSharpFunc во время во время компиляции, я не знаю способа получить неспециализированное представление, которое будет работать с отражением. Мой реальный вопрос заключается в следующем: могут ли функции F # быть специализированными во время выполнения с неизвестными типами?

Я могу придумать два способа решить эту проблему. Во-первых, proxy должен быть универсальным:

let call1<'p, 'res> (method: MethodInfo) (p: 'p) = method.Invoke(null, [| p :> obj |]) :?> 'res
let call2<'p1, 'p2, 'res> (method: MethodInfo) (p1: 'p1) (p2: 'p2) = method.Invoke(null, [| p1 :> obj; p2 :> obj |]) :?> 'res
…

let proxy1 (f: Expr<'a -> 'b>) (s: string) : 'a -> 'b =
    let (types, mi) = getMethodInfo f
    match types with
    | [_] -> call1<'a, 'b> mi
    | _ -> failwith ""

let proxy2 (f: Expr<'a -> 'b -> 'c>) : 'a -> 'b -> 'c =
    let (types, mi) = getMethodInfo f
    match types with
    | [_; _] -> call2<'a, 'b, 'c> mi
    | _ -> failwith ""
…

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

let f a b = a + b
let fproxy = proxy1 f // The result will be of type int -> (int -> int), not what we want at all!

Другой способ — создать для этой цели специальные подклассы FSharpFunc:

type call1<'a, 'res>(id, ps) =
    inherit FSharpFunc<'a, 'res>()
    override __.Invoke(x: 'a) = callImpl<'res> id ((x :> obj) :: ps)

type call2<'a, 'b, 'res>(id, ps) =
    inherit FSharpFunc<'a, FSharpFunc<'b, 'res>>()
    override __.Invoke(x) = call1<'b, 'res>(id, x :> obj :: ps) :> obj :?> FSharpFunc<'b, 'res>

… 

let proxy (f: Expr<'a -> 'b>) : 'a -> 'b =
    let (types, methodInfo) = getMethodInfo f
    match types with
    | [a] ->
        let t = typedefof<call1<_,_>>.MakeGenericType([| a; methodInfo.ReturnType |])
        t.GetConstructors().[0].Invoke([|methodInfo; []|]) :?> ('a -> 'b)
    | [a; b] ->
        let t = typedefof<call2<_,_,_>>.MakeGenericType([| a; b; methodInfo.ReturnType |])
        t.GetConstructors().[0].Invoke([|methodInfo; []|]) :?> ('a -> 'b)
    … 
    | _ -> failwith ""

Это будет работать, но, вероятно, не будет столь производительным, как то, что генерирует компилятор F#, особенно со всеми динамическими приведениями. Итак, опять же, есть ли способ специализировать функции F# во время выполнения, чтобы typedefof<call1<_,_>>.MakeGenericType([| a; methodInfo.ReturnType |]) можно было заменить прямым вызовом функции?


Ответы:


1

После долгих поисков кажется, что просто нет способа делать то, что я хочу. Однако я узнал, что другие люди также создают подклассы FSharpFunc для некоторых действительно странных случаев использования, так что это не совсем неслыханно. Я оставляю вопрос открытым на случай, если кто-то поделится своим мнением.

17.08.2019

2

Это не прямой ответ на ваш вопрос. Но это реализация, с которой я играл (надеюсь, это может быть немного полезно):

type MyF<'U, 'V>(consumeParam: obj -> obj option, cont: 'V) = 
    inherit FSharpFunc<'U, 'V>()
    override __.Invoke(v: 'U) =
        match consumeParam (v :> obj) with 
        | None -> cont
        | Some result -> result :?> 'V

let netCall (outputT: Type) (paramObjs: obj[]): obj = 
    //tcp http etc
    printfn "%A" paramObjs
    Activator.CreateInstance(outputT)

let proxy (f: 't) : 't = 
    let rec collectTypes (t: Type) (acc: Type list) = 
        if t.Name = "FSharpFunc`2" then 
            let args = t.GetGenericArguments()
            collectTypes args.[1] (args.[0]::acc)
        else t::acc           
    let tps = collectTypes (typeof<'t>) [] //here we collected all types from sugnature to array
    printfn "%A" (tps |> List.map(fun x -> x.Name) |> List.rev) //just for debug
    match tps with 
    | [] -> failwithf "Could not be here" //signature cannot be empty
    | [ _ ] -> f //provided param is not a function
    | outputT::inputT::otherT -> //take last two types: ... -> inputT -> outputT
        let mutable paramIndex = 0 //at each call of FSharpFunc we add param to array
        let paramsHolder: obj[] = Array.zeroCreate (otherT.Length + 1)     
        let consumeParam (paramValue: obj) = 
            paramsHolder.[paramIndex] <- paramValue
            paramIndex <- paramIndex + 1
            if paramIndex = paramsHolder.Length then //if all params given
                Some(netCall outputT paramsHolder)//network call 
            else None
        let initialFunc = //build initial func inputT -> outputT
            typedefof<MyF<_,_>>.MakeGenericType([| inputT; outputT |])
                .GetConstructors().[0].Invoke([| consumeParam :> obj; Activator.CreateInstance(outputT) |])
        let rec buildF (func: obj) otherT = //recursivelly build other funcs
            match otherT with 
            | [] -> func 
            | inputT::otherT -> 
                let newFunc = 
                    typedefof<MyF<_,_>>.MakeGenericType([| inputT; func.GetType().BaseType |])
                        .GetConstructors().[0].Invoke([| consumeParam :> obj; func |])
                buildF newFunc otherT
        let finalFunc = buildF initialFunc otherT
        finalFunc :?> 't  //final cast

[<EntryPoint>]
let main args =
    let myTestF1 a b = a + b
    let myTestF1Proxied = proxy myTestF1
    printfn "myTestF1Proxied created"
    let res = myTestF1Proxied 1 2
    let myTestF2 a b c d e = 1.0 + a + b * c - d + (float) e
    let myTestF2Proxied = proxy myTestF2
    printfn "myTestF2Proxied created"
    let res = myTestF2Proxied 1. 2. 3. 4. 5
    // let myTestF3 a b c = a::b::c
    // let myTestF3Proxied = proxy myTestF3
    // printfn "myTestF3Proxied created"
    // let res = myTestF3Proxied "test" "a" []
    printfn "Done"
    0

Вывод для первых двух тестов:

["Int32"; "Int32"; "Int32"]
myTestF1Proxied created
[|1; 2|]
["Double"; "Double"; "Double"; "Double"; "Int32"; "Double"]
myTestF2Proxied created
[|1.0; 2.0; 3.0; 4.0; 5|]

Третий тест падает на Activator.CreateInstance. Это интересный случай, потому что здесь проксируется функция let myTestF3 a b c = a::b::c. Это общий: 'a -> 'a -> 'список. Следовательно, это вопрос, как проксировать такие функции (предположим, что мы можем ограничить прокси с типами, допускающими значение NULL, или каким-то образом использовать Option‹'a>).

ОБНОВЛЕНИЕ

Вот улучшенный код:

1) Activator.CreateInstance удален - не смог создать FSharpList во время выполнения

2) invokeFunc — заглушка для netCall, но может использоваться на другой удаленной стороне. Функция рекурсивно применяет параметры к общей функции

3) MyF разделен на два класса: MyF и MyFInit - это позволяет удалить Activator.CreateInstance при первом вызове

Теперь третье испытание успешно. Код:

type MyF<'U, 'V>(consumeParam, cont: 'V) = 
    inherit FSharpFunc<'U, 'V>()
    override __.Invoke(v: 'U) = consumeParam (v :> obj); cont

type MyFInit<'U, 'V>(consumeParam: obj -> obj) = 
    inherit FSharpFunc<'U, 'V>()
    override __.Invoke(v: 'U) = consumeParam (v :> obj) :?> 'V

let invokeFunc f (outputT: Type) (paramObjs: obj[]) = 
    let rec invoke partiallyAppliedFunc givenParams = 
        match givenParams with
        | p::otherParams ->
            let methodInfo = partiallyAppliedFunc.GetType().GetMethod("Invoke", [| p.GetType() |])
            if isNull methodInfo then 
                partiallyAppliedFunc //fully applied ?
            else 
                let newFunc = methodInfo.Invoke(partiallyAppliedFunc, [| p |])
                invoke newFunc otherParams
        | _ -> 
            partiallyAppliedFunc //params are empty 
    invoke f (paramObjs |> Array.toList)

let netCall f (outputT: Type) (paramObjs: obj[]): obj = 
    //tcp http etc instead of invokeFunc
    printfn "%A" paramObjs
    let res = invokeFunc f outputT paramObjs
    printfn "Res: %A" res
    res

let proxy (f: 't) : 't = 
    let rec collectTypes (t: Type) (acc: Type list) = 
        if t.Name = "FSharpFunc`2" then 
            let args = t.GetGenericArguments()
            collectTypes args.[1] (args.[0]::acc)
        else t::acc           
    let tps = collectTypes (typeof<'t>) [] //here we collected all types from sugnature to array
    printfn "%A" (tps |> List.map(fun x -> x.Name) |> List.rev) //just for debug
    match tps with 
    | [] -> failwithf "Could not be here" //signature cannot be empty
    | [ _ ] -> f //provided param is not a function
    | outputT::inputT::otherT -> //take last two types: ... -> inputT -> outputT
        let mutable paramIndex = 0 //at each call of FSharpFunc we add param to array
        let paramsHolder: obj[] = Array.zeroCreate (otherT.Length + 1)   
        let consumeParamInit (paramValue: obj) = 
            paramsHolder.[paramIndex] <- paramValue
            paramIndex <- paramIndex + 1
            netCall f outputT paramsHolder//network call 
        let consumeParam (paramValue: obj) = 
            paramsHolder.[paramIndex] <- paramValue
            paramIndex <- paramIndex + 1
        let initialFunc = //build initial func inputT -> outputT
            typedefof<MyFInit<_,_>>.MakeGenericType([| inputT; outputT |])
                .GetConstructors().[0].Invoke([| consumeParamInit :> obj |])
        let rec buildF (func: obj) otherT = //recursivelly build other funcs
            match otherT with 
            | [] -> func 
            | inputT::otherT -> 
                let newFunc = 
                    typedefof<MyF<_,_>>.MakeGenericType([| inputT; func.GetType().BaseType |])
                        .GetConstructors().[0].Invoke([| consumeParam :> obj; func |])
                buildF newFunc otherT
        let finalFunc = buildF initialFunc otherT
        finalFunc :?> 't  //final cast

[<EntryPoint>]
let main args = 
    let myTestF1 a b = a + b
    let myTestF1Proxied = proxy myTestF1
    printfn "myTestF1Proxied created"
    let res = myTestF1Proxied 1 2
    let myTestF2 a b c d e = 1.0 + a + b * c - d + (float) e
    let myTestF2Proxied = proxy myTestF2
    printfn "myTestF2Proxied created"
    let res = myTestF2Proxied 1. 2. 3. 4. 5
    let myTestF3 a b c = a::b.ToString()::c
    let myTestF3Proxied = proxy myTestF3
    printfn "myTestF3Proxied created"
    let res = myTestF3Proxied "test" 1 []
    printfn "Done"
    0

Вывод:

["Int32"; "Int32"; "Int32"]
myTestF1Proxied created
[|1; 2|]
Res: 3
["Double"; "Double"; "Double"; "Double"; "Int32"; "Double"]
myTestF2Proxied created
[|1.0; 2.0; 3.0; 4.0; 5|]
Res: 9.0
["String"; "Int32"; "FSharpList`1"; "FSharpList`1"]
myTestF3Proxied created
[|"test"; 1; []|]
Res: ["test"; "1"]
Done
28.08.2019
  • Я думаю, что причина, по которой ваш третий тест дает сбой, заключается в том, что вы предполагаете, что все входные данные для функции имеют один и тот же тип, поэтому, когда вы получаете другой ввод (список в последнем параметре), ваш код дает сбой. 30.08.2019
  • @ Arshia001 - я разместил обновление выше. Нет, это не так: выходные параметры могут быть разных типов (см. вывод). Несмотря на то, что предоставленный код работает, полезно предоставить некоторые украшения для локального вызова функции. Но для удаленного вызова функции (я имею в виду ту же функцию) исходный код функции должен существовать на удаленной стороне, прежде чем мы отправим запрос. 30.08.2019
  • Конечно, мы предполагаем, что одни и те же библиотеки загружаются везде, где выполняется код. 30.08.2019
  • Новые материалы

    Кластеризация: более глубокий взгляд
    Кластеризация — это метод обучения без учителя, в котором мы пытаемся найти группы в наборе данных на основе некоторых известных или неизвестных свойств, которые могут существовать. Независимо от..

    Как написать эффективное резюме
    Предложения по дизайну и макету, чтобы представить себя профессионально Вам не позвонили на собеседование после того, как вы несколько раз подали заявку на работу своей мечты? У вас может..

    Частный метод Python: улучшение инкапсуляции и безопасности
    Введение Python — универсальный и мощный язык программирования, известный своей простотой и удобством использования. Одной из ключевых особенностей, отличающих Python от других языков, является..

    Как я автоматизирую тестирование с помощью Jest
    Шутка для победы, когда дело касается автоматизации тестирования Одной очень важной частью разработки программного обеспечения является автоматизация тестирования, поскольку она создает..

    Работа с векторными символическими архитектурами, часть 4 (искусственный интеллект)
    Hyperseed: неконтролируемое обучение с векторными символическими архитектурами (arXiv) Автор: Евгений Осипов , Сачин Кахавала , Диланта Хапутантри , Тимал Кемпития , Дасвин Де Сильва ,..

    Понимание расстояния Вассерштейна: мощная метрика в машинном обучении
    В обширной области машинного обучения часто возникает необходимость сравнивать и измерять различия между распределениями вероятностей. Традиционные метрики расстояния, такие как евклидово..

    Обеспечение масштабируемости LLM: облачный анализ с помощью AWS Fargate и Copilot
    В динамичной области искусственного интеллекта все большее распространение получают модели больших языков (LLM). Они жизненно важны для различных приложений, таких как интеллектуальные..