Finding a standard way to deprecate Elm code

Hi all,

We all maintain and write Elm code, and in the process of that, we sometimes deprecate code because we don’t want to see it replicated any more than it currently is.

If you’ve read my Stop the bleed article from today, you know that I wrote a rule to report the usage of deprecated values/types/modules/dependencies.

The way I went about it is to report references to anything that contains “deprecated” in its name (case insensitive), or that contains in its documentation a line that starts with @deprecated, similar to how @docs works.


somethingDEPRECATED = "x"

{-| Is equal to "y".

@deprecated We prefer "z" now.

-}
otherThing = "y"

value =
     somethingDEPRECATED
  -- ^^^^^^^^^^^^^^^^^^^ will be reported
  ++ otherThing
  -- ^^^^^^^^^^ will be reported

While this works, no one currently writes @deprecated in their documentation as far as I know, because that is neither an official nor standard way of tagging something as deprecated.

My aim is to discuss ideas on what a standard way could be, that in due time can be easily supported by official and community-driven Elm tools.

Goals

I personally think that deprecations should…

  • …be unambiguous and explicit: Tooling should not have to guess whether something is deprecated. We don’t want to have to do things like /(should|must) not use/.test(comment) as it will never be able to get things right (besides, not all packages are English focused).

  • …be easily detected: It should not require too much computing power to figure out whether something is deprecated or not, nor should it require complex logic, as many tools (compiler, IDEs, elm-review, …) will have to implement it in the same way. IDEs for instance can strike through references to deprecated items, but they have plenty of other things to do, so making this efficient would be great.

  • …be able to convey as much information as necessary: The most important thing a deprecation comment can do is to indicate what the alternative should be. Take this example
    of something I once deprecated. I wanted to convey the reason why it’s deprecated (I could have explained that better actually) and what to use instead. You might also want to add code samples,
    which I think could be valuable, or other information, like since what version it is deprecated. This can all take a large amount of space, and I think a deprecation warning should not have arbitrary limits (a single line, a single paragraph, …) that would impede on helpful documentation.

  • …have a deprecation message that is easily extractable. For instance, when elm-review detects the usage of something deprecated, it would be valuable to show the deprecation message to the user, so that they don’t have to look for the deprecated element’s documentation to find the recommended alternatives. This can be aided by having the deprecation text available in the docs.json file for instance. Extracting this message should not require a Markdown parser.

  • …fit well in documentation: The packages website, and IDEs in their documentation pop-ups, should be able to highlight the deprecation message in a way that makes it stand out and/or not feel out of place. Also, authors should be able to display the deprecation warning (almost) wherever they want to: at the top of the comment, at the bottom, in between two paragraphs, …
    This goal may very well be made easier or harder by the previous goal. I really like how the @docs approach allows for varying ways to document a module, and I think it can be valuable to keep this kind of flexibility.

Proposal

The idea I have in mind is to support two ways of deprecating things: Through a short @deprecated tag and a longer @deprecated-start/@deprecated-end tag duo.

@deprecated would allow writing up to paragraph’s worth of content, from @deprecated to the
To extract the message, find the first line that contains @deprecated, and take the comment’s contents until its end or until the next “\n\n”.


{-| Is equal to "y".

@deprecated We prefer "z" now.
Still part of the deprecation message.

Not part of the deprecation message anymore because there was "\n\n" in between.

You can use this function when you want to use "y".

-}
otherThing = "y"

@deprecated-start/@deprecated-end would allow writing more information.
To extract the message, take everything from the first @deprecated-start until the next @deprecated-end (which could be required to also be at the beginning of a line).


