 Hello everyone welcome to Functional Count 2022. We have with us Simon Thompson here joining us from Canterbury UK and he's going to be talking on language independent refactoring through language specific rewrites. Over to you Simon. Okay thanks very much and thanks for the invitation to speak at Functional Count. I've been thinking about refactoring over the last 20 years or so and this is a reflection on some ideas I've come to more recently and in particular I want to think about talk about the way in which we can build refactoring tools to be as trustworthy as possible and some ideas that have come out from that. So let me share my screen. Okay let's get started. So just to say what is refactoring? I'm sure we all understand what it is but let me just give a definition to get us started. So refactoring is the idea that we are transforming code to improve it in some way without changing what it does. We may be changing how it does it but we're not changing what the code does. Okay in practice what does that mean? We still despite what people have been predicting for the last 50 years or so it's still the case that a program or a system or project is a large body of text. We haven't moved beyond using text as a way that we represent programs. So in practice what it means is transforming this large body of text so that it's still recognizable and acceptable to the person, the people who wrote it. So we have to make the change and on the one hand we have to make a change to make sure it's correct. We haven't broken the code. On the other we have to do it in a way that the code we can see, look at it we can still recognize it's the code that we wrote or be it modified a bit. So we have those two tensions. We want to make important semantic changes but we want to leave the surface the way it appears looking as close to what it was before it was transformed. Now what's happening under the hood? I've said that we're working with with text but of course we're not. We're working with a complex semantic object that includes type system, it might include types, it might include type classes, it might include static and perhaps dynamic types, it includes bindings so associations of names to the place where they're defined, it might include various kind of effects, it might include a macro system, it can include a whole lot of different complexities but what's interesting is that those are the things which have to be dealt with in a compiler and I'll come back to this that idea a bit later on in the talk. But we're working, crucial thing for the moment is that we're working with a complex semantic object and we have to reconcile editing that complex semantic object with the textual form being okay. So that's our requirement. So how do we give people assurance that what we're doing to their code is safe? Now this is really quite serious what we're doing is we're writing a system which says we're going to modify your code but trust us it will not break your system, it will not break your code. So we have to find ways as I say we have to find ways of convincing users that our tools don't break their code. Well what's the state of the art at the moment? It is to say is the code still okay and typically that means some sort of regression testing. So the assumption when people refactor is that if the code still passes all the tests it passed before then the refactoring has been successful but I don't think that's good enough in practice. It requires us to have written a really comprehensive test suite and in practice I fear a lot of people just have not done that. So first we'll want to approach more sophisticated ways of providing guarantees of correctness. I'll talk about those. So we can improve the way we check that the code does what it should do and then I want to answer a second set of questions which is about how we build the system. The way we build a system can give assurance that the system is built in such a way that it's transparent, that it is engineered in such a way that we can see how it's working and therefore it gives us confidence that the system is doing what it should be doing and I'm going to come to that a bit later on and that's where the language independence, language dependence comes in. But let me just talk a tiny bit more about approaches we can take to improving the assurance that we want to give people about refactoring. Now when we talk about assurance there are two things potentially going on. One is we can use, it's a technique we can use, we can use testing or we can maybe scale it up and use some sort of automation, some logic, technology, proof, and other automated logic approaches. And also we can think of doing two things. We can think of testing an instance of refactoring. So what do I mean by that? What I mean there is, for example, in this project we've renamed this function from foo to bar and we want to check in this project that the refactoring has been successful. So we're checking a particular refactoring in a particular project and that's why that's the situation that we see at the moment. People will do that, will do that testing, they will on the particular instance and then move forward if it's been successful. What else could we do? Well the first area where we could do something a bit more, a bit more sophisticated, giving us a bit more assurance is instead of relying solely on regression tests, on unit tests, we can start to use techniques from property-based testing because what are we checking here? We're checking that the code before and the code after the refactoring behave in the same way. Now in the case of a renaming what we would like to see is that every client of that renamed function, so every function that calls the renamed function has the same behavior before and after and we live in a functional programming world where property-based testing is available and so one way of testing a particular instance is to use property-based testing to check every client of that function that has been refactored and that gives us a way of automatically testing code beyond having unit tests to use. So in fact this we can do that without there being any unit tests written because what we're saying we're writing the new test which is the code before equals the code after and so and using PBT in a functional programming context will allow us to check those kinds of properties. So we can improve things even in this case of testing instances by scratching our heads a bit and seeing what extra technology we can use to support our to increase our assurance. But let's think about testing a refactoring itself. We have a refactoring engine, we have a tool for doing refactoring and let's see if we can test that whole engine and how could we think of doing that? Well one thing we can do is just take the property-based testing approach the quick check approach one stage further. We can generate programs, we can generate refactoring instances and then we can do property-based testing with random inputs. Now if again this is help if we're working in a world which is purely functional this kind of approach will just fly. Again we don't have to write any tests ourselves we can in an entirely in an entirely synthetic way we can test our refactoring and there are two ways we can do this one is the what I was suggesting earlier on we can check the old code against the new code that's a nice that's potentially that approach works fine. There's also a situation sometimes this is something that we would happen when we wrote refactoring tools for Erlang. There may be two refactoring tools around and one great way of checking their behavior is to check the two tools against each other and we did that with a we were writing a tool called Wrangler. We had some colleagues in Hungary who'd written a tool called Refactor Erl and we generated random programs refactored them under both of those both those tools and in fact found bugs in both tools because we're using each tool as an oracle for the other as it were but so we can use if we have multiple tools we can do this differential testing this was worked on by Danny Digg and his colleagues for Java refactoring tools but also in a purely functional world we can simply check the old code against the new code with randomly generated inputs. Okay so we can we can improve what we um what refact and this is work that I've done with Daniel Corpacci and colleagues in Elta at Vrusheloran University in Hungary some time ago but it's work that is still stands it's work that can be applied in Haskell or OCaml as well as in the case we were looking at which was Erlang. So we can improve on the testing side we can go from looking at simple instances to looking at a whole all the all instances if you like of the a particular refactoring but what can we do using more powerful technologies what can we do if we want to start thinking about using logic using proof this is not so well developed but work we have done I know sometime ago in the late 2000s I worked with Nick Sultana and what we built there was a formal model of what's going on in renaming refactorings we built what's a potentially name capturing lambda calculus and then proved that under the conditions that we normally have aside conditions for renaming refactoring if we implemented when we implemented that we were able to avoid name capture so we were able to preserve the semantics of the program that way. So we have done work in that area and also this is part of a a larger project again with with colleagues at Bush Laureate University to to build to scale up the the work that we've done to a more practical refactoring and and there we've been we've done some initial work on using the K framework which is a framework for for building operational semantics of languages using that as a a framework for doing proof and the work that I'm talking about later on here about language independence fits into that that work on refactoring templates and how they can be used so we've got we can we can take this fully formal approach and this is part of a long-term research project this is what we're doing but there is a fourth and this intriguing option which let me just put that in front of you here we're trying to use some some proof technology to show that a particular instance of a refactoring is is correct. You remember what we're doing in a refactoring is we're taking code and rewriting it so what I said earlier on what we can do is test whether the code before and after has the same behavior fine we can do that but why not could we indeed automate a proof that the code before and the code after has the same behavior every client of the renamed function has the same behavior before and after well one way we could do that is to write a proof that those two those two client functions excuse me those two client functions have the same behavior we could do that we could do it by hand but wouldn't it be nice if we could do it in an automated way and I think there is for small examples for toy examples we've been able to to do this using an SMT solver using an SMT solver bound to jahascal but that was a small a small experiment in general of course proving that two functions are equivalent is a hugely complicated it's a big task to proving two sorting functions for example simple function but if the if the if the way in which those functions is implemented are very different then it's very difficult indeed to think about how we would how we'd automate a proof of their equivalence but that's not the case here what we're doing is taking a code base and modifying in a very stylized way to replace one set of names by another set of names for example but the structure of the code is relatively unchanged so I think in principle this should be something that would be suitable for for automated proof we're not there yet but I think that's in there that's there is an interesting challenge I think for anyone in the audience who wants to take that up so just to to close this this part of the the talk I think one thing that I wanted to to get across is that we do have a number of different approaches to to providing assurance that a particular refactoring tool does as it should do using either testing or formal logic and working either at the instance level or at the whole refactoring so that's one approach on the other hand in some situations people don't people don't necessarily want complete assurance people are happy I mean for example I've heard people say well actually if your um if your tool is 90 correct that's better than nothing um I'll I'll solve the I'll fix the final 10 percent you by looking by by I checking the the places checking the problematic places perhaps the the places that that make the types is the type checker break so by performing refactoring and use the type checker to to push push it through that's something that we all do in practice so maybe we shouldn't worry too much about correctness and on the other hand um correctness sometimes isn't enough that people might be happy with 90 correct but they have to have their layout 100 correct and they have to have the layout looking as it was before um so there are there are the human factors to this are are tricky and as I say um there can also be a programming language um dimension to this a classical programmer might well say well if it if it type checks after the refactoring it's probably okay well yes and no 90 percent of the time you're right there might be cases where it type checks afterwards but the program has still changed okay but let's let's set aside the the the question of proof and testing for a moment and think about another way of providing assurance to people to people who are using our systems and that the way we can do that is through the way that we build the system these are open source systems people can scrutinize them they can see how they're built and because of the way they're built we can provide assurance and let's let's see um let's see what that means in a bit more detail so we want to build tools that support are easy to use they're easy to extend so they have components which people can repurpose and we want implementations to be straightforward what does a refactoring tool look like really you can see it from a very high level you can see it has these four stages we take the text and you can see that we the starting text is slightly different from the text at the end we have done a little refactoring there we start with text we end with text but really the crucial crucial stages are working over this complex data structure that represents the program and really there are two stages to that process in the first case we we do some substantial analysis of how the um of what the program does we look at its static semantics its types its effects we resolve pre-processor commands and so on and so forth and then we provide a transformation so how can we try and give some some structure and coherence to to those stages now the other thing to be aware of is that the first stage there is probably provided by a compiler no what every compiler does is it will um it will take it takes text it extracts the the the syntactic structure of a program but then given that syntactic structure builds a whole edifice of of other analysis on top and so I think one thing that we should anyone building a tool should certainly aim to do is to reuse as much from a compiler as possible and that pushes that pushes the assurance question back onto the compiler builder now how transparent how well built how well architected is the compiler how easy is it to extract phases from that and that can be different different languages can provide different levels of support or non-support for um for doing that when we first started working with um with Haskell ghc this was around the 2000 it wasn't it really wasn't practical to work with ghc and so we worked with another framework called programatica which was was aiming to build something that was was more usable and modular but in fact never got never got quite delivered um so we worked with that and it was easier to work with that it was easier to extract complex information from it on the other hand um we ran we had the problem that we were never able to deal with real Haskell that is Haskell as processed by ghc rather than Haskell from Haskell 98 and the fantastic work of Alan Zimmerman who took our initial work with the hair refactoring and took it and might to work with ghc has been has had a huge impact on making the the tool usable in practice so front end we can really we can really get from um get from a suitably engineered compiler so let's think about the transformation stage what can we do to to architect the transformation phase in such a way that it um can give assurance well let's if we're thinking about architecture there are really three three ways we might think of of that we think of of abstractions how can we how can we hide how can we build tools in such a way that they are layered appropriately and using the right abstractions allow us to to concentrate our effort at different layers on different aspects of of what's going on we can reuse potentially components from other places and we can build those libraries that support particular kinds of of operations over these these complex structures and as I say we we can use we can take the compiler front end really as a component that we feed into this this discussion another very nice feature and this is a an abstraction that is has proved very useful in the refactoring field is to say let's separate out because what what's going on what are we doing when we um when we're doing the transformation what we're doing is we're traversing a tree and we're making changes in that tree okay it's a tree it's a maybe it's a a dag a directed acyclic graph that's sharing there it's also decorated with lots of semantic information but what we're doing is we're traversing through this tree in some way and we can write each each refactoring as a as a recursive function versus the tree there's a really nice abstraction here that says separate out the transformation from the traversal and this is the approach of strategic programming done by the Stratigo team and built into things like Strafanski and Haskell which have now turned into scrap your boilerplate so you've turned into two frameworks for generic programming in Haskell but the the the key approach the key abstraction there is to separate what you're doing to transform the tree from the way the order in which you do it and that's what strategic programming does so that's a really nice abstraction in there um but what and I think that's it that's a key guide what we want beyond that if we can find it is something um that's simple enough that's general enough and is also implementable um and so the key insights I think of this I wanted to get across in this talk uh are these um and this was really inspired by work with Daniel Hopachi um from Elta I when I first started working in this area and for quite a long period after that I came to the conclusion it's impossible to think of doing generic refactoring every language is so particular that it's impossible to think of doing language independent refactoring but I I think what I learned from Daniel is that we can a key insight is that actually we can factor those refactoring we do in a particular language into a language independent part and a language dependent part and that actually improves the engineering of the refactoring tool by making that separation um a second insight which I'm not going to talk about anymore here is that I talked earlier about how important layout was work we did particularly with Huking Lee on the hair tool and and the Wrangler tool for Erlang was to separate out layout dependent and layout independent aspects of refactoring so build a layout independent library on top of a layout dependent one but let's not talk anymore about that let's stick with our um our insight about looking for language independent refactoring and I think just a parenthetical point here this is this isn't just for functional languages the examples here are from functional language but I think these abstractions will work for OO as well and that's another you know that's another discussion point so let's look at an example let's look at typical refactoring that we do um in a functional language we do renaming we do generalization where we take a bit of a function bit of a function definition abstracted out and and instead pass that information in through a parameter we reorder arguments and so on it's possible to see all those refactoring as examples in one particular pattern and that pattern is what we could call the function transformation scheme and it says to to make this refactoring happen you have to do two things you have to change the definition um and you have to change all the applications so it's a function it has um it has a definition and then it has uses throughout the program I'm writing this in a sort of the language independent way um just to to get across the point this could be Haskell this could be Erlang this could be um OCaml or whatever that's what we have to do and each something like renaming reordering arguments regrouping arguments generalizing can all be implemented in this particular way so you know when we if we what we do in renaming is we change the name and the definition we change every instance what we do in in um in swapping two arguments but two argument function is that we have to swap those arguments in the definition and then swap every um pair of actual parameters when we this is a more interesting example here this is generalization what we're doing on the left is that we have a function with a particular piece of functionality here three what we're doing on the right is we have abstracted that out into a parameter and then at every instance we have to pass that behavior in we've generalized we've got this is a refactoring that will enable um us to write a typical refactory where we say well okay we've got a particular piece of functionality but we can see we can use that in other contexts if we got rid of that particular parameter three and then we could use the two argument function um so we've got those that pattern so I've been able to describe these examples in a language independent way we talk about the definition we talk about um the uses but I've only been able to do that by hiding complexity and I think this was the this was the insight that I hadn't this was the insight that was new because just look at what happens in practice when I talk about function application what have we got on the left we've got some examples from Haskell we've got f applied to x and y simple um on the second line we've got f applied but f is applied as an infix operator in the third example we've got f partially applied in the um in a map because f is functions in Haskell are carried um and in the the bottom example I've got an operator section I said oh let's turn f into an operator and then I want to pass in um a value to the first for the first argument um so we don't just have function application in Haskell we have these other variants we have partial application we have um we have do it yourself infix and so what about Erlang well Erlang in a way is um is worse we have we've got a definition of f there we've got a use of f in the second line in a spawn now there f is being passed in as an atom and we have to recognize that that atom in fact refers to the definition of a function um and in the bottom line we have this this Erlang notation when you pass a function in you have to say oh I want to pass in the function um g the the one argument function called g so applications are not all directly visible we have ways in which they're hidden and the ways in which they're hidden the the indirect ways you can you can produce function applications are um are particular to the the programming language so how can we factor this into something that is is um language specific but still has an aspect of language independence um I'm just talking about that partial application the other way I'm just explaining a reminder of that let's talk about how we can actually turn this into something that is um is language both language independent and language dependent here's our function transformation scheme how can we think about doing that factor factorization the way we can do it is by having a generic transformation so this is the way we write the transformation is the same pretty much in every language but then what we have specific to each language is a way of rewriting um there is a delts of what we see and this has implications for the way that we we write the way that we build tools but also implications for the way that we might try and prove those tools correct so let me just run through an example so here's the um here's the function transformation scheme we describe the new the new function f new and what we do is the language independent approach to this is to say just give me a description of the old function in terms of the new so how I get the old function using the new function so here we are we've got um the the the generalization example we saw earlier on it's just using Erlang syntax now and here I'm saying my new function is described at the top and here is my description of the old function and this is using Erlang notation so we say f old equals fun of x returning f new apply to x and three so I give that single description and we will see that we can use that plus some language specific rewriting to give us what we need to prevent this particular transformation so let's have a look at what at how this works so so here we are is is an application of the scheme the scheme is up there in in gray and what I've got now is a is an example of um a definition that that uses f so my old definition of g calls f on z plus two what do I do I in order to transform that I take f and I replace it with what um what the second line says so I rewrite it to so I write it as this application of this um function expression so you could say okay I've done my refactoring I've got a new version of g written in terms of the new f does my my code will work um you know all my client code is is going to work properly but of course the problem here is that it's completely unreadable it's and it's unacceptable so I've made the semantic transformation in this one step but the only way it's going to be acceptable to to readers of this is for me to to do some further work to make this actually readable and acceptable and of course what I do here is I say well I've got a function expression I've got a lambda expression applied to a value I can reduce that I've got a redux and what I can do is reduce that to um to this now you could say I could have done that anyway I don't have to do this with the this single function f there but the point is doing it this way has meant that I just had to give a single description which I can apply in other situations like this one I've now got a a a symbolic reference to this function f and what I all I need to do here is replace that function f with the same function expression as before and now I have um my new definition of g and that's I can't simplify that any further but what I've got is something that um that works just using that single semantic transformation I didn't have to say oh this is how you transform function applications this is how you transform a um the function past as a as an argument that single description of the transformation as a um described in the old in terms of the new and then rewriting when I can has given me um given me what we saw on the previous slide and has also given me um what we see here in terms of um the the redefinition of um the redefinition of um of g and given the way that Erlang works this is the best we can do right there's nothing else um um nothing else we can do there to to improve that that's if we are passing functions in to higher order functions and we transform them then we will finish up passing in functions expressions instead of simple um simple function names okay so there we've seen how the scheme can be applied but here's the rub we've got um we've got our transformation we said this is how you describe f of old in terms of f of new how do we know whether this is correct well the answer is we should check that this actually is a true assertion we've got the definition of f of old there we've got the definition of f of new and we have said this is how the two are related well let's prove it and it's not difficult in that situation to prove precisely um precisely that result um oh sorry and I thought I had that in in a slide obviously that slide has disappeared but you know if we rewrite um if we rewrite the instance of of um of f of new in there we get to um we can see that we do indeed have what we should have so we get the correctness of that refactoring just hinges on the single way that we rewrite the old to the new okay I'm getting close to the and we are we can begin to formalize this we're looking at um working on the the framework we're looking at is um is core Erlang so we're looking at um proving results of um about um about these sorts of transformations for Erlang either in um in the in the cop proof assistant using using um some sort of of logic okay but just to say there is more than one example of this and let me let me just show this quickly I talked there about transformations that were based on um based on changing the way a function was defined we can do similar transformations in terms of the way that our data is described so here a typical refactoring we might do we might have an extra type of expressions um here we've got numbers and multiplications we might say oh let's refactor that so we have we just have a two constructors and a number and then a binary operator um and we fact that we lift out the the particular add and multiply as examples of the the binary operator and here we see that we can describe the old constructor and apply to e and f in terms of the new that's the bin constructor applied to add e and f and we can use a similar approach there rewriting approach to transform this definition this recursive evaluator from the top definition to the to the lower one so what we're doing there is we're applying these rewritings not just inside expressions but inside patterns so on the um on the left hand side of a pattern match as well as on the as well as on the right so that's a different kind of um of transformation and it's one which um the correctness criterion um functions over this expression type should have the same behavior um but modulo the translation of values we can't expect them to have exactly the same behavior because we're representing data representing data in a different way so we could say well all functions that return things independent of expression should should have the same behavior that could be sufficient but we can also say if what we do is um a function of our expression that return expressions then if we apply the translation then those um those functions should still behave correctly so we have a similar correctness criterion there um and just to look at the correctness question and I think we've got a real win here that what we're now doing with this with this approach is that we have one transform per refactoring so what we did in the function case is we simply describe how the old function is implemented in terms of the new and replace all the old functions with that description so we have one transformation and one proof that that um that thing is correct and then what we might do is some generic if you like rewriting to tidy up the code but those rewrites are the ones that apply in every every sort of refactoring and we just use them to tidy up um tidy up so that the code becomes recognizable and these are we might use a number of them but we only need to prove each of them once not not for each refactoring but but for each language and so we've done we've got this factoring into a single correctness criterion per refactoring and then proving correctness of all the the the rewriting code now and I think this is something that you know really allows us to to think in a in a serious way about structuring a refactoring tool to support transparency of architecture and also to try to support a principled approach to proof so you know this this is something that we we're looking forward to implementing looking forward to moving this forward for the next few years but one thing that is is um was a surprise was in fact this work what this work has done is rediscovered something that we did 10 years ago on migrating um when you when you have a library migrating for an old api from the library to a new api and just just to take you through this code this was worked with working with hookingly just a simple example and there was a very nice example from the lang world there was a big change from the reg x library to the re library these are libraries for regular expressions that's quite substantial changes in the way that's the the structure of arguments to and the the way that you did indexing into regular expressions and so on quite substantial changes and they in the paper um you saw just back just there we we showed how you could um you could mitigate all those all those things and the approach is this i've just got some some simple pictures here if you imagine you have an api with a client and then you you create a new api so the client code doesn't fit that api any longer what do you do to mitigate that well you build an adapter so you you build an adapter you wrap around your your client code to um to allow or you wrap around the api so that your client code can still use it but the key here is actually what you want to do is then absorb the adapter into the client and rewrite it so you have a new client using the new api and that's what we showed there now this it's come it's come full circle we're still using the same technique but we're now saying in fact you can think of a refactoring as api changes they're really all about how you describe the old code in terms of the new code and so we've we this was an insight which was really not um we were not expecting so in particular in the in the the case of the all those the function patterns it's really about changing api and the way this approach of writing the adapter in which case described in the old in terms of the new and then rewriting the um using generic rewriting to um a non refactoring specific rewriting to absorb that into the code allows us to do things which give us idiomatic code so we're keeping we're able to to give code that's recognizable and um is correct um and hey Simon I just want to let you know we'll reach the end of time but we can probably take a few minutes yeah okay no that's fine I was just and I've got to my end slide pretty much so what's next looking at other patterns looking at data looking at proof looking at um extending this work and just thanks to Daniel Judith Peter Marton Dominic the Erlang ecosystem foundation who given us some funding for um incorporating Wrangler into the Erlang language server the rotor project team with whom I worked on a project in the UK and UKRI for support and um Nick Sultana who worked with me on the the proof work and then just to leave you with this that's that we can we can build um we can build this this degree of language independence factor into language between language independent language dependent by fact in between generic transformations and language specific rewrites and now I'm going to stop and thanks very much and apologies that I did run a tiny bit over but that's it from me thanks a lot Simon thanks for the wonderful talk um