Actualmente estoy trabajando en un intérprete simple para un lenguaje de programación y tengo un tipo de datos como este:
data Expr
= Variable String
| Number Int
| Add [Expr]
| Sub Expr Expr
Y tengo muchas funciones que hacen cosas simples como:
-- Substitute a value for a variable
substituteName :: String -> Int -> Expr -> Expr
substituteName name newValue = go
where
go (Variable x)
| x == name = Number newValue
go (Add xs) =
Add $ map go xs
go (Sub x y) =
Sub (go x) (go y)
go other = other
-- Replace subtraction with a constant with addition by a negative number
replaceSubWithAdd :: Expr -> Expr
replaceSubWithAdd = go
where
go (Sub x (Number y)) =
Add [go x, Number (-y)]
go (Add xs) =
Add $ map go xs
go (Sub x y) =
Sub (go x) (go y)
go other = other
Pero en cada una de estas funciones, tengo que repetir la parte que llama al código de forma recursiva con solo un pequeño cambio en una parte de la función. ¿Existe alguna forma de hacer esto de manera más genérica? Preferiría no tener que copiar y pegar esta parte:
go (Add xs) =
Add $ map go xs
go (Sub x y) =
Sub (go x) (go y)
go other = other
Y solo cambie un caso cada vez porque parece ineficiente duplicar código como este.
La única solución que se me ocurre es tener una función que llame primero a una función en toda la estructura de datos y luego recursivamente en el resultado de esta manera:
recurseAfter :: (Expr -> Expr) -> Expr -> Expr
recurseAfter f x =
case f x of
Add xs ->
Add $ map (recurseAfter f) xs
Sub x y ->
Sub (recurseAfter f x) (recurseAfter f y)
other -> other
substituteName :: String -> Int -> Expr -> Expr
substituteName name newValue =
recurseAfter $ \case
Variable x
| x == name -> Number newValue
other -> other
replaceSubWithAdd :: Expr -> Expr
replaceSubWithAdd =
recurseAfter $ \case
Sub x (Number y) ->
Add [x, Number (-y)]
other -> other
Pero siento que probablemente ya debería haber una forma más simple de hacer esto. ¿Me estoy perdiendo de algo?

Add :: Expr -> Expr -> Exprlugar deAdd :: [Expr] -> Expry elimine porSubcompleto.recurseAfterestáanadisfrazado. Es posible que desee mirar anamorfismos yrecursion-schemes. Dicho esto, creo que su solución final es lo más breve posible. Cambiar a losrecursion-schemesanamorfismos oficiales no ahorrará mucho.Respuestas:
¡Felicitaciones, acabas de redescubrir anamorfismos!
Aquí está su código, reformulado para que funcione con el
recursion-schemespaquete. Por desgracia, no es más corto, ya que necesitamos un poco de repetitivo para que la maquinaria funcione. (Puede haber alguna forma automática de evitar el repetitivo, por ejemplo, usando genéricos. Simplemente no lo sé).A continuación, su
recurseAfterse reemplaza con el estándarana.Primero definimos su tipo recursivo, así como el functor del que es el punto fijo.
Luego conectamos los dos con algunas instancias para que podamos desplegarnos
Expren el isomorfoExprF Expry plegarlo.Finalmente, adaptamos su código original y agregamos un par de pruebas.
Una alternativa podría ser definir
ExprF asolo y luego derivartype Expr = Fix ExprF. Esto ahorra algo de la placa anterior (por ejemplo, las dos instancias), a costa de tener que usarla enFix (VariableF ...)lugar deVariable ..., así como lo análogo para los otros constructores.Se podría aliviar aún más el uso de sinónimos de patrones (a costa de un poco más repetitivo, sin embargo).
Actualización: Finalmente encontré la herramienta automágica, usando la plantilla Haskell. Esto hace que todo el código sea razonablemente corto. Tenga en cuenta que el
ExprFfunctor y las dos instancias anteriores todavía existen debajo del capó, y todavía tenemos que usarlos. Solo ahorramos la molestia de tener que definirlos manualmente, pero eso solo ahorra mucho esfuerzo.fuente
Exprexplícitamente, en lugar de algo asítype Expr = Fix ExprF?Fix+ el constructor real. Usar el último enfoque con la automatización TH es mejor, en mi opinión.Como un enfoque alternativo, este también es un caso de uso típico para el
uniplatepaquete. Puede usarData.Datagenéricos en lugar de Template Haskell para generar el repetitivo, por lo que si obtieneDatainstancias para suExpr:entonces la
transformfunción fromData.Generics.Uniplate.Dataaplica una función recursivamente a cada anidadoExpr:Tenga en cuenta que, en
replaceSubWithAddparticular, la funciónfse escribe para realizar una sustitución no recursiva;transformlo hace recursivox :: Expr, por lo que está haciendo la misma magia a la función auxiliaranaque en la respuesta de @ chi:Esto no es más corto que la solución Template Haskell de @ chi. Una ventaja potencial es que
uniplateproporciona algunas funciones adicionales que pueden ser útiles. Por ejemplo, si usadescenden lugar detransform, transforma solo los elementos secundarios inmediatos que pueden darle control sobre dónde ocurre la recursión, o puede usarrewritepara volver a transformar el resultado de las transformaciones hasta llegar a un punto fijo. Una desventaja potencial es que el "anamorfismo" suena mucho mejor que "uniplate".Programa completo:
fuente