[swift-evolution] [Proposal] Scoped resources (like C# using statement)
Kevin Ballard
kevin at sb.org
Wed Dec 30 15:50:57 CST 2015
There's no rule-of-five if you explicitly define (as Rust does) that you
cannot override assign/copy/move. In Rust, a copyable struct is always
memcpy()'d. Anything that's not memcpy'd but still wants to support
explicit copying conforms to the Clone trait, and the compiler lets such
structs automatically derive Clone (the Clone trait provides a single
method .clone() that returns a copy of the struct). So it's basically
just init, which we already require (though we infer an init if you
don't otherwise define one), deinit which is totally optional, and then
just one line to derive a Clone implementation if appropriate (and of
course you can always manually implement Clone if you need special logic
to do so, e.g. the Arc type (Atomic Reference Counted) manually
implements Clone in order to bump the retain count, but that's actually
pretty rare, usually structs that need to actually do things (like bump
retain counts) on clone are composed of smaller pieces (like Arc) that
do that automatically).
Overall this system works pretty well. Because you can't override
assign/copy/move, you can move and copy structs around without worry
(just as we do in Swift). And if a struct is not copyable, you can
invoke .clone() and it's obvious from the code that you're doing
something more expensive than just a memcpy(). This system also
encourages the use of references when possible because it lets you avoid
the potentially-expensive clones (versus overriding copy, where you end
up invoking potentially-expensive copies without even realizing it). Of
course, this does require actually having references in the language to
begin with.
Ultimately, given Swift's current direction, I'm leaning right now
towards the idea of having uniquely-owned stack-allocated classes, and
that's basically just because classes are already a reference type,
which makes it a bit easier to explain adding magic that lets you say
things like non-mutating methods are acting on a reference (even though
it's stack-allocated) and provides an avenue for doing things like
saying things like `func foo(x: ref LockGuard<Bar>)` to represent the
idea of a pass-by-reference that's treated as @noescape (or the
alternative, `func foo(x: move LockGuard<Bar>)` that explicitly moves
the value, except we do need the @noescape part of passing a reference
to a uniquely-owned value so I think annotating the parameter with `ref`
or `in` makes more sense).
-Kevin Ballard
On Wed, Dec 30, 2015, at 01:40 PM, Dave Abrahams wrote:
>
> IMO the biggest problem with this approach is that it leads to a much
> more complicated programming model. The vast majority of structs
> *should* be copiable, which gets you into rule-of-five programming
> (where nearly every nontrivial struct needs to implement init, deinit,
> copy, assign, move) pretty quickly. I don’t think Swift should go
> down this road.
>
> We do need a way to handle atomics, but I think there are other ways
> to address some of these concerns, including optimizing away class
> heap allocations and refcounting in special cases.
>
> -Dave
>
>> On Dec 29, 2015, at 8:55 PM, Kevin Ballard via swift-evolution <swift-
>> evolution at swift.org> wrote:
>>
>> An alternative solution is to do what Rust and C++ do, which is to
>> use RAII. Which is to say, instead of introducing a new language
>> construct that's explicitly tied to a scope, you just use a struct to
>> represent the resource that you hold (e.g. a File that represents an
>> open file). Of course, this does require some changes to structs,
>> notably the addition of a deinit. And if structs have a deinit, then
>> they also need to have a way to restrict copies. This is precisely
>> what Rust does; any struct in Rust that implements Drop (the
>> equivalent to deinit) loses the ability to be implicitly copied (a
>> second trait called Clone provides a .clone() method that is the
>> normal way to copy such non-implicitly-copyable structs).
>>
>> This solution is elegant for a few reasons:
>>
>> 1. Once you allow deinit on structs (which is useful) and deal with
>> the fact that structs are no longer implicitly copyable (because
>> almost all deinit functions on structs won't work right if they're
>> called twice, such as on two copies), then RAII just sort of falls
>> out of all of this and doesn't require any specific language
>> features.
>> 2. It's more flexible, because you can actually return the RAII value
>> from a scope in order to extend its lifetime.
>> 3. The RAII value itself provides the means to access the resource
>> it's protecting; e.g. a Lock might return a LockGuard RAII value
>> from the .lock() method, and LockGuard provides the means to
>> access the protected value (as opposed to just having the lock
>> sitting next to the value, which makes it trivially easy to
>> accidentally access the value without holding the lock).
>> 4. In Rust, this pattern also integrates extremely well with Rust's
>> lifetime system (the system that prevents data races / memory
>> corruption at compile time), because e.g. a LockGuard contains the
>> lifetime of the Lock, which prevents you at compile-time from
>> attempting to lock the Lock while you already have it locked
>> (though it doesn't prevent deadlocks where you and another thread
>> try and lock two locks in opposite orders, but there is plenty of
>> stuff it does catch).
>>
>> The biggest problem with adding deinit to structs in Swift right now
>> is the fact that we don't have references, which means you can't take
>> a RAII struct and pass it to another function without losing it.
>> Heck, you can't even call a method on it, because `self` on non-
>> mutating methods in Swift is a value and not a reference (although we
>> could hand-wave that away and make `self` in non-mutating RAII
>> methods actually be an "in" reference internally, but this kind of
>> hand-waving doesn't work when passing the struct as an argument to
>> another function). So we'd actually have to introduce a special "in"
>> reference, which would be like "inout" except it doesn't actually
>> copy it out (and is guaranteed to actually pass a pointer, although
>> this pointer may be a pointer to a temporary). Except even that fails
>> if you want to have a computed property that returns an existing RAII
>> value (for example, having an Array of these things; barring
>> optimizations, the subscript getter returns a computed value). And
>> you can't generalize such "in" references beyond function arguments
>> without basically providing raw C pointers. Rust's lifetime system
>> lets them do it safely, but barring such a system, Swift can't really
>> do this (I assume it's obvious why we don't want to start using raw C
>> pointers everywhere).
>>
>> All that said, I think this is a problem Swift needs to solve,
>> because having non-copyable structs would be very useful. In
>> particular, I _really_ want some way to do atomics in Swift, but the
>> only safe way I can think of to do it requires non-copyable structs
>> (because it's not correct to do a nonatomic read (such as a memcpy)
>> of an atomic that's visible to other threads). I suppose you could
>> provide a way to override how struct copies work (e.g. so you could
>> do an atomic read of the old value), but it's rather problematic to
>> break the assumption that struct copies are cheap.
>>
>> Of course, you could model RAII with classes instead of structs, that
>> just has the overhead of heap allocation (+ atomic reference
>> counting) for every RAII value.
>>
>> -Kevin Ballard
>>
>> On Tue, Dec 29, 2015, at 08:02 PM, Trent Nadeau via swift-
>> evolution wrote:
>>> # Introduction
>>>
>>> Add a new `Scoped` protocol and enhance the do statement to
>>> automatically call enter/exit actions on resources.
>>>
>>> # Motivation
>>>
>>> Resources (e.g., locks, files, sockets, etc.) often need to be
>>> scoped to a block, where some action is taken at the start of the
>>> block and another is required at the end. Examples include locking
>>> and unlocking a lock in a critical section or closing a file at the
>>> end of a block.
>>>
>>> Doing this manually is possible using `defer` statements among other
>>> options, but this is error prone as a `defer` can be forgotten,
>>> `lock`/`unlock` calls for two locks can be switched due to a typo,
>>> etc. Having a dedicated language construct for this common case
>>> makes it easier to read and write while making code shorter and
>>> clearer.
>>>
>>> # Language Survey
>>>
>>> At least three major languages have widely used statements for this
>>> use case.
>>>
>>> ## C#
>>>
>>> C# has the `using` statement and the associated `IDisposable`
>>> interface.
>>>
>>> ```csharp using (StreamReader sr = new StreamReader(filename)) {
>>> txt = sr.ReadToEnd(); } ```
>>>
>>> C#'s solution only handles close/exit actions via the
>>> `IDisposable.Dispose()` method and so cannot easily be used with
>>> items such as locks; however, C# has the additional `lock` statement
>>> for that use case.
>>>
>>> ## Java
>>>
>>> Java has try-with-resources and the associated `AutoCloseable`
>>> interface.
>>>
>>> ```java try (BufferedReader br = new BufferedReader(new
>>> FileReader(path))) { return br.readLine(); } ```
>>>
>>> Java's solution only handles close/exit actions via the
>>> `AutoCloseable.close()` method and so cannot easily be used with
>>> items such as locks; however, Java has the additional `synchronized`
>>> statement for that use case.
>>>
>>> ## Python
>>>
>>> Python has with `with` statement and the associated `__enter__` and
>>> `__exit__` special methods that classes may implement to become a
>>> "context manager".
>>>
>>> ```python with lock, open(path) as my_file: contents =
>>> my_file.read() # use contents ```
>>>
>>> Python's solution handles both enter and exit actions and so this
>>> one construct is usable for locks as well as resources like sockets
>>> and files.
>>>
>>> # Proposed Solution
>>>
>>> We add a new protocol called `Scoped` to the standard library. Types
>>> for resources that have enter/exit actions will be extended to add
>>> conformance to this protocol.
>>>
>>> The `do` statement will be extended to allow a new `using
>>> <resources>` "suffix".
>>>
>>> # Detailed Design
>>>
>>> The `Scoped` protocol shall be as follows:
>>>
>>> ```swift public protocol Scoped { func enterScope() func
>>> exitScope() } ```
>>>
>>> The compiler statement will accept a new form for resources. For
>>> example,
>>>
>>> ```swift do using lock, let file = try getFileHandle() { //
>>> statements } ```
>>>
>>> As can be seen in the example above, the resources can be bindings
>>> that already exist (like `lock`) or can be new bindings. Bindings
>>> created with `do using` are not available outside of the scope of
>>> the `do using`. Only types conforming to `Scoped` may be using with
>>> `do using`. Use of non-conforming types will result in a compiler
>>> error.
>>>
>>> The above example would be syntactic sugar for the following:
>>>
>>> ```swift do { lock.enterScope() defer { lock.exitScope() }
>>>
>>> let file = try getFileHandle() file.enterScope() defer {
>>> file.exitScope() }
>>>
>>> // statements } ```
>>>
>>> # Framework Changes / Examples
>>>
>>> As an example of some real-world classes that would be useful with
>>> `Scoped`, from Foundation:
>>>
>>> ```swift // Would be nice to extend the NSLocking protocol instead,
>>> but that's not supported yet. extension NSLock: Scoped { func
>>> enterScope() { self.lock() }
>>>
>>> func exitScope() { self.unlock() } }
>>>
>>> extension NSFileHandle: Scoped { func enterScope() {}
>>>
>>> func exitScope() { self.closeFile() } } ```
>>>
>>> # Questions and Concerns
>>> * Bikeshedding protocol naming and scoping syntax * Should the enter
>>> and exit actions be allowed to throw errors?
>>>
>>> --
>>> Trent Nadeau
>>>
>>> _________________________________________________
>>> swift-evolution mailing list swift-evolution at swift.org
>>> https://lists.swift.org/mailman/listinfo/swift-evolution
>>
>>
>> _______________________________________________
>> swift-evolution mailing list swift-evolution at swift.org
>> https://lists.swift.org/mailman/listinfo/swift-evolution
-------------- next part --------------
An HTML attachment was scrubbed...
URL: <https://lists.swift.org/pipermail/swift-evolution/attachments/20151230/e2672a0c/attachment.html>
More information about the swift-evolution
mailing list