Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Make Option extend IterableOnce #8038

Merged
merged 2 commits into from
May 9, 2019
Merged

Conversation

lrytz
Copy link
Member

@lrytz lrytz commented May 6, 2019

scala/bug#11033

Option cannot be Iterable because we'd have to implement

protected def fromSpecific(coll: IterableOnce[A @uncheckedVariance]): C

@scala-jenkins scala-jenkins added this to the 2.13.1 milestone May 6, 2019
@lrytz
Copy link
Member Author

lrytz commented May 6, 2019

@szeiger pointed me to an older discussion: scala/collection-strawman#48

Option in 2.12 has an iterator method so it already depends on collections

-- @szeiger

I would worry that Option would be slowed down

-- @Ichoran

I also would be very reluctant to add superclasses to Option. @sjrd has a design to make options unboxed. [...] with added superclasses it's not clear we can do it.

-- @odersky

it's pretty clear we can't

-- @sjrd

@lrytz lrytz modified the milestones: 2.13.1, 2.13.0-RC2 May 6, 2019
@lrytz
Copy link
Member Author

lrytz commented May 6, 2019

Moving to RC2 just as a discussion item :)

@lrytz lrytz force-pushed the option-iterable-once branch from cd1ff6e to 9dbc90f Compare May 6, 2019 15:35
@SethTisue
Copy link
Member

I'd need some serious convincing on this

another old discussion: scala/bug#7941

@eed3si9n
Copy link
Member

eed3si9n commented May 6, 2019

Note Option already implements a bunch of Iterable methods in 2.13.x and 2.12.x because there's an implicit conversion to Iterable.

So I find it odd that we don't allow the following to be interchangeable:

scala> for { i <- List(1, 2); j <- Some("a") } yield (i, j)
res2: List[(Int, String)] = List((1,a), (2,a))

scala> for { j <- Some("a"); i <- List(1, 2) } yield (i, j)
<console>:12: error: type mismatch;
 found   : List[(Int, String)]
 required: Option[?]
       for { j <- Some("a"); i <- List(1, 2) } yield (i, j)
                               ^

@NthPortal
Copy link
Contributor

I am honestly not in favour - Option is not conceptually a collection, so I don't think we should make it one. I actually think we should get rid of the implicit conversion because it's confusing, inconsistent and special-case-y, but apparently that's unpopular.

@eed3si9n
Copy link
Member

eed3si9n commented May 7, 2019

I am not sure what intuition we can use to test if something is a collection or not. To me, personally Option[A] is a type of container that has either zero or one member. I don't know if that's a collection, but I think it's something we can iterate, map, and flatMap over. Kind of like String or tuples, but in a way much closer to List[A] than those. Given that we are happy with map or foreach on Option[A], I think it should at least be an Iterable.

If Scala goes the direction of we don't mix-and-match things in flatMap or for comprehension, and just allow datatypes/interfaces of the same type, I'd be on board with that, but I don't see that happening.

@sjrd
Copy link
Member

sjrd commented May 7, 2019

Given that we are happy with map or foreach on Option[A], I think it should at least be an Iterable.

With that argument, Future should be an Iterable. So I don't think this is a valid argument.

@lrytz
Copy link
Member Author

lrytz commented May 7, 2019

Could there be some negative consequences for the FP folks / cats?

In any case, given that it's not a clear yes, I don't think we should rush it for 2.13.

@dwijnand
Copy link
Member

dwijnand commented May 7, 2019

I missed it originally, so in case anyone else missed it: IterableOnce isn't the same beast (of a trait) that TraversableOnce/GenTraversableOnce is in 2.12. It only extends Any and requires:

def iterator: Iterator[A]
def stepper[B >: A, S <: Stepper[_]](implicit shape: StepperShape[B, S]): S
def knownSize: Int

That, in itself, makes it a more palatable. But there is also

@`inline` implicit def iterableOnceExtensionMethods[A](it: IterableOnce[A]): IterableOnceExtensionMethods[A]

which brings all the old methods back, albeit deprecated. So, on balance, I'm still unconvinced.

@szeiger
Copy link
Contributor

szeiger commented May 7, 2019

IterableOnce just means "has an iterator method". By this logic Option should be an IterableOnce but Future should not.

It is true that you get a lot of (potentially unwanted) collection operations via iterableOnceExtensionMethods but you already get all the same operations via the implicit conversion to Iterable.

I am generally in favor of making this change but there is still the caveat about unboxed options in Dotty (which may not be possible if Option extends IterableOnce), it's too late in the release cycle and scala/bug#11033 is a corner case with an acceptable workaround (add a type ascription to force adaptation to IterableOnce).

@SethTisue
Copy link
Member

SethTisue commented May 7, 2019

discussed at team meeting just now

there were two threads of discussion:

  • is it a good idea to change it this late in the 2.13 cycle
  • is it a good change at all

I'm a strong "no" on the first, a weaker "no" on the second

