Dear Erica: Why this Swift gotcha?

Hey There!  First of all I wanted to start by saying I read a lot of your stuff and am very appreciative of the time you take in writing down your thoughts, books and answering user emails.

I wanted to share with you a very interesting gotcha I stumbled upon today while working with Float currencies. Apparently when a Float is wrapped in an Optional, it loses its precision.  Here’s a screenshot to show what I’m talking about 🙂

3880CE58-D45C-4B56-89E6-BD091383024A

This isn’t a huge deal since unwrapping solves the problem, But I thought if someone would like an odd swift-WAT moment like this, It’d be you.  What do you think? Ever stumbled upon something of sorts?

Many thanks, Shai

I don’t believe this is a gotcha. I think what you’re seeing is that Optionals use different code to render their output descriptions than Floats. I created a workaround, submitted a bug report, and will explain here what’s actually going on:

To test whether an amount is “losing precision”, I started with your code:

let amount: Float? = 3.8
print(amount)
print(amount!)

And then I declared a non-optional amount:

let nonoptAmount: Float = 3.8

Floats are 4 bytes long. You can test this for the moment using sizeof(Float.self). (I have a proposal in that changes this call.) Your hypothesis states that unwrapping the value loses precision. So I performed an unsafe bitcast on both the unwrapped and the unwrapped amounts to UInt32, which is also 4 bytes long.

public func --><T, U>(value: T, target: U.Type) -> U? {
    guard sizeof(T.self) == sizeof(U.self) else { return nil }
    return unsafeBitCast(value, to: target)
}

let optbits = (amount! --> UInt32.self)!
let nonoptbits = (nonoptAmount --> UInt32.self)!

If the unwrapping has truly lost precision the two UInt32 values will have different bit representations, but they don’t:

print(String(optbits, radix: 2))
print(String(nonoptbits, radix: 2))

Screen Shot 2016-07-13 at 9.39.34 AM

Then I pushed the bits from the UInt32 back to a float and wrapped them in an optional, to make sure the results were the same. They were:

Screen Shot 2016-07-13 at 9.41.00 AM

What’s actually going wrong is that Optional does not conform to CustomStringConvertible, so when it prints it’s not leveraging the wrapped value’s preferred output style — assuming the wrapped value itself conforms to CustomStringConvertible.

So I went ahead and implemented an extension to let Optional take advantage of this protocol. Doing so, immediately changed the results of your print(amount)/print(amount!) output:

Screen Shot 2016-07-13 at 10.02.56 AM

Bug Report SR-2062

Update: Via Joe Groff at Swift JIRA:

This behavior is intentional. Optional does conform to CustomDebugStringConvertible.  We wanted to make it clear you’re printing a wrapped Optional so that in playgrounds or printf-debugging scenarios, it’s clear the value is wrapped.

8 Comments

  • This is very clever stuff, a great article as always!

  • Awesome to see why this was happening. Just to remind people, there is nothing “magical” about optionals except perhaps the syntactic sugar around them. An optional is just a generic enum with a .None and .Some case:

    public enum Optional : _Reflectable, NilLiteralConvertible {
    case None
    case Some(Wrapped)

    }

    Simple as that!

  • Thanks for the write up ! 🙂

  • FYI you should not use `Float` for currencies you should use the type `Decimal`

    let amount: Decimal? = Decimal(3.8)

    print(amount) // “Optional(3.8)\n”
    print(amount!) // “3.8\n”

  • Very nice. Why not `terminator:”)”`? It would avoid one copy, right?