access control proposal


(Drew Crawford) #1

The discussion so far seems to be between folks who think the existing model is obviously complete vs folks who think that it obviously isn’t, but there isn’t a lot of talk about the specific motivation for the feature.

I know a new access control modifier *looks like* a bell/whistle, but I believe *in practice* it is a *safety* feature, on the level of type safety or array-out-of-bounds. This result is counterintuitive.
   I’d like to present my motivation, which I think is pretty compelling. In several codebases, I have

final private class Foo { //this class is an implementation detail of Bar and is declared ‘private'
   private var _children : [Next] = [] //access only from special queue, as required by $EXTERNAL_REQUIREMENT
   let queue: dispatch_queue_t = dispatch_queue_create(“specialQueue", DISPATCH_QUEUE_SERIAL)
      func appendChild(n: Next) {
           dispatch_sync(queue) { _children.append(n) }
      }
}

final class Bar {
   let f = Foo()
   init() {
  f.appendChild(…)
       f._children[0].baz() //whoops: race condition
   }
}

   In words, the problem here is that:
   1. Foo is an implementation detail of Bar, and so Foo should be private

- Ignored:
   2. Foo has its own implementation detail, _children
   3. Because Foo is already private, _children cannot be “more” private
   4. Thus _children must be visible at the file level
   5. Since it is visible at the file level, it may be used
   6. If it is used, it will most likely cause a race condition
   7. Race conditions are very hard to debug and I need all the help I can get not to write them in the first place

   The solutions I see for this problem are as follows:

   1. This thread’s proposal (e.g., to invent a new access modifier, restricting _children to visibility below the file level)
   2. Vend a framework / module just for these two classes. I present this as silly for many realistic sizes of Foo and Bar.
   3. Promote Foo to internal visibility, when it is really a private implementation detail
   4. What I actually do in this codelisting, which is to use underscores as a naming convention, and pray nobody on the team screws it up, because the compiler is no help.

   I would like to hear from the folks who don’t see the need of another access modifier, what solution to this problem they plan to maintain in their own projects. Because this design pattern (queue-controlled ivar) is not uncommon, and the lack of a fourth access control keyword has produced real race conditions that I have spent real time debugging, that is completely avoidable via a keyword like classified/local/secret.

   Drew


(Brent Royal-Gordon) #2

  I would like to hear from the folks who don’t see the need of another access modifier, what solution to this problem they plan to maintain in their own projects. Because this design pattern (queue-controlled ivar) is not uncommon, and the lack of a fourth access control keyword has produced real race conditions that I have spent real time debugging, that is completely avoidable via a keyword like classified/local/secret.

My solution is to be careful when I’m writing code.

I mean, look, you’re right. In your case, three access modifiers isn’t enough to prevent this mistake; you need four. But, given four, you could provide an example that needs five. Given five, you could come up with one for six. Given N access modifiers, you can write a case where you need N + 1.

There will never be enough. At some point, you just have to draw the line and say “this is enough; beyond here, you’re just going to have to take responsibility for your code.” Swift draws the line at a single file, a cohesive unit of code that’s all visible at once (at least with scrolling), but which still can contain things with no direct relationship. Small enough that you ought to be able to keep the outline of it in your head, but large enough that there’s still plenty of flexibility.

This is absolutely a judgement call that reasonable people can disagree on. But personally, my judgement is that `private` is private enough.

ADDENDUM:

Actually, for this case, there *is* a way to achieve better safety without additional access modifiers. Create a new file and call it Synchronized.swift:

  // This could be a struct, but that leads to some odd copying semantics.
  final class Synchronized<Value> {
    private var value: Value
    private let queue = dispatch_queue_create(“specialQueue", DISPATCH_QUEUE_SERIAL)
    
    init(value: Value) {
      self.value = value
    }
    
    mutating func withValue<R>(closure: (inout value: T) -> R) -> R {
      var returnValue: R?
      dispatch_sync(queue) {
        returnValue = closure(value)
      }
      return returnValue!
    }
  }

Now in your original file, you write:

  final private class Foo { //this class is an implementation detail of Bar and is declared ‘private'
    private var _children = Synchronized(value: [] as [Next]) //access only from special queue, as required by $EXTERNAL_REQUIREMENT
       func appendChild(n: Next) {
            _children.withValue { (inout children: [Next]) in
      children.append(n)
    }
       }
  }
  
  final class Bar {
    let f = Foo()
    init() {
        f.appendChild(…)
        f._children[0].baz() // error—_children can’t do this directly, you need to call .withValue()
        f._children.value[0].baz() // error—value is private and inaccessible from here
    }
  }

Synchronized can, of course, be modified to support all sorts of other cases: shared queues, parallel read-only access, etc. The general concept of using *another* file’s `private` modifier to protect *this* file’s data can be used in many cases, and I think it’s a pretty good solution to problems like these.

···

--
Brent Royal-Gordon
Architechies


(Drew Crawford) #3

I mean, look, you’re right. In your case, three access modifiers isn’t enough to prevent this mistake; you need four. But,
given four, you could provide an example that needs five. Given five, you could come up with one for six. Given N access modifiers, you can write a case where you need N + 1.

Nope. What we're talking about (what I'm talking about, anyway) is a syntactically scoped access modifier. There is no N+1 case. There is no "even more scoped". There is no inductive case.

I mean, it is possible to write a troll proposal for an access modifier visible to the 3 lines before and after the declaration, or something. That much I grant. But troll proposals aside, a scoped access modifier is the quark of the access modifiers; there is no sensible way to go halvsies.

Actually, for this case, there *is* a way to achieve better safety without additional access modifiers. Create a new file and call it Synchronized.swift:

This is the solution that I ship. I think if we don't add a new visibility modifier, we should definitely do this in the standard library, because "resource synchronization" is a problem most modern programs have.

This particular implementation is insufficiently general, however--there are more ways to synchronize a resource than on a GCD dispatch queue. I do agree that a general solution can be constructed out of this example, and reasonable people can disagree about whether adding local or adding a more-general Synchronized is a better solution to this problem.

My solution is to be careful when I’m writing code.

It is actually *this* statement which is weak to an induction attack. We can *always* be more careful writing code. Why do we need typechecking, why do we need integer overflow trapping, why do we need array out of bounds exceptions? The C developer says they do not need half of Swift because their solution is to be more careful writing code. The C developer is naive.

But in *this* language, we adopt zero and low-cost abstractions in the name of safety. A scoped access modifier is a zero-cost abstraction that makes the language safer. We should adopt it. If for no other reason, than consistency with the other aspects of the language design. Either that or we should move integer overflow checks into the standard library for consistency with Synchronized, which I present as a troll proposal.

I mean, I don't know about you, but I get called in to look at projects regularly that have a MainViewController.swift that weighs 10KLOC. I didn't write it. I didn't make the mess. I just want tools to help me clean it up. 'private' is useless in that context, and it's a (sadly) typical context in iOS.


(Matthew Johnson) #4

I mean, I don't know about you, but I get called in to look at projects regularly that have a MainViewController.swift that weighs 10KLOC. I didn't write it. I didn't make the mess. I just want tools to help me clean it up. 'private' is useless in that context, and it's a (sadly) typical context in iOS.

+1. Messes like this are all too common in the real world.

Even worse, they often involve poorly thought out uses of uses of inheritance. We need tools to help clean them up and also a language designed to make it more difficult to unintentionally create them in the first place. This is a very non-trivial part of the argument for making final the default IMO.

Matthew


(Brent Royal-Gordon) #5

I mean, look, you’re right. In your case, three access modifiers isn’t enough to prevent this mistake; you need four. But,
given four, you could provide an example that needs five. Given five, you could come up with one for six. Given N access modifiers, you can write a case where you need N + 1.

Nope. What we're talking about (what I'm talking about, anyway) is a syntactically scoped access modifier. There is no N+1 case. There is no "even more scoped". There is no inductive case.

