
Codebase Refactoring with Go - junke
https://talks.golang.org/2016/refactor.article
======
blixt
I think the proposed alias has true merit in Go as has been shown by Russ Cox
and in other places. What I'm a bit confused about is why it's sold as a
transitionary tool but implemented as a feature.

If it's so easy and clean to use it may become a misused feature, causing
developers to write "temporary" code that just lingers around forever because
it's more convenient to let it be than to actually move to the new API. I'd
draw parallels with imports in Go versus imports in Python. I would wager that
a great majority of Python repos out there have lingering imports in some
module that doesn't need to be there. But in Go I can guarantee that there are
0% unnecessary imports.

For example, the declaration could be obvious that this is for compatibility
as opposed to a feature:

legacy const FUN_THINGS => funner.Things

Also, when compiling a package using the FUN_THINGS constant you'd be warned:

./main.go:10: using legacy alias: "clowns.FUN_THINGS" has moved to
"funner.Things"

These small things would help prod developers to actually fulfill the
transition as opposed to letting it linger.

~~~
dilap
So far, Go doesn't have warnings, which is great. Either it's an error, or
it's nothing.

This is great IMO, because I've seen too many codebases that just end up
having infinite amounts of warnings that never get fixed, making them (1)
useless and (2) annoying.

(Extraneous imports is a great example of something that in most languages
would probably be a warning (or nothing); I remember in the early days people
bitched like crazy about it, but on balance it's really really nice.
(goimports also removed a lot of the pain.))

I think it's clear that if this goes in, people will use it beyond just
refactoring. Honestly, I'm ok with that, if it's restricted to just types.

Here's something I've done from time to time:

func now() time.Time { return time.Now(); }

Just because typing time.Now() everywhere was getting tedious.

Having a type that's an explicit alias doesn't seem too bad to me.

type Time = time.Time

Sure, it's an extra step of indirection, but a very easy one.

(I feel like having variable aliases would be much more confusing, because
variables are mutable.)

    
    
       ===
    

And it's not like the situation is that different from right now. For example,
maybe I have some package with an api like:

    
    
        func DateMagic(t time.Time) time.Time
    

OK, super-obvious what it's taking.

Now maybe I'm using aliases and you see:

    
    
        func DateMagic(t Time) Time
    

Hmm, not quite as obvious, now you have to go lookup

    
    
        type Time = time.Time
    

But still today I can do

    
    
        func DateMagic(t x.Time) x.Time
    

Hmm what's x?

Oh

    
    
        import x "time"
    

Of course it could get a little pathalogical...

    
    
        func DateMagic(t DumbName) DumbName
    
        type DumbName = time.Time
    

but I feel like you're really trying there.

~~~
blixt
I don't think realiasing exported signatures is a good idea, in any language.
However, doing what you do in your package as a private function is perfectly
fine, that is for your own convenience.

Regardless, I don't think the argument that the Go maintainers are making is
that this is about number of characters saved. The point of an alias in the
first place is to make it more feasible to carry out breaking API changes
(which are an important part of any maturing API).

So if aliases are introduced for the purposes of making API transitions
possible over multiple commits, then I think they should be clearly designed
as such. Which means that it shouldn't tempt people to do what you just
described in your comment. Maybe that wouldn't be a bad thing, but if that
becomes the ultimate use for the aliases, that use case should play a bigger
role in the design of aliases.

Because right now, as far as I can tell, they're being designed mainly for API
transitions.

~~~
dilap
Well, I could easily have made it "func Now" or whatever in my current
package.

The point is, adding type alias just gives you the ability to do for types
what you can already do for variables and functions.

It's true that the driving case (as presented by rsc) is co-reorganizations,
but I think inevitably it'll get used for more than that. (Which is what many
people who are opposed are afraid of.)

I would not personally be a fan of making the feature so deliberately painful
to "discourage" use. Either add it and make it as nice as you can, or don't.
Why add ugly things to your language on purpose? :)

------
jcrites
I think it's fantastic that this problem has people's attention.

I have long wished that _code changes_ to a repository could be accompanied by
_code refactorings_ that are intended to be applied to code using that
repository. For example, if you rename f() to g(), then you could accompany
this by a refactoring that transforms existing callers of f() to use g() as
well. I'd envision this as a build step that tells you that automated repairs
are available.

The refactoring could be a small but limited program of its own, that is
evaluated against the program abstract syntax graph, and that can be as
powerful as is warranted or needed to properly transform programs using the
code. Moving or renaming code could be a relatively simple type of
refactoring. However, if you've renamed f(int) to g(int, int) such that
callers of f(N) should call g(N, 0), then a slightly more complex refactoring
script could handle that too. I would think of these refactoring scripts as
something like how Git treats changes during a rebase: if you are far behind
you might need to apply multiple of them to your code base in sequence to
bring code up to date.

