Coder Social home page Coder Social logo

retrie's Introduction

Retrie is a powerful, easy-to-use codemodding tool for Haskell.

Install

cabal update
cabal install retrie

Example

Assume you have some code, including functions like foo:

module MyModule where

foo :: [Int] -> [Int]
foo ints = map bar (map baz ints)

Someone points out that traversing the list ints twice is slower than doing it once. You could fix the code by hand, or you could rewrite it with retrie:

retrie --adhoc "forall f g xs. map f (map g xs) = map (f . g) xs"

Retrie applies the equation as a rewrite to all the Haskell modules it finds in the current directory:

 module MyModule where

 foo :: [Int] -> [Int]
-foo ints = map bar (map baz ints)
+foo ints = map (bar . baz) ints

Of course, now you might find this code more difficult to understand. You also learn that GHC will do this sort of optimization automatically, so you decide to undo your rewrite:

retrie --adhoc "forall f g xs. map (f . g) xs = map f (map g xs)"

Now you have your original code back.

Other Sources Of Equations

  • The --adhoc flag, above, admits anything you can specify in a RULES pragma.
  • You can apply actual RULES pragmas, in either direction, with --rule-forward and --rule-backward.
  • Since definitions in Haskell are themselves equations, you can unfold (or inline) function definitions with --unfold. You can also fold a function definition with --fold, replacing an instance of the function's body with a call to that function.
  • Type synonyms are also equations. You can apply type synonyms in either direction using --type-forward and --type-backward.

To try some examples, put the following into MyModule2.hs:

module MyModule2 where

maybe :: b -> (a -> b) -> Maybe a -> b
maybe d f mb = case mb of
  Nothing -> d
  Just x -> f x

type MyMaybe = Maybe Int

