Official design guidelines states that it’s good to keep tags and record constructors secret. Why is that good to keep them secret? Is that only because people don’t like when packages break?
E.g. here is two examples. First exposes tags of ButtonSize type and second exposes only helper function viewSmallButton. In the future button will have a config with different options, e.g. alignment, type, etc… Also, options could be added/removed/changed in the future.
If you have some union type, and it’s exposed, then adding a new member to it is a breaking change. Maybe not what you want!
But in general, it’s helpful to think about a module as a bunch of functions around a data structure. You wouldn’t want to expose the implementation details of that data structure, but if a union type happens to be part of your public API then so be it.
If you expose tags and records, you can have a simplified API in terms of functions but you would have to bump major version numbers even for simple internal data structures changes.
If you keep the tags and constructors secret, you have a more robust API and more flexibility in terms of internal representations. You pay for this with more functions because you need to provide access to creation of tags and access to record fields somehow.
So… think hard about what is appropriate.
Nobody likes when their code breaks because of someone else’s actions. This is why when designing a library it might be a very good idea to hide the implementation and expose a well thought API that will be backwards compatible as long as possible.
Let me add one more thing about this topic: capability of user-side pattern match.
If you expose whatever types from your library (be it union or records), its users can freely pattern match against them using case, while hiding them strips this ability. This is IMO very important consideration when you design APIs and type visibility.
this is the point.
For instance, Date.Month(..) is exposed but it is rightfully so, since I believe this union type does not require change in any forseeable future (at least as long as we are bound to this planet).
I agree with both of these quotes! In this case, the Big | Small constructors exist only to serve the public API; I wouldn’t call them implementation details. The big trade-off here is whether you want to get a compile error if you introduce a new type of button. You might want that, or you might not!
Let’s say you have a style guide which renders all your reusable widgets (buttons, dropdowns, etc.) so developers can browse them, check for visual regressions, etc. (We have one of these style guides at NoRedInk.) If the style guide pattern matches on ButtonSize, then when you introduce a new size, you’ll get a compile error on the style guide, which could remind you to incorporate the new size into it.
On the other hand, choosing not to expose it means that introducing a new size won’t result in errors. That could be desirable if you expect to be reusing it in lots of places and want to minimize upgrade friction when adding things to the module.
So it depends on which of those considerations you value more. I could see myself choosing one approach in one code base, and the other approach in another code base!
As Rich Hickey (father of clojure lang) says in his talk “Simplicity Matters”, sometimes there is no reason to be an implementation encapsulation because there is only data, no implementation to be. Opaque types are great for API stability but don’t over do it.
Opaque types can also help build speed. In particular, if you have a couple levels of nesting in your module hierarchy, opaque types can result in smaller intermediate results for Elm builds that then have to be consumed further up. The couple levels part arises because it doesn’t look like information gets stripped at the module that makes things opaque but rather at the module that consumes the opaque API so the benefits don’t really kick in until the next level up.