Swift Generics Evolution
timekl.com
timekl.com
These proposed changes continue in that vein :)
- As the post notes, `any Shape` is the same as Rust's `dyn Shape` - both languages originally allowed you to write simply `Shape` (i.e. a trait/protocol name) to get an existential type, but later decided that was confusing.
- `some Shape` is the same as Rust's `impl Shape`.
- `Shape<.Renderer == T>` is equivalent to Rust's `Shape<Renderer=T>`.
(This is not meant as a criticism of Swift; Rust has copied some things from Swift as well. Just interesting to note.)
I may have misunderstood what is planned for Swift though.
Incidentally, this in principle allows the optimiser to specialise the caller to the return type of this function, avoiding the existential altogether.
My reading was:
`some` -> Some specific type with these constraints which will be known at compile time
`any` -> Any value conforming to these constraints which may vary at runtime
I just wish cross platform support was a real priority. The Ubuntu packages are all there is, and much of the ecosystem is centered around (Mac/i)OS.
In any case, as rule of thumb, systems languages that come with an OS/platform usually win in the long run.
Like you say the approach is pretty different: when it comes to the tradeoff between safety/performance and developer ergonomics Rust leans toward the former where Swift leans toward the latter.
With the Java style of GC the minimum and maximum cost and timing of GC are just not so predictable so in real-world scenarios ARC based apps feel more fluent.
It's like playing games. You'll notice the 10FPS dips more than the difference between 50FPS or 60FPS on average.
Cascading deletes of highly nested data structures are similar to pause the world in tracing GCs.
If the way destruction is triggered does not take this into account, stack overflows are bound to happen.
Finally it introduces slowdowns in shared data structures used across threads.
Herb Sutter has a very good CppCon talk about these issues.
EDIT: Forgot to mention that Swift was the loosing language at CCC talk about implementing device drivers in memory safe languages. All the ones with tracing GC had a better outcome.
> Cascading deletes of highly nested data structures are similar to pause the world in tracing GCs.
Which is simply difficult for every language. But even then still more predictable than Java "we'll do it when we feel ready for it" approach.
"35C3 - Safe and Secure Drivers in High-Level Languages"
https://www.youtube.com/watch?v=aSuRyLBrXgI
https://github.com/ixy-languages/ixy-languages
The subject of Herb's talk was actually going through all the reference counting problems to introduce in the end a lightweight implementation of deferred pointers, which isn't nothing more than basic tracing GC.
I'm talking about predictable performance and guaranteed minimum performance. I don't think you're speaking about the same thing.
The video seems to be interesting anyway, so thanks for that.
"Wiping the floor" means getting last place against all tracing GC languages used to research the mentioned paper.
Yes, many JVM based UIs do suck, mostly because the authors didn't bother to learn how to use Swing properly by reading books like Filthy Rich Clients.
The thing with Swift UIs, is that they are actually C and C++ UIs, given that those are the languages used to implement Core Graphics, Core Animation and Metal. Additionally Cocoa is still mostly Objective-C, with performance critical sections making use of NSAutoreleasePool like in the old good NeXTSTEP days.
Coming back to Java, factory automation and high integrity systems are perfectly fine with it.
You sound like those people that complain that Objective-C is so slow compared to C because NSArray is much slower than it's C counterpart. Objective-C is always exactly as fast as C since you can always write C in Objective-C.
And if all Java based UI's suck despite the fact that it's the most popular programming language and despite the fact that it powers the most popular operating system you might start suspecting there's something wrong with it, but nope, you've read a paper and saw a video. About a kernel driver. OK.
Your focus on Java, without spending one second reading the paper benchmarks, from a well respected researcher in the CCC community, just demonstrates a typical defensive reaction among reference counting proponents.
Using C++ without the C part is the whole point of modern C++ and the Core Guidelines. In fact there are several benchmarks where the C++ version gets better optimized.
As for Android, it isn't Java as we know it, and any travel through /r/androiddev will teach Google might have tons of PhDs, but they certainly don't work on Android, given how a lot of things are implemented.
The only thing I see is "here's a ton of text and video to slough through made by people smarter than me", nothing concrete that undermines my original statement in any way.
And yes C++ cannot exist without C as it is an extension to C. You can write pure C in a C++ file and it will still work. You can write pure C in an Objective-C file and it will still work. You can write some ugly bastard syntax version of C in Swift and it will still work.
I'm very much willing to accept Swift's performance for a device driver (possibly the first device driver ever written in Swift) is worse than in many other languages if you stick to the safe bits of Swift. It just doesn't have much to do with what I wrote.
The Garbage Collection Handbook, chapter 5.
Just one key CS reference for compiler writers among a few others.
Lay people also use wrong terms when talking about other scientific fields, that doesn't make them right by quantity of use.
In Swift the reference counting is a implementation detail of automatic and transparent memory management. (that you only really have to care about when you have cycles)
That's why Swift definitely counts as a GC language for me.
Sure, if you put things behind a Arc<T> or shared_ptr you use reference counting in C++ and Rust, but it is not an inherent feature of the language.
Rc<T> basically means that there are multiple sources of control over T's lifetime, but these are all within a single thread; and Arc<T> signals that the control might extend to multiple threads. It's not "mere implementation" that we're dealing with here; it's the very semantics of the code as it relates to Rust's expanded take on the well-known RAII pattern.
Swift simply lacks an equivalent to either the Rc<> specifier or e.g. Rust's Box<>, which expresses the semantics of an object which is heap-allocated and accessed via an indirection, and verifiably has at all times a unique "owner" controlling its lifetime (as per usual RAII).
Thus, arguing whether some particular middle point is or isn't "garbage collection" isn't anywhere near as useful as people seem to suppose, especially if it's being argued in a context where "GC is morally bad in all forms" or something like that. It's all a bunch of tradeoffs and there is no single perfect answer to all problems.
Note that even most "manually allocated" languages don't actually fall into my manual extreme; generally something more granular and automatic than that is offered. However, while nothing except arguably raw assembler defaults to that "fully manual" allocation, it's a useful last-ditch option in a lot of places, often wrapped up with just a touch more automation into something called "arena allocation", where you don't care about where the arena lives in RAM per se and it integrates with the rest of your allocator otherwise, but is just a big slab of bytes otherwise. Even in the GC'd languages I've seen "just give me a big slab of bytes and go away" used, even if it has no formal support.
Personally, I'd consider "reference counting" as a "garbage collection" scheme if the references are counted automatically, and as manual memory management if you're in a context where you have to manage them manually, but YMMV. I break it down that way mostly because in the automatic case, you get the general advantages of automation, in that it's largely correct but often somewhat slower (because it can't elide anything), and managing them manually permits more sophisticated schemes but also is massively error prone (to the point I wouldn't use it for anything anymore; the benefits are available from other techniques and the costs are insanely high). So personally I'd go with smart pointers being an embedded method of garbage collection in a language/runtime that generally is written for manual memory management. It doesn't make the outer language a GC'ed language, nor is it some sort of betrayal of the manual memory management ethos or whatever. Real programs in C++/Rust at scale will tend to use lots of memory management techniques, many of them with high degrees of automation, but the language and runtime themselves are generally manually-managed. It doesn't matter how many smart pointers your program uses, arena allocation is always an option for your next bit of code.
Not entirely true; in the case of ARC in Swift/ObjC a lot of effort was put in to optimizing retains and releases wherever possible. The de facto standard platform for ObjC (Apple's) had a strong set of conventions around memory management already. They were formalized in such a way that some of the defensiveness required in manual retain/release could actually be proved unnecessary in ARC code. (A few semantic changes were made to ObjC as well to support ARC.) Not in all cases, of course, but also not in none.
"35C3 - Safe and Secure Drivers in High-Level Languages"
Yes, that GC book always gets cited to educate everyone on terminology but it never resolves or finalizes how "garbage collection" is actually discussed in regular conversations. I made previous comments on why citing that book just adds to the confusion of how people typically communicated that rc != gc before that GC book was published.
Another thing is combining protocols with generics. That feels a lot more convoluted than might be necessary.
Seeing people express exactly what is the problem and what could be a solution is impressive. I hope we’ll see reverse generics soon.
A small simple language is something Swift is not.
Go/C# pose no issue but things like c++ cause total wtf where other people’s taste/habits become mutually unintelligible dialects.
Even in my first few days, having read Apple's Swift documentation, I had to continuously resort to StackOverflow for answers, which frequently sent me to the language grammar (which had many mistakes in it, and I think still does), and the Swift bug tracker. The inflection point on the learning curve is right after "hello world", which makes for great demos, but that's it.
Personally, I think even Common Lisp does a better job at progressive disclosure. There's huge areas of CL that you can simply ignore. I worked in CL for years before I bothered to learn about conditions, special variables, or 90% of LOOP's features. In Swift, almost right away I had to read everything related to errors. You can't ignore it, except in the simplest program.
I studied generics in college, as part of my data structures class, or maybe my compilers class. I've watched Alexis Gallagher's "PATs" video at least 3 or 4 times. I still don't understand Swift generics. Or maybe I understand what tools are provided, but I don't understand why you'd build (or want) a tool that makes it so hard to, say, define a new type with an "isEqual" method to determine if it's the same as another.
Every Swift programmer hits the "protocol can only be used as a generic constraint" phase pretty quick. My complaint with Swift generics was never that they're too simple. It would be great if this were just a corner that PL/type-system geeks could geek out on, but it's an area that you can't ignore.
I’d argue that with type inference, immutability, and non-null as a default, Swift is an easy language for anyone to be productive quite quickly.
let x = “I’m immutable and not null”
let favorite = [“Java“, “Perl”, “Swift”].shuffled().firstAlthough I do believe Swift has "virtual properties" that are really a method under the hood.
The fact that you need to use function call syntax for things like this in other languages seems like it is unnecessarily exposing implementation details to the API because of limitations of the language.
For instance, let's imagine in some C++-like language I had two list implementations: one static and one dynamic:
class StaticList {
...
int count; // set at initialization time
}
class DynamicList {
...
int count() {
... // compute the count dynamically
return count;
}
}
Here `count` is conceptually the same between these two implementations, but if I want to get that value, I have to access it differently: int c1 = staticList.count;
int c2 = dynamicList.count();
But that difference is essentially an implementation detail which means nothing to the client. The Swift way just lets me express my interface however I decide is most fitting.In languages like C, C++ and Rust, properties are things that are definitely in memory, and functions/methods are things that may not be. If you have an interface like `count` that may be dynamic, it should be uniformly expressed as a method/function (that may have a trivial "return count;" implementation).
On the other, Swift has to put a non-trivial amount of infrastructure into making sure all the things with property syntax can behave as if they are backed by memory, so that an address/pointer can be generated for them (in the general case, a temporary pointer, that becomes invalid quickly).
Having "first" as a property feels like exposing implementation details to me. Sure, a array/vector is a contiguous slice of memory filled with items of a certain size. But the vector/array is is basically a pointer to the start of the memory + a size, so "first" is not a inherent part of the data structure, rather a computed/derived property.
When I see a property I think member of a struct, which is not the case here.
(ps: this is all really just hair splitting, it's a minor difference that you would get used to pretty quickly either way)
Btw, there is also .first(where:) which accepts a predicate.
@interface Foo : NSObject
{
NSString * name;
NSInteger count;
}
- (NSString)name;
- (void)setName:(NSString *)newName;
- (NSInteger)count;
@end
(This is still legal, of course, but bad practice now that there's `@property` synthesis as well as ivars being declarable in either an extension or the `@implementation` block.)Shuffled may accept arguments. First does not
The runtime cost of first is nearly zero, whereas shuffled is doing significant work (constant versus linear time)
It sort of doesn’t matter because autocomplete won’t let you do the wrong thing.
Genetics are indeed useful but such a can of worms that are not worth it IMO
I really like to learn everything about every tech that I use, bordering on obsession, and after a few solid times that I hit roadblocks or had to fight with the tool because it is too "simple", I started to grok the difference of "simple" and "easy".
Don't know if you were already referring to Rich Hickey's talk on this, but if you weren't, it might appeal to you. Simple Made Easy: https://www.infoq.com/presentations/Simple-Made-Easy
"Okay, the other critical thing about simple, as we've just described it, right, is if something is interleaved or not, that's sort of an objective thing. You can probably go and look and see. I don't see any connections. I don't see anywhere where this twist was something else, so simple is actually an objective notion. That's also very important in deciding the difference between simple and easy."
func allEncompassingShape() -> Shape {
return SpecialGiantShape()
}
This function returns some type that may be a subtype of Shape. func make_numbers() -> Sequence<Int> {
// Cannot specialize non-generic type 'Sequence'
}
func make_numbers() -> Sequence {
// Protocol 'Sequence' can only be used as a generic constraint because it has Self or associated type requirements
}
Whereas with this change you probably could at some point do something like this: func make_numbers() -> some Sequence<.Element == Int> func make_numbers() -> Sequence<Any>
This should work as long as Sequence is covariant on Element. You could return any possible Sequence here, like Sequence<Int> or Sequence<Double>. The user of the function wouldn't know.You could also do something like
func make_numbers() -> Sequence<Comparable&PartialOrder>In Java, the following function definition compiles just fine:
IShape getShape() {
return new Rectangle(20, 40);
}
And, assuming that `IShape` has a `draw()` method, you can write: IShape shape = getShape();
shape.draw();
It works because there is dynamic dispatch occurring at runtime: the JVM will look for the implementation of `draw()` in the `Rectangle` class (not sure of the exact mechanism, but that's the idea), and call it. To find it, the value (here, shape) must holds a reference either to its class so the JVM can go and look for the implementation, or to a table of all methods it implements (I think it's the first). So the compiler code doesn't know the layout of the concrete type used, it just add instructions to go look for the implementation at runtime. But that's fine, because any Object in Java is in fact like that: a fat pointer, containing a pointer to the data, and a pointer to the implementations.I won't work in Swift because, as far as I know, there is no dynamic dispatch, at least by default. When you write the following:
func render<T: Shape>(_ shape: T, at point: Point) { … }
The compiler will know what is the concrete type of `T`, and so will use its implementation of the `draw()` method. It won't be found at runtime, it is known at compile time. What this means is that a protocol is not a type. This is the important part. An interface in Java is a type, because it doesn't dictate how the method is called, it allows the concrete type to live under the hood, and call the right method at runtime. A class in Swift is a type because you know how to call it, how to access it. But a protocol is just a set of constraints on a type, not a type by itself.That's why you need the `some` in return position. Well, you don't really need the syntax, but it helps understanding the difference with the "same" Java code. The `some` keyword says that the function will returns some type that will implements the `Shape` protocol. It will in fact return the concrete type, but this is not included in the type signature, so it can change, it can hide implementation details, without making a breaking change in the API. It also means that the following won't compile (I'm not sure here, but it works that way in Rust):
func union(_ leftShape: some Shape, _ rightShape: some Shape) -> some Shape {
if isEmptyShape(leftShape) {
return rightShape
} else if isEmptyShape(rightShape) {
return leftShape
}
return Union(leftShape, rightShape)
}
Because here, all code path don't return the same concrete type. If you want different concrete types, you'll need the other keyword, `any`. `any` is in fact a lot like the interfaces in Java, because it uses dynamic dispatch under the hood (if it works like it does in Rust). The compiler will know how to turn the concrete type into the dynamically dispatched one. interface IList<T> {
IEnumerable<T> iterator();
}
it means that despite the fact that you may know the exact implementation of the IList, you have no idea of the exact type of the method iterator() at compile time because it's hidden behind an interface.Enter the existential types,
interface IList<T> {
type IEnumerator<T> Iterator; // this is a type declaration
Iterator iterator();
}
now if you have an implementation of IList, you will have class FunnyList<T>: IList<T> {
type FunnyListEnumerator<T> Iterator;
Iterator iterator();
}
so at compile time, when you use a FunnyList, the compiler knows the exact type of the method iterator().The other usual use case, which is a restricted version, is to be able to specified the variance at use site, like the wildcards in Java (List<? extends Foo>) which is a way to specify that you don't want a method to be parameterized but you want a type of a parameter to be covariant
void foo<T:Foo>(List<T> list) // the method is parameterized
void foo(List<some Foo> list) // the type is parameterized
C# doesn't have variance at use site, only at definition site with 'in' and 'out', Kotlin has both. Scala and C++ with type name form.In fact that is their execution model in UWP, Unity (Consoles, iOS, Xamarin (iOS), Android and some embedded deployments.
You can do it in one command:
$ swift package init --type executable $ swift package generate-xcodeprojIt's definitely not just this. Swift is very similar in design to Rust, and has a lot of the same goodies like an ML-inspired type system, but without the complications imposed by the borrow checker / lack of GC.
Panama will thankfully fix that.
Just don't expect it to ever be supported on Android.
But they surely will,
https://en.wikipedia.org/wiki/List_of_Java_virtual_machines#...