{-# RULES "myRule" forall x. maybe Nothing Just x = x #-}

foo :: Maybe Int
foo = maybe Nothing Just (Just 5)

Then try the following rewrites and check the contents of the module after each step:

retrie --type-backward MyModule2.MyMaybe
 module MyModule2 where

 maybe :: b -> (a -> b) -> Maybe a -> b
 maybe d f mb = case mb of
   Nothing -> d
   Just x -> f x

 type MyMaybe = Maybe Int

 {-# RULES "myRule" forall x. maybe Nothing Just x = x #-}

-foo :: Maybe Int
+foo :: MyMaybe
 foo = maybe Nothing Just (Just 5)
retrie --unfold MyModule2.maybe
 module MyModule2 where

 maybe :: b -> (a -> b) -> Maybe a -> b
 maybe d f mb = case mb of
   Nothing -> d
   Just x -> f x

 type MyMaybe = Maybe Int

-{-# RULES "myRule" forall x. maybe Nothing Just x = x #-}
+{-# RULES "myRule" forall x. case x of
+            Nothing -> Nothing
+            Just x1 -> Just x1 = x #-}

 foo :: MyMaybe
-foo = maybe Nothing Just (Just 5)
+foo = case Just 5 of
+  Nothing -> Nothing
+  Just x -> Just x
retrie --fold MyModule2.maybe
 module MyModule2 where

 maybe :: b -> (a -> b) -> Maybe a -> b
 maybe d f mb = case mb of
   Nothing -> d
   Just x -> f x

 type MyMaybe = Maybe Int

-{-# RULES "myRule" forall x. case x of
-            Nothing -> Nothing
-            Just x1 -> Just x1 = x #-}
+{-# RULES "myRule" forall x. maybe Nothing Just x = x #-}

 foo :: MyMaybe
-foo = case Just 5 of
-  Nothing -> Nothing
-  Just x -> Just x
+foo = maybe Nothing Just (Just 5)
retrie --rule-forward MyModule2.myRule
 module MyModule2 where

 maybe :: b -> (a -> b) -> Maybe a -> b
 maybe d f mb = case mb of
   Nothing -> d
   Just x -> f x

 type MyMaybe = Maybe Int

 {-# RULES "myRule" forall x. maybe Nothing Just x = x #-}

 foo :: MyMaybe
-foo = maybe Nothing Just (Just 5)
+foo = Just 5
retrie --type-forward MyModule2.MyMaybe
 module MyModule2 where

 maybe :: b -> (a -> b) -> Maybe a -> b
 maybe d f mb = case mb of
   Nothing -> d
   Just x -> f x

 type MyMaybe = Maybe Int

 {-# RULES "myRule" forall x. maybe Nothing Just x = x #-}

-foo :: MyMaybe
+foo :: Maybe Int
 foo = Just 5

Motivation

Refactoring tools fall on a spectrum. At one end is simple string replacement (grep and sed). At the other is parsing an abstract-syntax tree (AST) and directly manipulating it. Broadly, the tradeoffs are:

  • String manipulation

    • Hard to write: Essentially need to hand-roll a parser using a regular expression.
    • Limited power: Find and replace.
    • Fast.
  • AST manipulation

    • Hard to write: Requires extensive domain knowledge about language/parser.
    • Very powerful.
    • Slow: Parsing and traversing large codebases is expensive.

Retrie finds a middle ground:

  • Easy to write: Equations are defined using syntax of target language.
  • Powerful:
    • Equations are more powerful than regular expressions.
    • Rewrites can be scripted and enforce side-conditions (see below).
  • Fast: Search space is narrowed using grep before parsing. Time is (morally) proportional to the number of matches, not the number of target files.

Features

  • Power
    • Can rewrite expressions, types, and patterns.
    • Matching is up to alpha-equivalence.
    • Rewrites are equational: a quantifier that appears twice in the left-hand side must match the same expression (up to alpha-equivalence).
    • Inserts imports. (As specified by the user, and automatically in some cases.)
    • Rewrites can be scripted and have side conditions.
    • Uses GHC's parser, so supports all of the de facto Haskell language.
  • Correctness
    • Local scoping is respected. (Will not introduce shadowing/capture bugs.)
    • Impossible to match/rewrite an incomplete expression fragment.
    • Parentheses are automatically removed/inserted as needed.
  • Whitespace
    • Whitespace is ignored when matching. No fiddling with \s.
    • Whitespace is preserved in resulting expression.
  • Will not rewrite in comments. Existing comments are preserved.
  • Respects git/hg ignore files.

See retrie --help for a complete list of options.

Scripting and Side Conditions

Retrie can be used as a library to tackle more complex rewrites.

Consider the task of changing the argument type of a function from String to an enumeration:

fooOld :: String -> IO ()

data FooArg = Foo | Bar

fooNew :: FooArg -> IO ()

Retrie provides a small monadic DSL for scripting the application of rewrites. It also allows you to intercept and manipulate the result of matching the left-hand side of an equation. Putting those two together, you could implement the following refactoring:

{-# LANGUAGE OverloadedStrings #-}
module Main where
  
import Retrie
  
main :: IO ()
main = runScript $ \opts ->
  [rewrite] <- parseRewrites opts [Adhoc "forall arg. fooOld arg = fooNew arg"]
  return $ apply [setRewriteTransformer stringToFooArg rewrite]
  
argMapping :: [(FastString, String)]
argMapping = [("foo", "Foo"), ("bar", "Bar")]
  
stringToFooArg :: MatchResultTransformer
stringToFooArg _ctxt match
  | MatchResult substitution template <- match
  , Just (HoleExpr expr) <- lookupSubst "arg" substitution
  , L _ (HsLit _ (HsString _ str)) <- astA expr = do
    newExpr <- case lookup str argMapping of
      Nothing ->
        parseExpr $ "error \"invalid argument: " ++ unpackFS str ++ "\""
      Just constructor -> parseExpr constructor
    return $
      MatchResult (extendSubst substitution "arg" (HoleExpr newExpr)) template
  | otherwise = return NoMatch

Running this program would create the following diff:

 module MyModule3 where
  
 baz, bar, quux :: IO ()
-baz = fooOld "foo"
+baz = fooNew Foo
 
-bar = fooOld "bar"
+bar = fooNew Bar

-quux = fooOld "quux"
+quux = fooNew (error "invalid argument: quux")

Defining the stringToFooArg function requires knowledge of both the Retrie library and GHC's internal AST types. You'll find haddock/hoogle invaluable for both.

Reporting Bugs/Submitting Patches

To report a bug in the result of a rewrite, please create a test case (example) and submit it as an issue or merge request.

To report other bugs, please create a GitHub issue.

Build Status

License

Retrie is MIT licensed, as found in the LICENSE file.

retrie's People

Watchers

 avatar

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.