{-| Is equal to "y".

@deprecated-start This function should not be used anymore, because Z.

Instead you should use `z`, like in this example

```elm
doStuff z
\```

@deprecated-end

Not part of the deprecation message anymore.

You can use this function when you want to use "y".

-}
otherThing = "y"

I say I’m suggested the two, but having only the start+end duo would be enough, and I’m still on the fence as to whether the shorter one would be useful.

With regards to the goals I set above:

  • …be unambiguous and explicit: @deprecated is unlikely to be written in a comment. If it does, like the documentation of the NoDeprecated does, then requiring it not to be at the start of a line sounds like a reasonable limitation/requirement to me, just like for @docs.

  • …be easily detected: If the documentation has a line that starts with @deprecated (and/or @deprecated-start?), tag it as deprecated. It might still be somewhat on the expensive side computation-wise, as you still need to go through each line of every value/type/module in a project, but it sounds relatively easy at least. For code from dependencies, I think it would be valuable to have a “deprecated” boolean or something like that in the docs.json file for every value/type/module.

  • …have a deprecation message that is easily extractable. Same as above. Again, I think it would be valuable to have a field for the deprecation message in the docs.json file for every value/type/module, but maybe that would create redundant information with the documentation field (if we wish to keep the location of the deprecation flexible).

  • …be able to convey as much information as necessary: the start+end duo gives you as much space as you need to write what you want without limitations, including stuff like code blocks.

  • …fit well in documentation: Without any special formatting by the packages website and other tools, having the @ be shown may look a bit out of place there, but I think it’s to a reasonable extent, especially since other languages use a similar approach, it should be familiar to a number of people.

Writing-wise, it would be valuable to decide on whether tools will include the “@deprecated” word (with or without the @), so that authors know whether they should write “@deprecated because Z” or “@deprecated This is deprecated because Z”.
Also, deciding whether the contents between the start and end should/will be trimmed would be useful for the same reason.

Prior art

This is what I could find for other languages. Because I have very little or no experience with the following, I have no insight into the limits and benefits of each approach, and I’m hoping other people have more experience.

JavaScript (or more precisely JSDoc) and Java use the @deprecated approach. I am not entirely sure, but I think the deprecation message is limited to single paragraph.

Python (through a package) and
C# (through an “attribute” named “obsolete”) seem to use a decorator-like approach.

Haskell uses the DEPRECATED pragma, but I don’t yet understand how it works.

Both Python’s, C#'s and Haskell’s approaches seem like they are relatively easily detectable, but limits how/where the deprecation message shows up in the documentation (either at the very top or at the very bottom I imagine).

We could have something similar, but that would likely require the Elm language to support something like multiple documentation comments for an element, or a new dedicated syntax.

Unsolved by proposal

My proposal makes it possible to deprecate anything with a documentation comment: values, types and modules. It does not have a proposal for other levels:

  • Smaller: a single custom type constructor, a single record field, a nested record field, a parameter, etc. Maybe distinct tags like @deprecated-constructor-start/ @deprecated-constructor-end could make sense?
  • Bigger: How do you deprecate entire dependencies? Should that be a field in the package’s elm.json file, or through the usage of @deprecated inside the README? Or should that be additional metadata on the Elm package registry?

Feedback wanted

We have somewhat of a chicken and egg situation. There is no standard way of deprecating things because there is no official way, and (maybe?) we have no official way because no standard emerged.

I would really like us to come up with something that can later easily be picked up by the Elm language and official tools, yet still works out nicely in the meantime.

I think my proposal is neither perfect nor complete, but I hope that it’s a good starting point for a discussion! So please pitch in!

12 Likes

I really like the @deprecated doc keyword. It would be very useful in big packages like elm-geometry trying to bring new features while delaying API changes to next major versions. Or in general to warn users of incoming changes.

In my opinion, the simple @deprecated would be enough, and tooling could just show the whole documentation comment of the corresponding item. When you are deprecating something, your doc should reflect that, so I don’t think there is a need to mark a specific section of a doc comment.

Regarding dependencies, the most convenient would probably be in the elm.json. Adding an optional "deprecated": true field in elm.json does not seem to bother the elm compiler. So that’s maybe something we could do today already, even if it’s not picked up officially yet.

5 Likes

I think there is the bigger feature of attributes on elm functions/types.

Obviously if we have attributes then depreciation notices would just be one type of attribute.

Another use case for attributes is marking tests (we currently detect tests by doing full type inference to find things of the right type but I consider that a temporary solution).

I don’t have a design for attributes but I wouldn’t want a future design to hit a roadblock because we added doc comment depreciation notices without thinking about the wider picture.

Would be hugely excited about better depreciation notices in elm though!

4 Likes

I think you can do without the -start and -end variants by allowing @deprecated (or maybe @attr(deprecated) per Harry) to be used multiple times. Each paragraph tagged like that gets stripped of the tag, stripped of leading/trailing whitespace, and joined with a newline, to make the full deprecation comment.

1 Like

Deprecation notes could start and end like bullet points or quotes in markdown:

{-| Is equal to "y".

  - @deprecated This function should not be used anymore, because Z.

    Instead you should use `z`, like in this example

        doStuff z

Not part of the deprecation message anymore.

You can use this function when you want to use "y".

-}
otherThing = "y"

The nice thing is: we just need a single tag. In contrast to -start & -end, the deprecation message will also be rendered differently from the surrounding text – even without further support.