2009-12-20 16 views
8

Digamos que estoy construyendo un analizador para un lenguaje específico de dominio en F #.F # patrón de diseño

he definido una unión discriminada para representar expresiones:

type Expression = 
     | Equality of Expression*Expression 
     | NonEquality of Expression*Expression 
     | Or of Expression*Expression 
     | And of Expression*Expression 
     | If of Expression*Expression 
     | IfElse of Expression*Expression*Expression 
     | Bool of bool 
     | Variable of string 
     | StringLiteral of string 

Ahora, yo he construido un AST de tipo Expression y desea generar código para ello. Tengo una función que escribe inferencia y comprueba el tipo en una expresión.

Se define como

let rec InferType expr = 
     match expr with 
     | Equality(e1,e2) -> CheckTypes (InferType e1) (InferType e2) 
     | Or(e1,e2) -> CheckTypes (InferType e1) (InferType e2) 
     | And(e1,e2) -> CheckTypes (InferType e1) (InferType e2) 
     ... 

Y tengo otra función para generar código que sigue un patrón similar: Tomar una expresión, escribir declaraciones de patrones de coincidencia para cada elemento de la unión.

Mi pregunta es: ¿Es esta la manera idiomática de hacerlo en F #?

Me parece que sería más limpio si cada miembro de la unión definiera su propio InferType y GenerateCode localmente con él.

Si estuviera usando C#, definiría una clase base abstracta llamada Expression con métodos virtuales para InferType y GenerateCode y luego los anularía en cada subclase.

¿Hay alguna otra manera de hacerlo?

Respuesta

17

Me parece que sería más limpio si cada miembro de la unión define su propio InferType y GenerateCode localmente con él.

Creo que te refieres a "más familiar", no "limpiador".

Realmente, ¿es su ideal tener la implementación del generador de código distribuida en 10 clases diferentes?

Definitivamente hay una tensión fundamental entre si desea agrupar cosas "por tipo" o "por operación". El modo OO habitual es "por tipo", mientras que el modo FP (programación funcional) es "por operación".

En el caso de un compilador/intérprete (o la mayoría de las cosas que en OO dependen en gran medida del patrón Visitor), creo que "por operación" es la agrupación más natural. El generador de código para If y And y Or puede tener un poco en común; los typecheckers de los diversos nodos tendrán similitudes similares; si crea una impresora bonita, probablemente habrá rutinas de formateo comunes a todas las implementaciones de impresión de nodo bonito. Por el contrario, la impresión, la comprobación de tipos y la creación de código IfElse realmente no tienen mucho que ver uno con el otro, así que ¿por qué querría agruparlos en una clase IfElse?

(Para responder a sus preguntas: sí, esto es idiomático. ¿Hay alguna otra manera? Sí, puede hacerlo como lo haría en C#.Creo que te darás cuenta de que estás mucho menos contento con el modo C# y que el código también será 2-3 veces más grande de esa manera, sin ningún beneficio.)

+1

Gracias - Esta fue la respuesta que estaba buscando . –

9

Como programador de OCaml, diría que esto es completamente idiomático. Por cierto, esto te da una mejor separación de preocupaciones que si hubieras escrito una jerarquía de clase con métodos de clase. Tendría una modularidad similar en un lenguaje OO con un visitante InferType, pero sería mucho más código.

1

La otra cosa que normalmente harías en un el lenguaje funcional es definir una operación fold en el tipo de datos y luego definir las funciones de verificación de tipo y de generación de código en términos del doblez. En este caso particular, no estoy seguro de que te compra mucho porque la función de plegado tendría tantos argumentos que no va a ser particularmente fácil de comprender:

let rec fold eqE nonE andE orE ... = function 
| Equality(e1,e2) -> eqE (e1 |> fold eqE nonE ...) (e2 |> fold eqE nonE ...) 
| NonEquality(e1,e2) -> nonE ... 
... 

let inferTypes = fold checkTypes checkTypes checkTypes ... 
+1

¿Es ese 'fold' una forma más fea de construir un visitante? Todavía prefiero el estilo de OP, donde inferTypes se define directamente mediante una declaración de coincidencia de patrón. – Tobu

+2

@Tobu: personalmente me parece mucho menos feo que el patrón Visitor. Como dije, en este ejemplo en particular hay tantos casos diferentes que no creo que definir las cosas en términos de un pliegue sea mejor. En general, sin embargo, a menudo tiene sentido definir las operaciones de plegado en los tipos de datos recursivos (como 'List.fold' incorporado de F #). – kvb