Expresiones de código delimitadas

En este artículo se describen las expresiones de código delimitadas, una característica del lenguaje que permite generar expresiones de código de F# y trabajar con ellas mediante programación. Esta característica permite generar un árbol de sintaxis abstracta que representa el código de F#. Después, el árbol de sintaxis abstracta se puede recorrer y procesar según las necesidades de la aplicación. Por ejemplo, puede usar el árbol para generar código de F# o de otro lenguaje.

Expresiones delimitadas

Una expresión delimitada es una expresión de F# en el código que está delimitada de tal forma que no se compila como parte del programa, sino que se compila en un objeto que representa una expresión de F#. Puede marcar una expresión delimitada de dos maneras: con información de tipo o sin información de tipo. Si quiere incluir información de tipo, use los símbolos <@ y @> para delimitar la expresión. Si no necesita información de tipo, use los símbolos <@@ y @@>. En el código siguiente se muestran expresiones de código delimitadas con tipo y sin tipo.

open Microsoft.FSharp.Quotations
// A typed code quotation.
let expr : Expr<int> = <@ 1 + 1 @>
// An untyped code quotation.
let expr2 : Expr = <@@ 1 + 1 @@>

Es más rápido recorrer un árbol de expresión grande si no se incluye información de tipo. El tipo resultante de una expresión delimitada mediante símbolos con tipo es Expr<'T>, donde el parámetro de tipo tiene el tipo de la expresión según lo que determina el algoritmo de inferencia de tipos del compilador de F#. Cuando se usan expresiones de código delimitadas sin información de tipo, el tipo de la expresión delimitada es el tipo no genérico Expr. Puede llamar a la propiedad Raw en la clase Expr con tipo para obtener el objeto Expr sin tipo.

Hay varios métodos estáticos que permiten generar objetos de expresión de F# mediante programación en la clase Expr sin usar expresiones delimitadas.

Una expresión de código delimitada debe incluir una expresión completa. Para un enlace let, por ejemplo, necesita la definición del nombre enlazado y otra expresión que use el enlace. En la sintaxis detallada, se trata de una expresión que sigue a la palabra clave in. En el nivel superior de un módulo no es más que la siguiente expresión del módulo, pero en una expresión de código delimitada se requiere explícitamente.

Por consiguiente, la expresión siguiente no es válida.

// Not valid:
// <@ let f x = x + 1 @>

Pero las expresiones siguientes son válidas.

// Valid:
<@ let f x = x + 10 in f 20 @>
// Valid:
<@
    let f x = x + 10
    f 20
@>

Para evaluar las expresiones de código delimitadas de F#, debe usar el evaluador de expresiones de código delimitadas de F#. Proporciona compatibilidad para evaluar y ejecutar objetos de expresión de F#.

Las expresiones de código delimitadas de F# también conservan la información de restricción de tipo. Considere el ejemplo siguiente:

open FSharp.Linq.RuntimeHelpers

let eval q = LeafExpressionConverter.EvaluateQuotation q

let inline negate x = -x
// val inline negate: x: ^a ->  ^a when  ^a : (static member ( ~- ) :  ^a ->  ^a)

<@ negate 1.0 @>  |> eval

La restricción generada por la función inline se conserva en la expresión de código delimitada. Ahora se puede evaluar la forma delimitada de la función negate.

Tipo Expr

Una instancia del tipo Expr representa una expresión de F#. En la documentación de la biblioteca de F# están documentados tanto los tipos genéricos de Expr como los no genéricos. Para obtener más información, consulte Espacio de nombres FSharp.Quotations y Clase Quotations.Expr.

Operadores de inserción

La inserción le permite combinar expresiones de código delimitadas literales con expresiones que haya creado mediante programación o a partir de otra expresión de código delimitada. Los operadores % y %% permiten agregar un objeto de expresión de F# a una expresión de código delimitada. El operador % se usa para insertar un objeto de expresión con tipo en una expresión de código delimitada con tipo; el operador %% se usa para insertar un objeto de expresión sin tipo en una expresión de código delimitada sin tipo. Ambos son operadores de prefijo unarios. Por lo tanto, si expr es una expresión sin tipo de tipo Expr, el código siguiente es válido.

<@@ 1 + %%expr @@>

Y si expr es una expresión de código delimitada con tipo de tipo Expr<int>, el código siguiente es válido.

<@ 1 + %expr @>

Ejemplo 1

Descripción

En el ejemplo siguiente se muestra el uso de expresiones de código delimitadas para colocar código de F# en un objeto de expresión y, luego, imprimir el código de F# que representa la expresión. Se define una función println que contiene una función recursiva print que muestra un objeto de expresión de F# (de tipo Expr) en un formato descriptivo. Hay varios modelos activos en los módulos FSharp.Quotations.Patterns y FSharp.Quotations.DerivedPatterns que se pueden usar para analizar objetos de expresión. En este ejemplo no se incluyen todos los modelos posibles que podrían aparecer en una expresión de F#. Cualquier modelo no reconocido desencadena una coincidencia con el modelo de comodín (_) y se representa mediante el método ToString que, en el tipo Expr, le permite conocer el modelo activo que se va a agregar a la expresión de coincidencia.

