bonus:: translating_between_languages:: Script
Hello, I’m Chris Krycho and this is New Rustacean: a show about the Rust Programming Language and the people who use it. This is a bonus episode on translating between programming languages.
First up, a new sponsorship! I’m really excited to be partnering with Manning Books to bring you a discount on some video content they’ve put together. Even better because that video content is by Carol Nichols (whom I interviewed a couple years ago) and her husband Jake Goulding! I’ve been saying for years that we need more video content: it’s a huge way people learn. Manning’s Rust in Motion is one of the first major video courses for Rust I know of. You can get it for 40% off at deals.manning.com/new-rustacean – there’s a link in the show notes. That link actually gives you 40% off of anything at Manning, including their book Rust in Action, which is in early access preview. Thanks so much to Manning for sponsoring the show and building some great video content with Carol and Jake!
But. Let me tell you. When you start trying to translate things from TypeScript to Rust, stuff gets really weird, really fast.
One of the things that initially feels very similar between the two, for example, is the type system. And so far as it goes, that feeling is not totally wrong. Both languages give you generic types; both give you interface-like ways of expressing constraints and let you bound generics with those interface-like structures—literally
interface in TypeScript and of course
trait in Rust.
There are important differences in both the surface syntax and the underlying semantics, especially in the languages’ type systems, though, of course. The most obvious difference at the surface is that TypeScript has a structural type system, where it only cares about the shape of the objects you pass, where Rust has a nominal type system: two structs with the same fields are still different structs, because they have different names. What’s more, TypeScript has anonymous, bare union types but doesn’t have built-in tagged unions like Rust’s enum types. So in TypeScript you can say something is a string or a number, without any wrapping name; but you have to write a ton of boilerplate to get the equivalent of Rust’s
enum types. And vice versa: in Rust you get tagged unions for free, but you can never have a bare union type: you always have to do the boilerplate of wrapping those kinds of things in an enum. So there are real differences at this level!
But while I thought those would be the thing that would end up leading to the biggest mismatch, it turns out that accommodating for those isn’t that hard. You have to write some enum types out more explicitly, and you can throw away some of the workarounds and boilerplate that come from not having Rust’s
enum tagged union types in TypeScript, but it’s not that bad, because the semantics are relatively similar, even with those differences in surface syntax.
No, what gets really weird is the combination of TypeScript’s
interfaces not being isometric with Rust’s
traits, and ownership. In retrospect, the latter should have been obvious, but the
trait distinction was a lot less obvious.
Let’s start with the obvious one, and then we’ll use it as a way of circling back around to the
trait thing. TypeScript’s approach to interfaces and classes is (mostly) like interfaces and classes in most object oriented languages you’d be familiar with: both interfaces and classes can include both data and functions. Rust… doesn’t do that. At all. As we’ve talked about in recent teaching episodes, Rust makes a clean separation between data and behavior.
enum types have data.
trait types have behavior. And never the twain shall meet.
Layered on top of that is the ownership system: TypeScript is perfectly happy to let you have lots of shared mutable references to the same data, recursive types are be fairly idiomatic, and cycles between data structures are no big deal (though they can cause memory leaks if you’re not careful). All of this is perfectly normal for a garbage-collected language! Meanwhile, Rust has as its most basic distinctive the every piece of data has a single owner rule. Recursive types are not all that idiomatic, and have to be explicitly wrapped in some kind of pointer type; and shared mutable references to the same data are right out. Cyclical data relationships in Rust are just a quick way to pain: you can make them work, sometimes, but it’s hard and never natural.
So if you go to try to translate a TypeScript
interface straight into a Rust
trait, or a TypeScript
class straight into a Rust
struct, things get weird. This is because of the real differences between the two languages is in their base semantics—the deep structure and most important constraints of the languages. These aren’t things that are apparent from surface syntax, since the surface syntax of the two is pretty similar!
Now, as an aside, this doesn’t mean syntax doesn’t matter. People often step from “syntax doesn’t map perfectly to semantics” to “syntax is irrelevant,” but the second doesn’t follow from the first. Syntax can map more or less cleanly to the underlying semantics, and it can lead—or mislead—by the way it’s shaped. So what we ought to say is that while syntax matters, it’s far from the only thing that matters.
What I have ended up finding as I’ve worked on this translation project is that sometimes an
interface in TypeScript translates to a
trait in Rust… but sometimes it should be a
struct or an
enum! It depends on how it’s used and what the mechanics in play are. Likewise, things like an abstract base class in TypeScript don’t have any corresponding notion in Rust, so you have to dig a bit on what the intent is and then spend a while thinking about how you might express that intent in Rust.
Weak, but if you then need to handle, say, partial equality with the
PartialEq trait, you need to define local newtypes to avoid the orphan rule, and so on.
And that is a good reminder of why I think being a polyglot programmer is really good. Different languages, even the ones which are superficially similar, help us think in different ways.
I’m sure I’ll keep learning as I keep pushing forward on this, but I thought it would make for an interesting little aside here—thanks for listening, and I hope you enjoyed it!
Thanks to everyone who sponsors the show! This month’s $10-or-more sponsors included:
- Behnam Esfahbod
- Ryan Osial
- John Rudnick
- James Hagans II
- Peter Tillemans
- Alexander Payne
- Michael Mc Donnell
- Dan Abrams
- Joseph Marhee
- Bryan Stitt
- Rob Tsuk
- Daniel Mason
- Oluseyi Sonaiya
- Adam Green
- Nathan Sculli
- Graham Wihlidal
- Scott Moeller
- Joar Wandborg
- Nick Stevens
- Raph Levien
- Daniel Collin
- Chris Palmer
- Anthony Deschamps
- Nicolas Pochet
- Bryce Johnston
- Paul Naranja
- Ramon Buckland
- Matt Rudder
- Martin Heuschober
- Jonathan Knapp
- Aaron Turon
If you’d like to sponsor the show, you set up ongoing support at patreon.com/newrustacean, send a one-off at any of a number of other services listed at newrustacean.com, or get in touch directly. The website also has scripts and code samples for most of the teaching episodes as well as transcripts for many of the interviews, along with full show notes for every episode. You can find the notes for _this_ episode at <newrustacean.com/show_notes/bonus/functional_programming>.
If you're enjoying New Rustacean, please help others find it – by telling them about it in person, by sharing it on social media, or by rating and reviewing the show in your favorite podcast directory.
The show is on Twitter @newrustacean, or you can follow me there @chriskrycho. Tweet me with news, topic ideas, etc! You can also respond in the threads on the Rust user forums, Reddit, or Hacker News, or—and this will always be my favorite—just send me an email at [email protected]
Until next time, happy coding!
Auto Trait Implementations
impl RefUnwindSafe for Script
impl UnwindSafe for Script
type Error = Infallible
The type returned in the event of a conversion error.
The type returned in the event of a conversion error.