I'm really standing with Kevin on this one — no non-orthogonal features and special cases please. If we allow RAII- (DIRR-)-style constructors and destructors, we get a solution that is more general (needs no new syntax nor a separate language construct) and doesn't introduce syntactic noise (via the triangle of death).
Sent from my iPhone > On 30 Dec 2015, at 05:55, Kevin Ballard via swift-evolution > <[email protected]> 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 >> [email protected] >> https://lists.swift.org/mailman/listinfo/swift-evolution > > > _______________________________________________ > swift-evolution mailing list > [email protected] > https://lists.swift.org/mailman/listinfo/swift-evolution
_______________________________________________ swift-evolution mailing list [email protected] https://lists.swift.org/mailman/listinfo/swift-evolution
