aeson icon indicating copy to clipboard operation
aeson copied to clipboard

The `Solo` instance is incorrect

Open Qqwy opened this issue 1 year ago • 16 comments

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" ❗❗❗
  • () <-> "[]"

Qqwy avatar Oct 05 '24 10:10 Qqwy

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.

sjshuck avatar Dec 29 '24 17:12 sjshuck

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.

Qqwy avatar Jan 02 '25 22:01 Qqwy

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.

phadej avatar Jul 20 '25 14:07 phadej

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.

googleson78 avatar Jul 20 '25 17:07 googleson78

I don't see much point to remove the instance. Attach a warning in a minor release, change in a major release, done.

Bodigrim avatar Jul 20 '25 17:07 Bodigrim

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.

phadej avatar Jul 21 '25 05:07 phadej

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 avatar Aug 19 '25 20:08 julmb

@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.

phadej avatar Aug 19 '25 20:08 phadej

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 avatar Aug 19 '25 20:08 googleson78

@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...

phadej avatar Aug 19 '25 20:08 phadej

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.

julmb avatar Aug 20 '25 01:08 julmb

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...

sjshuck avatar Aug 20 '25 13:08 sjshuck

I would favor adding a note in the Haddocks.

sjshuck avatar Aug 20 '25 13:08 sjshuck

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).

googleson78 avatar Aug 20 '25 14:08 googleson78

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.

sjshuck avatar Aug 20 '25 15:08 sjshuck

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.

sjshuck avatar Aug 20 '25 16:08 sjshuck