#308 Aug 9, 2026

308. sin_cos — One Angle, Both Halves, No Swap Bug

Every rotation, polar conversion, and circle you draw needs sin and cos of the same angle — and every codebase has one spot where x got the sin and y got the cos.

Two separate calls means two chances to put the results in the wrong slot, and the compiler can’t help — both are f64. sin_cos returns the pair as a tuple, so the destructure names them once and the math below reads like math:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
let theta = 30.0_f64.to_radians(); // bite 307
let r = 2.0;

// two calls, two chances to swap them
let x = r * theta.cos();
let y = r * theta.sin();

// one call, one destructure
let (sin, cos) = theta.sin_cos();
let (x, y) = (r * cos, r * sin);
assert!((y - 1.0).abs() < 1e-15); // r·sin 30° = 1

One thing to burn in: the tuple is (sin, cos) — sin first, matching the method name, even though (x, y) order would put cos first. Destructure as let (s, c) = … and the names keep you honest.

The classic customer is a 2D rotation, where the same sin and cos appear four times:

1
2
3
4
5
6
7
fn rotate((x, y): (f64, f64), theta: f64) -> (f64, f64) {
    let (s, c) = theta.sin_cos();
    (x * c - y * s, x * s + y * c)
}

let (x, y) = rotate((1.0, 0.0), 90.0_f64.to_radians());
assert!(x.abs() < 1e-15 && (y - 1.0).abs() < 1e-15);

Without sin_cos that function either calls sin and cos twice each, or you write the two temporaries by hand — which is exactly where the swap sneaks in.

Don’t reach for it as a speed hack: std is free to implement it as the two calls you’d have written, and often does. The win is intent — one angle in, both halves out, and bite 300’s atan2 will happily turn the pair back into the angle:

1
2
let (s, c) = 1.0_f64.sin_cos();
assert!((s.atan2(c) - 1.0).abs() < 1e-15);

If a sin and a cos of the same angle sit within three lines of each other, they want to be one sin_cos.

← Previous 307. to_degrees / to_radians — Stop Hand-Typing 180.0 / PI Next → 309. classify — x > 0.0 Passed, Then 1.0 / x Returned inf