Построение AST с использованием цитат кода против деревьев выражений


У меня есть некоторые проблемы с построением дерева выражений. Я могу сделать то же самое, когда использую цитаты из кода, но мне не удалось сделать это с помощью выражений.

Сначала взгляните на мой подход, делающий это с помощью цитат из кода

    open Microsoft.FSharp.Quotations
    open Microsoft.FSharp.Quotations.Patterns
    open Microsoft.FSharp.Quotations.DerivedPatterns

    type Container<'a> = Container of 'a
    type FromD<'a> = {a: Container<'a>; b: Container<'a>}
    type ToD<'a> = {a: Container<'a>; b: Container<'a>}

    let private eval e = QuotationEvaluator.Evaluate e

    let f1 f =
        let ex =
            <@
                fun (x:FromD<'a>) ->
                {
                    a = f x.a;
                    b = f x.b
                } 
                : ToD<'b>
            @>
        eval ex

Подпись выше - (Container<'a> -> Container<'b>) -> (FromD<'a> -> ToD<'b>). Именно этого я и хотел. Дерево выражений, порожденное f1, является

Lambda (x,
        NewRecord (ToD`1,
                Application (ValueWithName (<fun:r1@60>, f),
                                PropertyGet (Some (x), a, [])),
                Application (ValueWithName (<fun:r1@60>, f),
                                PropertyGet (Some (x), b, []))))

Теперь некоторый тестовый код, который преобразует FromD в ToD и применяет преобразование к Container, а также

    let transform (Container (v:'a)) : Container<'b> = Container (sprintf "%A" v)

    [<Test>]
    let ``test F1`` () =
        let r1 = f1 transform {a = Container true; b = Container true}
        let r2 = f1 transform {a = Container 1; b = Container 2}
        printfn "F1: %A, F1: %A" r1 r2

Все в точности так, как я хотел, и r1 и r2 дают ожидаемые результаты.

Теперь я хочу воссоздать f1, используя выражения вместо цитат кода.
Это моя первая попытка (с некоторыми вспомогательными функциями)

//fields :: Type -> PropertyInfo []
let fields t = FSharpType.GetRecordFields t

//nameMap :: Type -> Map<string,PropertyInfo>
let nameMap t =
    t
    |> fields
    |> Array.map (fun x -> x.Name, x)
    |> Map.ofArray

let f2<'x, 't> f = 
    let xt = typeof<'x>
    let tt = typeof<'t>
    let ps = nameMap xt
    let x = Var("x", xt)
    let vx = Expr.Var(x)
    let fnv = Expr.ValueWithName(f, "f")
    let ex = 
        Expr.Lambda(x,
            Expr.NewRecord(tt,
                [
                    Expr.Application(fnv, Expr.PropertyGet(vx, ps.Item "a", []))
                    Expr.Application(fnv, Expr.PropertyGet(vx, ps.Item "b", []))
                ])) 

    let ex2 : Expr<'x -> 't> = ex |> Expr.Cast
    let ex3 = eval ex2
    ex3

И некоторый тестовый код

let ``test F2`` () =
    let r3 = (f2<FromD<bool>, ToD<string>> transform) {a = Container true; b = Container true}
    printfn "R3 %A" r3 

Теперь первое, что в этом случае подпись f2 является
(Container<obj> -> Container<string>) -> ('x -> 't)
вместо того, чтобы
(Container<'a> -> Container<'b>) -> (FromD<'a> -> ToD<'b>)
Так что каким-то образом тип infererrer немного нетерпелив по этому поводу один.

Это приводит затем к следующему сообщению об ошибке

System.ArgumentException : Type mismatch when building 'f': function argument type doesn't match. Expected 'tst+Container`1[System.Boolean]', but received type 'tst+Container`1[System.Object]'.
Parameter name: receivedType
at Microsoft.FSharp.Quotations.PatternsModule.checkTypesSR[a] (System.Type expectedType, System.Type receivedType, a name, System.String threeHoleSR) [0x00019] in <57acd2f6dff9fae1a7450383f6d2ac57>:0
at Microsoft.FSharp.Quotations.PatternsModule.checkAppliedLambda (Microsoft.FSharp.Quotations.FSharpExpr f, Microsoft.FSharp.Quotations.FSharpExpr v) [0x00084] in <57acd2f6dff9fae1a7450383f6d2ac57>:0
at Microsoft.FSharp.Quotations.PatternsModule.mkApplication (Microsoft.FSharp.Quotations.FSharpExpr v_0, Microsoft.FSharp.Quotations.FSharpExpr v_1) [0x00001] in <57acd2f6dff9fae1a7450383f6d2ac57>:0
at Microsoft.FSharp.Quotations.FSharpExpr.Application (Microsoft.FSharp.Quotations.FSharpExpr functionExpr, Microsoft.FSharp.Quotations.FSharpExpr argument) [0x00001] in <57acd2f6dff9fae1a7450383f6d2ac57>:0
at tst.f2[x,t] (Microsoft.FSharp.Core.FSharpFunc`2[T,TResult] f) [0x0005f] in <582303e818eafa12a7450383e8032358>:0
at tst.test F2 () [0x00005] in <582303e818eafa12a7450383e8032358>:0
at (wrapper managed-to-native) System.Reflection.MonoMethod:InternalInvoke (System.Reflection.MonoMethod,object,object[],System.Exception&)
at System.Reflection.MonoMethod.Invoke (System.Object obj, System.Reflection.BindingFlags invokeAttr, System.Reflection.Binder binder, System.Object[] parameters, System.Globalization.CultureInfo culture) [0x00038] in <8cd55ece525b4760b63de40980e005aa>:0

Таким образом, кажется, что есть некоторая проблема при построении дерева выражений, поскольку тип inferer говорит, что моя функция имеет тип param bool, но фактический param-это object.
Теперь я могу преодолеть это, переписав функцию следующим образом

let f2<'x, 't> f = 
    let xt = typeof<'x>
    let tt = typeof<'t>
    let ps = nameMap xt
    let x = Var("x", xt)
    let vx = Expr.Var(x)
    let fnv = Expr.ValueWithName(f, typeof<Container<bool> -> Container<string>>, "f")
    let ex = 
        Expr.Lambda(x,
            Expr.NewRecord(tt,
                [
                    Expr.Application(fnv, Expr.PropertyGet(vx, ps.Item "a", []))
                    Expr.Application(fnv, Expr.PropertyGet(vx, ps.Item "b", []))
                ])) 

    let ex2 : Expr<'x -> 't> = ex |> Expr.Cast
    let ex3 = eval ex2
    ex3

В этом случае я заставляю ValueWithName быть определенного типа вместо f.GetType().
Я создал для этого примера очень специфический тип (typeof<Container<bool> -> Container<string>>) также, чтобы сделайте этот пример более понятным.

Это поможет мне пройти этап строительства,а также работать с актерами.
Кроме того, дерево выражений, которое было построено, такое же, как и раньше.

Однако теперь он выходит из строя во время оценки со следующим сообщением об ошибке

System.ArgumentException : Argument types do not match
at System.Linq.Expressions.Expression.Constant (System.Object value, System.Type type) [0x00049] in <4a648327db854c86ab0ece073e38f4b3>:0
at FSharp.Quotations.Evaluator.QuotationEvaluationTypes.LetRecConvExpr (FSharp.Quotations.Evaluator.QuotationEvaluationTypes+ConvEnv env, Microsoft.FSharp.Core.FSharpOption`1[T] letrec, Microsoft.FSharp.Quotations.FSharpExpr inp) [0x00185] in <56703c1ea378c767a74503831e3c7056>:0
at FSharp.Quotations.Evaluator.QuotationEvaluationTypes.ConvExpr (FSharp.Quotations.Evaluator.QuotationEvaluationTypes+ConvEnv env, Microsoft.FSharp.Quotations.FSharpExpr inp) [0x00001] in <56703c1ea378c767a74503831e3c7056>:0
at FSharp.Quotations.Evaluator.QuotationEvaluationTypes.LetRecConvExpr (FSharp.Quotations.Evaluator.QuotationEvaluationTypes+ConvEnv env, Microsoft.FSharp.Core.FSharpOption`1[T] letrec, Microsoft.FSharp.Quotations.FSharpExpr inp) [0x02065] in <56703c1ea378c767a74503831e3c7056>:0
at FSharp.Quotations.Evaluator.QuotationEvaluationTypes.ConvExpr (FSharp.Quotations.Evaluator.QuotationEvaluationTypes+ConvEnv env, Microsoft.FSharp.Quotations.FSharpExpr inp) [0x00001] in <56703c1ea378c767a74503831e3c7056>:0
at FSharp.Quotations.Evaluator.QuotationEvaluationTypes+ConvExprs@703.Invoke (Microsoft.FSharp.Quotations.FSharpExpr inp) [0x00001] in <56703c1ea378c767a74503831e3c7056>:0
at Microsoft.FSharp.Primitives.Basics.List.map[T,TResult] (Microsoft.FSharp.Core.FSharpFunc`2[T,TResult] mapping, Microsoft.FSharp.Collections.FSharpList`1[T] x) [0x0003f] in <57acd2f6dff9fae1a7450383f6d2ac57>:0
at Microsoft.FSharp.Collections.ListModule.Map[T,TResult] (Microsoft.FSharp.Core.FSharpFunc`2[T,TResult] mapping, Microsoft.FSharp.Collections.FSharpList`1[T] list) [0x00001] in <57acd2f6dff9fae1a7450383f6d2ac57>:0
at FSharp.Quotations.Evaluator.QuotationEvaluationTypes.ConvExprs (FSharp.Quotations.Evaluator.QuotationEvaluationTypes+ConvEnv env, Microsoft.FSharp.Collections.FSharpList`1[T] es) [0x00007] in <56703c1ea378c767a74503831e3c7056>:0
at FSharp.Quotations.Evaluator.QuotationEvaluationTypes.LetRecConvExpr (FSharp.Quotations.Evaluator.QuotationEvaluationTypes+ConvEnv env, Microsoft.FSharp.Core.FSharpOption`1[T] letrec, Microsoft.FSharp.Quotations.FSharpExpr inp) [0x020e6] in <56703c1ea378c767a74503831e3c7056>:0
at FSharp.Quotations.Evaluator.QuotationEvaluationTypes.ConvExpr (FSharp.Quotations.Evaluator.QuotationEvaluationTypes+ConvEnv env, Microsoft.FSharp.Quotations.FSharpExpr inp) [0x00001] in <56703c1ea378c767a74503831e3c7056>:0
at FSharp.Quotations.Evaluator.QuotationEvaluationTypes.LetRecConvExpr (FSharp.Quotations.Evaluator.QuotationEvaluationTypes+ConvEnv env, Microsoft.FSharp.Core.FSharpOption`1[T] letrec, Microsoft.FSharp.Quotations.FSharpExpr inp) [0x027f0] in <56703c1ea378c767a74503831e3c7056>:0
at FSharp.Quotations.Evaluator.QuotationEvaluationTypes.ConvExpr (FSharp.Quotations.Evaluator.QuotationEvaluationTypes+ConvEnv env, Microsoft.FSharp.Quotations.FSharpExpr inp) [0x00001] in <56703c1ea378c767a74503831e3c7056>:0
at FSharp.Quotations.Evaluator.QuotationEvaluationTypes.Conv[a] (a e, System.Boolean eraseEquality) [0x0001d] in <56703c1ea378c767a74503831e3c7056>:0
at FSharp.Quotations.Evaluator.QuotationEvaluationTypes.CompileImpl[a] (a e, System.Boolean eraseEquality) [0x00001] in <56703c1ea378c767a74503831e3c7056>:0
at FSharp.Quotations.Evaluator.QuotationEvaluationTypes.Compile[a] (a e) [0x00001] in <56703c1ea378c767a74503831e3c7056>:0
at FSharp.Quotations.Evaluator.QuotationEvaluator.Evaluate[T] (Microsoft.FSharp.Quotations.FSharpExpr`1[T] e) [0x00001] in <56703c1ea378c767a74503831e3c7056>:0
at tst.f2[x,t] (Microsoft.FSharp.Core.FSharpFunc`2[T,TResult] f) [0x000f5] in <5823081418eafa12a745038314082358>:0
at tst.test F2 () [0x00005] in <5823081418eafa12a745038314082358>:0
at (wrapper managed-to-native) System.Reflection.MonoMethod:InternalInvoke (System.Reflection.MonoMethod,object,object[],System.Exception&)
at System.Reflection.MonoMethod.Invoke (System.Object obj, System.Reflection.BindingFlags invokeAttr, System.Reflection.Binder binder, System.Object[] parameters, System.Globalization.CultureInfo culture) [0x00038] in <8cd55ece525b4760b63de40980e005aa>:0
У кого-нибудь есть идея, что происходит?
1 3

1 ответ:

Тип f2 заканчивается на 'x -> 't, потому что именно так вы указали его в этой строке:

let ex2 : Expr<'x -> 't> = ex |> Expr.Cast

f2 он даже не знает о существовании таких вещей, как FromD и ToD, поэтому он не может иметь их в своем типе.
Однако если вы посмотрите на тип первой части r3 в вашем тесте, вы увидите, что это FromD<_> -> ToD<_>, потому что они указаны в качестве аргументов типа f2, чтобы заменить 'x и 't соответственно.

Что касается Container<obj> - это на самом деле немного хуже, чем ты думаешь. Если вы посмотрите на f2 изолированно, вы увидите, что его тип - obj -> 'x -> 't. Это потому, что в теле f2 нет ничего, что могло бы подсказать, каким должен быть Тип f. Таким образом, он принужден просто obj как конечный супертип всего.
Когда вы на самом деле используете f2 с аргументом transform для параметра f - это когда компилятор фиксирует тип f, чтобы быть Container<_> -> Container<string> (так как это тип transform), который позже становится Container<obj> -> Container<string>, потому что в программе нет ничего, что могло бы еще больше ограничить этот тип.

Из вышесказанного, исправление самоочевидно: просто объявите тип f явно.

let f2<'x, 't, 'a, 'b> (f: Container<'a> -> Container<'b>) = 
    ...
Это даст вам правильные типы еще до первого применения.

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

  1. Тип 'x должен быть записью с полями a и b типа 'a.
  2. тип 't должен быть записью с ровно двумя полями, названными a и b, объявленными в этом определенном порядке, и оба имеют тип 'b.
Такая конструкция кажется мне немного шаткой. Возможно, если бы вы описали свою первоначальную проблему (предпочтительно в виде отдельного вопроса), кто-то мог бы предложите более элегантное решение.

Если вы просто хотите "нанести карту на запись", я, возможно, рассмотрю менее амбициозное решение, например:

let fromDMap f (fromD: FromD<_>) : ToD<_> = { a = f fromD.a; b = f fromD.b }

// Usage:
let r3 = fromDMap transform {a = Container true; b = Container true}
Конечно, этот подход не будет работать, если вы хотите сделать "универсальную" функцию для отображения однофамильных полей произвольных типов. Но тогда я рискну предположить, что такая функция будет немного слишком общей.

P.S. ваша функция transform имеет объявленный тип, который является более универсальным, чем функция на самом деле является. Объявленный тип возврата - Container<'b>, но на самом деле он возвращает Container<string>. Таким образом, 'b принуждается быть string.