Skip to content

SIP-69: Existential containers #101

New issue

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

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

Already on GitHub? # to your account

Open
wants to merge 11 commits into
base: main
Choose a base branch
from
Open

Conversation

kyouko-taiga
Copy link
Contributor

No description provided.

@kyouko-taiga kyouko-taiga changed the title Existential containers SIP-69: Existential containers Dec 20, 2024
Defining `Polygon` as a type class rather than an abstract class to be inherited allows us to retroactively state that squares are polygons without modifying the definition of `Square`.
Sticking to subtyping would require the definition of an inneficient and verbose wrapper class.

Alas, type classes offer limited support for type erasure–the eliding of some type information at compile-time.
Copy link
Member

@SethTisue SethTisue Dec 21, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

you mean elided at runtime, I think? maybe I misunderstand, but also, I don't know why type erasure is relevant at all?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I did mean compile-time but the sentence can be phrased differently. The point is that you can't easily write a list of things that have the same type class. Meanwhile, it is easy to write a list of things that have the same upper bound.

xs.maxByOption((a) => a.witness.area(a.value))
```

The type `AnyPolygon` conceptually represents a type-erased polygon.
Copy link
Member

@SethTisue SethTisue Dec 21, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Again, I don't understand why we're talking about type erasure here. Maybe it makes sense somehow, but it's not explained sufficiently for me to follow it.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I realize I may simply have a different definition of "type erasure". I will rephrase.


One problem not addressed by the proposed encoding is the support of multiple type classes to form the interface of a specific container.
For example, one may desire to create a container of values whose types conform to both `Polygon` _and_ `Show`.
We have explored possible encodings for such a feature but decided to remove them from this proposal, as support for multiple type classes can most likely be achieved without any additional language change.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is the intent to answer this more fully before the proposal comes to a vote? Or do we expect it to remain an open question until some future iteration after the SIP lands?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would propose that as a future proposal, if necessary. That said it is likely that the SIP committee would not have to formally weigh in this addition because it would only relate to the way existential containers are encoded in the library, without any other change to the language.

Copy link
Member

@bishabosha bishabosha Jan 24, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

support for multiple type classes can most likely be achieved without any additional language change.

you suggest that you can synthesize a single witness: Polygon & Show that can resolve extension methods from either without a language change?

I guess the construction is the harder part - resolution would "just work"

For example, one may desire to create a container of values whose types conform to both `Polygon` _and_ `Show`.
We have explored possible encodings for such a feature but decided to remove them from this proposal, as support for multiple type classes can most likely be achieved without any additional language change.

Another open question relates to possible language support for shortening the expression of a container type and/or value.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ditto on whether this might get filled in soon, or it's definitely a "someday" thing?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's a someday thing but, again, these open questions can hopefully be answered without introducing more changes to the language.

kyouko-taiga and others added 5 commits January 24, 2025 10:33
Co-authored-by: Seth Tisue <seth@tisue.net>
Co-authored-by: Seth Tisue <seth@tisue.net>
Co-authored-by: Seth Tisue <seth@tisue.net>
Co-authored-by: Seth Tisue <seth@tisue.net>
The call to `largest` is illegal because, although there exist witnesses of the `Polygon` and `Hexagon`'s conformance to `Polygon`, no such witness exists for their least common supertype.
In other words, it is impossible to call `largest` with an heterogeneous sequence of polygons.

## Proposed solution
Copy link
Contributor

@lihaoyi lihaoyi Jan 24, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This section is under-specified. Reading it, I have no idea what the proposed solution is, even though I generally agree on the value of the feature being proposed

We need several more use case examples, which are less contrived than Square Rectangle and Polygon. Preferably from real open-source libraries

Someone should be able to read the proposed solution have zero idea how it is implemented, and still get the general idea of what the proposed language feature is about. Right now, that is not the case, so this section of the proposal is incomplete

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I can write other examples but I disagree that the one in the document is contrived. It also generalizes very easily to any situation involving a set of classes conforming to the same type class. Any instance of a situation where the "upper bound" of a set of types is defined retroactively would look like the proposed example.

def lookup(key: List[Containing[Hashable]]) = ???
def zipAll[E](xss: List[Containing[Iterator]{ type Element = E }]) = ???

Instead of justifying existential containers, however, I propose to clarify that the SIP is about sugaring the selection of a method rather than proposing the containers themselves, as those can be defined in library space.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You really need to justify both the language changes and library support. The language change is useless without the library support, but most people would not be familiar with this library-level technique.

List[Containing[Hashable]] is definitely a lot more concrete than Seq[Containing[Polygon]]. We need more examples like that.

  • I've found use for the equivalent of Seq[Containing[upickle.default.Writer]] myself, which contains a list of things that can be converted to JSON

  • Scalatags currently uses methods taking (args: Frag*) with implicit conversions from various types to Frag. it could conceivable instead use (args: Containing[Fragable]*) with a Fragable typeclass to render the value to HTML. Is that better or worse? Why?



A more formal exploration of the state of the art as been documented in a research paper presented prior to this SIP [2].

Copy link
Contributor

@lihaoyi lihaoyi Jan 24, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This proposal is missing a section on Alternatives. I can think of a few myself:

  • What if we just add a Containing[T] type to the standard library; people could then use it without needing a language/compiler change at all? Could it even be a third-party library?

  • What about implicit conversions and implicit constructors? Those are widely used today, often referred to as the "magnet pattern"

Why is the proposed solution better than the alternatives I listed here? What other alternatives should we be aware of, if any?


### Specification

Existential containers are encoded as follows:
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's not clear to me which part of this specification is user-land library code, and which section is compiler/language-level changes. Perhaps we can split it explicitly into two sections?


Another open question relates to possible language support for shortening the expression of a container type and/or value.

## Related work
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Apart from Swift, what other languages have a feature like this? Does Haskell have something similar? Or Rust? Both of those languages have typeclass-like features and presumably would encounter the same issues around e.g. hetoregenous collections. How do they solve it, or do they not?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The proposal mentions dynamic traits in Rust. Those work almost exactly like Swift but are slightly less expressive. The proposal also cite a paper documenting other related work from scientific literature.

I'll add a note mentioning Haskell's ExistentialQuantification extension.

@jducoeur
Copy link

Comment from the peanut gallery: I agree with Haoyi that the structure of the proposal is currently a little confusing. Specifically, since it jumps directly to implementation details, it's very unobvious what the userland code would look like. I'd recommend putting a motivating example of "this is what the end result would look like" before getting into the details of the encoding of Containing.

That said, I love the meat of this proposal -- it's reifying exactly the pattern that I've had to build by hand a number of times for various systems (and which I've been occasionally teaching to my teams), so it would be a real win IMO if we had an official and concise solution.

type Self

/** A value together with an evidence of its type conforming to some type class. */
sealed trait Containing[Concept <: TypeClass]:
Copy link
Member

@bishabosha bishabosha Jan 24, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

does it only work with this upper bound or can it be { type Self <: AnyKind } - so you are not forced to extend this TypeClass trait?


## Appendix

The following is a possible implementation of existential containers.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Here's a version that doesn't rely on the experimental modularity language import (and doesn't require the typeclass to extend TypeClass):

/** A value together with an evidence of its type conforming to some type class. */
sealed trait Containing[TC[_]]:
  /** The type of the contained value. */
  type Value: TC as witness

  /** The contained value. */
  val value: Value

object Containing:

  /** Wraps a value of type `V` and a corresponding typeclass instance `TC[V]` into a `Containing[TC]`. */
  def apply[TC[_]](v: Any)[V >: v.type](using TC[V]): Containing[TC] =
    new Containing[TC]:
      type Value >: V <: V
      val value: Value = v


### Specification

Assuming the existence of an abstraction named `Containing[TC]` for representing containers pairing an arbitrary value with a witness of its conformance to some type class `TC` in the standard library, the compiler injects the selection of the `value` field implicitly when a method of `Containing[TC]` is selected.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since this is in the Specificationsection, can I assume that "the compiler injects ..." is in fact part of what this SIP specifies? If so, it should be fleshed out:

  • How does this interact with other adaptations, including implicit conversions and apply insertion?
  • In which condition does this adaptation triggers exactly? Is Containing treated specially by the compiler or can we (and should we) generalize this?

Otherwise, this should be in a separate section (example or appendix)

stage: design
status: submitted
presip-thread: n/a
title: SIP-NN - Existential Containers
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The SIP number is already known

### Other concerns

This document has been written under the experimental modularity improvements for Scala 3.
Although the proposed feature is fully expressible without those changes, the encoding of existential containers can only work with the "old" (i.e., the one currently used in production) or "new" type class style.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I had to read this sentence a few times to get the point so maybe it would be good to rephrase it somehow (it's not clear at first which expression only refers to)

# for free to join this conversation on GitHub. Already have an account? # to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

8 participants