The article points out an important need for gradual repair to be possible.
Along with a way to express that transition backwards-compatibly for a period
of time, an automated way to apply the refactoring steps could make adoption
even easier. In this fashion, refactoring and improvements could have far
lower costs for libraries, APIs, etc. than they do in today's languages.

~~~
aikah
> an automated way to apply the refactoring steps could make refactoring a
> breeze

It is called alias, a feature that should have been there a long time ago but
"thanks" to a minority of gophers it's still not there. Go fundamental design
issue is that it conflates namespaces with urls. It's not something that can
be fixed by a third party tool.

~~~
jcrites
I'm not a Go user myself, and my comment is about programming in general
rather than Go specifically (though Go seems to be leading the way in this
area).

Does alias help Go programs automatically transform themselves? Alias seems to
provide direct compatibility between types, but doesn't seem to provide any
way to automatically refactor code based on the alias, does it?
[https://github.com/golang/go/issues/16339](https://github.com/golang/go/issues/16339)

Basically what I'm thinking is something like an alias feature where when A
has been renamed to B, such that the name A has been left for backwards
compatibility as an alias to B, then an automation layer could also help by
automatically offering to rename A to B in existing code.

~~~
skybrian
I think the reason it's not normally done is that for small open source
projects, there are only a handful of usages and each author can easily do it
manually. For a large monorepo, we do have (or are building) specialized
search-and-replace tools to replace deprecated API usages with usages of the
new API.

To scale this in the open source world, perhaps someone could search Github
for Go projects and send out pull requests automatically. But people writing
non-opensource code would still be pretty much on their own.

~~~
jcrites
Thinking really blue-sky:

I'm imagining the refactoring rules as something that could perhaps ship
alongside the library as part of its development release. Like, let's say
we've released the latest version of our library which renames A to B. The
release could include a refactoring rule that describes how to update from
previous versions of the library to the latest version: a rule that identifies
all references to A and replaces them with B. References that tooling cannot
identify and fix can also still work for a time via the alias feature.

To make these rules easy to author, perhaps we could provide some form of
assistance at the source control layer -- perhaps we could infer and propose
refactorings based on the diffs that we see. If we see a diff renaming the
method A to B, then we could see that and offer to construct a refactoring
rule for all consuming code. Or with full IDE support, the IDE refactoring
tool could call into the language service refactoring function to construct
the rule.

To refactor in this language and platform, you'd rename your method, stage the
commit, and then ask the language to analyze the change. It would detect the
rename, offer to create a refactoring rule, and then you could accept that
rule and apply it to your own code base (internal usage). You could have the
option to save that refactoring rule as part of the package release. It could
say: when consuming code upgrades from version 7 of this library to version 8
(or commit hash xxx to yyy), apply these refactorings. Indeed, even if the
library does not ship refactoring rules -- or as an alternative to that model
-- users could run the inference tool on the diff of changes to the library
source. Shipping refactoring rules in the release would allow this scheme to
work even if users don't have access to library source, though.

Imagine our library has hundreds of consumers on GitHub. When they next pick
up a version of our library, and try to build the source, our build system
could inform the user that refactorings are available and offer to apply them.
The user clicks "accept", reviews the changes, and hopefully the package
builds and its tests pass after that.

If we make this system really reliable, then we could apply these changes as
the default course of action. Ideally the user would apply the refactorings
and commit the changes to their source code, but even if the user doesn't, we
could apply the changes to a temporary copy every time they build. Or the
compiler could do it semantically for them, depending on how the rules work.
This way even users who aren't willing to actively participate can still
benefit from the capability. If your consuming package falls far behind the
latest library, then the set of refactorings you need to apply to your code in
order to build it could grow quite large and brittle, but it may still work (A
changes to B today, and is moved into another package tomorrow). Like a rebase
of many commits, you'd apply the rules one by one.

The goal would be to make it really easy to ship these refactoring rules as
part of releases as a library vendor, and really easy to apply the rules as a
consumer. The typical change would be small quality of life changes like
renames and moves.

I wonder how a capability like this could transform the way we release
software. Today, releasing a breaking change is anathema. It's anathema
because we know it causes massive pain for users. If we had the ability to
ease that pain, and make the upgrade process really easy or even automatic,
then it could perhaps significantly change the way in which we think about
interface contracts. Everyone who builds libraries is familiar with a time
where you got the interface wrong, and really wish you could change it, but
it's too late now because the library is too entrenched. This kind of system
would make it possible for us to dig ourselves out of those problems and
continuously improve even codebases that are widely used. Of course I have my
head in the stars at this point, but I believe that all of this is plausible.

[I'm not a Go user, but these concepts have been something I've wanted to
explore as features of a pet programming language I've been designing off-and-
on.]

~~~
stcredzero
_Of course I have my head in the stars at this point, but I believe that all
of this is plausible._

It's not only plausible. I know of environments where it would be easy to
code-up.

~~~
smaddox
And yet interface compatability/continuity would still be important unless you
can automatically change all of the deployed instances without inducing any
race conditions or transition issues. This can only be done through a
backwards compatible layer or bifurcation of the install base. So really,
nothing would be fundamentally different.

~~~
stcredzero
_And yet interface compatability /continuity would still be important unless
you can automatically change all of the deployed instances without inducing
any race conditions or transition issues._

The VisualWorks Package system could theoretically be applied at runtime. It
would create a shadow of the meta-level objects, then the new meta-level could
be applied atomically with "shape changes" of instantiated objects happening
all at once.

This could 1) still incur a pause in the program and 2) in practice, bugs
surfaced preventing the widespread use of the facility in running servers.