I mean, it is possible to write a troll proposal for an access modifier visible to the 3 lines before and after the declaration, or something. That much I grant. But troll proposals aside, a scoped access modifier is the quark of the access modifiers; there is no sensible way to go halvsies.

I actually immediately thought of several things you might want:

• You have two logical “groups” of properties and methods in a type, and you want to only permit access within a group. But both groups have stored properties, so they must be defined in the scope of the main type definition.
• You have types nested within your type. You want methods in your own type to have access, but not methods in those nested types.
• Alternatively, you have types nested within your type. You want the outer type to have access to things in the nested type, but not any other type.

I don’t think these are troll proposals. They *are* a little more limited, a little more esoteric, but you could say the same thing about `local` vs. `private`. And in some cases you can emulate some of these with the four access modifiers you want, but again, you could say the same thing about `local` vs. `private`.

Actually, for this case, there *is* a way to achieve better safety without additional access modifiers. Create a new file and call it Synchronized.swift:

This is the solution that I ship. I think if we don't add a new visibility modifier, we should definitely do this in the standard library, because "resource synchronization" is a problem most modern programs have.

This is not a bad idea. The only potential issue I can see with it is that, if it’s made too general, it might be difficult to construct a Synchronized instance correctly.

My solution is to be careful when I’m writing code.

It is actually *this* statement which is weak to an induction attack. We can *always* be more careful writing code. Why do we need typechecking, why do we need integer overflow trapping, why do we need array out of bounds exceptions? The C developer says they do not need half of Swift because their solution is to be more careful writing code. The C developer is naive.

But in *this* language, we adopt zero and low-cost abstractions in the name of safety. A scoped access modifier is a zero-cost abstraction that makes the language safer. We should adopt it. If for no other reason, than consistency with the other aspects of the language design. Either that or we should move integer overflow checks into the standard library for consistency with Synchronized, which I present as a troll proposal.

But it’s not zero-cost. It’s another thing to learn, another thing to think about, another way you have to mentally analyze your code. It has to be balanced against the goal of keeping the language small and simple. And many people, when they do that, find this idea wanting.

I mean, I don't know about you, but I get called in to look at projects regularly that have a MainViewController.swift that weighs 10KLOC. I didn't write it. I didn't make the mess. I just want tools to help me clean it up. 'private' is useless in that context, and it's a (sadly) typical context in iOS.

`local` is going to be equally useless in this context, because MainViewController.swift will almost certainly contain one 9.999KLOC `class` block. Cleaning up is, practically by definition, messy and tiring.

···

--
Brent Royal-Gordon
Architechies


(Ilya Belenkiy) #6

+1 on both comments.

···

--
Ilya Belenkiy
On Sat, Dec 12, 2015 at 6:20 PM Matthew Johnson via swift-evolution < swift-evolution@swift.org> wrote:

> I mean, I don't know about you, but I get called in to look at projects
regularly that have a MainViewController.swift that weighs 10KLOC. I
didn't write it. I didn't make the mess. I just want tools to help me
clean it up. 'private' is useless in that context, and it's a (sadly)
typical context in iOS.

+1. Messes like this are all too common in the real world.

Even worse, they often involve poorly thought out uses of uses of
inheritance. We need tools to help clean them up and also a language
designed to make it more difficult to unintentionally create them in the
first place. This is a very non-trivial part of the argument for making
final the default IMO.

Matthew

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


(Drew Crawford) #7

I don’t think these are troll proposals. They *are* a little more limited, a little more esoteric, but you could say the same thing about `local` vs. `private`. And in some cases you can emulate some of these with the four access modifiers you want, but again, you could say the same thing about `local` vs. `private`.

You're right. Your examples have convinced me of this point. There exist non-troll proposals for further access modifiers.

But it’s not zero-cost. It’s another thing to learn, another thing to think about, another way you have to mentally analyze your code.

I meant zero performance cost. Of course all features have "cost" if we mean "cognitive overhead". Type safety for example has a huge cognitive overhead. Think back to the days of "Bool is not an NSString". But the benefit of typesafety is large.

In this case, the cognitive overhead is small, and so is the benefit. But I think the value-per-unit-cost is similar. In both cases the compiler helps you not do something very very bad, that is hard to debug in ObjC.

many people, when they do that, find this idea wanting.

Who? You? Then build an argument around that. I don't know who "many people" are or what their justification is.

My justification is essentially that A) something like Synchronized is a problem nearly everybody has and B) the difficulty of defining a class-based solution in an optimal way.

On B, we seem to agree:

it might be difficult to construct a Synchronized instance correctly.

So I can only conclude you disagree about A. However, I think my A is much stronger than is strictly necessary to demand a language feature. There are plenty of language features that not everyone uses, so the fact that you don't have a need for it (or even "many people") is not really a counterargument I am able to understand.

···

On Dec 13, 2015, at 3:57 PM, Brent Royal-Gordon <brent@architechies.com> wrote:


(Ilya Belenkiy) #8

All of these examples for more access control could and should be
refactored to have simple interfaces with completely hidden implementation
details.

The discussion has become unnecessarily complicated. This proposal is about
a fundamental concept of software engineering: provide a small and simple
interface that serves a particular purpose and hide implementation details.

Implementation details need to be hidden for several reasons:
- ensure that the invariant of the public API holds true regardless of how
the public API is used. For example, a stack's element count and relative
order changes only with push() and pop().
- the implementation could be changed without affecting how the API is
used. For example, a stack backed by an array could turn into a stack
backed by a linked list.
- the API that the user has to deal with is small. If the user can see the
internals, he has to constantly separate what's public (and allowed) and
what is private (and could change).

Currently, the only 2 ways to express this concept in Swift are
1) use private and put the implementation into a separate file
2) use _ or some other convention to distinguish public APIs from the
functions that are used for implementation

Using separate files works, but it doesn't work well for related concepts.
It often makes sense to group similar concepts / interfaces / APIs in one
file. This solution comes at the expense of not being able to do this,
which I find extremely limiting. An analogy: it makes sense to put separate
chapters of a book into different files, but a file per paragraph is
extremely inconvenient.

Using _ also helps, but it's coding by convention. It is still possible and
easy to break it and break the code. And we see it all the time with ObjC
developers trying to use private APIs to fix a bug or add functionality
that is not public. It also forces the programmer to sort through all the _
in search for the APIs that he can use. And if he first sees a similar
private API, it takes a mental strain not to use it and keep searching for
the pubic version (if there is one).

Using local solves the problem an all accounts:
- the compiler enforces that the invariant holds true (as long as the
implementation is correct)
- the auto completion shows only the methods that are available instead of
showing the APIs that the user should not use
- the code can be organized in a way that makes sense with grouping of
related code into one file without introducing oversharing of APIs.

Local is also very much in line with Swift's focus on strong type system,
elimination of accidental mistakes, and code clarity. If we have guard that
can be easily replaced by an if statement, I don't see how local is a
problem. If you say "but guard helps eliminate lots of nested if
statements", I can reply "but local helps eliminate lots of tiny files".
Any arguments about cognitive overhead, making language bigger, etc. can be
argued the same way. The biggest difference though is that local is not
only about clarity and convenience, it's also about correctness. I would
even argue that local should be the default and that any other access level
modifier should be a deliberate choice by the programmer.

