A simple functional implementation
OK, I said in the title that your first FizzBuzz implementation might not workââthen again, it might. You might write it like this which follows, for example, a way that works fine.
These programs will both produce the desired output and theyâre evidently very similar visually. The main thing to note about the Rust version is that println!()
needs as its first argument a string literal, a formatting string; in Python itâd be equivalent to print('{}'.format(i))
.
But how about if we decided that we wanted only one print call in the code base? Hereâs how it might go:
Figure 2: storing the result in a variable, so as to use only one print statement.
if
block as an expression; the whole result
variable assignment is actually unnecessary, we could just put the if
block straight where itâs subsequently used. Ruby programmers may recognise this as expression orientation. As a Python programmer when I came to Rust, I thought it just a gimmick to let you omit return
; but I rapidly discovered that itâs a marvellous paradigm. Rust totally ruined Python for me.
Rust (wonât compile)
for i in 1..101 {
let result = if i % 15 == 0 {
"FizzBuzz"
} else if i % 5 == 0 {
"Buzz"
} else if i % 3 == 0 {
"Fizz"
} else {
i
};
println!("{}", result);
}
Python
for i in range(1, 101):
if i % 15 == 0:
result = 'FizzBuzz'
elif i % 5 == 0:
result = 'Buzz'
elif i % 3 == 0:
result = 'Fizz'
else:
result = i
print(result)
This Rust code may seem all very well, but itâs actually not, owing to its strict typing rules. For what is the type of the variable result
? The first three branches of the ifâexpression produce strings and the fourth an integer:
Figure 2a: compiler output for Figure 2.
{integer}
identifies a partiallyâresolved type and is used for integer literals, when the compiler hasnât yet determined what actual type it will be. An integer literal can be of type u8
, i8
, u16
, i16
, u32
, i32
, u64
, i64
, u128
, i128
, usize
or isize
. If the compiler doesnât get any hints as to what it should be, itâll end up choosing i32
.
error[E0308]: if and else have incompatible types
--> <anon>:10:9
|
7 | } else if i % 3 == 0 {
| ____________-
8 | | "Fizz"
| | ------ expected because of this
9 | | } else {
10 | | i
| | ^ expected `&str`, found integer
11 | | };
| |_____- `if` and `else` have incompatible types
error: aborting due to previous error
This clearly wonât do. How about we turn it into a string first?
Figure 3: adding .to_string()
to the code from Figure 2.
for i in 1..101 {
let result = if i % 15 == 0 {
"FizzBuzz"
} else if i % 5 == 0 {
"Buzz"
} else if i % 3 == 0 {
"Fizz"
} else {
i.to_string()
};
println!("{}", result);
}
This is where we stray from the familiar parts of computer science that most people are familiar with to a part that far fewer people will relate to. Because this doesnât work.
Figure 3a: compiler output for Figure 3.
Note that from Rust 1.58 this produces a spurious second error related to integer type inference; Iâve reported this, hopefully itâll be resolved soon.
error[E0308]: if and else have incompatible types
--> <anon>:10:9
|
7 | } else if i % 3 == 0 {
| ____________-
8 | | "Fizz"
| | ------ expected because of this
9 | | } else {
10 | | i.to_string()
| | ^^^^^^^^^^^^^ expected `&str`, found struct `String`
11 | | };
| |_____- `if` and `else` have incompatible types
error: aborting due to previous error
âWhat?â I hear you saying; âarenât they both strings? Whatâs the deal with this &str (how do I even pronounce it? [I canât even decide whether to spell its indefinite article âaâ or âanâ. Sometimes I say âan ampersand strâ, sometimes âa strâ, sometimes âa str referenceâ or âa str sliceâ (inverted order). &'static str
I call âa static strâ. &'a str
I mostly say something like âa str slice, lifetime aâ.]) and String?â Now indeed I need to be more specific than I was in the previous analysis of the types of the branches: the first three branches didnât just produce âstringsâ, they each produced a &str
; the fourth branch produced a generic integer, which might be of a number of types (though in the absence of any further information it would become a i32
, the default type of integer). Languages like Python, Ruby and JavaScript unify their integer types (actually, JavaScript unifies its number types altogether), while languages like C⯠and Java and Go have a variety of integral types of different sizes, so there being multiple integer types is familiar to many. But even CâŻ, Java and Go each have just one type of string.
Rust doesnât. It has two.
Two types of strings? What is this?
We could look at a simple explanation and continue on our way, but weâve come so far down this pathway that we might as well go the rest of the wayââunderstanding specifically whatâs going on is definitely worthwhile. Why can C⯠and Java and Go get away with one String type and Rust canât? To answer this, we have to step down to the level of memory management.
CâŻ, Java and Go are all managed languages (also known as garbage collected languages). That is, they have a runtime which takes care of the allocation and freeing of memory at the appropriate times; when nothing is using the string any more, it can be freed. They can thus return references to a string and not need to worry about liveness issues: parts of a string that are in use will not be freed.
There is a tradeâoff here for them; as a general rule, such languages have immutable stringsââif you concatenate two strings, it involves a completely new allocation. (This means that a solution that modifies a string by adding âFizzâ if itâs divisible by three and then âBuzzâ if itâs divisible by five and then the number if the string is still empty may well involve two allocations for multiples of fifteen, though something called string interning which is commonly employed may reduce or fix that, depending on how itâs done and how clever any optimiser is.) This is, I presume, because the alternatives are worse: copying for any slicing will lead to a very significant increase in memory usage for most programs; and allowing mutation of one string to potentially affect others derived from it is terrible for correctness, and it could lead to nasty data race conditions in what is for practical purposes a primitive type, and it could also, for anything using UTFâ16 or UTFâ8, lead to illegal code points in a subslice (substring); sure, most of these problems arise in other places, but having it in their string types would be much worse. (I said that they only have one string type, but that also is typically not quite trueââthere are enough cases that need more efficient mutable strings that they tend to have such a type. For example, Java and .NET have things called StringBuilder
.)
Rustâs model is somewhat different, not being a garbage collected language, being centred around its language-wide model of ownership, where each object is owned in one place at a time, though other places may safely borrow references to it.
String
is an owned type. That is, it has exclusive ownership of the contents of the string; and when it passes out of scope, the memory for the contents of the string will be freed immediately. For this reason, any substring canât be of the type String
, for there will be no connection between the two, and so when one passed out of scope, the other would become invalid, leading to a loss of memory safety. And so instead it is that slices (substrings) use a type which is a reference to the contents that something else ownsââ&str
. Rust, through its concept of lifetimes, is able to guarantee that no slice outlives the actual String
, and so memory safety is ensured.
Lifetimes have syntax, 'like_this
, with one special lifetime 'static
that indicates the life of the program. But when thereâs only one possible value or an obvious way things should be hooked up, you can normally skip writing the lifetime; this is called lifetime elision. Weâll encounter lifetimes a bit later in this article, but so far theyâve all been elided. Just to prepare you: a string slice of lifetime a is spelled &'a str
, and string literals (being stored in the binary) are &'static str
.
If youâre interested in learning more about lifetimes at this point, the Rust Book has a chapter on them.
Back to the fizzing and buzzing
So then, the problem is that one of them is an owned string while the other three are string slices (references to statically defined strings). How shall we resolve it? How about we try making them all string slices (that is, of type &str
):
for i in 1..101 {
let result = if i % 15 == 0 {
"FizzBuzz"
} else if i % 5 == 0 {
"Buzz"
} else if i % 3 == 0 {
"Fizz"
} else {
&*i.to_string()
};
println!("{}", result);
}
Figure 4: adding &*
to the code from Figure 3 to convert the String
to &str
.
String
dereferences to str
ââviz., it implements Deref<Target = str>
ââso &*string
produces a string slice, type &str
, pointing to the contents of the String
.
That seems like a good idea, right? Sorry, that wonât do:
Figure 4a: compiler output for Figure 4.
error[E0716]: temporary value dropped while borrowed
--> <anon>:10:11
|
7 | } else if i % 3 == 0 {
| ____________-
8 | | "Fizz"
9 | | } else {
10 | | &*i.to_string()
| | ^^^^^^^^^^^^^ creates a temporary which is freed while still in use
11 | | };
| | -
| | |
| |_____temporary value is freed at the end of this statement
| borrow later used here
|
= note: consider using a `let` binding to create a longer lived value
error: aborting due to previous error
Here weâre running into a lifetime issue; the String
created by i.to_string()
is not being stored anywhere, and so it is freed at the end of the else
block. Thus the reference to it canât escape that block, like we were trying to make happen. This is a memory safety bug that the Rust compiler caught; in some languages it would wind up as a dangling pointer, which is a Very Bad Thing.
Now in this case, the compiler gave us a suggestion: âconsider using a `let` binding to create a longer lived valueâ. Not all of the compilerâs suggestions will be workable, but I think this one always is if it makes it. We can indeed lift the String
variable to outside the else
block; the compiler was only caring that the object being referenced needed to be valid for the body of the for
loop. Hereâs how that looks:
for i in 1..101 {
let x;
let result = if i % 15 == 0 {
"FizzBuzz"
} else if i % 5 == 0 {
"Buzz"
} else if i % 3 == 0 {
"Fizz"
} else {
x = i.to_string();
&*x
};
println!("{}", result);
}
Figure 5: taking a reference as in Figure 4, but storing the owned string in the outer scope: this works.
This way, the String
lives until the end of the forâloop, which is longer than result
needs to be valid for, and so result
is allowed to refer to it.
How about we make it a String
?
Another direction that we could go is to make all of the branches return owned strings:
for i in 1..101 {
let result = if i % 15 == 0 {
"FizzBuzz".to_string()
} else if i % 5 == 0 {
"Buzz".to_string()
} else if i % 3 == 0 {
"Fizz".to_string()
} else {
i.to_string()
};
println!("{}", result);
}
Figure 6: String
all the way: this works, at a runtime cost.
This will work fine, but it has meant that a new chunk of memory is allocated for all values of i
, rather than just for the ones which arenât factors of three or five.
Making it a function
Weâve gone as far as we can in this direction without things becoming absurd; how about if we alter the parameters of the problem so that weâre not printing the result, but rather returning it from a function?
Hereâs what we might start with:
Figure 7: the fizz_buzz
function, with String
s.
Rust
fn fizz_buzz(i: i32) -> String {
if i % 15 == 0 {
"FizzBuzz".to_string()
} else if i % 5 == 0 {
"Buzz".to_string()
} else if i % 3 == 0 {
"Fizz".to_string()
} else {
i.to_string()
}
}
for i in 1..101 {
println!("{}", fizz_buzz(i));
}
Python
def fizz_buzz(i):
if i % 15 == 0:
return 'FizzBuzz'
elif i % 5 == 0:
return 'Buzz'
elif i % 3 == 0:
return 'Fizz'
else:
return i
â
for i in range(1, 101):
print(fizz_buzz(i))
We now have an extra layer of encapsulation around it which demonstrates clearly that the solution when we were producing a &str
of hoisting the String
variable declaration x
to the loop wonât work, because that variable would be going out of scope also, being contained inside the function. (Try it if youâre not sure; the return type is unrepresentable in Rustâs type system, as there is no suitable lifetimeââx
wonât live for 'static
(the duration of the process) and thereâs nothing else to tie it to.)
And so, simply because weâve put it in a function, we have a little inefficiencyââweâre allocating new strings for multiples of three or five when it shouldnât be necessary.
Enter Cow<str>
Fortunately, Rust has algebraic data types (known as enums in the language), and thereâs a convenient type defined in the standard library as something that is either a string slice or an owned string.
Here are the relevant definitions (without any of the methods that make it more useful):
pub enum Cow<'a, B: ?Sized + 'a> where B: ToOwned {
Borrowed(&'a B),
Owned(<B as ToOwned>::Owned)
}
impl ToOwned for str {
type Owned = String;
fn to_owned(&self) -> String {
âŚ
}
}
Figure 8: the std::borrow::Cow
definition and how Cow<'a, str>
comes from that.
The ToOwned
implementation is defined in library/alloc/src/str.rs
in the Rust repository, if youâre interested; but it doesnât really matter.
And lo! Lifetime syntax makes an appearance.
Iâll admit that to be somewhat opaque to the inexperienced, so letâs fill in the generics to get an idea of approximately what it ends up for real life:
pub enum Cow<'a, str> {
Borrowed(&'a str),
Owned(String),
}
Figure 9: an approximation of Cow<str>
.
(This is for demonstration purposes only; the grammar is not correct.)
Now the particular type that we wish to pay attention to is Cow<'static, str>
, which is either a &'static str
or a String
, and which dereferences to str
[Cow<B>
implements Deref<Target = B>
; put another way, given a Cow<str>
x
, &*x
produces a &str
and you can mostly just treat a Cow<'a, str>
as a &'a str
.]). Why is this type so interesting for us? Because it is completely owned data, containing no nonâ'static
lifetimes. In other words, if we create one of these inside a function, it is entirely selfâcontained, not having any references to things inside the function and we can return it without any trouble. Expressed even more sloppily: this is a type that can contain a string literal, or an owned string that you have constructed.
For assistance in creating these objects, there are implementations of From
for Cow<str>
; thanks to these, if you have a &str
or a String
, you can write Cow::from(x)
or, where it can be inferred that the result must be Cow<str>
, x.into()
, instead of having to write the specific enum variant, Cow::Borrowed(x)
or Cow::Owned(x)
.
As mentioned, Cow<str>
dereferences to str
, allowing us to very often treat it as though it were a &str
. A prime example of this is the manner in which we can use it directly in string formatting, using that typeâs std::fmt::Display
implementation with the {}
format string (fmt::Display
is the direct parallel of __str__()
in Pythonâs string formatting, or to_s()
, toString()
, &c. in various other languages, though it works directly with a writer allowing you to skip the writing to an intermediate string if you want; the to_string()
method on any type implementing fmt::Display
uses this mechanism to produce a String
).
Hereâs what it looks like:
use std::borrow::Cow;
fn fizz_buzz(i: i32) -> Cow<'static, str> {
if i % 15 == 0 {
"FizzBuzz".into()
} else if i % 5 == 0 {
"Buzz".into()
} else if i % 3 == 0 {
"Fizz".into()
} else {
i.to_string().into()
}
}
for i in 1..101 {
println!("{}", fizz_buzz(i));
}
Figure 10: a FizzBuzz function returning Cow<'static, str>
: this works.
Great! Weâve reduced the work the computer needs to do and made our admittedly trivial case faster.
But can we go further?
Writing our own enum and implementing std::fmt::Display
efficiently
Of course, what weâre representing for each term in the sequence isnât actually âa static string slice or an owned stringâ, itâs ââFizzâ, âBuzzâ, âFizzBuzzâ or a numberâ. We just converted all the choices to strings eagerly; we could just as easily make it lazy, avoiding the extra allocation where possible (and in fact they are all avoidable).
Letâs make an enum of our own, which weâll call Term
.
While weâre at it, weâll implement std::fmt::Display
, which means that we can print to stdout without even needing to create the intermediate String
. Weâll also implement std::fmt::Debug
, which answers to the format string {:?}
and is for giving a developer-oriented representation of the object, akin to Pythonâs __repr__()
and Rubyâs inspect
; it can be just the same thing in this case.
Figure 11: using a dedicated data type to represent the real possibilities efficiently.
The suggested Python code is thoroughly contrived, as is going to be the case in all languages without algebraic data types; I provide it for the sake of users not familiar with algebraic data types to at least give an impression of how it works.
Rust
use std::fmt;
enum Term {
Fizz,
Buzz,
FizzBuzz,
Number(i32),
}
impl fmt::Display for Term {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match *self {
Term::Fizz => f.write_str("Fizz"),
Term::Buzz => f.write_str("Buzz"),
Term::FizzBuzz => f.write_str("FizzBuzz"),
Term::Number(num) => write!(f, "{}", num),
}
}
}
impl fmt::Debug for Term {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
fmt::Display::fmt(self, f)
}
}
fn fizz_buzz(i: i32) -> Term {
if i % 15 == 0 {
Term::FizzBuzz
} else if i % 5 == 0 {
Term::Buzz
} else if i % 3 == 0 {
Term::Fizz
} else {
Term::Number(i)
}
}
for i in 1..101 {
println!("{}", fizz_buzz(i));
}
Python analogue (thoroughly contrived)
class Term:
def __init__(self, value):
self._value = value
def __str__(self):
if self is Term.Fizz:
return "Fizz"
elif self is Term.Buzz:
return "Buzz"
elif self is Term.FizzBuzz:
return "FizzBuzz"
else:
return str(self._value)
def __repr__(self):
return str(self)
@staticmethod
def Number(number):
return Term(number)
# Pretend that these are opaque
Term.Fizz = Term(object())
Term.Buzz = Term(object())
Term.FizzBuzz = Term(object())
def fizz_buzz(i):
if i % 15 == 0:
return Term.FizzBuzz
elif i % 5 == 0:
return Term.Buzz
elif i % 3 == 0:
return Term.Fizz
else:
return Term.Number(i)
for i in range(1, 101):
print(fizz_buzz(i))
Observe also that this is doing a really good job of actually representing the data contained, though for this trivial case it wouldnât matter much if it didnât. We could just as easily have replaced the first three variants of that enum with a single Word(&'static str)
variant, using Word("FizzBuzz")
et al. for the values. (In fact, that was actually the first version I wrote of this step of the processââsee, even I was fixed on using strings where itâs simply unnecessary!)
We could go further, showing how to write a dedicated iterator, but with how Rustâs iterator chaining works, this isnât really necessaryââyou can just write (1..101).map(fizz_buzz)
. That gives a lot more flexibility; as soon as you have something implementing Iterator<Item = i32>
, you can just tack .map(fizz_buzz)
onto the end of it and you have a type implementing Iterator<Item = Term>
.
That loop could be rewritten in this style, incidentally:
Figure 12: mapping an integer iterator to the fizz_buzz
function.
Rust
for f in (1..101).map(fizz_buzz) {
println!("{}", f);
}
Python
for f in map(fizz_buzz, range(1, 101)):
print(f)
Whichever of these sorts of ways we choose to do it, we end up with the good olâ FizzBuzz lines:
1
2
Fizz
4
Buzz
Fizz
7
8
Fizz
Buzz
11
Fizz
13
14
FizzBuzz
16
17
Fizz
19
Buzz
Fizz
22
23
Fizz
Buzz
26
Fizz
28
29
FizzBuzz
31
32
Fizz
34
Buzz
Fizz
37
38
Fizz
Buzz
41
Fizz
43
44
FizzBuzz
46
47
Fizz
49
Buzz
Fizz
52
53
Fizz
Buzz
56
Fizz
58
59
FizzBuzz
61
62
Fizz
64
Buzz
Fizz
67
68
Fizz
Buzz
71
Fizz
73
74
FizzBuzz
76
77
Fizz
79
Buzz
Fizz
82
83
Fizz
Buzz
86
Fizz
88
89
FizzBuzz
91
92
Fizz
94
Buzz
Fizz
97
98
Fizz
Buzz
Conclusion
So now you have it: why your first FizzBuzz implementation in Rust might not work. Some of the difficult parts of it are typical of statically typed languages, and some more of them are more specific to Rust. (As a matter of fact, the situation would be similar in C++, but C++ lets you do all sorts of silly things and doesnât give much in the way of memory safety. Donât bother arguing with me on that point though, Iâm just going off the word of othersââI donât know C++ especially well.)
Weâve covered how Rustâs ownership model can prevent you from writing things in certain ways that you might be used to, and why (though not the concrete benefits of doing this); also how Rust is able to express more efficient concepts; weâve seen how enums (algebraic data types) can be used to model data more precisely and efficiently.
Hopefully youâve seen the power of this and it interests you.
Is this an increased conceptual burden? Yes.
Is it a nuisance? Occasionally. (My experience is that it saves trouble at least as often as it causes it.)
Does it allow you to improve your programâs efficiency? Certainly, and with complete confidence. While in the past these things have required a loss of certainty about safety and correctness properties, in Rust you donât need to make it a tradeoff.
Does it make code easier to reason about? In simple cases like this you wonât see that, but these general attributes of the ownership model and algebraic data types really do help in more complex cases. (I really miss these things when working in Python.)
These matters end up both a good thing and a bad thing about Rust; sometimes you will love them, and occasionally you will hate them. But I at least donât hate them often at all.
Should you use Rust? Well, I would suggest that you at least try it if you havenât. You may well find it not ready or fit for your purposes. Because of its focus on systems development, it is more cumbersome for many higherâlevel tasks, though even on those it can be surprisingly effective.
Finally, if youâre not familiar with Rust or got lost at any point, I suggest you go through the official documentation; the Rust book has sections dealing with lifetimes and ownership, enums and a whole lot more. I also wrote Rust ownership the hard way, an article that you may find helpful in understanding this crucial defining feature of Rust. If you still have questions, places like #rust-beginners on irc.mozilla.org are great. (Pretty much everyone agrees that the Rust community is superb. You will get good answers there.)
Appendix: other FizzBuzz techniques
This section is by no means comprehensive, but includes a few extras that have been pointed out over time.
1. match
instead of if
People often suggest replacing the chained conditionals with a match
block, which incidentally lets you skip the âsuperfluousâ i % 15
:
for i in 1..101 {
match (i % 3, i % 5) {
(0, 0) => println!("FizzBuzz"),
(_, 0) => println!("Buzz"),
(0, _) => println!("Fizz"),
(_, _) => println!("{}", i),
}
}
Figure 13: Figure 1, but with match
instead of chained if
.
Application of this technique to other examples is left as an exercise for the reader.
Opinions differ on which is nicer. In this particular case I personally prefer the if
version, but if you introduced a third word Iâd switch to the match
version.
2. std::fmt::Display
trait objects
This possibility was brought to my attention in September 2019, sitting somewhere between the code in Figure 2 and the code in Figure 11, by using a dynamic trait object:
for i in 1..101 {
let result: &dyn std::fmt::Display = if i % 15 == 0 {
&"FizzBuzz"
} else if i % 5 == 0 {
&"Buzz"
} else if i % 3 == 0 {
&"Fizz"
} else {
&i
};
println!("{}", result);
}
Figure 14: making Figure 2 work by the use of std::fmt::Display
trait objects.
Application of this technique to other examples is again left as an exercise for the reader.
This is a worthwhile technique to be aware of: if there exists a trait that covers the required functionality shared between the types you wish to store in the one variable, a dynamic trait object can be a viable option instead of making your own enum. If you wanted to be able to inspect the values to determine what they semantically were, this would be insufficient, and something like the Term
enum would be called for, but so long as all you want to do is format them for display, a std::fmt::Display
trait object is fine.
3. And if you really like fiddling with FizzBuzz optimisationâŚ
Feel free. Hereâs the final conclusion of that lot, updated to compile now, plus making OUT
a byte string for improved legibility (!?):
#![no_std]
#![feature(lang_items, start, rustc_private)]
extern crate libc;
static OUT: [u8; 413] = *b"\
1\n2\nFizz\n4\nBuzz\nFizz\n7\n8\nFizz\nBuzz\n11\nFizz\n13\n14\nFizzBuzz\n\
16\n17\nFizz\n19\nBuzz\nFizz\n22\n23\nFizz\nBuzz\n26\nFizz\n28\n29\nFizzBuzz\n\
31\n32\nFizz\n34\nBuzz\nFizz\n37\n38\nFizz\nBuzz\n41\nFizz\n43\n44\nFizzBuzz\n\
46\n47\nFizz\n49\nBuzz\nFizz\n52\n53\nFizz\nBuzz\n56\nFizz\n58\n59\nFizzBuzz\n\
61\n62\nFizz\n64\nBuzz\nFizz\n67\n68\nFizz\nBuzz\n71\nFizz\n73\n74\nFizzBuzz\n\
76\n77\nFizz\n79\nBuzz\nFizz\n82\n83\nFizz\nBuzz\n86\nFizz\n88\n89\nFizzBuzz\n\
91\n92\nFizz\n94\nBuzz\nFizz\n97\n98\nFizz\nBuzz\n";
#[start]
fn start(_argc: isize, _argv: *const *const u8) -> isize {
unsafe {
core::arch::asm!(
"syscall",
in("rax") 1, // syscall number = write
in("rdi") 1, // fd = stdout
in("rsi") OUT.as_ptr(),
in("rdx") OUT.len(),
out("rcx") _,
out("r11") _,
lateout("rax") _,
);
}
0
}
#[lang = "eh_personality"] extern fn eh_personality() {}
#[panic_handler] extern fn panic_handler(_: &core::panic::PanicInfo) -> ! { loop {} }
Figure 15: optimising the FizzBuzz problem roughly as far as humanly possible.
Applying this technique should be left unexercised by the reader: itâs provided for demonstration purposes only, and should not be attempted at home or at work.
(This code depends on Linux syscall conventions, so it wonât work on other platforms.)