[swift-evolution] Allow FloatLiteralType in FloatLiteralConvertible to be aliased to String

Rainer Brockerhoff rainer at brockerhoff.net
Mon May 9 12:56:38 CDT 2016


On 5/9/16 14:01, Joe Groff via swift-evolution wrote:
> 
>> On May 8, 2016, at 10:30 PM, Morten Bek Ditlevsen <bek at termestrup.dk> wrote:
>>
>> This would be an excellent solution to the issue.
>> Do you know if there are any existing plans for something like the DecimalLiteralConvertible?
> 
> Not that I know of. Someone would have to submit a proposal.
> 
>>
>> Another thought:
>> Would it make sense to have the compiler warn about float literal precision issues?
>> Initialization of two different variables with the exact same literal value could yield different precision results if one had a FloatLiteralType aliased to Float80 and the other aliased to Float.
> 
> That's definitely a possibility. We already have machinery in place to raise errors when integer literals overflow Int* types, and we could do something similar for float literals that have excessive precision.

For compilation, it would probably be overkill to show even a warning
for non-representable numbers like 0.1 assigned to a binary
floating-point type, but perhaps such a warning might be acceptable in a
playground?


>> On Fri, May 6, 2016 at 6:46 PM Joe Groff <jgroff at apple.com> wrote:
>>
>>> On May 6, 2016, at 9:42 AM, Stephen Canon <scanon at apple.com> wrote:
>>>
>>>
>>>> On May 6, 2016, at 12:41 PM, Joe Groff via swift-evolution <swift-evolution at swift.org> wrote:
>>>>
>>>>>
>>>>> On May 6, 2016, at 2:24 AM, Morten Bek Ditlevsen via swift-evolution <swift-evolution at swift.org> wrote:
>>>>>
>>>>> Currently, in order to conform to FloatLiteralConvertible you need to implement
>>>>> an initializer accepting a floatLiteral of the typealias: FloatLiteralType.
>>>>> However, this typealias can only be Double, Float, Float80 and other built-in
>>>>> floating point types (to be honest, I do not know the exact limitation since I have
>>>>> not been able to read find this in the documentation).
>>>>>
>>>>> These floating point types have precision limitations that are not necessarily
>>>>> present in the type that you are making FloatLiteralConvertible.
>>>>>
>>>>> Let’s imagine a CurrencyAmount type that uses an NSDecimalNumber as the
>>>>> representation of the value:
>>>>>
>>>>>
>>>>> public struct CurrencyAmount {
>>>>> public let value: NSDecimalNumber
>>>>> // .. other important currency-related stuff ..
>>>>> }
>>>>>
>>>>> extension CurrencyAmount: FloatLiteralConvertible {
>>>>> public typealias FloatLiteralType = Double
>>>>>
>>>>> public init(floatLiteral amount: FloatLiteralType) {
>>>>>   print(amount.debugDescription)
>>>>>   value = NSDecimalNumber(double: amount)
>>>>> }
>>>>> }
>>>>>
>>>>> let a: CurrencyAmount = 99.99
>>>>>
>>>>>
>>>>> The printed value inside the initializer is 99.989999999999995 - so the value
>>>>> has lost precision already in the intermediary Double representation.
>>>>>
>>>>> I know that there is also an issue with the NSDecimalNumber double initializer,
>>>>> but this is not the issue that we are seeing here.
>>>>>
>>>>>
>>>>> One suggestion for a solution to this issue would be to allow the
>>>>> FloatLiteralType to be aliased to a String.  In this case the compiler should
>>>>> parse the float literal token: 99.99 to a String and use that as input for the
>>>>> FloatLiteralConvertible initializer.
>>>>>
>>>>> This would mean that arbitrary literal precisions are allowed for
>>>>> FloatLiteralConvertibles that implement their own parsing of a String value.
>>>>>
>>>>> For instance, if the CurrencyAmount used a FloatLiteralType aliased to String we
>>>>> would have:
>>>>>
>>>>> extension CurrencyAmount: FloatLiteralConvertible {
>>>>> public typealias FloatLiteralType = String
>>>>>
>>>>> public init(floatLiteral amount: FloatLiteralType) {
>>>>>   value = NSDecimalNumber(string: amount)
>>>>> }
>>>>> }
>>>>>
>>>>> and the precision would be the same as creating an NSDecimalNumber from a
>>>>> String:
>>>>>
>>>>> let a: CurrencyAmount = 1.00000000000000000000000000000000001
>>>>>
>>>>> print(a.value.debugDescription)
>>>>>
>>>>> Would give: 1.00000000000000000000000000000000001
>>>>>
>>>>>
>>>>> How does that sound? Is it completely irrational to allow the use of Strings as
>>>>> the intermediary representation of float literals?
>>>>> I think that it makes good sense, since it allows for arbitrary precision.
>>>>>
>>>>> Please let me know what you think.
>>>>
>>>> Like Dmitri said, a String is not a particularly efficient intermediate representation. For common machine numeric types, we want it to be straightforward for the compiler to constant-fold literals down to constants in the resulting binary. For floating-point literals, I think we could achieve this by changing the protocol to "deconstruct" the literal value into integer significand and exponent, something like this:
>>>>
>>>> // A type that can be initialized from a decimal literal such as
>>>> // `1.1` or `2.3e5`.
>>>> protocol DecimalLiteralConvertible {
>>>>  // The integer type used to represent the significand and exponent of the value.
>>>>  typealias Component: IntegerLiteralConvertible
>>>>
>>>>  // Construct a value equal to `decimalSignificand * 10**decimalExponent`.
>>>>  init(decimalSignificand: Component, decimalExponent: Component)
>>>> }
>>>>
>>>> // A type that can be initialized from a hexadecimal floating point
>>>> // literal, such as `0x1.8p-2`.
>>>> protocol HexFloatLiteralConvertible {
>>>>  // The integer type used to represent the significand and exponent of the value.
>>>>  typealias Component: IntegerLiteralConvertible
>>>>
>>>>  // Construct a value equal to `hexadecimalSignificand * 2**binaryExponent`.
>>>>  init(hexadecimalSignificand: Component, binaryExponent: Component)
>>>> }
>>>>
>>>> Literals would desugar to constructor calls as follows:
>>>>
>>>> 1.0 // T(decimalSignificand: 1, decimalExponent: 0)
>>>> 0.123 // T(decimalSignificand: 123, decimalExponent: -3)
>>>> 1.23e-2 // same
>>>>
>>>> 0x1.8p-2 // T(hexadecimalSignificand: 0x18, binaryExponent: -6)
>>>
>>> This seems like a very good approach to me.
>>
>> It occurs to me that "sign" probably needs to be an independent parameter, to be able to accurately capture literal -0 and 0:
>>
>> // A type that can be initialized from a decimal literal such as
>> // `1.1` or `-2.3e5`.
>> protocol DecimalLiteralConvertible {
>>  // The integer type used to represent the significand and exponent of the value.
>>  typealias Component: IntegerLiteralConvertible
>>
>>  // Construct a value equal to `decimalSignificand * 10**decimalExponent * (isNegative ? -1 : 1)`.
>>  init(decimalSignificand: Component, decimalExponent: Component, isNegative: Bool)
>> }
>>
>> // A type that can be initialized from a hexadecimal floating point
>> // literal, such as `0x1.8p-2`.
>> protocol HexFloatLiteralConvertible {
>>  // The integer type used to represent the significand and exponent of the value.
>>  typealias Component: IntegerLiteralConvertible
>>
>>  // Construct a value equal to `hexadecimalSignificand * 2**binaryExponent * (isNegative ? -1 : 1)`.
>>  init(hexadecimalSignificand: Component, binaryExponent: Component, isNegative: Bool)
>> }
>>
>> -Joe
> 
> _______________________________________________
> swift-evolution mailing list
> swift-evolution at swift.org
> https://lists.swift.org/mailman/listinfo/swift-evolution
> 


-- 
Rainer Brockerhoff  <rainer at brockerhoff.net>
Belo Horizonte, Brazil
"In the affairs of others even fools are wise
In their own business even sages err."
http://brockerhoff.net/blog/



More information about the swift-evolution mailing list