···

--
Ilya Belenkiy

On Sun, Dec 13, 2015 at 4:58 PM Brent Royal-Gordon via swift-evolution < swift-evolution@swift.org> wrote:

>> I mean, look, you’re right. In your case, three access modifiers isn’t
enough to prevent this mistake; you need four. But,
>> given four, you could provide an example that needs five. Given five,
you could come up with one for six. Given N access modifiers, you can write
a case where you need N + 1.
>
> Nope. What we're talking about (what I'm talking about, anyway) is a
syntactically scoped access modifier. There is no N+1 case. There is no
"even more scoped". There is no inductive case.
>
> I mean, it is possible to write a troll proposal for an access modifier
visible to the 3 lines before and after the declaration, or something.
That much I grant. But troll proposals aside, a scoped access modifier is
the quark of the access modifiers; there is no sensible way to go halvsies.

I actually immediately thought of several things you might want:

• You have two logical “groups” of properties and methods in a type, and
you want to only permit access within a group. But both groups have stored
properties, so they must be defined in the scope of the main type
definition.
• You have types nested within your type. You want methods in your own
type to have access, but not methods in those nested types.
• Alternatively, you have types nested within your type. You want the
outer type to have access to things in the nested type, but not any other
type.

I don’t think these are troll proposals. They *are* a little more limited,
a little more esoteric, but you could say the same thing about `local` vs.
`private`. And in some cases you can emulate some of these with the four
access modifiers you want, but again, you could say the same thing about
`local` vs. `private`.

>> Actually, for this case, there *is* a way to achieve better safety
without additional access modifiers. Create a new file and call it
Synchronized.swift:
>
> This is the solution that I ship. I think if we don't add a new
visibility modifier, we should definitely do this in the standard library,
because "resource synchronization" is a problem most modern programs have.

This is not a bad idea. The only potential issue I can see with it is
that, if it’s made too general, it might be difficult to construct a
Synchronized instance correctly.

>> My solution is to be careful when I’m writing code.
>
> It is actually *this* statement which is weak to an induction attack.
We can *always* be more careful writing code. Why do we need typechecking,
why do we need integer overflow trapping, why do we need array out of
bounds exceptions? The C developer says they do not need half of Swift
because their solution is to be more careful writing code. The C developer
is naive.
>
> But in *this* language, we adopt zero and low-cost abstractions in the
name of safety. A scoped access modifier is a zero-cost abstraction that
makes the language safer. We should adopt it. If for no other reason,
than consistency with the other aspects of the language design. Either
that or we should move integer overflow checks into the standard library
for consistency with Synchronized, which I present as a troll proposal.

But it’s not zero-cost. It’s another thing to learn, another thing to
think about, another way you have to mentally analyze your code. It has to
be balanced against the goal of keeping the language small and simple. And
many people, when they do that, find this idea wanting.

> I mean, I don't know about you, but I get called in to look at projects
regularly that have a MainViewController.swift that weighs 10KLOC. I
didn't write it. I didn't make the mess. I just want tools to help me
clean it up. 'private' is useless in that context, and it's a (sadly)
typical context in iOS.

`local` is going to be equally useless in this context, because
MainViewController.swift will almost certainly contain one 9.999KLOC
`class` block. Cleaning up is, practically by definition, messy and tiring.

--
Brent Royal-Gordon
Architechies

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


(Matthew Johnson) #9

I don’t think these are troll proposals. They *are* a little more limited, a little more esoteric, but you could say the same thing about `local` vs. `private`. And in some cases you can emulate some of these with the four access modifiers you want, but again, you could say the same thing about `local` vs. `private`.

You're right. Your examples have convinced me of this point. There exist non-troll proposals for further access modifiers.

There are definitely further refinements to access control that could be desired. I have given them some thought as this thread has proceeded. There would be ways to accommodate them through refinement of the 'scope' access level.

For example, we could allow a syntactic 'scope {}' block which would be transparent for all purposes except introducing a scope for access control. We could also allow labeled scopes and allow the 'scope' access control level to specify a label, for example to allow an inner type to expose scoped members to the outer type but not other code in the same file.

While this potential exists I think it introduces complexity that is probably not worth it. I am only mentioning these ideas to demonstrate that a 'scope' access modifier is something that can be refined to provide more control if enough compelling use cases were identified. We wouldn't need to add entirely new access modifiers to address such use cases.

Matthew


(Brent Royal-Gordon) #10

But it’s not zero-cost. It’s another thing to learn, another thing to think about, another way you have to mentally analyze your code.

I meant zero performance cost. Of course all features have "cost" if we mean "cognitive overhead". Type safety for example has a huge cognitive overhead. Think back to the days of "Bool is not an NSString". But the benefit of typesafety is large.

In this case, the cognitive overhead is small, and so is the benefit. But I think the value-per-unit-cost is similar. In both cases the compiler helps you not do something very very bad, that is hard to debug in ObjC.

And I’m saying that I think the benefit is even smaller than you think it is, because you can usually get the same benefits by other means, and the resulting code will be *even safer* than `local` would give you. Again, consider the “value protected by a queue” case. Using `local` here limits the amount of code which could contain an access-without-synchronization bug, but `Synchronized` *completely eliminates* that class of bugs. Synchronized is a *better*, *safer* solution than `local`.

I believe that *most*—certainly not all, but most—uses of `local` are like this. `local` would improve the code's safety, but some sort of refactoring would be even better. Because of that, I don’t think `local` is as valuable as you think it is.

many people, when they do that, find this idea wanting.

Who? You? Then build an argument around that. I don't know who "many people" are or what their justification is.

I’m sorry, I don’t mean to make it sound like I’m speaking for some big, ill-defined posse. I just mean that different people will draw the line on the necessary cost-to-benefit in different places, and for some, this feature will fall on the wrong side of the line. People who don’t like this feature don’t misunderstand it; they just have a different subjective assessment of its value.

My justification is essentially that A) something like Synchronized is a problem nearly everybody has and B) the difficulty of defining a class-based solution in an optimal way.

On B, we seem to agree:

it might be difficult to construct a Synchronized instance correctly.

So I can only conclude you disagree about A. However, I think my A is much stronger than is strictly necessary to demand a language feature. There are plenty of language features that not everyone uses, so the fact that you don't have a need for it (or even "many people") is not really a counterargument I am able to understand.

As far as a standard Synchronized is concerned, I agree with you that it’s a good idea. I am simply *worried* that we may have trouble coming up with a design that’s flexible enough to accommodate multiple synchronization methods, but still makes it easy to create a properly-configured Synchronized object. For example, something like this would be so complicated to configure that it would virtually defeat the purpose of having a Synchronized type:

  class Synchronized<Value> {
    init(value: Value, mutableRunner: (Void -> Void) -> Void, immutableRunner: (Void -> Void) -> Void) { … }
    …
  }

So I’m going to think out loud about this for a minute. All code was written in Mail.app and is untested.

I suppose we start with a protocol. The ideal interface for Synchronized would look something like this:

  protocol SynchronizedType: class {
    typealias Value
    func withMutableValue<R>(mutator: (inout Value) throws -> R) rethrows -> R
    func withValue<R>(accessor: Value throws -> R) rethrows -> R
  }