(if we agree it's too late for 2.13, we have time to think on the second question. Dale and Lukas have pointed out that IterableOnce is a thinner interface than the alternatives we had in ≤2.12. so I should think on whether I'm clinging to my ≤2.12 position on this out of habit)

@julienrf
Copy link
Contributor

julienrf commented May 7, 2019

Maybe it is fine to make Option implement IterableOnce, after all. I'm a bit worried by strongly coupling Option with the collections, but I can not really find a strong argument against this. And as other have mentioned, there is already a coupling because of the conversion to Iterable. If we discover that making Option implement IterableOnce is not a good idea, we can probably easily remove that inheritance relationship in 2.14 and still be source compatible with existing code bases because of the conversion to Iterable.

@julienrf
Copy link
Contributor

julienrf commented May 7, 2019

About IerableOnceExtensionMethods: are we sure that operations currently provided by the conversion to Iterable won't become deprecated if Option extends IterableOnce?

@diesalbla diesalbla added the library:base Changes to the base library, i.e. Function Tuples Try label May 7, 2019
@lrytz
Copy link
Member Author

lrytz commented May 8, 2019

That part seems fine:

scala> Option(1).min
res7: Int = 1

scala> (Option(1): IterableOnce[Int]).min
                                      ^
       warning: method min in class IterableOnceExtensionMethods is deprecated (since 2.13.0): Use .iterator.min instead
res8: Int = 1

@adriaanm adriaanm modified the milestones: 2.13.0-RC2, 2.14.0-M1 May 8, 2019
@adriaanm
Copy link
Contributor

adriaanm commented May 8, 2019

Discussing with @odersky, he felt the regression of having to ascribe an explicit type to the result of the function is worse than the risk (if any) of having Option extend IterableOnce (even taking into consideration the potential of a future unboxed Option type). So, while our process says "a workaround disqualifies a bug from being an RC blocker", we could argue that this ascription and reliance on the option2iterable implicit is not a valid workaround.

I'm tentatively rescheduling for RC2 based on this, but let's have one more round of discussion before we merge.

@adriaanm adriaanm modified the milestones: 2.14.0-M1, 2.13.0-RC2 May 8, 2019
@adriaanm
Copy link
Contributor

adriaanm commented May 8, 2019

To streamline the discussions, let's discuss two points:

  1. timing of the change
  2. conceptually, should Option be an IterableOnce, or more concretely: do the methods that superclass add to Option make sense.

I lean towards "yes" on both counts. I think this is important enough to fix now, rather than in 2.14.

@julienrf
Copy link
Contributor

julienrf commented May 8, 2019

The added methods are: knownSize and stepper. The iterator method was already there in 2.12.

stepper is a variant of iterator that provides better interoperability with Java (IIUC).

@sjrd
Copy link
Member

sjrd commented May 8, 2019

Is there really no other fix for the original issue? Like adding an overload of flatMap that accepts an Option?

@lrytz
Copy link
Member Author

lrytz commented May 8, 2019

Yes, adding overloads would also work, see scala/bug#11033 (comment). We'd have to also do it for other collections that intorduce a flatMap overload, like BitSet, IntAccumulator.

@adriaanm
Copy link
Contributor

adriaanm commented May 8, 2019

I'm not convinced (yet?) adding overloads for flatMap is a cleaner fix than extending IterableOnce.

@szeiger
Copy link
Contributor

szeiger commented May 9, 2019

Because you'd have to allocate your ErasedNone(level) every time

You'd cache these instances up to some level (and have special code paths for level 0 and possibly level 1 for further optimization). The erasure of None is the ErasedNone(0) singleton. There is no other representation for None.

@martijnhoekstra
Copy link
Contributor

martijnhoekstra commented May 9, 2019 via email

@szeiger
Copy link
Contributor

szeiger commented May 9, 2019

Btw, that fact that Option is iterable more than once but can't implement Iterable (because of fromSpecific) suggests the name Iterable is misleading/confusing. I assume it's too late to change that.

When starting on a blank slate, I would rename IterableOnce to Iterable and Iterable to Collection but this would be a big, breaking change for Iterable compared to Scala 2.12.

@joshlemer
Copy link
Contributor

@martijnhoekstra

Sorry, I didn't mean that the English name sounds better one way or the other, but that the definition / semantics of Iterable are more suitable to Option than IterableOnce is, because the difference between Iterable and IterableOnce is that Iterable can be iterated multiple times while an IterableOnce cannot (necessarily). Option can be iterated many times, so it belongs in Iterable.

@adriaanm
Copy link
Contributor

adriaanm commented May 9, 2019

When starting on a blank slate, I would rename IterableOnce to Iterable and Iterable to Collection but this would be a big, breaking change for Iterable compared to Scala 2.12.

Let's add to the docs to help with people's mental model.

@mslinn
Copy link

mslinn commented May 9, 2019

The 2.12 Scaladoc for Option says, and have said for years, on line 2:

The most idiomatic way to use an scala.Option instance is to treat it as a collection or monad and use map,flatMap, filter, or foreach

Most books and training material build on this. The fallout from changing this message, and hence people's mental model, would be considerable.

@lrytz
Copy link
Member Author

lrytz commented May 9, 2019

Option doesn't need to inherit from IterableOps[A, Option, Option[A]], it could instead just inherit from Iterable[A] (with IterableOps[A Iterable, Iterable[A]])

Ah, I didn't consider that variant! I pushed that change for once, to make the discussion more concrete.

@lrytz
Copy link
Member Author

lrytz commented May 9, 2019

Thinking about it, the downside of the fuller change (to extend Iterable instead of IterableOnce) is that it's going to be a much bigger hurdle if we ever want to revert it. For example to move to an unboxed implementation. Option would lose methods/overloads, and it no longer conforming to Iterable is more severe than no longer conforming to IterableOnce (given the size of the two interfaces).

So overall, extending IterableOnce seems to be the right compromise: it fixes the flatMap issue, and it basically means formalizing an existing property of Option: the fact that it implements iterator.

@lrytz lrytz force-pushed the option-iterable-once branch from e6dbd26 to 294f098 Compare May 9, 2019 13:25
@sjrd
Copy link
Member

sjrd commented May 9, 2019

The erasure of None is the ErasedNone(0) singleton. There is no other representation for None.

Here is a variant that does that: sjrd/scala-unboxed-option#4
It does remove one case in USome.apply(), so that's nice. But it does require the public object UNone to extend the private[uoption] class WrappedNone, a design I'm not really fond of.

You'd cache these instances up to some level (and have special code paths for level 0 and possibly level 1 for further optimization).

That's literally what lazy val wrap does.

@adriaanm adriaanm force-pushed the option-iterable-once branch from 294f098 to a002aca Compare May 9, 2019 14:50
@adriaanm adriaanm merged commit 4cf49c2 into scala:2.13.x May 9, 2019
@SethTisue SethTisue added the release-notes worth highlighting in next release notes label May 9, 2019
adriaanm added a commit to scalacommunitybuild/jackson-module-scala that referenced this pull request May 10, 2019
adriaanm added a commit to scalacommunitybuild/jackson-module-scala that referenced this pull request May 10, 2019
adriaanm added a commit to scalacommunitybuild/jackson-module-scala that referenced this pull request May 10, 2019
@Sciss
Copy link

Sciss commented May 22, 2019

In 2.13.0-RC2, it's not nice getting deprecation messages for Option#toSeq, Option#mkString. These are reasonable methods I think...

@adriaanm
Copy link
Contributor

scala> Option(1).toSeq // print <TAB>

scala.Option.option2Iterable[Int](scala.Option.apply[Int](1)).toSeq // : Seq[Int]

To be clear, the deprecation is about option2iterable, which we decided was redundant/too much, especially now that Option is an IterableOnce. re: mkString -- yeah, maybe Option should implement it directly?

@adriaanm
Copy link
Contributor

So, yeah, now I can't remember why we didn't mix IterableOnceOps into Option -- it doesn't have the problematic Iterable methods (fromSpecific etc).

@Sciss
Copy link

Sciss commented May 22, 2019

Perhaps because it contains a number of methods that don't make sense for Option? On the other hand, it means all methods are redefined, e.g. collect, contains without implementing a particular interface.

@julienrf
Copy link
Contributor

IterableOnceOps would also bring take, drop, etc. to Option. I’m afraid this will add too much noise to Option’s API.

@SethTisue
Copy link
Member

SethTisue commented May 22, 2019

discussion on this at https://gitter.im/scala/contributors?at=5ce4f1ab13e9854e3346eb2c (started by @Sciss) (UPDATE: archived at scala/scala-lang#1317 (comment))

this seems especially annoying:

scala 2.13.0-RC2> Option(3).toIterable
                        ^
                  warning: method option2Iterable in object Option is deprecated (since 2.13.0): use `option.toList` or `option.iterator` instead
res0: Iterable[Int] = Iterable(3)

the workaround is Option(3).toList: Iterable[Int], but yuck. (note that Option has toList directly, but no other toX methods except via option2Iterable)

@adriaanm adriaanm mentioned this pull request May 27, 2019
54 tasks
@lrytz
Copy link
Member Author

lrytz commented May 28, 2019

#8106 un-deprecates option2Iterable

mariogalic pushed a commit to mariogalic/docs.scala-lang that referenced this pull request Apr 10, 2021
- Clarify whether Option is considered a collection
- Explain implicit conversion option2iterable
- Address comment scala/scala#8038 (comment) documenting metal model of Iterable -> Collection and IterableOnce -> Iterable
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
library:base Changes to the base library, i.e. Function Tuples Try release-notes worth highlighting in next release notes
Projects
None yet
Development

Successfully merging this pull request may close these issues.