haskell functional-programming

haskell - Cómo reducir la duplicación de código cuando se trata con tipos de suma recursiva



functional-programming (2)

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?


¡Felicitaciones, acabas de redescubrir anamorfismos!

Aquí está su código, reformulado para que funcione con el paquete de recursion-schemes . 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ándar ana .

Primero definimos su tipo recursivo, así como el functor del que es el punto fijo.

{-# LANGUAGE DeriveFunctor, TypeFamilies, LambdaCase #-} {-# OPTIONS -Wall #-} module AnaExpr where import Data.Functor.Foldable data Expr = Variable String | Number Int | Add [Expr] | Sub Expr Expr deriving (Show) data ExprF a = VariableF String | NumberF Int | AddF [a] | SubF a a deriving (Functor)

Luego conectamos los dos con algunas instancias para que podamos desplegar Expr en el ExprF Expr isomórfico y plegarlo de nuevo.

type instance Base Expr = ExprF instance Recursive Expr where project (Variable s) = VariableF s project (Number i) = NumberF i project (Add es) = AddF es project (Sub e1 e2) = SubF e1 e2 instance Corecursive Expr where embed (VariableF s) = Variable s embed (NumberF i) = Number i embed (AddF es) = Add es embed (SubF e1 e2) = Sub e1 e2

Finalmente, adaptamos su código original y agregamos un par de pruebas.

substituteName :: String -> Int -> Expr -> Expr substituteName name newValue = ana $ /case Variable x | x == name -> NumberF newValue other -> project other testSub :: Expr testSub = substituteName "x" 42 (Add [Add [Variable "x"], Number 0]) replaceSubWithAdd :: Expr -> Expr replaceSubWithAdd = ana $ /case Sub x (Number y) -> AddF [x, Number (-y)] other -> project other testReplace :: Expr testReplace = replaceSubWithAdd (Add [Sub (Add [Variable "x", Sub (Variable "y") (Number 34)]) (Number 10), Number 4])

Una alternativa podría ser definir ExprF a solo, y luego derivar el type Expr = Fix ExprF . Esto ahorra algo de la placa anterior (por ejemplo, las dos instancias), a costa de tener que usar Fix (VariableF ...) lugar de Variable ... , así como lo análogo para los otros constructores.

Uno 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 functor ExprF 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.

{-# LANGUAGE DeriveFunctor, DeriveTraversable, TypeFamilies, LambdaCase, TemplateHaskell #-} {-# OPTIONS -Wall #-} module AnaExpr where import Data.Functor.Foldable import Data.Functor.Foldable.TH data Expr = Variable String | Number Int | Add [Expr] | Sub Expr Expr deriving (Show) makeBaseFunctor ''''Expr substituteName :: String -> Int -> Expr -> Expr substituteName name newValue = ana $ /case Variable x | x == name -> NumberF newValue other -> project other testSub :: Expr testSub = substituteName "x" 42 (Add [Add [Variable "x"], Number 0]) replaceSubWithAdd :: Expr -> Expr replaceSubWithAdd = ana $ /case Sub x (Number y) -> AddF [x, Number (-y)] other -> project other testReplace :: Expr testReplace = replaceSubWithAdd (Add [Sub (Add [Variable "x", Sub (Variable "y") (Number 34)]) (Number 10), Number 4])


Como un enfoque alternativo, este también es un caso de uso típico para el paquete uniplate . Puede usar Data.Data generics en lugar de Template Haskell para generar el repetitivo, por lo que si deriva instancias de Data para su Expr :

import Data.Data data Expr = Variable String | Number Int | Add [Expr] | Sub Expr Expr deriving (Show, Data)

entonces la función de transform de Data.Generics.Uniplate.Data aplica una función recursivamente a cada Expr anidado:

import Data.Generics.Uniplate.Data substituteName :: String -> Int -> Expr -> Expr substituteName name newValue = transform f where f (Variable x) | x == name = Number newValue f other = other replaceSubWithAdd :: Expr -> Expr replaceSubWithAdd = transform f where f (Sub x (Number y)) = Add [x, Number (-y)] f other = other

Tenga en cuenta que, en replaceSubWithAdd en particular, la función f se escribe para realizar una sustitución no recursiva; transform hace recursivo en x :: Expr , por lo que está haciendo la misma magia a la función auxiliar que ana hace en la respuesta de @ chi:

> substituteName "x" 42 (Add [Add [Variable "x"], Number 0]) Add [Add [Number 42],Number 0] > replaceSubWithAdd (Add [Sub (Add [Variable "x", Sub (Variable "y") (Number 34)]) (Number 10), Number 4]) Add [Add [Add [Variable "x",Add [Variable "y",Number (-34)]],Number (-10)],Number 4] >

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 usa descend en lugar de transform , transforma solo los elementos secundarios inmediatos que pueden darle control sobre dónde ocurre la recursión, o puede usar rewrite 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:

{-# LANGUAGE DeriveDataTypeable #-} import Data.Data -- in base import Data.Generics.Uniplate.Data -- package uniplate data Expr = Variable String | Number Int | Add [Expr] | Sub Expr Expr deriving (Show, Data) substituteName :: String -> Int -> Expr -> Expr substituteName name newValue = transform f where f (Variable x) | x == name = Number newValue f other = other replaceSubWithAdd :: Expr -> Expr replaceSubWithAdd = transform f where f (Sub x (Number y)) = Add [x, Number (-y)] f other = other replaceSubWithAdd1 :: Expr -> Expr replaceSubWithAdd1 = descend f where f (Sub x (Number y)) = Add [x, Number (-y)] f other = other main = do print $ substituteName "x" 42 (Add [Add [Variable "x"], Number 0]) print $ replaceSubWithAdd e print $ replaceSubWithAdd1 e where e = Add [Sub (Add [Variable "x", Sub (Variable "y") (Number 34)]) (Number 10), Number 4]