You could obviously write separate types like:

  class QueueSynchronized<Value>: SynchronizedType {
    private var value: Value
    private let queue: dispatch_queue_t
    
    init(value: Value, queue: dispatch_queue_t = dispatch_queue_create(“QueueSynchronized”, DISPATCH_QUEUE_CONCURRENT)) {
      self.value = value
      self.queue = queue
    }
    
    func withMutableValue<R>(@noescape mutator: (inout Value) throws -> R) rethrows -> R {
      var ret: R?
      var blockError: NSError?
      dispatch_barrier_sync(queue) {
        do {
          ret = try mutator(&value)
        }
        catch {
          blockError = error
        }
      }
      if let error = blockError {
        throw error
      }
      return ret!
    }
    
    func withValue<R>(@noescape accessor: Value throws -> R) rethrows -> R {
      var ret: R?
      var blockError: NSError?
      dispatch_sync(queue) {
        do {
          ret = try accessor(value)
        }
        catch {
          blockError = error
        }
      }
      if let error = blockError {
        throw error
      }
      return ret!
    }
  }

and:

  class NSLockSynchronized<Value>: SynchronizedType {
    private var value: Value
    private var lock: NSLock
    
    init(value: Value, lock: NSLock = NSLock()) {
      self.value = value
      self.lock = lock
    }
    
    func withMutableValue<R>(@noescape mutator: (inout Value) throws -> R) rethrows -> R {
      lock.lock()
      defer { lock.unlock() }

      return try mutator(&value)
    }
    
    func withValue<R>(@noescape accessor: Value throws -> R) rethrows -> R {
      // XXX I don’t know how to get concurrent reads with Cocoa locks.
      lock.lock()
      defer { lock.unlock() }

      return try accessor(value)
    }
  }

But that’s not a very satisfying design—so much boilerplate. Maybe we make the thing you’re synchronizing *on* be the protocol?

  protocol SynchronizerType: class {
    func synchronizeForReading(@noescape accessor: Void -> Void)
    func synchronizeForWriting(@noescape mutator: Void -> Void)
  }
  
  final class Synchronized<Value> {
    private var value = Value
    private let synchronizer: SynchronizerType
    
    init(value: Value, on synchronizer: SynchronizerType) {
      self.value = value
      self.synchronizer = synchronizer
    }

    func withMutableValue<R>(@noescape mutator: (inout Value) throws -> R) rethrows -> R {
      var ret: R?
      var blockError: NSError?
      synchronizer.synchronizeForWriting {
        do {
          ret = try mutator(&value)
        }
        catch {
          blockError = error
        }
      }
      if let error = blockError {
        throw error
      }
      return ret!
    }
    
    func withValue<R>(@noescape accessor: Value throws -> R) rethrows -> R {
      var ret: R?
      var blockError: NSError?
      synchronizer.synchronizeForReading {
        do {
          ret = try accessor(value)
        }
        catch {
          blockError = error
        }
      }
      if let error = blockError {
        throw error
      }
      return ret!
    }
  }

  extension dispatch_queue: SynchronizerType {
    func synchronizeForReading(@noescape accessor: Void -> Void) {
      dispatch_sync(self, accessor)
    }
    
    func synchronizeForWriting(@noescape mutator: Void -> Void) {
      dispatch_barrier_sync(self, mutator)
    }
  }
  
  extension NSLock: SynchronizerType {
    func synchronizeForReading(@noescape accessor: Void -> Void) {
      // XXX I don’t know how to get concurrent reads with Cocoa locks.
      lock()
      accessor()
      unlock()
    }
    
    func synchronizeForWriting(@noescape mutator: Void -> Void) {
      lock()
      mutator()
      unlock()
    }
  }

Huh. That’s…actually pretty clean. And I suppose if you want it to default to using a dispatch queue, that’s just a default value on Synchronized.init(value:on:), or a new init(value:) added in an extension in LibDispatch. I thought this would be a lot hairier.

So, yeah, I guess I like the idea of a standard Synchronized. A well-designed version (one with something like SynchronizerType) can work with multiple locking mechanisms, without resorting to something error-prone like passing in closures. +1 for that.

···

--
Brent Royal-Gordon
Architechies


(ilya) #11

I believe that *most*—certainly not all, but most—uses of `local` are

like this. `local` would improve the code's safety, but some sort of
refactoring would be even better. Because of that, I don’t think `local` is
as valuable as you think it is.

+1.

Let's try to promote existing access mechanisms, such as separating code
into a number of files, first, before deciding that we need new keywords.

···

On Mon, Dec 14, 2015 at 4:53 AM, Brent Royal-Gordon via swift-evolution < swift-evolution@swift.org> wrote:

>> But it’s not zero-cost. It’s another thing to learn, another thing to
think about, another way you have to mentally analyze your code.
>
> I meant zero performance cost. Of course all features have "cost" if we
mean "cognitive overhead". Type safety for example has a huge cognitive
overhead. Think back to the days of "Bool is not an NSString". But the
benefit of typesafety is large.
>
> In this case, the cognitive overhead is small, and so is the benefit.
But I think the value-per-unit-cost is similar. In both cases the compiler
helps you not do something very very bad, that is hard to debug in ObjC.

And I’m saying that I think the benefit is even smaller than you think it
is, because you can usually get the same benefits by other means, and the
resulting code will be *even safer* than `local` would give you. Again,
consider the “value protected by a queue” case. Using `local` here limits
the amount of code which could contain an access-without-synchronization
bug, but `Synchronized` *completely eliminates* that class of bugs.
Synchronized is a *better*, *safer* solution than `local`.

I believe that *most*—certainly not all, but most—uses of `local` are like
this. `local` would improve the code's safety, but some sort of refactoring
would be even better. Because of that, I don’t think `local` is as valuable
as you think it is.

>> many people, when they do that, find this idea wanting.
>
> Who? You? Then build an argument around that. I don't know who "many
people" are or what their justification is.

I’m sorry, I don’t mean to make it sound like I’m speaking for some big,
ill-defined posse. I just mean that different people will draw the line on
the necessary cost-to-benefit in different places, and for some, this
feature will fall on the wrong side of the line. People who don’t like this
feature don’t misunderstand it; they just have a different subjective
assessment of its value.

> My justification is essentially that A) something like Synchronized is a
problem nearly everybody has and B) the difficulty of defining a
class-based solution in an optimal way.
>
> On B, we seem to agree:
>
>> it might be difficult to construct a Synchronized instance correctly.
>
> So I can only conclude you disagree about A. However, I think my A is
much stronger than is strictly necessary to demand a language feature.
There are plenty of language features that not everyone uses, so the fact
that you don't have a need for it (or even "many people") is not really a
counterargument I am able to understand.

As far as a standard Synchronized is concerned, I agree with you that it’s
a good idea. I am simply *worried* that we may have trouble coming up with
a design that’s flexible enough to accommodate multiple synchronization
methods, but still makes it easy to create a properly-configured
Synchronized object. For example, something like this would be so
complicated to configure that it would virtually defeat the purpose of
having a Synchronized type:

        class Synchronized<Value> {
                init(value: Value, mutableRunner: (Void -> Void) -> Void,
immutableRunner: (Void -> Void) -> Void) { … }
                …
        }

So I’m going to think out loud about this for a minute. All code was
written in Mail.app and is untested.

I suppose we start with a protocol. The ideal interface for Synchronized
would look something like this:

        protocol SynchronizedType: class {
                typealias Value
                func withMutableValue<R>(mutator: (inout Value) throws ->
R) rethrows -> R
                func withValue<R>(accessor: Value throws -> R) rethrows ->
R
        }

