[swift-evolution] [Proposal] Scoped resources (like C# using statement)
Matthew Johnson
matthew at anandabits.com
Wed Dec 30 08:49:05 CST 2015
Sent from my iPad
> On Dec 29, 2015, at 10: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.
+1 to using RAII for this.
I want to see all of the enhancements you describe that would enable structs to be used.
The situation for classes isn't necessarily quite as dire you make it sound. In some cases the optimizer will perform stack promotion. There was a brief discussion about this a few days ago. I don't recall which thread.
There is one challenge with using classes however. ARC does not guarantee an instance will stay alive until the end of the scope. Depending on the use case you may need this guarantee.
So in the end, improved language support for RAII one way or another is very desirable. I don't expect to see major improvements in this area sin Swift 3, but maybe there are some smaller improvements that can be made.
>
> -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/9e658308/attachment.html>
More information about the swift-evolution
mailing list