Skip to content

Warn for type constructor aliases w/ suggestions#2932

Closed
justinwoo wants to merge 1 commit intopurescript:masterfrom
justinwoo:type-constructor-alias
Closed

Warn for type constructor aliases w/ suggestions#2932
justinwoo wants to merge 1 commit intopurescript:masterfrom
justinwoo:type-constructor-alias

Conversation

@justinwoo
Copy link
Copy Markdown
Contributor

@justinwoo justinwoo commented Jun 10, 2017

This is a PR for the slightly crazy idea I had to ban simple* type aliases. I chose to just make a warning for now with a suggestion that writes out a newtype form.

* In this case just meaning "is a type constructor on its own"

In screenshots:

screen shot 2017-06-10 at 2 56 49 pm

screen shot 2017-06-10 at 2 56 55 pm

screen shot 2017-06-10 at 2 57 03 pm

@no-longer-on-githu-b
Copy link
Copy Markdown
Contributor

Could you explain what "simple" means?

@kritzcreek
Copy link
Copy Markdown
Member

Does this also warn in situations where we're using the synonym arguments in multiple positions, or are applying parts of the aliased type constructor?

For example in Halogen:

type ParentHTML f g p m = HTML (ComponentSlot HTML g m p (f Unit)) (f Unit)

@hdgarrood
Copy link
Copy Markdown
Contributor

I think type Pair a = Tuple a a should be allowed without a warning, and as far as I can tell it would produce a warning with this code. What do we think about only warning when the type arguments list is empty?

@no-longer-on-githu-b
Copy link
Copy Markdown
Contributor

Check should be applied after eta reduction.

@paf31
Copy link
Copy Markdown
Contributor

paf31 commented Jun 11, 2017

I'm not sure I agree that aliases for type constructors are always a bad thing. What if I just want to use a type synonym to make a name shorter? Or what if I just want to use a new naming convention without changing semantics?

-- Maybe if I work with lots of Scala developers ...
type Option = Maybe

@hdgarrood I don't think your example would be an issue here since Tuple a a is a type application, not just a type constructor.

@justinwoo
Copy link
Copy Markdown
Contributor Author

Aren't all type constructor aliases confusing? I think any case of Option : Maybe or Result : Either or whatnot would just end up being confusing since the constructor names don't match either, and you'd very quickly(?) run into cases where you'd need to start using the concrete constructors (at least, for easy to understand code)

I hope to not warn on type applications though, even if it's as simple as Maybe String, since I have many cases where I use type aliases for those like type FilesRoute = Route Unused FilesData

@hdgarrood
Copy link
Copy Markdown
Contributor

Ah I see, thanks.

Having thought about this a little more, and similar to what @paf31 said, I am a little uneasy about putting this into the compiler as a warning. I agree that this kind of type alias is almost always a bad idea, but also I'm not sure I'm comfortable having the compiler generate a warning about it in every case.

@paf31
Copy link
Copy Markdown
Contributor

paf31 commented Jun 11, 2017

I don't think we should try to prevent users from renaming things.

@jdegoes
Copy link
Copy Markdown

jdegoes commented Jun 11, 2017

I agree with @justinwoo. A type alias is not a rename because it does not create appropriate constructors and deconstructors. In practice, type aliases like this are used in places where a newtype would be superior, because of the added type safety, yet there is reluctance to introduce a newtype because of the attendant ceremony. Warning on silly type aliases like this and focusing on reducing that ceremony (which is already pretty minimal at this point) seems like it can only improve the quality of the code.

@garyb
Copy link
Copy Markdown
Member

garyb commented Jun 11, 2017

I'm also not a fan of type-renaming things when it's purely a rename. For all the above reasons, plus they make error messages worse, since the error will still talk about Maybe even if the Option synonym was used in the code.

@paf31
Copy link
Copy Markdown
Contributor

paf31 commented Jun 11, 2017

Every type alias is just renaming, so by this logic, they're all bad, right? I would say lenses give a nice example where aliases are doing something worthwhile, but even then you have all of the same error message issues.

I agree it's almost never a good practice to use aliases like this, but then what is a good use case for an alias? Can we characterize those? If not, I'd rather not add a warning for something so specific.

Edit: following up on "they're all bad", I would actually seriously consider just removing type aliases completely 😄

@jdegoes
Copy link
Copy Markdown

jdegoes commented Jun 11, 2017

Every type alias is just renaming, so by this logic, they're all bad, right? I would say lenses give a nice example where aliases are doing something worthwhile, but even then you have all of the same error message issues.

In my opinion, a type alias is only useful for the sole case in which you are extracting type duplication from type declarations. i.e. it's the moral equivalent of using a let binding for an expression to avoid repeating the expression in multiple places. In this case the name is not useful, per se, it's almost irrelevant, the point of using the alias is to remove the duplication to make maintenance easier.