------
pcwalton
I agree with this idea, and I'm glad that the Go team has come around to this.
:)

Type aliases end up being very important when you have things like complicated
closure types and generics. They are also useful for migration, as the article
notes. It's also nice for packages to be able to reexport names from other
packages: perhaps your library depends on some internal packages that you only
want to expose a few types from.

One word of advice for Go developers if this is implemented: Pay attention to
getting error messages in the compiler right. Ideally you want to refer to
types by the name the programmer was using at the place the error occurred,
not the underlying internal name. This requires hanging on to typedef
information through more internal stages of the compiler than just name
resolution. Clang does a great job of this with its "a.k.a." functionality.

------
stcredzero
One issue with gradual refactorings, is that such projects can be abandoned,
resulting in harder to read code. Such things could accumulate, like unused
imports would if Go didn't have a mechanism to prevent that.

------
pdeuchler
As someone who drank the Golang koolaid this proposal kind of infuriates me.

Time after time, i've had to proselytize that Go is a good language precisely
because of its simplicity and restrictions. Simple, explicit, easy to write,
easy to understand, and most importantly easy to maintain code is the payoff
for not having generics, not having syntax niceties, limited meta-programming,
having to type out boilerplate, etc. etc.

We've been continually told that our use cases for missing features are just
opportunities to explore the existing tooling to solve our problems, and to be
honest it really hasn't been terrible advice. In fact, i've used the existing
go tooling myself at work to do semi-large scale refactoring of the kind that
aliases are supposed to solve. It wasn't terrible, and the solution ended up
being rather clear and straightforward once I got over my aversion to code
generation.

We've also been told that we need to program better to get the most out of
Go... use interfaces, version APIs and include that versioning in import
paths, separate concerns religiously, almost fanatically. If we just follow
these rules then we'll experience Go nirvana and all will be well.

And yet when it comes time to practice what they preach, instead of having to
take the same path as everyone else, Google just throws its weight around as
de facto owners of the language and adds a feature to make their lives easier.
A feature that doesn't even apply to most users of the language (the original
argument for type aliases was for refactoring of extremely large codebases).
Type aliasing seems like a huge outlier in the KISS philosophy of Go, and
regardless of syntax or technical concerns the fact that Google can on a whim
break all of their previous Ivory Tower positions that us plebes have had to
deal with doesn't sit well with me.

Oh, and lets not forget that there was some sketchiness about the technical
reasons for wanting type aliases at the beginning (we were told it was
_absolutely necessary_ for either something in the Context package or in k8s,
i don't remember precisely) which magically resolved itself without aliases,
and the very first PR (for some experimental GUI package) using aliases was an
example of _exactly_ the kind of poor practices that aliases allow, which we
were assured was never going to happen because Google would only use aliases
in the std lib if absolutely necessary.

I guess I shouldn't be surprised at this, but I'm pretty disappointed that
it's already happening when we're still in 1.0. If the Go maintainers truly
want Go to continue it's current trajectory and not turn into the next
overwrought Big Co language they'll have to be as resistant to change from the
inside as they are to change from the outside.

Edit: reworded first pp

~~~
aikah
> limited meta-programming,

Go reflection is one of the most complicated reflection I've ever seen in any
language. Go's / C interop is one of the most convoluted implementation in
existance. To pretend that Go is simple at first place is a lie. Go is not
simple, it has a limited syntax, which is quite not the same. Go will evolve,
whether you like it or not, even if it takes a whole new generation of Go
developers and maintainers to get there.

~~~
pjmlp
This is what I keep saying, history has proven that languages that eschew
complexity and happen to enjoy mainstream adoption, end up turning into the
thing they were fighting against.

------
kminehart
I really like Go. It's a good language.

------
anoncoder007
You can learn more about Golang here
[https://www.livecoding.tv/learn/golang/](https://www.livecoding.tv/learn/golang/)