You could obviously write separate types like:

        class QueueSynchronized<Value>: SynchronizedType {
                private var value: Value
                private let queue: dispatch_queue_t

                init(value: Value, queue: dispatch_queue_t =
dispatch_queue_create(“QueueSynchronized”, DISPATCH_QUEUE_CONCURRENT)) {
                        self.value = value
                        self.queue = queue
                }

                func withMutableValue<R>(@noescape mutator: (inout Value)
throws -> R) rethrows -> R {
                        var ret: R?
                        var blockError: NSError?
                        dispatch_barrier_sync(queue) {
                                do {
                                        ret = try mutator(&value)
                                }
                                catch {
                                        blockError = error
                                }
                        }
                        if let error = blockError {
                                throw error
                        }
                        return ret!
                }

                func withValue<R>(@noescape accessor: Value throws -> R)
rethrows -> R {
                        var ret: R?
                        var blockError: NSError?
                        dispatch_sync(queue) {
                                do {
                                        ret = try accessor(value)
                                }
                                catch {
                                        blockError = error
                                }
                        }
                        if let error = blockError {
                                throw error
                        }
                        return ret!
                }
        }

and:

        class NSLockSynchronized<Value>: SynchronizedType {
                private var value: Value
                private var lock: NSLock

                init(value: Value, lock: NSLock = NSLock()) {
                        self.value = value
                        self.lock = lock
                }

                func withMutableValue<R>(@noescape mutator: (inout Value)
throws -> R) rethrows -> R {
                        lock.lock()
                        defer { lock.unlock() }

                        return try mutator(&value)
                }

                func withValue<R>(@noescape accessor: Value throws -> R)
rethrows -> R {
                        // XXX I don’t know how to get concurrent reads
with Cocoa locks.
                        lock.lock()
                        defer { lock.unlock() }

                        return try accessor(value)
                }
        }

But that’s not a very satisfying design—so much boilerplate. Maybe we make
the thing you’re synchronizing *on* be the protocol?

        protocol SynchronizerType: class {
                func synchronizeForReading(@noescape accessor: Void ->
Void)
                func synchronizeForWriting(@noescape mutator: Void -> Void)
        }

        final class Synchronized<Value> {
                private var value = Value
                private let synchronizer: SynchronizerType

                init(value: Value, on synchronizer: SynchronizerType) {
                        self.value = value
                        self.synchronizer = synchronizer
                }

                func withMutableValue<R>(@noescape mutator: (inout Value)
throws -> R) rethrows -> R {
                        var ret: R?
                        var blockError: NSError?
                        synchronizer.synchronizeForWriting {
                                do {
                                        ret = try mutator(&value)
                                }
                                catch {
                                        blockError = error
                                }
                        }
                        if let error = blockError {
                                throw error
                        }
                        return ret!
                }

                func withValue<R>(@noescape accessor: Value throws -> R)
rethrows -> R {
                        var ret: R?
                        var blockError: NSError?
                        synchronizer.synchronizeForReading {
                                do {
                                        ret = try accessor(value)
                                }
                                catch {
                                        blockError = error
                                }
                        }
                        if let error = blockError {
                                throw error
                        }
                        return ret!
                }
        }

        extension dispatch_queue: SynchronizerType {
                func synchronizeForReading(@noescape accessor: Void ->
Void) {
                        dispatch_sync(self, accessor)
                }

                func synchronizeForWriting(@noescape mutator: Void ->
Void) {
                        dispatch_barrier_sync(self, mutator)
                }
        }

        extension NSLock: SynchronizerType {
                func synchronizeForReading(@noescape accessor: Void ->
Void) {
                        // XXX I don’t know how to get concurrent reads
with Cocoa locks.
                        lock()
                        accessor()
                        unlock()
                }

                func synchronizeForWriting(@noescape mutator: Void ->
Void) {
                        lock()
                        mutator()
                        unlock()
                }
        }

Huh. That’s…actually pretty clean. And I suppose if you want it to default
to using a dispatch queue, that’s just a default value on
Synchronized.init(value:on:), or a new init(value:) added in an extension
in LibDispatch. I thought this would be a lot hairier.

So, yeah, I guess I like the idea of a standard Synchronized. A
well-designed version (one with something like SynchronizerType) can work
with multiple locking mechanisms, without resorting to something
error-prone like passing in closures. +1 for that.

--
Brent Royal-Gordon
Architechies

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


(Ilya Belenkiy) #12

These are very nice refinements if we set a goal to make those scenarios
work. I think that they they would just need to be refactored though. If
the access level is so convoluted, there is something wrong with the API.

···

--
Ilya Belenkiy

On Sun, Dec 13, 2015 at 6:53 PM Matthew Johnson via swift-evolution < swift-evolution@swift.org> wrote:

I don’t think these are troll proposals. They *are* a little more limited,
a little more esoteric, but you could say the same thing about `local` vs.
`private`. And in some cases you can emulate some of these with the four
access modifiers you want, but again, you could say the same thing about
`local` vs. `private`.

You're right. Your examples have convinced me of this point. There exist
non-troll proposals for further access modifiers.

There are definitely further refinements to access control that could be
desired. I have given them some thought as this thread has proceeded.
There would be ways to accommodate them through refinement of the 'scope'
access level.

For example, we could allow a syntactic 'scope {}' block which would be
transparent for all purposes except introducing a scope for access
control. We could also allow labeled scopes and allow the 'scope' access
control level to specify a label, for example to allow an inner type to
expose scoped members to the outer type but not other code in the same file.

While this potential exists I think it introduces complexity that is
probably not worth it. I am only mentioning these ideas to demonstrate
that a 'scope' access modifier is something that can be refined to provide
more control if enough compelling use cases were identified. We wouldn't
need to add entirely new access modifiers to address such use cases.

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


(Ilya Belenkiy) #13

"I’m sorry, I don’t mean to make it sound like I’m speaking for some big,
ill-defined posse. I just mean that different people will draw the line on
the necessary cost-to-benefit in different places, and for some, this
feature will fall on the wrong side of the line. People who don’t like this
feature don’t misunderstand it; they just have a different subjective
assessment of its value."

I am not sure that this is true. Do you really see no value in completely
hiding implementation details of an API and making it enforceable by the
compiler?

···

On Sun, Dec 13, 2015 at 8:53 PM Brent Royal-Gordon via swift-evolution < swift-evolution@swift.org> wrote:

>> But it’s not zero-cost. It’s another thing to learn, another thing to
think about, another way you have to mentally analyze your code.
>
> I meant zero performance cost. Of course all features have "cost" if we
mean "cognitive overhead". Type safety for example has a huge cognitive
overhead. Think back to the days of "Bool is not an NSString". But the
benefit of typesafety is large.
>
> In this case, the cognitive overhead is small, and so is the benefit.
But I think the value-per-unit-cost is similar. In both cases the compiler
helps you not do something very very bad, that is hard to debug in ObjC.

And I’m saying that I think the benefit is even smaller than you think it
is, because you can usually get the same benefits by other means, and the
resulting code will be *even safer* than `local` would give you. Again,
consider the “value protected by a queue” case. Using `local` here limits
the amount of code which could contain an access-without-synchronization
bug, but `Synchronized` *completely eliminates* that class of bugs.
Synchronized is a *better*, *safer* solution than `local`.

I believe that *most*—certainly not all, but most—uses of `local` are like
this. `local` would improve the code's safety, but some sort of refactoring
would be even better. Because of that, I don’t think `local` is as valuable
as you think it is.

