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!
yreturn!
? En caso afirmativo, ¿hay alguna razón para no hacerlo? Hago estas preguntas porque no he encontrado ningún ejemplo usandolet!
yreturn!
.
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.