Построение 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 ответ:
Тип
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>) = ...
Но берегись!
Поскольку вся обработка выполняется во время выполнения, компилятор не может гарантировать безопасность ввода во всех местах. Поэтому вы должны сами позаботиться о том, чтобы защититься от них. Вот некоторые (хотя, возможно не все) из вещей, на которые опирается ваш код, которые не подлежат принудительному исполнению во время компиляции:Такая конструкция кажется мне немного шаткой. Возможно, если бы вы описали свою первоначальную проблему (предпочтительно в виде отдельного вопроса), кто-то мог бы предложите более элегантное решение.
- Тип
'x
должен быть записью с полямиa
иb
типа'a
.- тип
'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
.