>> many people, when they do that, find this idea wanting.
>
> Who? You? Then build an argument around that. I don't know who "many
people" are or what their justification is.

I’m sorry, I don’t mean to make it sound like I’m speaking for some big,
ill-defined posse. I just mean that different people will draw the line on
the necessary cost-to-benefit in different places, and for some, this
feature will fall on the wrong side of the line. People who don’t like this
feature don’t misunderstand it; they just have a different subjective
assessment of its value.

> My justification is essentially that A) something like Synchronized is a
problem nearly everybody has and B) the difficulty of defining a
class-based solution in an optimal way.
>
> On B, we seem to agree:
>
>> it might be difficult to construct a Synchronized instance correctly.
>
> So I can only conclude you disagree about A. However, I think my A is
much stronger than is strictly necessary to demand a language feature.
There are plenty of language features that not everyone uses, so the fact
that you don't have a need for it (or even "many people") is not really a
counterargument I am able to understand.

As far as a standard Synchronized is concerned, I agree with you that it’s
a good idea. I am simply *worried* that we may have trouble coming up with
a design that’s flexible enough to accommodate multiple synchronization
methods, but still makes it easy to create a properly-configured
Synchronized object. For example, something like this would be so
complicated to configure that it would virtually defeat the purpose of
having a Synchronized type:

        class Synchronized<Value> {
                init(value: Value, mutableRunner: (Void -> Void) -> Void,
immutableRunner: (Void -> Void) -> Void) { … }
                …
        }

So I’m going to think out loud about this for a minute. All code was
written in Mail.app and is untested.

I suppose we start with a protocol. The ideal interface for Synchronized
would look something like this:

        protocol SynchronizedType: class {
                typealias Value
                func withMutableValue<R>(mutator: (inout Value) throws ->
R) rethrows -> R
                func withValue<R>(accessor: Value throws -> R) rethrows ->
R
        }

You could obviously write separate types like:

        class QueueSynchronized<Value>: SynchronizedType {
                private var value: Value
                private let queue: dispatch_queue_t

                init(value: Value, queue: dispatch_queue_t =
dispatch_queue_create(“QueueSynchronized”, DISPATCH_QUEUE_CONCURRENT)) {
                        self.value = value
                        self.queue = queue
                }

                func withMutableValue<R>(@noescape mutator: (inout Value)
throws -> R) rethrows -> R {
                        var ret: R?
                        var blockError: NSError?
                        dispatch_barrier_sync(queue) {
                                do {
                                        ret = try mutator(&value)
                                }
                                catch {
                                        blockError = error
                                }
                        }
                        if let error = blockError {
                                throw error
                        }
                        return ret!
                }

                func withValue<R>(@noescape accessor: Value throws -> R)
rethrows -> R {
                        var ret: R?
                        var blockError: NSError?
                        dispatch_sync(queue) {
                                do {
                                        ret = try accessor(value)
                                }
                                catch {
                                        blockError = error
                                }
                        }
                        if let error = blockError {
                                throw error
                        }
                        return ret!
                }
        }

and:

        class NSLockSynchronized<Value>: SynchronizedType {
                private var value: Value
                private var lock: NSLock

                init(value: Value, lock: NSLock = NSLock()) {
                        self.value = value
                        self.lock = lock
                }

                func withMutableValue<R>(@noescape mutator: (inout Value)
throws -> R) rethrows -> R {
                        lock.lock()
                        defer { lock.unlock() }

                        return try mutator(&value)
                }

                func withValue<R>(@noescape accessor: Value throws -> R)
rethrows -> R {
                        // XXX I don’t know how to get concurrent reads
with Cocoa locks.
                        lock.lock()
                        defer { lock.unlock() }

                        return try accessor(value)
                }
        }

But that’s not a very satisfying design—so much boilerplate. Maybe we make
the thing you’re synchronizing *on* be the protocol?

        protocol SynchronizerType: class {
                func synchronizeForReading(@noescape accessor: Void ->
Void)
                func synchronizeForWriting(@noescape mutator: Void -> Void)
        }

        final class Synchronized<Value> {
                private var value = Value
                private let synchronizer: SynchronizerType

                init(value: Value, on synchronizer: SynchronizerType) {
                        self.value = value
                        self.synchronizer = synchronizer
                }

                func withMutableValue<R>(@noescape mutator: (inout Value)
throws -> R) rethrows -> R {
                        var ret: R?
                        var blockError: NSError?
                        synchronizer.synchronizeForWriting {
                                do {
                                        ret = try mutator(&value)
                                }
                                catch {
                                        blockError = error
                                }
                        }
                        if let error = blockError {
                                throw error
                        }
                        return ret!
                }

                func withValue<R>(@noescape accessor: Value throws -> R)
rethrows -> R {
                        var ret: R?
                        var blockError: NSError?
                        synchronizer.synchronizeForReading {
                                do {
                                        ret = try accessor(value)
                                }
                                catch {
                                        blockError = error
                                }
                        }
                        if let error = blockError {
                                throw error
                        }
                        return ret!
                }
        }

        extension dispatch_queue: SynchronizerType {
                func synchronizeForReading(@noescape accessor: Void ->
Void) {
                        dispatch_sync(self, accessor)
                }

                func synchronizeForWriting(@noescape mutator: Void ->
Void) {
                        dispatch_barrier_sync(self, mutator)
                }
        }

        extension NSLock: SynchronizerType {
                func synchronizeForReading(@noescape accessor: Void ->
Void) {
                        // XXX I don’t know how to get concurrent reads
with Cocoa locks.
                        lock()
                        accessor()
                        unlock()
                }

                func synchronizeForWriting(@noescape mutator: Void ->
Void) {
                        lock()
                        mutator()
                        unlock()
                }
        }

Huh. That’s…actually pretty clean. And I suppose if you want it to default
to using a dispatch queue, that’s just a default value on
Synchronized.init(value:on:), or a new init(value:) added in an extension
in LibDispatch. I thought this would be a lot hairier.

So, yeah, I guess I like the idea of a standard Synchronized. A
well-designed version (one with something like SynchronizerType) can work
with multiple locking mechanisms, without resorting to something
error-prone like passing in closures. +1 for that.

--
Brent Royal-Gordon
Architechies

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


(Brent Royal-Gordon) #14

Do you really see no value in completely hiding implementation details of an API and making it enforceable by the compiler?

On the contrary, I see great value in hiding implementation details. But, as I’ve previously explained, I don’t think adding a slew of different levels of access is the answer here. In many cases where `local` appears to be valuable, the real problem is that several concerns are being mixed together. That’s what’s wrong in the _queue/_children case: the concern of synchronizing access to _children has been improperly mixed with the concern of actually manipulating the _children array. Only by properly separating those concerns can you actually prevent bugs when accessing _children—and that separation is easily achieved with a separate file and `private`.

I’ve seen you make this argument several times:

If you were writing a book, would you want to put every paragraph into a separate file?

But that’s not what we’re actually contemplating here. A software project is a bit like an encyclopedia. Each article covers one subject, and not all subjects are the same. Some articles are short; some are long. But you would never glom several short articles together if they were about different subjects.

Similarly, each file in a project should handle one concern. And it is concerns—not types, not scopes—which have implementation details that should be hidden from the outside world. Almost by definition, if there’s something in one part of a file that shouldn’t be accessed by another part, those two don’t belong in the same file. It doesn’t matter if some of the resulting files are only ten lines—you’re not going to run out of inode numbers, are you?

···

--
Brent Royal-Gordon
Architechies


(Ilya Belenkiy) #15

