The `Solo` instance is incorrect
Solo is the canonical 1-element tuple type.
However, the implementation added in PR https://github.com/haskell/aeson/pull/891 treat it as any other normal 1-element type, which is incorrect.
This becomes problematic when you have code that wants to seriaize/deserialize generically over tuples of various arities:
- (10, 20, 30) <-> "[10, 20, 30]"
- (10, 20) <-> "[10, 20]"
- Solo 10 <-> "10" ❗❗❗
- () <-> "[]"
I think "incorrect" is a bit strong but the argument is reasonable. The problem is it would break 4+ years' behavior. I'd be curious to see
code that wants to seriaize/deserialize generically over tuples of various arities
because I suspect a custom datatype implementing a subset of HList functionality would better serve, or demonstrate that genericity isn't worth the trouble, depending on the genericity desired.
You're absolutely astute that this change would be backwards-incompatible and thus could only be introduced in a breaking PVP release. That of course shouldn't be done lightly, and this issue by itself by no means is enough to warrant such a breaking change in and of itself. But if there is another change that needs a breaking version release, this problem could be resolved at the same time.
What I ended up doing in the codebase where I first encountered this, is creating a separate datatype called Single that functions as Solo.
Repo here, details of encoding/decoding tuples here.
Indeed, creating a Tuple-like custom type like HList would be another solution.
While I'm sympathetic with the reasoning of treating Solo as 1-tuple, we can also argue that Solo is a canonical type for a "Box", i.e. adding extra lazyness around a thing. IIRC that was my reasoning back then when I added Solo instance, I was thinking of it more as of indirection box rather than 1-tuple.
The recent #1141 makes me really ponder whether to change the instance. I'll leave this issue open. Maybe I'll change it when considering next release with other breacking changes. This change alone doesn't warrant a major release of aeson though.
Following on from the discussion in the other duplicate issue:
then removed for a lot of releases,
That alone makes this an non-option
Maybe I'm just ignorant about some common PVP knowledge and Haskell library principles, but out of curiosity: how is a breaking release which changes behaviour better than a breaking release which removes the instance entirely (at least temporarily)? The latter seems to much more explicitly "warn" users of this instance about it changing - I think everyone relying on it would have their code break at runtime if they're writing app code and just bump aeson's major version.
I don't see much point to remove the instance. Attach a warning in a minor release, change in a major release, done.
how is a breaking release which changes behaviour better than a breaking release which removes the instance entirely
aeson tries to make breaking changes rarely, as it takes ecosystem to catch up (Stackage e.g .may take whole LTS cycle, about half a year at worst). And then whole bound updating has to be done another time. It only will cause people to write bounds like aeson < 3 (which some sadly already do), if that bad practice spreads, it would force me to bump aeson to 3.0.0.0 then 4.0.0.0. And then people will write stupid bounds like <1000 (like they also do for some other packages). TL;DR people don't like breaking releases.
On the other hand, I also don't like adding a warning, as I have no idea when the change will happen if at all. Currently I'm not committed t o, nor rejected the idea. As I said "maybe I'll change it", currently I'm open to the idea, but the opinion may swing either way: maybe I'll be very much in favour, or very much against.
If pushed to make a prompt decision, then it will be no change.
I have implemented a Single newtype in multiple of my projects already since I could not find a type with an instance that is like the one I expected Solo to have. So I personally would be happy about this change.
@julmb that is what makes me uneasy. If you only need a "completion", then you probably should use some newtype indeed. But Solo is data. It's really a singleton tuple. An additional box.
It would be somewhat wrong to wrap values in an additional box just to get different instance. That is what newtypes indeed are for.
But then if it was a newtype it would not have the same strictness semantics that the rest of tuples have, i.e. MkSolo bot /= bot, similarly to how (bot, bot) /= bot. So I guess the opinion on boxedness depends on how many properties of other tuples you want to keep overall when you say that "Solo is the 1-tuple similarly to how (,) is the 2-tuple"
@googleson78, @julmb didn't say whether he has newtype Single = Single (Solo a) or newtype Single = Single a. If the latter, then they don't care about strictness semantics...
I have newtype Single a = Single a, and I use it for example to parse the JSON response of a REST API that is known to always put a singleton list in a certain spot.
I do not really care about the strictness of Single a in the same way that I do not really care about the strictness of (a, b) when using that to parse JSON that is known to have a list with exactly two elements. As far as I know, aeson does not use lazy IO or infinite data structures, so the only reason I can think of to care about strictness in this context is maybe performance.
It would be somewhat wrong to wrap values in an additional box just to get different instance. That is what
newtypes indeed are for.
I agree in principle, but I guess there are a lot of other types that I use primarily based on their instances rather than their strictness.
The problem with changing these instances is, if MkSolo foo is serialized and sent somewhere, and then the change happens in aeson, the Haskell program will still compile and run but the receiving service will choke. It's an insidious breaking change.
The only thing that would weight against that for me is if I could see an example of generic code that will get easier to write/reason about if we have Solo behave like () and (,). I doubt that exists.
It's kind of arbitrary anyway that tuples have array codecs; tuple elements are different types; probably they should be Objects but we don't know what the keys are; that they have instances at all is a convenience...
I would favor adding a note in the Haddocks.
The only thing that would weight against that for me is if I could see an example of generic code that will get easier to write/reason about if we have Solo behave like () and (,). I doubt that exists.
The original reason why I'm in this thread is because I encountered a case where I want to expect/serialise to a singleton array, and found Solo to be unable to fulfill that role via its instance in aeson, despite my expectations based on its description in base and the existing tuple instances. The same seems to also be true for julmb, and OP
I'm not sure if this is what you're looking for. Do you mean something else by "generic code"?
The problem with changing these instances is, if MkSolo foo is serialized and sent somewhere, and then the change happens in aeson, the Haskell program will still compile and run but the receiving service will choke. It's an insidious breaking change.
Hence why some people suggest doing this change only with a major version bump, so as to not allow silently accepting the new aeson version. My personal preference would be to break all code that uses the json instance for Solo, and then reintroduce it later in a major bump, so that it's extremely hard for someone to accidentally use the new instance. This is extremely unrealistic from a ecosystem/library POV though, so I think something that's more realistic, although still seemingly unlikely to happen currently, is deprecating this instance temporarily via the new instance deprecation mechanism and then later changing it in a major bump.
It's kind of arbitrary anyway that tuples have array codecs; tuple elements are different types; probably they should be Objects but we don't know what the keys are; that they have instances at all is a convenience...
I don't agree that they're arbitrary. If you think of tuples as heterogenous lists of a fixed length (which from your previous comment regarding HLists appears to be the case), their instances make perfect sense canonically, unless we want to argue that lists shouldn't serialise to json arrays. I realise that this is going into somewhat ideological/philosophical territory (should we consider tuples to be lists of fixed length?), but I feel that this is a relatively common (if not the primary) way to look at tuples - I think it's not a coincidence that the type Vector (being a list of known length) uses the same word for its name as "vector" in linear algebra (essentially meaning a list of fixed length, i.e. a tuple).
Do you mean something else by "generic code"?
Yes, a preexisting library offering a kind-polymorphic mechanism to formally represent the class of tuples, and Solo is hard-coded as the 1-tuple, and people want to use it but are scared off by Solo's ToJSON1 instance. That's how I interpreted OP when I quoted them above.
I just checked, and constructing a 1-tuple in Template Haskell produces a Solo. TH qualifies as such a kind-polymorphic mechanism. This does not support my argument.