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 -> Expr
lugar deAdd :: [Expr] -> Expr
y elimine porSub
completo.recurseAfter
estáana
disfrazado. 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-schemes
anamorfismos oficiales no ahorrará mucho.Respuestas:
¡Felicitaciones, acabas de redescubrir anamorfismos!
Aquí está su código, reformulado para que funcione con el
recursion-schemes
paquete. 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
recurseAfter
se 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
Expr
en el isomorfoExprF Expr
y plegarlo.Finalmente, adaptamos su código original y agregamos un par de pruebas.
Una alternativa podría ser definir
ExprF a
solo 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
ExprF
functor 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
Expr
explí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
uniplate
paquete. Puede usarData.Data
genéricos en lugar de Template Haskell para generar el repetitivo, por lo que si obtieneData
instancias para suExpr
:entonces la
transform
función fromData.Generics.Uniplate.Data
aplica una función recursivamente a cada anidadoExpr
:Tenga en cuenta que, en
replaceSubWithAdd
particular, la funciónf
se escribe para realizar una sustitución no recursiva;transform
lo hace recursivox :: Expr
, por lo que está haciendo la misma magia a la función auxiliarana
que en la respuesta de @ chi:Esto no es más corto que la solución Template Haskell de @ chi. Una ventaja potencial es que
uniplate
proporciona algunas funciones adicionales que pueden ser útiles. Por ejemplo, si usadescend
en lugar detransform
, transforma solo los elementos secundarios inmediatos que pueden darle control sobre dónde ocurre la recursión, o puede usarrewrite
para 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