If you were writing a book, would you want to put every paragraph into a
separate file?

···

On Mon, Dec 14, 2015 at 2:49 AM ilya via swift-evolution < swift-evolution@swift.org> wrote:

> I believe that *most*—certainly not all, but most—uses of `local` are
like this. `local` would improve the code's safety, but some sort of
refactoring would be even better. Because of that, I don’t think `local` is
as valuable as you think it is.

+1.

Let's try to promote existing access mechanisms, such as separating code
into a number of files, first, before deciding that we need new keywords.

On Mon, Dec 14, 2015 at 4:53 AM, Brent Royal-Gordon via swift-evolution < > swift-evolution@swift.org> wrote:

>> But it’s not zero-cost. It’s another thing to learn, another thing to
think about, another way you have to mentally analyze your code.
>
> I meant zero performance cost. Of course all features have "cost" if
we mean "cognitive overhead". Type safety for example has a huge cognitive
overhead. Think back to the days of "Bool is not an NSString". But the
benefit of typesafety is large.
>
> In this case, the cognitive overhead is small, and so is the benefit.
But I think the value-per-unit-cost is similar. In both cases the compiler
helps you not do something very very bad, that is hard to debug in ObjC.

And I’m saying that I think the benefit is even smaller than you think it
is, because you can usually get the same benefits by other means, and the
resulting code will be *even safer* than `local` would give you. Again,
consider the “value protected by a queue” case. Using `local` here limits
the amount of code which could contain an access-without-synchronization
bug, but `Synchronized` *completely eliminates* that class of bugs.
Synchronized is a *better*, *safer* solution than `local`.

I believe that *most*—certainly not all, but most—uses of `local` are
like this. `local` would improve the code's safety, but some sort of
refactoring would be even better. Because of that, I don’t think `local` is
as valuable as you think it is.

>> many people, when they do that, find this idea wanting.
>
> Who? You? Then build an argument around that. I don't know who "many
people" are or what their justification is.

I’m sorry, I don’t mean to make it sound like I’m speaking for some big,
ill-defined posse. I just mean that different people will draw the line on
the necessary cost-to-benefit in different places, and for some, this
feature will fall on the wrong side of the line. People who don’t like this
feature don’t misunderstand it; they just have a different subjective
assessment of its value.

> My justification is essentially that A) something like Synchronized is
a problem nearly everybody has and B) the difficulty of defining a
class-based solution in an optimal way.
>
> On B, we seem to agree:
>
>> it might be difficult to construct a Synchronized instance correctly.
>
> So I can only conclude you disagree about A. However, I think my A is
much stronger than is strictly necessary to demand a language feature.
There are plenty of language features that not everyone uses, so the fact
that you don't have a need for it (or even "many people") is not really a
counterargument I am able to understand.

As far as a standard Synchronized is concerned, I agree with you that
it’s a good idea. I am simply *worried* that we may have trouble coming up
with a design that’s flexible enough to accommodate multiple
synchronization methods, but still makes it easy to create a
properly-configured Synchronized object. For example, something like this
would be so complicated to configure that it would virtually defeat the
purpose of having a Synchronized type:

        class Synchronized<Value> {
                init(value: Value, mutableRunner: (Void -> Void) -> Void,
immutableRunner: (Void -> Void) -> Void) { … }
                …
        }

So I’m going to think out loud about this for a minute. All code was
written in Mail.app and is untested.

I suppose we start with a protocol. The ideal interface for Synchronized
would look something like this:

        protocol SynchronizedType: class {
                typealias Value
                func withMutableValue<R>(mutator: (inout Value) throws ->
R) rethrows -> R
                func withValue<R>(accessor: Value throws -> R) rethrows
-> R
        }

You could obviously write separate types like:

        class QueueSynchronized<Value>: SynchronizedType {
                private var value: Value
                private let queue: dispatch_queue_t

                init(value: Value, queue: dispatch_queue_t =
dispatch_queue_create(“QueueSynchronized”, DISPATCH_QUEUE_CONCURRENT)) {
                        self.value = value
                        self.queue = queue
                }

                func withMutableValue<R>(@noescape mutator: (inout Value)
throws -> R) rethrows -> R {
                        var ret: R?
                        var blockError: NSError?
                        dispatch_barrier_sync(queue) {
                                do {
                                        ret = try mutator(&value)
                                }
                                catch {
                                        blockError = error
                                }
                        }
                        if let error = blockError {
                                throw error
                        }
                        return ret!
                }

                func withValue<R>(@noescape accessor: Value throws -> R)
rethrows -> R {
                        var ret: R?
                        var blockError: NSError?
                        dispatch_sync(queue) {
                                do {
                                        ret = try accessor(value)
                                }
                                catch {
                                        blockError = error
                                }
                        }
                        if let error = blockError {
                                throw error
                        }
                        return ret!
                }
        }

and:

        class NSLockSynchronized<Value>: SynchronizedType {
                private var value: Value
                private var lock: NSLock

                init(value: Value, lock: NSLock = NSLock()) {
                        self.value = value
                        self.lock = lock
                }

                func withMutableValue<R>(@noescape mutator: (inout Value)
throws -> R) rethrows -> R {
                        lock.lock()
                        defer { lock.unlock() }

                        return try mutator(&value)
                }

                func withValue<R>(@noescape accessor: Value throws -> R)
rethrows -> R {
                        // XXX I don’t know how to get concurrent reads
with Cocoa locks.
                        lock.lock()
                        defer { lock.unlock() }

                        return try accessor(value)
                }
        }

But that’s not a very satisfying design—so much boilerplate. Maybe we
make the thing you’re synchronizing *on* be the protocol?

        protocol SynchronizerType: class {
                func synchronizeForReading(@noescape accessor: Void ->
Void)
                func synchronizeForWriting(@noescape mutator: Void ->
Void)
        }

        final class Synchronized<Value> {
                private var value = Value
                private let synchronizer: SynchronizerType

                init(value: Value, on synchronizer: SynchronizerType) {
                        self.value = value
                        self.synchronizer = synchronizer
                }

                func withMutableValue<R>(@noescape mutator: (inout Value)
throws -> R) rethrows -> R {
                        var ret: R?
                        var blockError: NSError?
                        synchronizer.synchronizeForWriting {
                                do {
                                        ret = try mutator(&value)
                                }
                                catch {
                                        blockError = error
                                }
                        }
                        if let error = blockError {
                                throw error
                        }
                        return ret!
                }

                func withValue<R>(@noescape accessor: Value throws -> R)
rethrows -> R {
                        var ret: R?
                        var blockError: NSError?
                        synchronizer.synchronizeForReading {
                                do {
                                        ret = try accessor(value)
                                }
                                catch {
                                        blockError = error
                                }
                        }
                        if let error = blockError {
                                throw error
                        }
                        return ret!
                }
        }

        extension dispatch_queue: SynchronizerType {
                func synchronizeForReading(@noescape accessor: Void ->
Void) {
                        dispatch_sync(self, accessor)
                }

                func synchronizeForWriting(@noescape mutator: Void ->
Void) {
                        dispatch_barrier_sync(self, mutator)
                }
        }

        extension NSLock: SynchronizerType {
                func synchronizeForReading(@noescape accessor: Void ->
Void) {
                        // XXX I don’t know how to get concurrent reads
with Cocoa locks.
                        lock()
                        accessor()
                        unlock()
                }

                func synchronizeForWriting(@noescape mutator: Void ->
Void) {
                        lock()
                        mutator()
                        unlock()
                }
        }

