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 🙂
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))
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:
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:
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!
Not really, optionals are covariant, regular generics aren’t. There’s special code to handle them all the way down to the type checker (http://stackoverflow.com/q/37352122/2305521)
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”
I’m pretty sure Decimal is a Swift 3 type.
Thinking you meant NSDecimalNumber, for now
Very nice. Why not `terminator:”)”`? It would avoid one copy, right?