Swift is a more convenient Rust
blog.namangoel.com
blog.namangoel.com
1. A large part of why many people love Rust is that it's the first time they've used an ML family language. One of the innovations of Rust was to create a community that felt like home to Unix hackers who weren't programming language nerds.
2. Rust is the first language to bring non-GC automatic memory management to the mainstream. It won't be the last and it might well be the worst. Other languages in this space include Swift (as in OP's post), OCaml[1], and Scala[2]. These alternatives take the approach that tracking ownership is not the default, which is fine for the majority of programs and far more ergonomic.
There's also a corollary
3. The age of Smalltalk is over. It's now the age of ML. (Amusingly, going backwards in time, from 1983 to 1975). The languages that dominated the 2000s (Ruby, Python, Javscript, PHP) were all more or less derived from Smalltalk (everything is an object! dynamic types! runtime metaprogramming!) The new languages (Rust, Scala, Swift, Kotlin, etc.) are ML family languages. Similarly, back in the day you could learn, say, Ruby, and still be reasonably proficient in Python or JS. Now you can learn, say, Scala, and pick up Rust or Swift reasonably easily.
Scala is quite a bit older than the others, I'd put it in a previous generation.
I always saw Kotlin as a more convenient Java, without Scalas heavy tilt towards FP.
Java also added the equivalent of sum types with sealed interfaces and exhaustive pattern matching, does that make Java an ML language?
Many older languages by now have incorporated valuable parts of FP/ML (including Javascript, Java, C++, ...)
Borrowing some concepts from ML doesn't put these languages in the ML family.
not really, the type-system-sublanguage is still modelled like classy OOP, not more like MLy modules.
> Java also added the equivalent of sum types with sealed interfaces and exhaustive pattern matching, does that make Java an ML language?
I don't think so. Java heavily relies on mutation and you can see this throughout the whole ecosystem and even the JVM whereas OCaml doesn't. Kotlin also relies on mutation (it relies on Java's stdlib after all) whereas Scala has its own stdlib with both mutable and immutable classes but defaulting to immutability almost everywhere.
So if anything, Scala can be called an ML language imho.
To nitpick, the first language to bring non-GC automatic memory management to the mainstream would be either Forth or C through stack memory.
For heaps, we have reference counting, often handled automatically through the stack (e.g., C++ smart pointers).
The thing Rust brought to mainstream is not automatic memory management - they're a dime a dozen with and without GC. It's an ownership model that is strict about whether you have read or write access within the type system. At the same time, this is also where most developer friction stems from, as it's quite unique, and you're often faced with a trade-off between explaining exactly what you want at compile-time at higher design cost, or bailing out more to runtime checks with e.g. RefCell while getting fewer guarantees about code correctness from the compiler.
All with stack allocation.
I can’t really comment on what counted as mainstream before C though. Not old enough for that.
Goggle, Bing, DuckDuckGo, ChatGPT,... will gladly provide a list.
To define what was mainstream we'd need to talk to people coding at the time and ask them about popularity, adoption, tooling availability, etc. and what languages people were likely to use rather than what was available.
And no, I have no need for researching that as I have no use of the information - the point is to say that automatic memory management in mainstream languages predate rust by decades and is present in most languages, not "this specific language should get credit".
Besides, any Assembly programmer is well aware of what stack memory is all about, bring us even further back than 1958.
The whole point was that these well-known mainstream languages provided the feature claimed to first have arrived in Rust, to make it clear that it was not just theoretical or some obscure language.
It's not my fault that you misunderstood the topic or the idea of what makes something mainstream, and instead read this as an elaborate history class stating the absolute first discovery of the idea.
> Besides, any Assembly programmer is well aware of what stack memory is all about, bring us even further back than 1958.
The instructions you refer to were designed to support stack-based languages, not the other way around.
Stack memory is decades older than C.
Emphasis mine.
It's not C. You do manual memory management there. You have to call malloc/free.
Didn't use Forth, so I can't say anything about it.
Stack based allocation are essentially registers, right?
That you also have access to manual memory on the heap doesn’t matter. You can also do manual memory management in Rust if you want, as one has to do at times.
It's not rocket science. If you are calling malloc/free to maintain your memory you're doing manual memory management.
> You can also do manual memory management in Rust
You can do Garbage Collection in C, it doesn't make it Garbage Collected language.
You're confusing default memory management with what's possible.
By your logic, Arena allocators can be used in many different languages, including GC ones like Java. So that means GC languages like Java are manually memory managed. Which is defeats the definition of it.
Sure, and when I do the same in Rust I'm also doing manual memory management. So by your definition, both Rust and C are manual memory languages.
> You're confusing default memory management with what's possible.
Ah, so we care about the default, which I pressume is what the language semantics themselves provide, rather than focusing on what the standard libraries can provide you?
In that case C is an automatic memory language, because the language semantics only provide you stack memory. malloc/free are just random functions in the standard library after all, just like Rust's `Box::new` and `std::alloc`.
See, the point is that you're opting into manual memory management in C from an automatic model. We are so used to the stack that we forget that it's the OG fully automatic zero-cost memory management system, and in case of C++, can be used to implement fully automatic heap memory as well - in which case you never need to call malloc/free/new/delete.
(And no, it doesn't count that your smart pointer calls new/delete, because then you also need to count Box::new calling std::alloc)
> By your logic, Arena allocators can be used in many different languages, including GC ones like Java.
Uh, even in bog standard Java without any shenanigans you are using an "arena allocator". A GC doesn't change how allocators work, it just responsible for calling free.
(Caveat about moving vs. non-moving garbage collectors and ones that have multiple arenas, but that's not relevant here and an entire topic of its own.)
You're being very thick on purpose. In Rust you need to reach for foreign functions to implement malloc/free.
> So by your definition, both Rust and C are manual memory languages.
No. By my definition what is the default semantics determines if it's manual or automatic. It's CS 101.
But if you want to play these semantics games, you just admitted C is GC language and thus unsuitable for kernel development.
> Uh, even in bog standard Java without any shenanigans you are using an "arena allocator".
No, no you aren't. At least not explicitly. I assume you mean GC, if it has or doesn't have arenas is implementation detail.
It also hinges on Wikipedia being correct that Arena IS manual memory management, which is unsubstantiated at best.
I think you're mistakenly thinking of calling out to the (rust-lang maintained) libc crate's malloc/free functions. That's not the case - the standard library provides `std::alloc`, which is the allocator also backing Box and Vec.
> No. By my definition what is the default semantics determines if it's manual or automatic. It's CS 101.
Your definition of default semantics - "in C it's not what the language does but what happens when you call random library functions, while in Rust it's the opposite" - makes no sense at all.
C isn't considered a manual language because of default semantics, but because people have chosen to mainly rely on such paradigm.
> No, no you aren't. At least not explicitly. I assume you mean GC, if it has or doesn't have arenas is implementation detail.
Yes, whether any allocator has an arena is an implementation detail.
Whether you call `malloc` or `new`, or you have Go or Java do a heap allocation for you (which, to nitpick, is not actually the job of the garbage collector), the use of an arena is an implementation detail. In case of GC, the availability of optimizations also depend heavily on whether it's a moving GC.
Again, emphasis on the word, default. How are you using Box and Vec. Are you by default encouraged to drop them manually via Allocator?
No, you aren't. You're heavily discouraged via usage of `unsafe`, you are discouraged because the compiler does automatic `alloc`/`drop`.
You have to go out of your way to do manual memory management.
> Your definition of default semantics
I showed you what happens when you apply the non-default semantics of a language to C. You end up with nonsensical statements like C is a GC language. Just because a style is possible in language X doesn't mean language X is of that style.
> C isn't considered a manual language because of default semantics, but because people have chosen to mainly rely on such paradigm.
And why is that? Because the default affordance of the language makes it so that way of usage is the most natural to most users. You could put everything in the stack, but that would be extremely torturous for most users, so they use malloc/free.
You give people a knife, of course they will grab it by the handle.
{
int8_t x = 1;
}
allocates a byte on the stack, binds it to a variable named x, sets it to 1, then deallocates it from the stack. There is no explicit allocation or deallocation.As an optimization it can be put into a register instead of the stack, again without explicit allocation and deallocation (this is done by the compiler playing musical chairs with registers).
I would not consider this manual. Manual would instead be something like in embedded programming
int8_t* y = (int8_t*)0xB4DC0FF3;
*y = 1;
because one needs to keep track of used memory locations (do allocation management)Rust's memory management is not automatic, you have to explicitly manage memory. It's just made extremely easy for you by the language thanks to stuff like RAII, and there is checks that prevent memory safety violations like double free. But those checks won't prevent leaks, although the many lints do make it harder to forget about a value.
Also, Swift's refcounting can be seen as a form of gc.
By this logic no language on earth has automatic memory management. I've spent time troubleshooting a memory leak in JavaScript in the past month, caused by someone keeping a pointer around longer than necessary.
Rust's memory management is automatic in that you can write entire Rust programs without once calling `free` manually. I'm not sure what definition of automatic you're working off of, but if it excludes JavaScript it doesn't seem especially useful.
In JavaScript's case it's an implementation detail, but one that is extremely relevant when maintaining something in production.
[0] As a silly example: a JavaScript implementation could technically store object references as a URL of an API that allows interacting with the object, as long as this is transparent to the user and the program behaves the same as a pointer implementation (minus non-functional characteristics like performance).
You can use things like WeakMap or WeakRef as one solution, but there are usually better options
This is contrast to something like C/Zig, where things are fully manual, or something like Python or Javascript where things are fully handled for you.
If you mean "drop," it's just syntactic sugar for calling a trait that manually deallocates the memory and that you're free to reimplement.
It feels like people are equating "manual memory management" with "onerous memory management." People are usually going to want to do the boring thing with memory, and everything in the standard library by default does the boring thing with memory. If you write boring structs and enums, you'll derive boring memory management. But it's not part of the language, it's part of the library.
I meant `free` because I'm contrasting with C.
> It feels like people are equating "manual memory management" with "onerous memory management."
Manual memory management is onerous, but I'm very specifically talking about manual. If I can write a whole program without thinking about memory, the language does not require manual memory management. It may support it, but it doesn't require it the way that C or Zig do. You do not "have to explicitly manage memory" as OP claimed.
It would be unacceptable to allow this because Drop::drop says it only takes a mutable reference &mut T (and indeed it does) but now the thing is destroyed, so, that wasn't just a mutable reference at all!
You can call core::mem::drop<T> but well, look at it, here's the code:
pub fn drop<T>(_x: T) {}
Like, duh, we give it a T and then it doesn't give anything, the T is gone. That's not magic library code by the way, if we make our own:
pub fn vanish<T>(_x: T) {}
Now we can call our vanish function and the same happens.
So yeah, automatic memory management.
As with the rest of the language, the automatic memory mangement requires that your program has no mistakes - which requires inhuman amounts of care during implementation and testing. So, that's obviously a spectacularly bad idea, but it's still automatic memory management.
Personally I'd put the bar around not requiring people to distinguish different pointer types (owned vs shared). Languages like Java, JS, Python, Swift, Go, etc all have this "everything is a smart pointer" paradigm, (or at least the strong and widely used default). I'd say Rust is manually managed because you need to think about which pointer type to use and using gc'd pointer types like Arc has a syntax overhead (like say when a pointer is cloned).
Rust makes the distinction between stack and heap references, like C++. Other, more high-level languages don't - there's only one kind of reference, you can't take a reference to a stack object. Maybe you implement that by making all objects heap allocating (Java) or you just say they have to be copied every time (C# struct). That's really where the difference is.
There's a lot of juicy, juicy performance there. The problem is taking references to stack variables is problematic. Tracking heap objects with a GC or a ref counter is really trivial in comparison IMO, at least when you try to combine the systems.
Not only instance methods on C# structs are implicitly byref, you can easily pass structs by reference via ref, out and in keywords. On top of that, ref structs can hold `byref` pointers aka 'ref' keyword which can point to arbitrary memory, or have references to other structs/variables/anything.
There is also regular C syntax with &T and T* for unmanaged references/pointers. On top of that, .NET's compiler has gotten very good at struct optimizations and pretty much ensures they stay in registers all the time unless address-taken, including SIMD registers for Vector<T> and Vector128/256/512<T> even their "deconstructed" form when specified width is not supported so they get handled as e.g. 256x2. There was a big jump in codegen quality in .NET 8 which can now sometimes trade blows with GCC and Clang around struct optimizations.
All these features are first-class and are heavily used by all kinds of performance-sensitive code.
Also, structs can implement interfaces and can be generic arguments that satisfy interface constraints, which works exactly like generics with trait bounds in Rust - you get a generic instantiation aka monomorphized function body, making the abstraction zero-cost.
> you can easily pass structs by reference via ref, out and in keywords. On top of that, ref structs can hold `byref` pointers aka 'ref' keyword which can point to arbitrary memory
These are not features I've encountered. I wonder how you solve dangling references when those references could point to automatic stack variables.
The handling is transparent, in a way. As implemented by CoreCLR, class-type generic arguments have shared representation named __Canon. This means that, for example, a `Dictionary<int, string>` has a generic instantiation indicated as `Dictionary<int, __Canon>` where __Canon is an implicit generic type argument passed alongside relevant calls. Statics referencing that do get exact address, and there is quite a bit of complexity regarding runtime handling of this as far as virtual calls and other edge cases are involved, but as a programmer you are never exposed to that directly. It's an implementation detail, and even un-monomorphized cases work rather fast in most situations, like standard data containers.
> These are not features I've encountered. I wonder how you solve dangling references when those references could point to automatic stack variables.
Not sure what you mean by automatic stack variables, but the idea behind byref pointers\managed references\'ref's is that they are not allowed to be boxed or otherwise placed on the heap.
This lifetime restriction enables key scenarios:
- byrefs can point to object interiors without hindering GC throughput
- byrefs can point to stack memory, allowing `var span = (stackalloc byte[32]);` and more
- byrefs can point to any unmanaged memory without requiring excessive range checks by GC
This way you can use `ref T` to represent any memory location and `Span<T>` to represent any contiguous memory range up to 2B elements, without having to carry around bespoke overloads and types that disambiguate between containers and memory sources, much like you would usually see in most other GC-based languages.
There is also additional lifetime analysis in Roslyn to prevent you from returning 'scoped' 'ref's to an outer scope, like having a ref point to an integer in the current method body and returning it to the caller - this will not compile, unless you override it with unsafe [UnscopedRef] which you should never do unless you are absolutely certain (every time I used it and was absolutely certain, the compiler was right and I was not :D). This also works "through" ref structs and other tricky scenarios, which means you can return a ref that points to a middle of heap-allocated array - the scope of object exceeds current method, and byref can keep the array rooted even if no other reference to it exists. The main restriction is byrefs can mostly flow "downward" as not to escape the scope they originate from, but that's a given in most scenarios in Rust just as much.
There is a basic walkthrough about byrefs here: https://learn.microsoft.com/en-us/dotnet/csharp/language-ref...
C# is good at systems programming - it also has portable SIMD and intrinsics, static linking, native compilation and zero-cost FFI. Engineers getting surprised by this fact rather than upset is the happier but unfortunately less frequent outcome :)
By restricting them, to, essentially, disallow storing `ref`s on the heap.
That's what I meant by "strong and widely used default". Defaults and syntactic sugar do matter and influence the type of code written.
When people bitch about the borrow checker, what they usually mean is “std::move is a great wristwatch but god damn does it chafe as a chastity belt”.
I like a lot of things about Rust: it’s got compiler checked exhaustive pattern match and a good initialization syntax and Either baked in and fucking correct package management and a ton of other things.
I’ll look at Clean to see if linear-only memory management can be done gracefully. Rust is a lot of cool things but not the language that demonstrated that.
I don’t mean to be obtuse but I don’t see what that has to do with linear typing as a fucking weird mandatory default?
Rust was shaped by Mozilla wanting a language to replace parts of Firefox with. A language that is low-level enough to give very high performance, enforces correctness through the type system, and yet has many abstractions so it feels modern and convenient to use.
Rust doesn't have any automatic allocations. You can wrap every type in `Arc<Mutex<T>>` and treat it like a very verbose Python, but that's the programmers choice.
The design decisions have been validated by the success of Rust in certain domains.
There is a reason why more than one programming language exists...
If it wasn’t actively seeking world domination I’d be like, cool let’s use that sometimes.
Rust is so hell bent on a Rust monoculture ranging from stuff like TRACTOR to actively opposing interop that a few of us who know our shit have to be like “easy now” once in a while.
I’m trying to decide between tiktoken and sentencepiece for a new vocabulary at the moment, and it would be easier in some ways to go with tiktoken.
But I want it to run faster than a fart at inference time, which means I’m linking libtorch, which means I’m writing C++.
And fuck, link to Rust from C++? Let’s take another look at the Google stuff.
Rust just uses the C ABI for interop (including interop with Rust code that might not be part of the same build, such as dynamically-linked program objects). So you just have to come up with a plain C API for the interface and write wrappers on both sides of the divide that reference the C API. There are crates that will help with this, even achieving something like a "stable ABI" for Rust interop.
(And we'll probably see some work into interop with Swift itself (which has a stable ABI of its own) once there's enough interest in that as a memory safe language.)
But bindgen/cbindgen/cxx/etc exist for a reason: it hurts like a root canal.
In my particular case, tiktoken is substantially like 600 lines of Rust. If I have a deadline do I fuck with the linker or just write it again? I’m marshaling std::string here, fuck let’s just write it.
What I’m not going to re-write because I’m not a magnificent maniac like geohotz? I’m not going to rewrite libtorch. Which is written in C++ like all the software you can’t really live without.
Hylo specifically says it intends "world domination" (next year in fact, having apparently completed all the delayed 2023 goals and also all its 2024 goals this year, I guess maybe they're going to do it all in December?)
But I don't really see this from Rust. I'm sure Hylo's "world domination" is a joke, but, so is the "Rust Evangelism Strike Force". Rust people I've interacted with strike me as very much more accepting of the "better tool" theory than of some weird cult or panacea. When somebody in a Rust forum says (e.g. about JPEG XL) "Ooh we should use Rust" and I say "No, WUFFS. This problem is what WUFFS is for" the responses tend to be a mix of "Yeah, I guess" and "I didn't know that existed. Thanks for the link" rather than cultist denial.
I admire both the technical sophistication and passion you folks bring to the table, the one true language thing just isn’t my bag. I pine for the Halcyon days when HN was run by the Rust people rather than the LLM idiots.
Keep doing what you’re doing, one hacker to another.
Now GHC does stack allocate, but it’s hard to count on without really aggressive hinting.
An `async {}` block is not evaluated, but converted into a generator/state machine that lives on the stack, and that has to be advanced by calling a poll method. You can move it to the heap , but that does not happen automatically.
Sharing of values is obviously much more awkward due to mutability and no automatic cloning though.
But to be clear, anyone with the username the_duke is probably really cool, and you clearly know your stuff as well as having style, so count me as a fan.
But not so much in the conditional branch sense of the phrase “control flow”.
More like, I’ve got a bunch of references and one of them is a reference to a function I might evaluate, damn, I need that reference and the transitive closure of everything it knows about, all of which are probably on the heap.
A space leak, much like any memory leak is when you or the compiler or the runtime or whoever misplaces such reference.
GHC having a closure in scope in no way immediately means there is a space leak. Plenty of Haskell code that compiles to thunks will run all day in an amortized fixed-size heap.
Much like Erlang, which is what Joe Armstrong had designed before he said the thing about jungles you’ve quoted.
And it is in no way difficult or even really rare to drop an Arc in a table and leak all over the place.
Rust has all the same computer science problems as anything else. Full stop.
The laziness as a default just makes boxing closure values a sensible default, when the type system requires the boxing of everything in general.
Parametric polymorphism is kind of orthogonal to that. It’s related I guess. It can involve some kind of runtime dispatch, but usually that gets JITed or inlined or whatever unless you really insult the compiler.
And even full frontal invokedynamic it’s still like, is it in the BTB and the I-cache? Ok well we’re still doing business.
If you want non-default safety you do not need to leave C++. That is why unlike all these new languages, Rust makes difference.
https://saityi.github.io/sml-tour/tour/02-09-mutable-refs.ht...
(OCaml has something similar too.)
It's pretty clear that we're talking about very broad families, which is okay—Rust has more in common with ML than it does with Smalltalk or ALGOL (though the ALGOL heritage is definitely present).
In Rust Vec<Option<Infallible>> is just a counter. Really, that's not theory that'll just happen by default because of how type arithmetic works.
In C++ you can't even write down an equivalent type, let alone say how it would be represented, the type system blows up long before we get there.
Separately `Vec<T>`, if `T` is a zero-sized type, never allocates and has a capacity of usize::MAX. Then, when you push into the Vec, because the value you push is zero-sized, there's no allocation and no data to write anywhere. Therefore the only effect is to increment the length counter.
1) lets dispense with the Allocator, for a typical Vec the global Allocator is used, so this type has no size, every Vec is talking about the same global Allocator, its state is tracked internally to itself. We need not consider this object further†
2) now lets dispense with that unique pointer. Its semantics are crucial if T had non zero size because this is why Rust knows the associated memory which is pointed to is "owned" by the Vec. However, for a zero size T this pointer is entirely unused.
3) Capacity at last actually is used, it's a machine word sized integer, so on a modern computer that's 64-bits, 8 bytes to store the capacity of the Vec, which will be the maximum possible unsigned integer of that kind, usize::MAX. It is set to this value when the Vec is created (because the size of T was zero) and never changes.
4) Length is also used, despite not needing to store any data for T the Vec is finite and this tracks how many of the zero size item are in the Vec. Thus, it's a counter.
† In C++ they use the "Empty Base Optimisation" to avoid needing space for such things, in Rust their size is just Zero and so they won't be stored.
What is the use case? Vec<T> is a generic type (Rust's generic growable array of T) so although Vec<Option<Infallible>> seems somewhat useless as a concrete type, it is likely to sometimes occur in generic code.
Example: generic code to do a bunch of potentially fallible operations and remember whether and how they failed for later summarisation may make a type Vec<Option<E>> where E is the failure type. When the operation wasn't actually fallible E is Infallible and instead of an actual growable array type we're just making a trivial counter, our summary is going to inevitably say that all N operations were successful, any code for the "List of failures" summary should even get trimmed as dead code in this case since it depends on an Infallible object and the compiler knows Infallible cannot exist.
There seems to be slightly more compile time work done in Rust but using the use case you described the concept of a Vec<Option<Infallible>> can just as easily be represented as std::vector<std::nullopt_t> with very similar semantics although possibly slightly less optimisation, although I would think if it was actually used in the language extensively I would see library implementors makeing it entirely compile time just like in rust.
I don't disagree that algebraic data types are nice and that Rust has an interesting implementation of them, however this specific example doesn't seem unrepresentable in C++ using std::option and std::variant although the actual semantics relating to usage is easier in Rust.
pub enum Foo {
Foo,
Bar(A),
Baz(B),
Qux(C),
}
pub enum A {}
pub enum B {}
pub enum C {}
The optimization applied to Vec also applies to any zero-sized type.I spent some time trying this out in Godbolt†, and then by hand, and then reading the Microsoft STL, and exactly as I'd assumed before I read your comment it was a growable array of one byte objects, with the type std::nullopt_t, 400 objects? 400 bytes. 4 million objects? 4 million bytes.
† Including longer than I'd like to admit forgetting that C++ silently copies large objects without telling you and so my diagnostic was actually making a new std::vector because I forgot to write an ampersand to make it a reference...
It is still possible I missed something and so I invite you to tell me what that was. But overall my thought is that you've missed the whole point and are in the same place as when C++ programmers get confused and think Empty types (like Infallible) and Zero Size Types (like Option<Infallible>) are somehow the same, which is like confusing zero (The additive identity) with one (the multiplicative identity).
One of these is not like the others. Javascript derives from Lisp (first-class functions, lambdas, closures), not Smalltalk.
Or maybe you think of the fact that JS uses prototype-based OOP, which makes it closer to what one would do with Scheme (something inspired from CLOS I guess) than Smalltalk ?
CLOS is an evolution from Flavors and similar Lisp packages, and outside Lisp, Julia would be the closest to it, in modern times, not counting Dylan.
Isn't prototype-based OOP roughly a decade older? -> for example "Director" from Ken Kahn is from 1976.
Although Wikipedia also points out that, https://en.wikipedia.org/wiki/Prototype-based_programming
Actually your remark has made me dive into alternative prototype based languages.
Director Guide: https://dspace.mit.edu/bitstream/handle/1721.1/6302/AIM-482b...
Creation of Computer Animation from Story Descriptions: https://dspace.mit.edu/bitstream/handle/1721.1/16012/0720063...
The other mentioned tool was Thinglab, by Alan Borning
https://www.ijcai.org/Proceedings/77-1/Papers/085.pdf
"The Smalltalk notion of an object has been modified and extended in a number of ways. In ThingLab, objects are constructed interactively by editing and making descendants of prototypes. An abstraction hierarchy is used that allows arbitrarily many levels of both parents and descendants, with the problem of objects with several parents being handled by the technique of merging. There is no distinction between classes and instances."
"The Programming Language Aspects of ThingLab, a Constraint-Oriented Simulation Laboratory" https://dl.acm.org/doi/pdf/10.1145/357146.357147
"CLASSES VERSUS PROTOTYPES IN OBJECT-ORIENTED LANGUAGES" https://dl.acm.org/doi/pdf/10.5555/324493.324538
Available in the Browser, running in Smalltalk: https://constraints.cs.washington.edu/thinglab/
Ugh, “Arc”, aka (automatic) reference counting, is an implementation of GC as well. It may have certain characteristics, like having worse throughput, corner cases, and better predictability/latency characteristics, but it's GC nonetheless. That's not manual memory management. Swift is not a non-GC language.
And that's the big issue that people are missing — Rust tackles some hard problems regarding memory management, and a friendlier alternative exists: garbage collection.
This paper makes a strong argument that “pure” tracing garbage collection and “rote” reference counting are effectively duals at the opposite ends of a spectrum of implementation choices.
For example, GC’s often optimize differently for young vs. old objects. How do they distinguish these? By “counting” (one bit) their references across one or more traces in a series of sweeps.
What you are equating with "garbage collection" is Mark-Sweep, which is an another way to implement GC, with a very different set of tradeoffs. (Broadly, better throughput at the expense of higher latency.)
You can call a pointer to a value a "one-element static array."
This feels like people arguing that atheism is a kind of religion. Borrow checking and a library of safe default primitives make it so you don't really have to think about memory management; except to the extent that you generally stick to those primitives, and that you have to install the borrow checker into your own head to write Rust comfortably (although the compiler will help.)
Rust doesn't have any garbage collection, but it can just assume how you would want to deal with memory by forcing you to be very specific about when values should be created or destroyed.
The whole time I was like "what in the absolute hell is a ML language".
Honestly kind of shocked to see this type of mentality...wow.
If you don’t know ML, knowing what it stands for won’t help you. It’s like JS, either you know what JS is or you don’t. It’s not about what it means, it’s about what it is.
> A large part of why many people love Rust is that it's the first time they've used an ML family language.
You wouldn’t search "ML" but "ML family language".
Or copy paste the whole thing and ask ChatGPT what ML means in this context.
ML = Machine Learning
ML = Machine Language
ML = Markup Language
ML = Meta Language
And I always see ML in the Meta Language sense to be the biggest culprit where writers assume the reader knows it, even though it's probably the least widely known.
I'd argue that Erlang (with gen_servers being "objects") has more in common with Smalltalk than these.
What drew people to a lot of your examples (Javascript, PHP especially) was the runtimes rather than the language features. Your example sets aren't just demarcated by Smalltalk-ishness/ML-ness but more by runtime type (Scala & Kotlin are odd examples given the VM but for the most part your latter examples have build-time compilation while the former have plaintext interpreters).
We're definitely in a tools-heavy era of programming where, unless you're writing bash scripts, even very basic applications in interpreted languages are layered with a slew of transpilation/compilation/etc., but generally speaking I still don't see the majority of people moving wholesale away from plaintext runtime interpreters. Where are the ML-ish competitors in that space?
The nitpicky take: a lot of these languages come with a repl / console. E.g. the most recent version of Scala builds Scala CLI[1] into the language. You can run `scala repl` and just type code into it, or run `scala SomeFile.scala` and it will compile and run `SomeFile.scala`. There is special syntax for writing dependencies so that a single file can pull in the libraries it needs.
The 5head thought leader take: the traditional model for typed languages has two phases: compile time and run time. Types exist at compile time. This is inadequate for many applications, particularly interactive ones. E.g. a data scientist doesn't know the shape (type) of the data until it is loaded. It should be possible to infer the type from the data once it is loaded and make that type available to the rest of the program. We know how to do this (it's called staging) but it's just not available in the vast majority of languages. Staging, and metaprogramming in general, is perhaps the next great innovation in programming languages (which will take us from ML to Lisp).
In general, the challenge for these new languages is to reach "down" into the simpler scriptier applications, instead of the "serious" programming they are usually built for.
>Rust is the first language to bring non-GC automatic memory management to the mainstream. . . . Other languages in this space include . . . OCaml[1]
But your own link[1] says,
>The OCaml compiler does not statically track lifetimes. Instead, it relies on a garbage collector to figure out a suitable lifespan for each value at runtime. Values are collected only after they become unreferenced, so OCaml programs are memory-safe. To a first approximation, this model requires allocating all values on the heap. Fortunately, OCaml’s generational GC can efficiently handle . . .
Scala does run-time garbage collecting, too (or rather the JVM does, which Scala depends on at run-time) unless I'm very much mistaken.
That said, the difference between the two has a lot less to do with the language itself than the world surrounding it. You can use Swift cross platform but it’s very obvious that Apple platforms are the primary target. Rust has a rich and varied package system, about the only assumption most packages make is that you’re using the Rust standard library. By comparison lot of Swift packages (which is a much smaller ecosystem anyway that’s slowly transitioning away from Cocapods, Carthage etc) will lean on OS APIs that won’t work if you compile for Linux or WASM[1].
I want Swift to be a more convenient Rust but it just isn’t there. e.g look at IBM abandoning Swift on the server not that long ago.
[1] for example, this blog post:
https://swiftrocks.com/weak-dictionary-values-in-swift
Discusses making a dictionary with weak values in Swift. It has a homegrown Swift version then discusses using NSMapTable… which isn’t available on Linux. But you wouldn’t know that reading the article because the assumption is that you’re running on an Apple platform.
I doubt that .NET has much to do with it. There are just many more JavaScript developers who know how to make UIs.
While they do their best to open source .NET and related SDK tooling, the whole IDE story has been a bit of a mess, rewriting VS4Mac only to kill it shortly after the rewrite reached 1.0, C# DevKit under the same license as VS, the stuff that will never leave Windows or VS to Mac/Linux, Xamarin.Forms rewrite, while not taking GNU/Linux into consideration, and using Catalyst for macOS MAUI backend, dotnet watch drama, some of the ASP.NET features seen as way to sell Azure stuff,....
If you look closer, you will notice that usually the React Native or Electron based applications are from business units that are mostly C++ focused, that never had great love for .NET in first place.
While the team tries to cater to new generations with making .NET development a great experience, even with some constraints in cross-platform deployments, I think they would gain more by fixing the bad perception .NET still gets in UNIX shops, which those upper management decisions don't make any better, regardless of how great .NET happens to be.
That was back in 2019..
They did it well, but not invented?
There very many kinds of influeces: https://www.reddit.com/r/rust/comments/le7m54/is_it_fair_to_...
Especially Cyclone, I think: https://en.m.wikipedia.org/wiki/Cyclone_(programming_languag...
[1] https://pauillac.inria.fr/~fpottier/slides/fpottier-2007-05-...
In many cases Swift would be a better choice than Rust, when the convenience and developer experience is worth trading for some performance. However, Swift's biggest problem is that any usage outside of the Apple ecosystem is a second(or third) class citizen. Until this is solved, Swift will remain mostly an Apple only language, regardless of how nice it is.
I think a time goes on this is more and more only a perception and not a reality. However perception is really important because it’ll mean even if it’s a great language with a great set of tools and libraries for all platforms it will not have a community of developers outside of the Apple ecosystem.
The issue is now - the use of Swift outside of the Apple ecosystem is remarkably stifled, given the lack of Apple support.
A language truly acquires mainstream mindshare and community, only if it can extend its use beyond Apple's OSes.
It’s an open source project that’s more and more detached from Apple itself every release, with lots of contributors making it better and better on Android and windows all the time.
It is possible to get a vscode ide environment working for cross platform development and debugging today, which was not that easy or possible even a few years ago.
There is at least one pretty sizable software project that has shipped a product on Android and windows using swift.
I think swift 6 is going to be great for cross platform uses, but convincing folks to try it still won’t be easy.
You can install Swift on Linux if you want and code away, just like with Rust - but as it hasn't really caught on for anything other than building apps for the Apple ecosystem, it's not a particularly normal thing to do.
However, it can be a bumpy ride. For example: “swift test” (via the CLI) doesn’t work if Xcode is not installed. https://github.com/swiftlang/swift-package-manager/issues/43...
I would just emphasise that the OP was about _convenience_ and Swift-without-Xcode on macOS is not smooth sailing.
Even if you don’t have Homebrew, the first time you try to run `swift` in a Terminal you’ll get a GUI prompt which lets you install the Developer Tools in two clicks.
Or you can run `xcode-select --install` (comes preinstalled, no Xcode needed).
You could also argue that e.g. Rust is not cargo, but almost every Rust programmer will be using cargo. Sure I could use something else, but why would I? Of course the analogy is not perfect, because the "why would I?" is clear for Xcode: it's bad and macOS only :D
All I'm trying to say is: defaults matter. Most people will not be writing Swift in VS Code (on macOS).
Equalling Xcode and Cargo makes no sense. A similar situation to Xcode and Swift would be like Visual Studio and C# on Windows. Many developers use these tools, but they are not the language ecosystem, and the Cargo equivalents are entirely separate (Nuget, Cocoapods).
I'm still really sad Jetbrains decided to sunset AppCode :(
So the quality of tooling/IDEs is definitely a factor, I just don't see myself using Swift outside of the Apple ecosystem when there are so many other alternatives.
struct Task {
future: Mutex<Option<BoxFuture<'static, ()>>>,
task_sender: SyncSender<Arc<Task>>,
} (defrecord Task [future task-sender])
Probably used something like this: (defn create-task []
(let [future (atom nil)
task-sender (async/chan)]
(->Task future task-sender)))
Not sure it makes sense but a pretty much direct translation as far as it goes. (s/def ::future atom?))
(s/def ::task-sender async/chan?)
(s/def ::task
(s/keys :req-un [::future ::task-sender]))
(s/fdef create-task
:ret ::task)
(stest/instrument `create-task)
But then I don't think you'd reach for something like Clojure if static typing is something that you require. (defstruct Task
(future :type Future)
(task-sender :type Sender))
And have SBCL warn me when I try to jam the wrong type in. type Task struct {
Sender chan Task
}
no?Got directed from slog to tokio-tracing for logging, so dove into setting up my basic "--log-level", "--log-format" CLI options I add to every non-trivial program in Rust and ran into...whatever is going on with configuring it (namely, tokio-subscriber seems to encode all the configuration information into types, so you can't just return a Subscriber or Subscriber builder from a function and then add it as a logger...the documentation gives no hint on what trait or type I'm meant to return here).
Since rustc uses tracing I really hope it's the first...
However the type explicitness is, IMO, one of its strengths. It lets you build up types that e.g. in C++ we're not a given, had properties about the behavior buried in docs, etc.
Thus even newbies get to bump into it quite fast.
The chapter that example is from includes the disclaimer:
> In this section, we'll cover the underlying structure of how Futures and asynchronous tasks are scheduled. If you're only interested in learning how to write higher-level code that uses existing Future types and aren't interested in the details of how Future types work, you can skip ahead to the async/await chapter.
And by reading it I mean I actually read the thing front to back before touching a keyboard. Then I started to experiment with some simple code and after a couple of months I had some actually programs that I use often. Obviously I rewrote them after a year but hey ho, it’s all a learning experience.
On first glance you're like wtf is going on, but you can derive backwards what is happening without having to look for a discussion on language behavior under the hood. Automagic is nice sometimes but I'm the kind of person that needs to say the each word of an acronym to process it.
Rust is like that in a way. You have a mutex on an option type, the option has a heap allocated future that contains data that can live for the lifetime of the program.
This is clear to me because I don't need to fill in blanks. My memory is terrible, I've forgotten so many things I've learned in the past. I could pick them back up quite quickly but I don't have the little facts ready to go. If i wanted to use that future, I know I need to check the mutex lock, check if the option contains a Some(), etc.
Sure this isn't for everyone, but I'm glad we have a tool like this gaining popularity. I have little interest in studying the arcane knowledge of C++ and sorting out what is current and what is obsolete, then arguing with a 30 year veteran that their technique is 20 years stale.
I'm gonna make it worse first by substituting the `BoxFuture` type alias with its definition but that makes it easier to explain.
future: Mutex<Option<Pin<Box<dyn Future<Output = ()> + Send + 'static>>>>,
What this means is that the future field stores something that is a:- Dynamically dispatched object (with a V-Table) that implements methods from the Future trait, and returns nothing () = void. (dyn Future<Output = ()>)
- That thing needs to be sendable between Threads (+ Send)
- And must potentially live forever (+ 'static)
- The dynamic dispatch stuff only works with heap allocations because the V-Table pointer is stored in the double wide smart-pointer. In this case we use a Box which means only one thing/thread can hold it at a time (unlike reference counted Rc/Arc).
- That allocation of the Heap allocation must not be moved around in memory. (Pin<...>)
- that thing might not be there (it can be Null), but replacing/taking/putting the thing will be secured by the mutex so that's a nice guarantee for multithreading (Option<...>)
- access to a thing is synchronised via a mutex (Mutex<...>)
So while it looks horrible at first sight, it is simply telling you a lot about the guarantees of that particular type.
You can do the exact same thing in Rust:
impl Coin {
fn valueInCents(&self) -> u8 {
match self {
Self::Penny => 1,
Self::Nickel => 5,
Self::Dime => 10,
Self::Quarter => 25,
}
}
}I don't think Swift has union types, but Rust does and so does C++ and in both languages the unions, just like their main product type (C++ class, Rust struct) can have methods defined on them. The enumeration "types" in the C-like languages are special and it's because they really aren't proper types at all, they're just integers wearing a funny hat.
One of Rust's most important post 1.0 types MaybeUninit<T> is actually a union, lots of Rust's important types, in 1.0 and since, are enums.
Their notional status as "strict types" means nothing in a language which doesn't really care anyway, that's why memory_order::relaxed < cv_status::timeout is true
In Rust if you write nonsense like that it doesn't compile, these aren't comparable things. In C++ they're just integers, so of course you can see that relaxed (the integer zero) is less than timeout (the integer one) ...
They are just integers wearing funny hats.
No it isn't. https://godbolt.org/z/bz7EhMhMT
You also can't add them, or subtract them.
Yes behind the scenes their integers, but almost certainly this is the case in Rust too. You can SOMETIMES coerce an enum class instance back to an int if you do unsafe casts (on purpose).
However whether you can add or subtract them is harder to guess than you seem to have assumed
https://godbolt.org/z/3vdazcr7E
Of course the bit pattern representation is the same in Rust. The point isn't the representation or we'd be talking about machine code. The point is the ergonomics.
These types aren't (shouldn't be) integers, but in C and C++ they are anyway.
This isn't true. Copy-on-write semantics are implemented only for arrays, dictionaries, and strings. Swift value-types are copied immediately.
https://docs.swift.org/swift-book/documentation/the-swift-pr...
The documentation has oversimplified a bunch of the mental model and it causes incorrect understanding.
Swift’s documentation is quite bad TBH. The biggest thing that still needs to be improved a lot.
If you are developing with Swift but not using a Mac at all, I would love to hear how your experience has been.
My only beef is that the binaries are quite large. I’m hoping the new Foundation will improve things, in the interim I’m trying to eliminate my Foundation dependencies.
Funnily enough, I'm using C# to solve quite a similar type of tasks. It's a really pleasant experience.
Not counting having to learn about IL trimming, and the whole AOT compatible libraries.
It's a very straightforward process - you pass a single flag, maybe specify optimization preference and instruction set target, and it gives you the binary upon 'dotnet publish'ing the project.
The process of static linking (if you care about this scenario), with other static dependencies written in C/C++/Rust is not too different from other toolchains - you specify `DirectPInvoke` and `NativeLibrary` properties in .csproj, and they are linked into the final product as a part of the build process. You may need to forward linker arguments[0] for the imports referenced by those however, but this is expected regardless of .NET.
I think it's fair to criticize the additional compatibility effort required for high-level user libraries that rely on un-analyzable reflection, reflection emit or assembly loading, but none of these features usually have any relevance in the domain of systems programming.
When you write a project from scratch, you never have to think about whether it's native compilation or "JIT+CIL assemblies sandwich" executable, or anything else. It just works.
For example https://github.com/codr7/sharpl - the author was pretty much learning C# on the go and it needed exactly 0 changes besides adding `PublishAot` property to make it output a native executable.
[0]: https://github.com/U8String/U8String/blob/main/Examples/Inte...
However this is somehow cumbersome versus doing a plain "aot-lang-compiler source -o binary".
Which is the experience I think the team should strive on as goal, especially for newcomers.
More so when comparing IDE experience of something like Delphi or Swift to keep it in context (press build), versus Visual Studio (besides csproj, create publish properties file, followed by a solution publish, which is hardly documented still).
But...it works in this exact way?
dotnet publish -o folder -p:PublishAot=true
Does not need -p argument either, as noted in the previous comment, if `PublishAot` is specified in .csproj, like with any AOT template (e.g. dotnet new console --aot or dotnet new webapiaot).On Visual Studio, I have started to recommend to newcomers to avoid its publishing UI which is convoluted and is easy to get side-tracked with. CLI offers much cleaner UX. Not that it matters outside of Windows. But you don't need to create publish file either, just tick the boxes you care about in the modal window.
Edit: you can't be serious, no one in their sane mind would consider a single extra keyword to get the final product once you're done writing code a learning curve too steep, unless you're in a bad mood and want to make a bad faith argument. Consider what you make the comparison against - C and C++ with their CMake, Ninja, or even raw MSBuild, Swift is barely better too. Rust with Cargo is about the same amount of effort as .NET CLI. Cmon, Pjmlp.
CLI only isn't the answer, if you want better adoption.
This is an older mechanism that works differently to NativeAOT, it's also used by host-installed runtime to improve startup latency, and you can do your own R2R, either full or granular, to further improve this: https://learn.microsoft.com/en-us/dotnet/core/deploying/read... It can slow down the build quite a bit from what I've seen.
NativeAOT is different - when you use it, ILLink (which previously was the Mono Linker and now has evolved) trims all unreachable and links together all of the remaining CIL bytecode and metadata from the CIL assemblies that the application/library consists of. After that, ILC (IL AOT Compiler) compiles everything (and performs AOT-specific optimizations) into a single static bundle emitted as COFF or PE (or Mach-O?) file containing machine code. Then the toolchain invokes a host-provided linker which produces the final native executable or library, much like it happens with C, C++ or Rust. It even has the OS-specific symbol format, so you can feed it to the same tools that work with native code.
Technically speaking, .NET's compiler still retains the name "RyuJIT", but in practice it's not JIT-specific and ILC drives the same back-end as JIT compilation at runtime, but with different optimizations and options.
This is a new feature that was introduced in .NET 7, and has substantially improved further in 8 and upcoming 9. Completely different output aside, NAOT builds take less time than the ones that have R2R from my experience.
Your mileage may vary depending on how big the application is, how complex linking it requires and how much the native code to compile there is.
The main difference with Rust is that compilation time whether it's good or bad has much less relevance because for development you can use JIT (both debug and release, i.e. plain 'dotnet run') or even hot-reload which can recompile actively running code without restarting the application with 'dotnet watch' which works surprisingly well.
We get to enjoy both worlds, and picking the best deployment options as per scenario.
Rust development experience would be much better if they adopted a similar approach, which by the way, is common on other ML inspired languages.
I work on small-ish embedded Linux systems and I've been looking for an alternative to C for a long time. Rust does not fit the bill, both because I don't enjoy the ergonomics of it (but I could get over that were it not for the other issues) and because the binaries are huge, so unless you go for a busybox-style multicall binary (and even then) you'll be wasting a lot of space. Both the standard size reduction techniques and splitting out things into shared objects (the ABI instability is not an issue on an embedded system which always gets compiled as a whole anyway) don't really move the needle compared to what you can achieve using plain C.
I've been meaning to give Swift a shot for applications like this. From what I've seen I had assumed that it would have less of a tendency to monomorphize everything (like Rust and C++ like to do when you use them idiomatically), leading to less binary size bloat.
You also mention binary size, but in relation to a library - is it that the absolute cost of including the library in the image is too high or is there a per-binary effect here as well?
Could you maybe share some of your experiences with Swift on embedded Linux in the context of that project? What is working well, what are the warts? What kind of distribution (like buildroot, Yocto, ...) are you using?
Sorry for the ton of questions, it's just something that has been on my bucket list for quite a while and it very exciting to hear that somebody is already doing this.
Zig and Swift do it and I feel like it makes things harder to read, not easier.
`.variant` vs `Type::Variant`
IIRC the syntax is optional (you can include the type name) but it seems obvious that in any sufficiently long or complex code, not having the type name close would be annoying, especially if you didn’t have IDE like capabilities in your editor.
That `.variant` syntax is annoying as hell to dig through when you don't have an IDE to rely on for jumping around. Rust (or your choice of other more explicit language) is just generally way more clear about what's being used.
The way I usually settle on describing it is that this feature solved for people writing code, but code is read more than it's written, and thus I don't believe this tradeoff was worthwhile.
Do you have a link for an RFC or GitHub issue or anything? Would be curious to see.
Example in the playground: https://play.rust-lang.org/?version=stable&mode=debug&editio...
{
use MyEnum::*;
match my_enum {…}
}However outside very specific use cases, where no kind of automatic resource management is allowed, no matter what, like in high integrity computing, or critical kernel code, approaches that mix and match both solutions are much more ergonomic.
Swift isn't the only one going down this route, we see it as well in D, Chapel, Linear Haskell, OCamml Effects, Mojo, Hylo, Koka, Veronica, and probably many other research languages being born during the last 5 years.
Because in the end, programming language ergonomics and expressiveness really matters for wide scale adoption.
Yeah, maybe chicken nuggets only appear more popular than broccoli. But I think since we don't have any actual evidence to say otherwise we can assume that's because they are in fact more popular than broccoli.
I would happily have something 50-100% slower than C++/Rust with good interop, alas there seems to be very little / nothing. Cython for various reasons isn't ideal.
And despite Swift for Tensorflow being killed, there’s still decent python interop.
Now, setting up a project for that is probably quite hard because the documentation is non-existent.
Using an browser set to autodirect to https by default, all I get is some huge warning because the host use a certificate for svbtle, the service used to host this blog.
I know some people think that because some blogpost is public it can be served without ssl, but I think it is still nice to leave the choice of the reader to disclose or not to his provider and everyone in between what he is reading or not without seeing horrible warnings.
this works:
enum TreeNode<T> {
Leaf(T),
Branch(Vec<TreeNode<T>>),
}
https://play.rust-lang.org/?version=stable&mode=debug&editio...otherwise if it was just tuple of `TreeNode` there would be E0072 https://doc.rust-lang.org/stable/error_codes/E0072.html
Not quite right:
It sounds kind of intriguing but I know very little about the language.
Rust is currently the defactor choice if you want to make something with js/python bindings, of id you want to speed up a dynamic bottleneck because it makes it super easy.
Swift doesn't even have a good ffi.
(Though I wouldn't fault anyone for making an argument that Swift is still more ergonomic here)
What's holding swift is not having a good story to call a swift compiled extension from Python/JS/Ruby/PHP.
Python's community is huge, very active, Python is everywhere, and it needs compiled extensions. And tooling to make them BFF, like pyo3 and maturin, is great
And that's how we got cryptography, pydantic core, polar... Which motives even tooling to be written in rust as a side effect, and how we have uv and ruff.
The scripting community is the most active, if you got them on your side, you get access to a huge pool of devs.
Thankfully, Facebook and Microsoft are making it happen.
Maybe spend some time reading about other JIT languages expose SIMD to developers, without requiring folks to go down to C.
Which incidentally is what all those Python DSLs for GPGPU APIs also do, with the caveat of not being that useful for general purpose programming outside machine learning algorithms.
> call a swift compiled extension from Python/JS/Ruby/PHP
The story there is excellent.
Here's a Swift function callable from C
@cdecl("myfunc")
public static func f(){...}
C/C++ types are mostly handled automatically (including sharing memory management), but it can be tricky.To build a library to load and call from any language:
swiftc mine.swift -emit-library -o libmine.dylib
E.g., calling from java 22 FFI:https://foojay.io/today/java-panama-polyglot-swift-part-2/
But most people instead use a slower json server network interface because (sadly) json is the lingua franca for scripting and Swift Codable makes it trivial to map the json to a swift type.
(Yes, there's arguments that untyped throws are better for library code because it leaves you more room to add more errors as you become aware of the need for them without breaking the contract with your users, but really, client code is going to be written to the errors you're throwing anyway (i.e. Hyrum's Law). I much prefer my errors to be typed so that if an error is added, the library version has to be bumped to indicate the new incompatibility with old code. And for my own, non-library code, typed errors make sure I'm handling all the error cases.)
So no, Swift is not a "more convenient Rust". It is not Rust at all. It is more of a variant of C#/Java. And I haven't seen any indication that it is an improvement over either. The only reason to use Swift is having to deal with Apple.
Article may have overlooked this since it's been available in recent 5.x and becomes default in 6:
Swift 6 brings complete concurrency enabled by default
By far the biggest change is that complete concurrency checking is enabled by default. Unless you're very fortunate indeed, there's a very good chance your code will need some adjustment – it's no surprise the Swift team made it optional in earlier versions to give folks time to evaluate what's changing.
Swift 6 improves concurrency checking further, and the Swift team say it "removes many false-positive data-race warnings" that were present in 5.10. It also introduces several targeted changes that will do wonders to make concurrency easier to adopt – if you tried with 5.10 and found things just too gnarly to figure out, hopefully some of the changes in Swift 6 will help.
Easily the biggest is SE-0414, which defines isolation regions that allow the compiler to conclusively prove different parts of your code can run concurrently.
https://www.hackingwithswift.com/articles/269/whats-new-in-s...
How does it work with immutable objects?
I've been using Swift a long time and don't quite get what this is referring to.
Also:
> Swift too gives you complete type-safety without a garbage collector.
Type-safety and memory-safety are two entirely different things; this is an odd sentence.
Latest Swift release (June 11th) added ownership system.
Swift is better for some UI and some servers/operating systems. A considerable problem with Swift's adoption is that zero attention has been given to ergonomics on other platforms which has left it as the Xserve of programming languages for quite a while now.
If the Foundation framework and SwiftUI paid serious attention to non-Apple platforms, then Swift itself might have a chance. Until then, even C# is going to look more attractive for cross-platform devs that don't want to drown their software in ifdefs.
Swift is a language with a lot of potential, but it has a lot of work ahead of it, the work that was done for .NET's performance, cross-platform support, tooling ecosystem and OSS-focused development process.
*both of which have SwiftUI style extensions for declarative UI definition and MVU patterns. Avalonia even has the F# version of that.
Swift's adoption outside of Apple developers forced into using it is more or less sabotaged by Apple's decisions. Whether they realize it or not, this behavior is undermining Swift's credibility and likely pushing people onto C++/Rust where they can use LLVM without limitations.
They started late, but isn’t https://github.com/apple/swift-foundation paying serious attention to non-Apple platforms now?
SwiftUI, I think, isn’t as essential, as it isn’t really possible to create a good cross-platform Ui library (and yes, that means I think running iOS apps on MacOS can’t provide an optimal experience)
The worst part is, Swift is a perfectly fine language on paper. It's got Lattner behind it, it's got literal trillion-dollar corporation funding behind it, and interfaces with LLVM to boot. It's lovely! My problem with Swift has always been Apple's attitude towards their own language and how desperate they are to reject non-ecosystem users. My life is too short to put up with nonworking language primitives in 2024. It pains me to say it, since Swift is so cool, but there are several better languages.
With the recent C# focus given to WinUI and WinAppSDK, despite C++/WinRT underpinnings, maybe caused by failure to deliver VS tooling for C++/WinRT after almost a decade, or ongoing cybersecurity considerations, maybe WinDev attitute will change, although I see them more eager to adopt Rust than C#, across the Microsoftshpere blogs and technical notes.