Huh. That’s…actually pretty clean. And I suppose if you want it to
default to using a dispatch queue, that’s just a default value on
Synchronized.init(value:on:), or a new init(value:) added in an extension
in LibDispatch. I thought this would be a lot hairier.

So, yeah, I guess I like the idea of a standard Synchronized. A
well-designed version (one with something like SynchronizerType) can work
with multiple locking mechanisms, without resorting to something
error-prone like passing in closures. +1 for that.

--
Brent Royal-Gordon
Architechies

_______________________________________________
swift-evolution mailing list
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


(Ilya Belenkiy) #16

I am not going to run out of space, but I'd rather not have 10000000 files.
Also, i think that concern is too abstract to be useful. Implementation
details that are hidden by a public API is much more concrete. And as soon
as it is stated that way, there is often a lot of value in grouping related
APIs and implementations into one file:

- consistent APIs
- consistent implementations

For example, it may make sense to put all array based implementations of
data structures into one file or put related data structures in one file
(or both).

The language shouldn't dictate how to organize code. The existing solution
is great for modules and libraries, but it doesn't work well at the level
of a specific API.

The analogy of an encyclopedia can work in favor of local. If you look at
Wikipedia, it doesn't put every section of an article into a separate page.
It's all in one place.

···

--
Ilya Belenkiy

On Mon, Dec 14, 2015 at 9:02 AM Brent Royal-Gordon <brent@architechies.com> wrote:

> Do you really see no value in completely hiding implementation details
of an API and making it enforceable by the compiler?

On the contrary, I see great value in hiding implementation details. But,
as I’ve previously explained, I don’t think adding a slew of different
levels of access is the answer here. In many cases where `local` appears to
be valuable, the real problem is that several concerns are being mixed
together. That’s what’s wrong in the _queue/_children case: the concern of
synchronizing access to _children has been improperly mixed with the
concern of actually manipulating the _children array. Only by properly
separating those concerns can you actually prevent bugs when accessing
_children—and that separation is easily achieved with a separate file and
`private`.

I’ve seen you make this argument several times:

> If you were writing a book, would you want to put every paragraph into a
separate file?

But that’s not what we’re actually contemplating here. A software project
is a bit like an encyclopedia. Each article covers one subject, and not all
subjects are the same. Some articles are short; some are long. But you
would never glom several short articles together if they were about
different subjects.

Similarly, each file in a project should handle one concern. And it is
concerns—not types, not scopes—which have implementation details that
should be hidden from the outside world. Almost by definition, if there’s
something in one part of a file that shouldn’t be accessed by another part,
those two don’t belong in the same file. It doesn’t matter if some of the
resulting files are only ten lines—you’re not going to run out of inode
numbers, are you?

--
Brent Royal-Gordon
Architechies


(Matthew Johnson) #17

The language shouldn't dictate how to organize code. The existing solution is great for modules and libraries, but it doesn't work well at the level of a specific API.

+1. One of my favorite things about Swift is much more flexible physical code organization and an established practice of moving away from one file per class and towards placing related code together regardless for various sense of “related”.

For example, one may choose to place extensions to a number of types adding conformances to the same protocol in a single file. I imagine physical layout like this is not uncommon in Swift code. This does not necessarily mean you want these extensions to see implementation details of each other. By adding a ‘scope’ access modifier we are able to properly hide these implementation details regardless of physical layout.

This also improves robustness if you later decide to modify the physical organization of your code without changing visibility of the implementation details of the extensions.

Matthew


(David Owens II) #18

Let’s take this it’s next logical conclusion: I have two types that need to access each other’s members for some significant performance gain. Let’s call these types A and B. Now, A also needs access to the inner members for C for the same reason (but note that B does not). I currently have other related types all in the same file, let’s say D and E.

Now, all members should be “local” to the types, but remember, A needs to access B's and C's members and the other types do not. Also, I can’t simply move the them to different files because they have non-overlapping “local” usage requirements. So do I need to create a “local friend” modifier so only particular types can access the inner details?

There’s a line somewhere where complexity becomes more burdensome. I’m not sure if “local” is that line, but I absolutely do know that “local friend” is across that line (because of course we’ll need private friend and internal friend modifiers as well…).

I imagine physical layout like this is not uncommon in Swift code. This does not necessarily mean you want these extensions to see implementation details of each other. By adding a ‘scope’ access modifier we are able to properly hide these implementation details regardless of physical layout.

As mentioned above, what if you only want to make them available to some of the extensions?

-David


(David Owens II) #19

I agree that you can concoct arbitrarily complex scenarios and a line must be drawn somewhere. IMO the best place to draw the line is when you start considering something that is not super straightforward to explain and is not a natural extension of the obviously necessary access modifiers.

IMO ‘scope’ passes this test and all of the complex counter-examples do not. It is the logical conclusion of a simple narrowing of visibility from “everyone” to “module” to “file” to “scope”. It is simple to explain and understand. Those who don’t like it don’t need to use it. Anything more complex is unlikely to pass such a test.

I think the simplest counter-example is your own example for extensions. Each extensions will need access to different internals of the the type it’s applied to. So when it comes time to add that extension, you’ll be forced to promote the access control from “local” to “private”.

Another straight-forward one is a subclass. Since “local” would be “scope” based, a subclass would also knot have access to those members defined as local in the super class, so they’d have to be promoted to private and thus available to all code within the file.

I think “local” fits this definition:

IMO the best place to draw the line is when you start considering something that is not super straightforward to explain and is not a natural extension of the obviously necessary access modifiers.

It’s not an obviously necessary modifier as it’s usage is extremely limited and requires to be bounced up a level is a lot of design considerations, such as extensions and subclasses. There are certainly times where “local” could be used, but my opinion is that it’s not worth complexity for the limited value that it actually brings to the table.

-David

···

On Dec 14, 2015, at 8:58 AM, Matthew Johnson <matthew@anandabits.com> wrote:


(Matthew Johnson) #20

I agree that you can concoct arbitrarily complex scenarios and a line must be drawn somewhere. IMO the best place to draw the line is when you start considering something that is not super straightforward to explain and is not a natural extension of the obviously necessary access modifiers.

IMO ‘scope’ passes this test and all of the complex counter-examples do not. It is the logical conclusion of a simple narrowing of visibility from “everyone” to “module” to “file” to “scope”. It is simple to explain and understand. Those who don’t like it don’t need to use it. Anything more complex is unlikely to pass such a test.

Matthew

···

On Dec 14, 2015, at 10:52 AM, David Owens II <david@owensd.io> wrote:

Let’s take this it’s next logical conclusion: I have two types that need to access each other’s members for some significant performance gain. Let’s call these types A and B. Now, A also needs access to the inner members for C for the same reason (but note that B does not). I currently have other related types all in the same file, let’s say D and E.

Now, all members should be “local” to the types, but remember, A needs to access B's and C's members and the other types do not. Also, I can’t simply move the them to different files because they have non-overlapping “local” usage requirements. So do I need to create a “local friend” modifier so only particular types can access the inner details?

There’s a line somewhere where complexity becomes more burdensome. I’m not sure if “local” is that line, but I absolutely do know that “local friend” is across that line (because of course we’ll need private friend and internal friend modifiers as well…).

I imagine physical layout like this is not uncommon in Swift code. This does not necessarily mean you want these extensions to see implementation details of each other. By adding a ‘scope’ access modifier we are able to properly hide these implementation details regardless of physical layout.

As mentioned above, what if you only want to make them available to some of the extensions?

-David