typelevel / typelevel/cats

Please remove/deprecate Foldable for Tuples

Open
#4,430 6 comments 4 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

Dominant language
Scala
Stars
5.5k
Forks
1.2k
Avg merge
2d 10h
Merged PRs (30d)
5

Description

This issue was probably debated before, sorry if I'm bringing up an old issue.

I just bumped into a bug of my making — had to wait on the completion of 3 IO tasks. The code went through several iterations, first using race, then using parMapN, as I needed the results, but then signatures changed, so I wanted something that simply waited on them all and ignored the results:

(
  task1,
  task2,
  task3,
).parSequence_

The behavior of this code is perplexing, especially for those familiar with its prior use of parMapN. The compiler should throw an error, but it doesn't. And this happens because an instance of Foldable[(A0, A1, *)] is defined, which makes absolutely no sense to me.

I was told that the reason for why Foldable gets defined on tuples, is due to key-value pairs. I am assuming that people want to do stuff like:

import cats.syntax.all._

("key", Option("value")).sequence
//=> val res0: Option[(String, String)] = Some((key,value))

("key", Right("value"): Either[Throwable, String]).sequence
//=> val res1: Either[Throwable, (String, String)] = Right((key,value))

("key", List("v1", "v2")).sequence
//=> val res2: List[(String, String)] = List((key,v1), (key,v2))

// ☝️ This last one is already getting into the weeds, TBH

However, I'd argue that, even for tuples of 2 elements, the Foldable instance is still error-prone. That is because not all tuples of 2 elements are key-value pairs. This is a semantic that's only available in context. Because, obviously, this code is also wrong:

(task1, task2).sequence_

Interestingly, the instance is right-leaning. We define Foldable[(A0, *)] and not Foldable[(*, A0)]. We define Foldable[(A0, A1, *)] and not Foldable[(A0, *, A2)] or Foldable[(*, A1, A2)]. Why is that? Why are the semantics of Either available here as well? Is this some sort of associativity convention that's imported from Haskell? (e.g., type currying)

In Scala, I don't think it makes any sense, apart from just picking a convention that happens to be consistent with the ordering of key-value pairs, or of Either. I mean, in Either, one value is clearly more important than the other. It's the value that represents the happy-path and that doesn't short-circuit flatMap. Well, in a tuple, I don't see how you can say the same thing. As (ioTask1, ioTask2) is a very common tuple in Typelevel Scala codebases, and it's clearly not a key-value pair.

The biggest problem that I see with Foldable on tuples is that, visually, you expect that operation to hit all elements in your “collection”. A tuple is a collection, even if it's a heterogeneous one. If a foldLeft operation only works on the last element of our collection, then it invalidates our mental model for how Foldable behaves. Again, look at this piece of code:

(
  task1,
  task2,
  task3,
).parSequence_

I can't make too many assumptions for others, but my eyes first see a list of IO objects, separated by commas. Without much knowledge of the type system, I can only describe this as a list of things. I expect Foldable to describe loops that go over all those things. If it can't, due to the types not matching, then might as well not define Foldable for it. And that's because my eyes don't see different types, and the compiler isn't telling that this here is a tuple that might not be compatible with what I'm trying to do. Both my senses and my compiler are failing me here.

Whatever use-cases these instances have, they pale in comparison to the potential for bugs this generates. Especially since, in the case of IO we deal with Unit or other results we can ignore, therefore the _ versions (or the .void calls) are pretty common.

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Review the existing tuple Foldable instances and the six-comment discussion, then trace how they enable parSequence_, sequence_, and sequence_. Determine whether the requested removal or deprecation is appropriate and document the chosen behavior, affected uses, and completion criteria.

Written by the indexing model from the issue text.

Assessment

Tech stack
scala
Domain
backend-api-design
Issue type
Refactor
Difficulty
5/5
Estimated time
Over a week
Activity status
Stale
Clarity
Mostly clear
Newbie friendliness
25/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.