Código

module Print
open Microsoft.FSharp.Quotations
open Microsoft.FSharp.Quotations.Patterns
open Microsoft.FSharp.Quotations.DerivedPatterns

let println expr =
    let rec print expr =
        match expr with
        | Application(expr1, expr2) ->
            // Function application.
            print expr1
            printf " "
            print expr2
        | SpecificCall <@@ (+) @@> (_, _, exprList) ->
            // Matches a call to (+). Must appear before Call pattern.
            print exprList.Head
            printf " + "
            print exprList.Tail.Head
        | Call(exprOpt, methodInfo, exprList) ->
            // Method or module function call.
            match exprOpt with
            | Some expr -> print expr
            | None -> printf "%s" methodInfo.DeclaringType.Name
            printf ".%s(" methodInfo.Name
            if (exprList.IsEmpty) then printf ")" else
            print exprList.Head
            for expr in exprList.Tail do
                printf ","
                print expr
            printf ")"
        | Int32(n) ->
            printf "%d" n
        | Lambda(param, body) ->
            // Lambda expression.
            printf "fun (%s:%s) -> " param.Name (param.Type.ToString())
            print body
        | Let(var, expr1, expr2) ->
            // Let binding.
            if (var.IsMutable) then
                printf "let mutable %s = " var.Name
            else
                printf "let %s = " var.Name
            print expr1
            printf " in "
            print expr2
        | PropertyGet(_, propOrValInfo, _) ->
            printf "%s" propOrValInfo.Name
        | String(str) ->
            printf "%s" str
        | Value(value, typ) ->
            printf "%s" (value.ToString())
        | Var(var) ->
            printf "%s" var.Name
        | _ -> printf "%s" (expr.ToString())
    print expr
    printfn ""


let a = 2

// exprLambda has type "(int -> int)".
let exprLambda = <@ fun x -> x + 1 @>
// exprCall has type unit.
let exprCall = <@ a + 1 @>

println exprLambda
println exprCall
println <@@ let f x = x + 10 in f 10 @@>

Output

fun (x:System.Int32) -> x + 1
a + 1
let f = fun (x:System.Int32) -> x + 10 in f 10

Ejemplo 2

Descripción

También puede usar los tres modelos activos del módulo ExprShape para recorrer árboles de expresión con menos modelos activos. Estos modelo activos pueden ser útiles cuando se quiere recorrer un árbol, pero no se necesita toda la información de la mayoría de los nodos. Cuando se usan estos modelos, cualquier expresión de F# coincide con uno de los tres modelos siguientes: ShapeVar si la expresión es una variable, ShapeLambda si la expresión es una expresión lambda o ShapeCombination si la expresión es cualquier otra cosa. Si recorre un árbol de expresión mediante los modelos activos, como en el ejemplo de código anterior, debe usar muchos más modelos para controlar todos los tipos de expresión de F# posibles y el código será más complejo. Para obtener más información, consulte Modelo activo ExprShape.ShapeVar|ShapeLambda|ShapeCombination.

El ejemplo de código siguiente se puede usar como base para recorridos más complejos. En este código, se crea un árbol de expresión para una expresión que implica una llamada de función, add. El modelo activo SpecificCall se usa para detectar cualquier llamada a add en el árbol de expresión. Este modelo activo asigna los argumentos de la llamada al valor exprList. En este caso, solo hay dos, por lo que se extraen y se llama a la función de forma recursiva en los argumentos. Los resultados se insertan en una expresión de código delimitada que representa una llamada a mul mediante el operador de inserción (%%). La función println del ejemplo anterior se usa para mostrar los resultados.

El código de las demás ramas del modelo activo simplemente vuelve a generar el mismo árbol de expresión, por lo que la única modificación en la expresión resultante es el cambio de add a mul.

Código

module Module1
open Print
open Microsoft.FSharp.Quotations
open Microsoft.FSharp.Quotations.DerivedPatterns
open Microsoft.FSharp.Quotations.ExprShape

let add x y = x + y
let mul x y = x * y

let rec substituteExpr expression =
    match expression with
    | SpecificCall <@@ add @@> (_, _, exprList) ->
        let lhs = substituteExpr exprList.Head
        let rhs = substituteExpr exprList.Tail.Head
        <@@ mul %%lhs %%rhs @@>
    | ShapeVar var -> Expr.Var var
    | ShapeLambda (var, expr) -> Expr.Lambda (var, substituteExpr expr)
    | ShapeCombination(shapeComboObject, exprList) ->
        RebuildShapeCombination(shapeComboObject, List.map substituteExpr exprList)

let expr1 = <@@ 1 + (add 2 (add 3 4)) @@>
println expr1
let expr2 = substituteExpr expr1
println expr2

Resultados

1 + Module1.add(2,Module1.add(3,4))
1 + Module1.mul(2,Module1.mul(3,4))

Vea también