All other uses of type aliases are design smells, and warning against them would be preferable to me in any code base I work on or oversee.

I think you can detect this with relatively simple logic: a type alias that does not apply strictly more type parameters than it accepts is always bad. For example, type Width = Int is bad because it accepts and applies zero type parameters. The type alias type Tuple' a = Tuple a a is not bad because it accepts one type parameter and applies it twice. The type alias type MapStr v = Map String v is good because it accepts one type parameter and applies two. Etc.

@garyb
Copy link
Copy Markdown
Member

garyb commented Jun 11, 2017

I would actually seriously consider just removing type aliases completely 😄

I find that idea somewhat horrifying 😆. There are cases where having aliases improves readability of types hugely, reducing duplication or avoiding the need to write out entire complicated constructions. Even just f ~> g instead of forall a. f a -> g a is super convenient.

I think the suggestion here is just type X = Y cases, when there is no type application at all. type File = String, type Length = Number, etc.

@paf31
Copy link
Copy Markdown
Contributor

paf31 commented Jun 11, 2017

I don't think it's possible to characterize bad aliases, but I agree that Alias = TypeConstructor is almost always bad, and probably worth a warning.

However, we should probably handle things like type MyList a = List a as well.

@garyb
Copy link
Copy Markdown
Member

garyb commented Jun 11, 2017

However, we should probably handle things like type MyList a = List a as well.

Definitely! I have a branch with a warning for "over applied" aliases that I did months ago, I'll spruce it up and get it open as a WIP at least.

xs -> " " ++ (intercalate " " $ T.unpack . fst <$> xs)
ctr = case ty of
TypeConstructor (Qualified _ (ProperName tn)) -> T.unpack tn
_ -> ""
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we use error here?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah, yeah, definitely.

where
name' = T.unpack name
args' = case args of
[] -> ""
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same here

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In this section, I just want to print out an empty string if I don't have any type arguments to my type synonym. Should I rewrite this section in some way?

CannotGetFileInfo{} -> "CannotGetFileInfo"
CannotReadFile{} -> "CannotReadFile"
CannotWriteFile{} -> "CannotWriteFile"
TypeConstructorAlias{} -> "TypeConstructorAlias"
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

After this is merged, can you please add docs to the docs repo for this error too?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, definitely needs some docs

renderSimpleErrorMessage OverlappingNamesInLet =
line "The same name was used more than once in a let binding."
renderSimpleErrorMessage (TypeConstructorAlias _ _ _) =
line "The type synonym provided was an alias for a Type Constructor. These kinds of types can lead to confusing programs."
Copy link
Copy Markdown
Contributor

@paf31 paf31 Jun 11, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we say this?

This type synonym aliases a type constructor. Consider using a newtype instead.

"confusing programs" is a bit vague, and we can give examples in the docs repo instead.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Perhaps "consider using a newtype or using the original type instead"?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍

@joneshf
Copy link
Copy Markdown
Member

joneshf commented Jun 12, 2017

Throwing my hat in. I'd be really interested to see how removal of type aliases played out. I feel like life would be better overall. It would push us to make using newtypes even easier to work with; they're already pretty easy to use with newtype deriving and Data.Newtype. But also, it would eliminate a bad design choice that we only suggest to you shouldn't do. Sort of like orphan instances.

@parsonsmatt
Copy link
Copy Markdown
Contributor

I strongly disagree with removing type aliases entirely. They're an extremely useful tool in Haskell development. Removing them would make development with eg Servant or Lens totally untenable. It would require us to write StateT s Identity a everywhere instead of State s a (etc).

I would be annoyed to see the original post's warnings from the compiler, mostly because it seems more like an appropriate thing for a linter tool to have as an option. I use type aliases like these somewhat frequently in development when I expect the type of some related group of functions to change, or if I'm not certain of it. For example:

type Filter = Foo -> Bar -> Either [String] Quux

hasThing :: Filter
hasWhatever :: Filter
hasSomeOther :: Filter
-- repeat about 40 times

then a change to the signature (eg, Either [String] Quux -> AccValidation [Error] Quux) becomes much easier without having to resort to sed trickery.

@vendethiel
Copy link
Copy Markdown
Contributor

I think the warning is just to alias types like Number or String

@paf31
Copy link
Copy Markdown
Contributor

paf31 commented Jun 12, 2017

I strongly disagree with removing type aliases entirely.

Don't worry, I don't think anyone is seriously considering it 😄

because it seems more like an appropriate thing for a linter tool to have as an option

Now that you bring it up, having this as a linter option makes a lot of sense. The question is, should all linter options be baked into the compiler as warnings? Without the ability to filter them on the editor side, I think probably not.

