haskellgeneric-programmingscrap-your-boilerplate

Haskell's Scrap Your Boilerplate (SYB) - applying transformation only once instead of everywhere


What's the best way to apply a transformation to a tree only once instead of everywhere using SYB? For instance, in the following simplified expression, there are several instances of Var "x", and I want to replace the first instance with Var "y" only.

data Exp = Var String | Val Int | Plus Exp Exp |...

myExp = Val 5 `Plus` Var "x" `Plus` Val 5 `Plus` Var "x" ...

This can't be done using the everywhere combinator since it will try to transform all instances of Var "x" to Var "y".

EDIT (after posting): Looks like somewhere is what I am looking for.


Solution

  • Being a SYB beginner myself, my answer is more like a guess, but seems to work.

    Combinator somewhere recommended by Neil Brown probably doesn't do exactly what you want. It's defined as

    -- | Apply a monadic transformation at least somewhere
    somewhere :: MonadPlus m => GenericM m -> GenericM m
    
    -- We try "f" in top-down manner, but descent into "x" when we fail
    -- at the root of the term. The transformation fails if "f" fails
    -- everywhere, say succeeds nowhere.
    -- 
    somewhere f x = f x `mplus` gmapMp (somewhere f) x
    

    where

    -- | Transformation of at least one immediate subterm does not fail 
    gmapMp :: forall m. MonadPlus m => (forall d. Data d => d -> m d) -> a -> m a
    

    But we need to transform at most once. For this it seems that gmapMo will be better:

    -- | Transformation of one immediate subterm with success 
    gmapMo :: forall m. MonadPlus m => (forall d. Data d => d -> m d) -> a -> m a
    

    So I made my own combinator:

    {-# LANGUAGE DeriveDataTypeable, RankNTypes #-}
    import Control.Monad
    import Data.Maybe (fromMaybe)
    import Data.Data
    import Data.Typeable (Typeable)
    import Data.Generics.Schemes
    import Data.Generics.Aliases
    
    -- | Apply a monadic transformation once.
    once :: MonadPlus m => GenericM m -> GenericM m
    once f x = f x `mplus` gmapMo (once f) x
    

    If the substitution fails, it returns mzero, otherwise it returns the substituted result. If you don't care if the substitution fails (no matches), you could use something like

    once' :: (forall a. Data a => a -> Maybe a) -> (forall a. Data a => a -> a)
    once' f x = fromMaybe x (once f x)
    

    With these, we can do some replacements:

    data Exp = Var String | Val Int | Plus Exp Exp
      deriving (Show, Typeable, Data)
    
    myExp = Val 5 `Plus` Var "x" `Plus` Val 5 `Plus` Var "x"
    
    replM :: (MonadPlus m) => Exp -> m Exp
    replM (Var "x") = return $ Var "y"
    replM t         = mzero
    
    main = do
        -- `somewhere` doesn't do what we want:
        print $ (somewhere (mkMp replM) myExp :: Maybe Exp)
    
        -- returns `Just ..` if the substitution succeeds once,
        -- Nothing otherwise.
        print $ (once (mkMp replM) myExp :: Maybe Exp)
        -- performs the substitution once, if possible.
        print $ (once' (mkMp replM) myExp :: Exp)
    
        -- Just for kicks, this returns all possible substitutions
        -- where one `Var "x"` is replaced by `Var "y"`.
        print $ (once (mkMp replM) myExp :: [Exp])