(Pitch) Conformance Regions


(Ross O'Brien) #1

This idea was had during the SE-0159 Review regarding removing fileprivate
and I'm creating a new discussion thread for its consideration. It's
neither in favour of nor against keeping fileprivate, but is intended to
address an idiom which has led to some resentment against fileprivate.

Copy-pasting from my original post on this:

When we declare a type, we declare properties and functions which that type
has.
When we extend a type, we add functions to the type. (I'm including
computed properties in this.)
It has become an idiom of Swift to declare an extension to a type for each
protocol we want it to conform to, for reasons of code organisation and
readability. This can be true even if conformance to the protocol was a
primary intent of creating the type in the first place.

The intent of the scoped access level (one use of the current private keyword)
is to allow programmers to create properties and functions which are
limited to the scope of their declaration. A protocol conformance can be
written, with the aid of helper functions, in the confidence that the
helper functions are not visible outside the extension, minimising their
impact on other components of the module.
However, some protocol conformances require the type to have a specific
property, which the extension cannot facilitate. Some protocol conformances
don't require a property, but it would be really useful to have one, and
again an extension can't facilitate.

Example: we want to be able to write this, but we can't:

private protocol Bar

{

  var integer : Int { get }

  func increment()

}

struct Foo

{

}

extension Foo : Bar

{

  var integer : Int

  private var counter : Int

  func increment()

  {

  counter += 1

  }

}

This leads to a workaround: that properties are added to the original type,
and declared as fileprivate. They're not intended to be visible to any
scope other than the conforming extension - not even, really, to the type's
original scope.

Continuing the example: we've compromised and written this:

struct Foo

{

  fileprivate var integer : Int

  fileprivate var counter : Int

}

extension Foo : Bar

{

  func increment()

  {

  counter += 1

  }

}

This is not a fault of fileprivate (though it's a clunky name), or private.
Renaming these levels does not solve the problem. Removing private, such
that everything becomes fileprivate, does not solve the problem. The
problem is in the extension system.

Proposal:
Suppose we approached extensions differently.

Suppose we created a 'conformance region' inside a type declaration - a
scope nested within the type declaration scope - and that this conformance
region had its own access level. It's inside the type declaration, not
separate from it like an extension, so we can declare properties inside it.
But literally the only properties and functions declared inside the region
but visible anywhere outside of it, would be properties and functions
declared in the named protocol being conformed to.

So, visually it might look like this:

struct Foo

{

  conformance Bar // or conformance Foo : Bar, but since the region is
inside Foo that's redundant

  {

  var integer : Int // visible because Foo : Bar, at Bar's access level

  var counter : Int = 0 // only visible inside the conformance scope,
because not declared in Bar

  func increment() // visible because Foo : Bar, at Bar's access level

  {

  counter += 1

  }

  }

}

I've introduced a new keyword for this example, conformance, though it may
be clear enough to keep using extension. As the extension is inside the
type there's no need to redeclare the type being extended. From this
example, Foo conforms to Bar, in the same file; it's just been written
inside Foo's type declaration, and indented one level, instead of after it.

Aspects worth considering (some already pointed out by others):
The original idea for this is that the conformance region exists only to
allow the type to conform to a protocol (though possibly more than one),
and that only properties and functions declared in those protocols would be
accessible outside of the region, at whatever access level the protocol(s)
originally declared.
Existing access terms (internal, fileprivate, etc.) could be used to
increase the visibility (to a maximum of the visibility of the declared
type, e.g. a public property in a conformance region of an internal type
conforming to a fileprivate protocol would be an internally visible
property). This would introduce no new keywords.
However, as this defines a new default level within a region of the
language, an explicit keyword might be preferred and a default level of
internal might be more intuitive.

This idea presently assumes that conformance regions do not nest. An inner
nested type would be able to declare conformance regions in its
declaration, and cannot be extended inside another conformance region of
the outer type. However, there might be different thoughts on this?

We might consider conformance regions in generic types where the associated
type meets certain conditions.

This is an additive pitch. It doesn't affect extensions which
'retroactively' conform types to protocols; it just more visibly identifies
active conformances from retroactive conformances.
The pitch is intended to better express the intent of an existing idiom,
which may reduce the frustration users have with fileprivate. It's not a
replacement to fileprivate. It may be worth postponing SE-0159's resolution
until the effect of this on Swift is seen.

I've likely missed things from the comments of others since I posted this
earlier this week. But I welcome your thoughts.

Ross


(Félix Cloutier) #2

What will you do if you want to privately adopt two protocols that require identical members?

Also, you write that you want to be able to write members into extensions but you can't. Have you considered pushing to make it possible?

···

Le 30 mars 2017 à 10:07, Ross O'Brien via swift-evolution <swift-evolution@swift.org> a écrit :

This idea was had during the SE-0159 Review regarding removing fileprivate and I'm creating a new discussion thread for its consideration. It's neither in favour of nor against keeping fileprivate, but is intended to address an idiom which has led to some resentment against fileprivate.

Copy-pasting from my original post on this:

When we declare a type, we declare properties and functions which that type has.
When we extend a type, we add functions to the type. (I'm including computed properties in this.)
It has become an idiom of Swift to declare an extension to a type for each protocol we want it to conform to, for reasons of code organisation and readability. This can be true even if conformance to the protocol was a primary intent of creating the type in the first place.

The intent of the scoped access level (one use of the current private keyword) is to allow programmers to create properties and functions which are limited to the scope of their declaration. A protocol conformance can be written, with the aid of helper functions, in the confidence that the helper functions are not visible outside the extension, minimising their impact on other components of the module.
However, some protocol conformances require the type to have a specific property, which the extension cannot facilitate. Some protocol conformances don't require a property, but it would be really useful to have one, and again an extension can't facilitate.

Example: we want to be able to write this, but we can't:

private protocol Bar
{
  var integer : Int { get }
  func increment()
}

struct Foo
{
}

extension Foo : Bar
{
  var integer : Int

  private var counter : Int
  func increment()
  {
    counter += 1
  }
}

This leads to a workaround: that properties are added to the original type, and declared as fileprivate. They're not intended to be visible to any scope other than the conforming extension - not even, really, to the type's original scope.

Continuing the example: we've compromised and written this:

struct Foo
{
  fileprivate var integer : Int
  fileprivate var counter : Int
}

extension Foo : Bar
{
  func increment()
  {
    counter += 1
  }
}

This is not a fault of fileprivate (though it's a clunky name), or private. Renaming these levels does not solve the problem. Removing private, such that everything becomes fileprivate, does not solve the problem. The problem is in the extension system.

Proposal:
Suppose we approached extensions differently.

Suppose we created a 'conformance region' inside a type declaration - a scope nested within the type declaration scope - and that this conformance region had its own access level. It's inside the type declaration, not separate from it like an extension, so we can declare properties inside it. But literally the only properties and functions declared inside the region but visible anywhere outside of it, would be properties and functions declared in the named protocol being conformed to.

So, visually it might look like this:

struct Foo
{
  conformance Bar // or conformance Foo : Bar, but since the region is inside Foo that's redundant
  {
    var integer : Int // visible because Foo : Bar, at Bar's access level

    var counter : Int = 0 // only visible inside the conformance scope, because not declared in Bar

    func increment() // visible because Foo : Bar, at Bar's access level
    {
      counter += 1
    }
  }
}

I've introduced a new keyword for this example, conformance, though it may be clear enough to keep using extension. As the extension is inside the type there's no need to redeclare the type being extended. From this example, Foo conforms to Bar, in the same file; it's just been written inside Foo's type declaration, and indented one level, instead of after it.

Aspects worth considering (some already pointed out by others):
The original idea for this is that the conformance region exists only to allow the type to conform to a protocol (though possibly more than one), and that only properties and functions declared in those protocols would be accessible outside of the region, at whatever access level the protocol(s) originally declared.
Existing access terms (internal, fileprivate, etc.) could be used to increase the visibility (to a maximum of the visibility of the declared type, e.g. a public property in a conformance region of an internal type conforming to a fileprivate protocol would be an internally visible property). This would introduce no new keywords.
However, as this defines a new default level within a region of the language, an explicit keyword might be preferred and a default level of internal might be more intuitive.

This idea presently assumes that conformance regions do not nest. An inner nested type would be able to declare conformance regions in its declaration, and cannot be extended inside another conformance region of the outer type. However, there might be different thoughts on this?

We might consider conformance regions in generic types where the associated type meets certain conditions.

This is an additive pitch. It doesn't affect extensions which 'retroactively' conform types to protocols; it just more visibly identifies active conformances from retroactive conformances.
The pitch is intended to better express the intent of an existing idiom, which may reduce the frustration users have with fileprivate. It's not a replacement to fileprivate. It may be worth postponing SE-0159's resolution until the effect of this on Swift is seen.

I've likely missed things from the comments of others since I posted this earlier this week. But I welcome your thoughts.

Ross
_______________________________________________
swift-evolution mailing list
swift-evolution@swift.org
https://lists.swift.org/mailman/listinfo/swift-evolution


(Jeremy Pereira) #3

It has become an idiom of Swift to declare an extension to a type for each protocol we want it to conform to, for reasons of code organisation and readability. This can be true even if conformance to the protocol was a primary intent of creating the type in the first place.

But this is only a convention. It’s not a compiler error to declare protocol conformance on the min type definition and it is not wrong to declare conformance on the main type definition.

Suppose we created a 'conformance region' inside a type declaration - a scope nested within the type declaration scope - and that this conformance region had its own access level. It's inside the type declaration, not separate from it like an extension, so we can declare properties inside it. But literally the only properties and functions declared inside the region but visible anywhere outside of it, would be properties and functions declared in the named protocol being conformed to.

So, visually it might look like this:

struct Foo
{
  conformance Bar // or conformance Foo : Bar, but since the region is inside Foo that's redundant
  {
    var integer : Int // visible because Foo : Bar, at Bar's access level

    var counter : Int = 0 // only visible inside the conformance scope, because not declared in Bar

    func increment() // visible because Foo : Bar, at Bar's access level
    {
      counter += 1
    }
  }
}

Here is a problem with the above. Suppose you hadn’t helpfully annotated each of the vars with a comment telling us where it is visible, a reader couldn’t tell at a glance what the visibility of each var is without looking it up in the protocol.

IMO this adds a lot of unnecessary complexity in order to offer language support for one particular way of laying out your code.

···

On 30 Mar 2017, at 18:07, Ross O'Brien via swift-evolution <swift-evolution@swift.org> wrote:


(Charles Srstka) #4

Huge +1 in general, but with one exception:

···

On Mar 30, 2017, at 12:07 PM, Ross O'Brien via swift-evolution <swift-evolution@swift.org> wrote:

struct Foo
{
  conformance Bar // or conformance Foo : Bar, but since the region is inside Foo that's redundant
  {
    var integer : Int // visible because Foo : Bar, at Bar's access level

    var counter : Int = 0 // only visible inside the conformance scope, because not declared in Bar

    func increment() // visible because Foo : Bar, at Bar's access level
    {
      counter += 1
    }
  }
}

Making ‘counter’ private here without being annotated as such is just going to cause confusion. If a member is to be private to the conformance, it should have the ‘private’ keyword in front of it to make that clear. If we want to enforce that the only non-private members in the conformance should be members that actually go toward implementing the conformance, then we should just have the above generate a compiler error, with the fix-it being to add ‘private’ to the declaration.

Massive +1 otherwise.

Charles


(Robert Widmann) #5

Any proposal that seeks to add private conformances needs to answer to the dynamic side of Swift, most importantly: What does this return:

let f = Foo()
f is Bar

If the answer is “it depends”, it needs to be explicitly stated where and how.

···

On Mar 30, 2017, at 1:07 PM, Ross O'Brien via swift-evolution <swift-evolution@swift.org> wrote:

This idea was had during the SE-0159 Review regarding removing fileprivate and I'm creating a new discussion thread for its consideration. It's neither in favour of nor against keeping fileprivate, but is intended to address an idiom which has led to some resentment against fileprivate.

Copy-pasting from my original post on this:

When we declare a type, we declare properties and functions which that type has.
When we extend a type, we add functions to the type. (I'm including computed properties in this.)
It has become an idiom of Swift to declare an extension to a type for each protocol we want it to conform to, for reasons of code organisation and readability. This can be true even if conformance to the protocol was a primary intent of creating the type in the first place.

The intent of the scoped access level (one use of the current private keyword) is to allow programmers to create properties and functions which are limited to the scope of their declaration. A protocol conformance can be written, with the aid of helper functions, in the confidence that the helper functions are not visible outside the extension, minimising their impact on other components of the module.
However, some protocol conformances require the type to have a specific property, which the extension cannot facilitate. Some protocol conformances don't require a property, but it would be really useful to have one, and again an extension can't facilitate.

Example: we want to be able to write this, but we can't:

private protocol Bar
{
  var integer : Int { get }
  func increment()
}

struct Foo
{
}

extension Foo : Bar
{
  var integer : Int

  private var counter : Int
  func increment()
  {
    counter += 1
  }
}

This leads to a workaround: that properties are added to the original type, and declared as fileprivate. They're not intended to be visible to any scope other than the conforming extension - not even, really, to the type's original scope.

Continuing the example: we've compromised and written this:

struct Foo
{
  fileprivate var integer : Int
  fileprivate var counter : Int
}

extension Foo : Bar
{
  func increment()
  {
    counter += 1
  }
}

This is not a fault of fileprivate (though it's a clunky name), or private. Renaming these levels does not solve the problem. Removing private, such that everything becomes fileprivate, does not solve the problem. The problem is in the extension system.

Proposal:
Suppose we approached extensions differently.

Suppose we created a 'conformance region' inside a type declaration - a scope nested within the type declaration scope - and that this conformance region had its own access level. It's inside the type declaration, not separate from it like an extension, so we can declare properties inside it. But literally the only properties and functions declared inside the region but visible anywhere outside of it, would be properties and functions declared in the named protocol being conformed to.

So, visually it might look like this:

struct Foo
{
  conformance Bar // or conformance Foo : Bar, but since the region is inside Foo that's redundant
  {
    var integer : Int // visible because Foo : Bar, at Bar's access level

    var counter : Int = 0 // only visible inside the conformance scope, because not declared in Bar

    func increment() // visible because Foo : Bar, at Bar's access level
    {
      counter += 1
    }
  }
}

I've introduced a new keyword for this example, conformance, though it may be clear enough to keep using extension. As the extension is inside the type there's no need to redeclare the type being extended. From this example, Foo conforms to Bar, in the same file; it's just been written inside Foo's type declaration, and indented one level, instead of after it.

Aspects worth considering (some already pointed out by others):
The original idea for this is that the conformance region exists only to allow the type to conform to a protocol (though possibly more than one), and that only properties and functions declared in those protocols would be accessible outside of the region, at whatever access level the protocol(s) originally declared.
Existing access terms (internal, fileprivate, etc.) could be used to increase the visibility (to a maximum of the visibility of the declared type, e.g. a public property in a conformance region of an internal type conforming to a fileprivate protocol would be an internally visible property). This would introduce no new keywords.
However, as this defines a new default level within a region of the language, an explicit keyword might be preferred and a default level of internal might be more intuitive.

This idea presently assumes that conformance regions do not nest. An inner nested type would be able to declare conformance regions in its declaration, and cannot be extended inside another conformance region of the outer type. However, there might be different thoughts on this?

We might consider conformance regions in generic types where the associated type meets certain conditions.

This is an additive pitch. It doesn't affect extensions which 'retroactively' conform types to protocols; it just more visibly identifies active conformances from retroactive conformances.
The pitch is intended to better express the intent of an existing idiom, which may reduce the frustration users have with fileprivate. It's not a replacement to fileprivate. It may be worth postponing SE-0159's resolution until the effect of this on Swift is seen.

I've likely missed things from the comments of others since I posted this earlier this week. But I welcome your thoughts.

Ross
_______________________________________________
swift-evolution mailing list
swift-evolution@swift.org
https://lists.swift.org/mailman/listinfo/swift-evolution


(Vladimir) #6

What will you do if you want to privately adopt two protocols that require
identical members?

I believe the same thing we do currently: conform to both protocols in the same declaration(like class C: P1, P2 {...}) or conform to second protocol without duplicate members(i.e. part of them was declared in P1 conformance).

Also, you write that you want to be able to write members into extensions
but you can't. Have you considered pushing to make it possible?

Personally I think that proposed feature does not worth special keyword, IMO 'extension' keyword could play this role well. Also, with 'extension' keyword you can have just a region of code you want to group(or probably isolate), 'conformance' means you should conform to some protocol.

I started the pitch "Nested extensions and stored properties", but actually I was waiting for Ross, and though that Ross decided to not start the separate thread, and as soon as Ross started it - I suggest to discuss the feature in this "Conformance Regions" thread and ignore my thread.

My idea was to allow stored properties inside extensions that are declared inside type definition scope, instead of new keyword. Then stored properties declared in such extension will be just a part of type declaration.

All the rules for access modifiers/levels for extensions will be the same as for "normal" extensions - i.e. for example 'scoped' modifier inside such extensions will work just like it works currently - such member not accessible outside of scope(extension).

Copy&paste of example from my previous thread:

class MyType { // internal
   public var a = 10 // internal(because of type's access level)
   var b = 20 // internal
   scoped var c = 30 // scoped for type and for nested extensions

   // default and max level is internal, bounded by type's access level
   extension MyProtoB {
     scoped var h = 40 // scoped for extension, inaccessible outside of it

     var i = 50 // internal
     internal var j = 60 // internal
     public var k = 70 // public->internal(type's access level)
   }

   // default and max level is fileprivate, bounded by type's access level
   fileprivate extension MyProtoA {
     scoped var d = 10 // scoped for extension, inaccessible outside of it

     var e = 20 // fileprivate
     internal var f = 20 // fileprivate
     public var g = 30 // fileprivate

     func foo() { print(c, i, m) } // can access c, i, m
   }

   // default and max level is public, bounded by type's access level
   public extension MyProtoC {
     scoped var l = 80 // scoped for extension, inaccessible outside of it

     var m = 90 // public -> internal(type's access level)
     internal var n = 100 // internal
     public var o = 110 // public->internal(type's access level)
   }
}

Also we should disallow 'scoped' modifier for nested and "normal" extensions in this case.

···

On 30.03.2017 21:13, Félix Cloutier via swift-evolution wrote:

Le 30 mars 2017 à 10:07, Ross O'Brien via swift-evolution >> <swift-evolution@swift.org <mailto:swift-evolution@swift.org>> a écrit :

This idea was had during the SE-0159 Review regarding
removing fileprivate and I'm creating a new discussion thread for its
consideration. It's neither in favour of nor against keeping fileprivate,
but is intended to address an idiom which has led to some resentment
against fileprivate.

Copy-pasting from my original post on this:

When we declare a type, we declare properties and functions which that
type has.
When we extend a type, we add functions to the type. (I'm including
computed properties in this.)
It has become an idiom of Swift to declare an extension to a type for
each protocol we want it to conform to, for reasons of code organisation
and readability. This can be true even if conformance to the protocol was
a primary intent of creating the type in the first place.

The intent of the scoped access level (one use of the
current private keyword) is to allow programmers to create properties and
functions which are limited to the scope of their declaration. A protocol
conformance can be written, with the aid of helper functions, in the
confidence that the helper functions are not visible outside the
extension, minimising their impact on other components of the module.
However, some protocol conformances require the type to have a specific
property, which the extension cannot facilitate. Some protocol
conformances don't require a property, but it would be really useful to
have one, and again an extension can't facilitate.

Example: we want to be able to write this, but we can't:

private protocol Bar
{
  var integer : Int { get }
  func increment()
}

struct Foo
{
}

extension Foo : Bar
{
  var integer : Int

  private var counter : Int
  func increment()
  {
  counter += 1
  }

}

This leads to a workaround: that properties are added to the original
type, and declared as fileprivate. They're not intended to be visible to
any scope other than the conforming extension - not even, really, to the
type's original scope.

Continuing the example: we've compromised and written this:

struct Foo
{
  fileprivate var integer : Int
  fileprivate var counter : Int
}

extension Foo : Bar
{
  func increment()
  {
  counter += 1
  }
}

This is not a fault of fileprivate (though it's a clunky name),
or private. Renaming these levels does not solve the problem. Removing
private, such that everything becomes fileprivate, does not solve the
problem. The problem is in the extension system.

Proposal:
Suppose we approached extensions differently.

Suppose we created a 'conformance region' inside a type declaration - a
scope nested within the type declaration scope - and that this
conformance region had its own access level. It's inside the type
declaration, not separate from it like an extension, so we can declare
properties inside it. But literally the only properties and functions
declared inside the region but visible anywhere outside of it, would be
properties and functions declared in the named protocol being conformed to.

So, visually it might look like this:

struct Foo
{
  conformance Bar // or conformance Foo : Bar, but since the region is
inside Foo that's redundant
  {
  var integer : Int // visible because Foo : Bar, at Bar's access level

  var counter : Int = 0 // only visible inside the conformance scope,
because not declared in Bar

  func increment() // visible because Foo : Bar, at Bar's access level
  {
  counter += 1
  }
}

I've introduced a new keyword for this example, conformance, though it
may be clear enough to keep using extension. As the extension is inside
the type there's no need to redeclare the type being extended. From this
example, Foo conforms to Bar, in the same file; it's just been written
inside Foo's type declaration, and indented one level, instead of after it.

Aspects worth considering (some already pointed out by others):
The original idea for this is that the conformance region exists only to
allow the type to conform to a protocol (though possibly more than one),
and that only properties and functions declared in those protocols would
be accessible outside of the region, at whatever access level the
protocol(s) originally declared.
Existing access terms (internal, fileprivate, etc.) could be used to
increase the visibility (to a maximum of the visibility of the declared
type, e.g. a public property in a conformance region of an internal type
conforming to a fileprivate protocol would be an internally visible
property). This would introduce no new keywords.
However, as this defines a new default level within a region of the
language, an explicit keyword might be preferred and a default level of
internal might be more intuitive.

This idea presently assumes that conformance regions do not nest. An
inner nested type would be able to declare conformance regions in its
declaration, and cannot be extended inside another conformance region of
the outer type. However, there might be different thoughts on this?

We might consider conformance regions in generic types where the
associated type meets certain conditions.

This is an additive pitch. It doesn't affect extensions which
'retroactively' conform types to protocols; it just more visibly
identifies active conformances from retroactive conformances.
The pitch is intended to better express the intent of an existing idiom,
which may reduce the frustration users have with fileprivate. It's not a
replacement to fileprivate. It may be worth postponing SE-0159's
resolution until the effect of this on Swift is seen.

I've likely missed things from the comments of others since I posted this
earlier this week. But I welcome your thoughts.

Ross
_______________________________________________
swift-evolution mailing list
swift-evolution@swift.org <mailto:swift-evolution@swift.org>
https://lists.swift.org/mailman/listinfo/swift-evolution

_______________________________________________
swift-evolution mailing list
swift-evolution@swift.org
https://lists.swift.org/mailman/listinfo/swift-evolution