unison
unison copied to clipboard
update docs to reflect that unique types are the default
This may be controversial, but I'm bringing it up because I think that there are lots of good reasons for it, and particularly because it is such a stumbling block for newcomers.
Background
See #2251 and #2352 for background.
In the early days of Unison, types were structural by default and you had to add a unique
keyword to make them non-structural. At some point people realized that unique types were almost always what you want, and there was a proposal to instead make unique
the default. At the time of #2251 and #2352 the thought was that swapping the default was an abrupt change in behavior that might have negative consequences, so instead a structural
keyword was added the default was removed. So for the 2 years since #2352 was merged users have been explicitly specifying structural
vs unique
when defining a type. I think that this is enough of a transition period to switch the default to switch the default to uniqueand elide
unique` in the pretty-printer.
Why I think unique is a good default
Most types should be unique, not structural.
type UserId = UserId Nat
and type EpochMillis = EpochMillis Nat
are definitely not intended to be treated as the same type, and the same follows for most types that appear in application code.
Structural types arguably have their place (Option
, Either
, {Abort}
, {Stream a}
, and {Throw a}
come to mind), but I think that the types that are structural in nature tend to come up much more in core libraries like base
than in user/app code. That is, end-users (especially newcomers) are more likely to be defining unique types than structural types. And I would anticipate that people who are working on core structural types are likely to be more comfortable with Unison and/or sophisticated type systems.
structural/unique is an obstacle for newcomers
Requiring (and pretty-printing) unique
all over the place is an obstacle for newcomers. After seeing hello world in a language, one of the first things that you'll encounter is a data type. If that data type looks like:
unique type User = {
name : Text,
age : Year
}
then everything else will be intuitive, but that unique
is going to jump out as something that you don't understand and need to learn. It's true that you will still encounter structural
when you view Optional
, Either
, etc, but maybe that comes a little bit later and I think that it's still a win to avoid the non-obvious keyword for 90% of types.
It's too easy to define a structural type that doesn't make sense
I've encountered quite a few structural types that don't really make sense as structural types. #3615 mentions a category or two of these that the compiler doesn't help you identify.
Also it's really common to see newcomer posts in Slack that define structural types with Text
/Nat
/Float
fields which conceptually are unique
. I'm not sure why these tend to be structural
as opposed to unique
, but if I were to speculate, the thought process is something like "I don't know what unique means, but this type isn't special; it just defines the structure of a user struct".
Unique type churn is a resolved issue
I used to define types as structural while I was developing them just to avoid unique type churn. But this problem was resolved a while ago (yesssssss! 🎉), which removes the biggest footgun with unique types.
Reduction in noise
Most of the time you want unique
, so why the extra noise of writing or pretty-printing it?
Sgtm
I almost forgot to mention the most important reason: @alvaroc1 has advocated for this to lower the barrier to his child learning Unison.
Sounds good to me too! It's time.
From the perspective of an end user, I agree with this. I woudln't say I struggled with it, but it was another thing to learn early on with Unison, like in TypeScript deciding between an interface
or type
, which for historical reasons existed in parallel, but nowadays are almost exactly the same.
If there were an easy, TypeScript-like way to instantiate records, then maybe it might be nice to have an explicit structural/unique decision when someone designs the type, but there isn't, right?
Like, structural and duck typing makes so much sense if you can do something like
type Foo = { id: String, name: String }
and later just
x = { id: "123123", name: "Rizzmaster 4000" }
but having to create records through x = Foo "123123" "Rizzmaster 4000"
structural typing sort of loses its utility for me since I'm passing it all through a nominal type system for instantiation. And something like Foo.default |> id "123123" |> name "Rizzmaster 4000"
doesn't get one any closer to the utility of record types.
Most types are valuable because they represent some more concrete thing, while structural types seem to be more useful when the shape matters (I suppose that's why it's deemed "structural"!). I.e., things more closely aligned with category theory or other "core operations" like how @ceedubs mentions in the ticket body itself, things in base
.
Subtasks:
- [ ] update code
- [ ] update docs
Hey! This sounds like something I'd like to do!
Just one detail, do you want to keep the unique
keyword (just make it optional) or does it have to go? And does this also apply to abilities?
@unorsk wonderful thank you! 🎉
do you want to keep the unique keyword (just make it optional) or does it have to go?
My inclination would be to keep it as a keyword. But like you said it would be optional, and the pretty-printer would never use it.
does this also apply to abilities?
Yes.
Merged!
@ChrisPenner just one thing left – updating the docs. I haven't had time yet to look into that, but from what I understand it's just a couple of pages (starting here) that have to be updated on unison share.
@rlmark Do you know if this can be considered done?