@justinwoo
Copy link
Copy Markdown
Contributor Author

@nwolverson's VSCode/Atom editor extensions let you exclude specific warnings at-will, and I've used this to not warn on inferred wildcard and function signatures for my tutorial videos to not have too much noise, e.g. https://twitter.com/jusrin00/status/850375735864950784

Probably someone should make some easy way for IDE server clients to get this list of warnings?...

@natefaubion
Copy link
Copy Markdown
Contributor

I'm not in favor of this as a warning. Which programs are confusing? If we preserved aliases after desugaring, would they still be confusing? What bugs would be prevented by following this warning?

@natefaubion
Copy link
Copy Markdown
Contributor

I feel like this strongly implies we should also warn for aliases at the value level. Maybe someone wants to preserve backwards compatibility under a rename?

@hdgarrood
Copy link
Copy Markdown
Contributor

Maybe someone wants to preserve backwards compatibility under a rename?

Very good point. This is the strongest argument against this warning I've seen so far, I think.

@natefaubion
Copy link
Copy Markdown
Contributor

If we preserved synonyms after desugaring, which is a longstanding issue, then the errors could probably be far less confusing, and I think this warning would be moot:
Could not match Int with Pattern (ie. String) or something.

@jdegoes
Copy link
Copy Markdown

jdegoes commented Jun 12, 2017

Maybe someone wants to preserve backwards compatibility under a rename?

That won't work because you won't get constructors / deconstructors just from a type alias. In any case, you're arguing to avoid making something a warning to allow teams to increase technical debt. I'd say that's an excellent argument for making it a warning. 😉

@justinwoo justinwoo force-pushed the type-constructor-alias branch from d0456af to c5524f1 Compare June 12, 2017 19:27
@hdgarrood
Copy link
Copy Markdown
Contributor

The whole point of the technical debt metaphor is that it's sometimes reasonable to take out a loan as long as you pay it off later.

@justinwoo justinwoo force-pushed the type-constructor-alias branch from c5524f1 to 7f449d0 Compare June 12, 2017 19:36
@paf31
Copy link
Copy Markdown
Contributor

paf31 commented Jun 12, 2017

Aren't all type operator aliases just as bad as these sorts of aliases?

Edit: I'll elaborate my thinking here. Why do we use infix operator aliases for type constructors like Tuple? Maybe we define infix Tuple as * or something because we want to think of tupling as like multiplication for certain purposes. But I still want to work with Tuple along with its set of instances.

As another example, maybe in my UI library, I want to define a synonym type Beside = Coproduct or something for combining components, because Beside is more natural, but really I do want (exactly) a Coproduct with all its instances. Here a newtype would be unnecessary, and I see nothing wrong with using a type alias.

@garyb
Copy link
Copy Markdown
Member

garyb commented Jun 12, 2017

I don't think so - operators don't attempt to convey meaning, the way type File = String suggests.

@garyb
Copy link
Copy Markdown
Member

garyb commented Jun 12, 2017

Maybe this is controversial enough that we don't do it though. Seems like there's not a consensus here, and it is the kind of thing a separate linter could deal with. It's not so much a "it's likely you're making a mistake"-level lint warning as it is a "that's a questionable choice".

@paf31
Copy link
Copy Markdown
Contributor

paf31 commented Jun 12, 2017

I agree that type File = String and type Beside = Coproduct are different, but distinguishing them isn't really something the compiler can do, since it would need to know the user's intent.

@justinwoo
Copy link
Copy Markdown
Contributor Author

justinwoo commented Jun 12, 2017

I'd be okay with shelving this for now if needed, but I'd like to really try polling more users somehow about this. Type aliasing constructors is a very common way I've shot my feet off in the past (and sometimes in the present/future), and it seems like something where 90%+ of the cases would benefit from this being a warning. Maybe needs a "maybe later" tag or something 😛

@paf31
Copy link
Copy Markdown
Contributor

paf31 commented Jun 12, 2017

@justinwoo Could you please create an issue?

@jdegoes
Copy link
Copy Markdown

jdegoes commented Jun 14, 2017

type Filter = Foo -> Bar -> Either [String] Quux

This is takes 0 type parameters and applies 6, so would be valid under my proposed rule for bad type aliases. Applying more than you require is a sign you're doing work, i.e. reducing duplication. Applying less than or the same as what you require is a sign you're doing no work, i.e. it's purely "semantic" change with no actual benefit to reducing duplication.

@paf31 paf31 changed the title add warning for type constructor aliases w/ suggestions Warn for type constructor aliases w/ suggestions Jul 6, 2017
@paf31
Copy link
Copy Markdown
Contributor

paf31 commented Sep 10, 2017

Closing as this needs an issue to discuss, but let's reopen this if we come to a decision. Thanks!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.