f# dsl computation-expression query-expressions

f# - Expresiones de cálculo extendidas sin for..in..do



dsl computation-expression (2)

Lo que quiero decir con expresiones de cálculo extendidas es expresiones de cálculo con palabras clave personalizadas definidas a través del atributo CustomOperation .

Cuando leo sobre expresiones de computación extendidas , me encuentro con IL DSL muy bueno por @kvb:

let il = ILBuilder() // will return 42 when called // val fortyTwoFn : (unit -> int) let fortyTwoFn = il { ldc_i4 6 ldc_i4_0 ldc_i4 7 add mul ret }

Me pregunto cómo se componen las operaciones sin usar for..in..do construct. Mi intuición es que comienza con el miembro x.Zero , pero no he encontrado ninguna referencia para verificarlo.

Si el ejemplo anterior es demasiado técnico, aquí hay un DSL similar donde se enumeran los componentes de una diapositiva sin for..in..do :

page { title "Happy New Year F# community" item "May F# continue to shine as it did in 2012" code @"…" button (…) } |> SlideShow.show

Tengo algunas preguntas estrechamente relacionadas:

  • ¿Cómo se define o usa una expresión computacional extendida sin el miembro For (es decir, proporciona un pequeño ejemplo completo)? No me preocupo mucho si ya no son mónadas, estoy interesado en desarrollar DSL.
  • ¿Podemos usar expresiones de computación extendidas con let! y return! ? En caso afirmativo, ¿hay alguna razón para no hacerlo? Hago estas preguntas porque no he encontrado ningún ejemplo usando let! y return! .

Debo admitir que no entiendo completamente cómo funcionan las expresiones de cómputo cuando usa características de expresión de consulta como el atributo CustomOperation . Pero aquí hay algunos comentarios de algunos de mis experimentos que podrían ayudar ...

En primer lugar, creo que no es posible combinar libremente las funciones de expresión de cálculo estándar ( return! Etc.) con operaciones personalizadas. Aparentemente se permiten algunas combinaciones, pero no todas. Por ejemplo, si defino la operación personalizada a la left y return! ¡Entonces solo puedo usar la operación personalizada antes de return! :

// Does not compile // Compiles and works moves { return! lr moves { left left } return! lr }

En cuanto a los cálculos que usan solo operaciones personalizadas, la mayoría de las operaciones comunes de control ( orderBy , reverse y este tipo) tienen un tipo M<''T> -> M<''T> donde M<''T> es un tipo (posiblemente genérico) que representan lo que estamos construyendo (por ejemplo, una lista).

Por ejemplo, si queremos construir un valor que represente una secuencia de movimientos de izquierda / derecha, podemos usar el siguiente tipo de Commands :

type Command = Left | Right type Commands = Commands of Command list

Las operaciones personalizadas, como la left y la right pueden transformar los Commands en Commands y agregar el nuevo paso al final de la lista. Algo como:

type MovesBuilder() = [<CustomOperation("left")>] member x.Left(Commands c) = Commands(c @ [Left]) [<CustomOperation("right")>] member x.Right(Commands c) = Commands(c @ [Right])

Tenga en cuenta que esto es diferente del yield que devuelve una sola operación - o comando - y por lo tanto, el yield necesita Combine para combinar varios pasos individuales si usa operaciones personalizadas, entonces nunca necesita combinar nada porque las operaciones personalizadas gradualmente construyen el valor de Commands como un todo. Solo necesita un valor inicial de Commands vacíos que se usa al principio ...

Ahora, esperaría ver a Zero allí, pero en realidad llama a Yield con la unidad como un argumento, por lo que necesita:

member x.Yield( () ) = Commands[]

No estoy seguro de por qué este es el caso, pero Zero menudo se define como Yield () , por lo que quizás el objetivo sea usar la definición predeterminada (pero como dije, también esperaría usar Zero aquí ... )

Creo que tiene sentido combinar operaciones personalizadas con expresiones computacionales. Si bien tengo opiniones sólidas sobre cómo se deben usar las expresiones de cálculo estándar, realmente no tengo una buena intuición sobre los cálculos con operaciones personalizadas. Creo que la comunidad todavía necesita resolver esto :-). Pero, por ejemplo, puede extender el cálculo anterior de esta manera:

member x.Bind(Commands c1, f) = let (Commands c2) = f () in Commands(c1 @ c2) member x.For(c, f) = x.Bind(c, f) member x.Return(a) = x.Yield(a)

(En algún momento, la traducción empezará a requerir For y Return , pero aquí pueden definirse como Bind y Yield , y no entiendo completamente cuándo se usa qué alternativa).

Entonces puedes escribir algo como:

let moves = MovesBuilder() let lr = moves { left right } let res = moves { left do! lr left do! lr }


Me alegra que te haya gustado el ejemplo de IL. La mejor manera de entender cómo se deshacen las expresiones es probablemente mirar la spec (aunque es un poco densa ...).

Ahí podemos ver que algo como

C { op1 op2 }

se desugara de la siguiente manera:

T([<CustomOperator>]op1; [<CustomOperator>]op2, [], fun v -> v, true) ⇒ CL([<CustomOperator>]op1; [<CustomOperator>]op2, [], C.Yield(), false) ⇒ CL([<CustomOperator>]op2, [], 〚 [<CustomOperator>]op1, C.Yield() |][], false) ⇒ CL([<CustomOperator>]op2, [], C.Op1(C.Yield()), false) ⇒ 〚 [<CustomOperator>]op2, C.Op1(C.Yield()) 〛[] ⇒ C.Op2(C.Op1(C.Yield()))

En cuanto a por qué se usa Yield() lugar de Zero , se debe a que si hubiera variables en el alcance (p. Ej., Porque usó algunos lets o estuvimos en un bucle for, etc.), obtendría el Yield (v1,v2,...) pero Zero claramente no puede ser usado de esta manera. Tenga en cuenta que esto significa que la adición de un let x = 1 superfluo let x = 1 en el ejemplo lr de Tomas no se compilará, porque se llamará Yield con un argumento de tipo int lugar de unit .

Hay otro truco que puede ayudar a comprender la forma compilada de las expresiones de cómputo, que consiste en (ab) usar el soporte de comillas automáticas para las expresiones de cómputo en F # 3. Simplemente defina un miembro de la Quote no hacer nada y haga que Run simplemente devuelva su argumento:

member __.Quote() = () member __.Run(q) = q

Ahora su expresión de cálculo evaluará la cotización de su forma desugarada. Esto puede ser bastante útil al depurar cosas.