All About Swift’s LosslessStringConvertible

Losslessly Convert Types to and from Strings

Dennis Vennink
7 min readJun 5, 2018

One of Swift’s lesser known, but powerful, protocols is LosslessStringConvertible. A type that conforms to it can be converted to a String, and vice versa, in a lossless, unambiguous way. Its most practical applications lie in data serialisation and parsing; think of (de)serialising custom data formats and parsing command line arguments. It was proposed in SE-0089 and was implemented in Swift 3.

I’ll cover its implementation, walk you through all conforming types from the Standard Library, show you how you can create your own custom, conforming type and end with a conclusion.

I’ll be using the Swift REPL so you can follow along.

Implementation

In order for a type to conform, it needs to implement two members; an initialiser and a property.

The first requirement is init?(_ description:):

public protocol LosslessStringConvertible: CustomStringConvertible {
init? (_ description: String)
}

It knows exactly how to create a value of a conforming type from a given String. If description can’t be converted to a value, then nil is returned.

Some conforming types from the Standard Library implement this requirement slightly differently; some might not be failable and the initialisation parameter might have another name. But what they all have in common though is a single labelless, initialisation parameter of type String.

The second requirement is description, by way of inheriting the CustomStringConvertible protocol:

public protocol CustomStringConvertible {
var description: String { get }
}

description must create a textual value-preserving representation of the original value. A type that already conforms to CustomStringConvertible might not necessarily be suitable; since you can’t override its description property, you’ll have to work within the confines it gives you.

Conforming Types From The Standard Library

As of Swift 4.1, the protocols from the Standard Library that inherit LosslessStringConvertible are FixedWidthInteger and StringProtocol. The types that conform to the former are: Int, Int8, Int16, Int32, Int64, UInt, UInt8, UInt16, UInt32 and UInt64. The types that conform to the latter are String and Substring. Other conforming types from the Standard Library are: Bool, Character, Double, Float, Float80 and Unicode.Scalar.

Bool

Bool implements the first requirement as init?(_ description:). If description is any value other than "true" or "false", then the result is nil. Also note that it’s case-sensitive:

$ swift
Welcome to Apple Swift version 4.1 (swiftlang-902.0.34 clang-902.0.30). Type :help for assistance.
1> Bool("true")
$R0: Bool? = true
2> Bool("True")
$R1: Bool? = nil

The second requirement, description, turns true into "true" and false into "false":

  3> true.description
$R2: String = "true"

Character

Character implements the first requirement as init(_ s:). This is also the designated initialiser as there is no Character literal. s must be a String containing a single extended grapheme cluster:

  4> Character("🦄")
$R3: Character = {
_representation = smallUTF16 {
smallUTF16 = 3716470846
}
}
5> Character("ab")
Fatal error: Can't form a Character from a String containing more than one extended grapheme cluster

The second requirement, description, returns a single-character String:

  6> Character("🦄").description
$R4: String = "🦄"

Double, Float And Float80

Things become more interesting with Double, Float and Float80. These all implement the first requirement as init?(_ text:). text only accepts (hexa)decimal floating-point representations without any underscores and special representations for infinity and NaN.

The following examples will use Double, but Float and Float80 are identical.

Let’s check out which decimal floating-point representations are valid:

  7> Double("42")
$R5: Double? = 42
8> Double("42.0")
$R6: Double? = 42
9> Double("42e0")
$R7: Double? = 42
10> Double("042")
$R8: Double? = 42
11> Double("4_2")
$R9: Double? = nil

Hexadecimal floating-point representations behave identical:

 12> Double("0x2a")
$R10: Double? = 42
13> Double("0x2a.0")
$R11: Double? = 42
14> Double("0x2ap0")
$R12: Double? = 42
15> Double("0x02a")
$R13: Double? = 42
16> Double("0x2_a")
$R14: Double? = nil

In both cases, text supports leading zeros, but not underscores. Binary and octal representations are the only numeric representations that are not accepted:

 17> Double("0b101010")
$R15: Double? = nil
18> Double("0o52")
$R16: Double? = nil

Let’s check out the special representations for infinity and NaN:

 19> Double("-inf")
$R17: Double? = -Inf
20> Double("Inf")
$R18: Double? = +Inf
21> Double("+INFINITY")
$R19: Double? = +Inf
22> Double("-nan")
$R20: Double? = NaN
23> Double("NaN")
$R21: Double? = NaN
24> Double("+NAN")
$R22: Double? = NaN

text accepts "inf" and "infinity" for infinity and "nan" for NaN. Both are case-insensitive and can be prefixed with - or +. NaN may also be suffixed with a payload in parentheses; it should consist of a sequence of decimal digits or the characters 0X or 0x followed by a sequence of hexadecimal digits:

 25> Double("nan(42)")
$R23: Double? = NaN
26> Double("nan(0x2a)")
$R24: Double? = NaN

The second requirement, description, returns a decimal floating-point representation as a String, regardless of the input:

 27> 1.0.description
$R25: String = "1.0"
28> Float(1.0).description
$R26: String = "1.0"
29> Float80(1.0).description
$R27: String = "1.0"

Int And UInt

Int, Int8, Int16, Int32, Int64, UInt, UInt8, UInt16, UInt32 and UInt64 all implement the first requirement as init?(_ description:). description only accepts decimal representations. These may be prefixed with a + or -, followed by one or more of 0-9. description must be representable in the specified type, e.g., for Int, "9223372036854775807" is the maximum representable integer; when description is out of bounds nil is returned:

 30> Int("+42")
$R28: Int? = 42
31> Int("9223372036854775808")
$R29: Int? = nil

Note that any other representation is invalid and results in nil:

 32> Int("0_042")
$R30: Int? = nil
33> Int("0b101010")
$R31: Int? = nil
34> Int("0o52")
$R32: Int? = nil
35> Int("0x2a")
$R33: Int? = nil
36> Int("0.42e2")
$R34: Int? = nil
37> Int("0x2ap0")
$R35: Int? = nil

The second requirement, description, returns a decimal representation:

 38> 42.description
$R36: String = "42"

String

String is the most straight forward out of all types; init(_ characters:) and description both return the argument:

 39> String("forty-two")
$R37: String = "forty-two"
40> "forty-two".description
$R38: String = "forty-two"

Substring

Substring is similar to String; the first requirement is implemented as init(_ content:), where content can be any String:

 41> Substring("forty-two")
$R39: Substring = {
_slice = {
_startIndex = {
_compoundOffset = 0
_cache = utf16
}
_endIndex = {
_compoundOffset = 36
_cache = utf16
}
_base = "forty-two"
}
}

The second requirement, description, creates a new String, removing the reference to the original String:

 42> Substring("forty-two").description
$R40: String = "forty-two"

Unicode.Scalar

Unicode.Scalar implements the first requirement as init?(_ description:), where description should be a String containing a single character representing exactly one Unicode scalar value. It returns nil if description is invalid:

 43> Unicode.Scalar("🦄")
$R41: Unicode.Scalar? = U'🦄'
44> Unicode.Scalar("\u{1F984}")
$R42: Unicode.Scalar? = U'🦄'
45> Unicode.Scalar("ab")
$R43: Unicode.Scalar? = nil

The second requirement, description, creates a single-character String:

 46> Unicode.Scalar("🦄")!.description
$R44: String = "🦄"

Creating A Custom, Conforming Type

Let’s create a custom type. A two-dimensional Point type should give a good overview of what’s going on:

 47> struct Point {
48. let x: Double
49. let y: Double
50. }

The next step is to turn the type into a textual value-preserving representation. We can use any, or a combination, of the following methods. First, we can use positioning to preserve values, e.g., given 1.0 for x and 2.0 for y, the simplest representation would look something like "1.0 2.0". Second, we can make it more readable by adding punctuation, e.g., "1.0, 2.0". Third, we can group the values by adding parentheses or other bracket delimiters, e.g., "(1.0 2.0)". Fourth, we can label the values, e.g., "x 1.0 y 2.0". This also enables us to put the values in any order, e.g., "y 2.0 x 1.0". Fifth, we can add type information, e.g., "Point 1.0 2.0".

For our purposes we’ll combine all of these methods into a representation that is identical to the type’s initialiser: "Point(x: 1.0, y: 2.0)".

Now we’re ready to make the type conform to LosslessStringConvertible.

The first requirement, init?(_ description:), uses a regular expression to extract the x and y properties from description. Then, we’ll call Double.init?(_ text:) to convert these properties. If all goes well, we’ll call the designated initialiser init(x:y:), otherwise, nil is returned:

 51> extension Point: LosslessStringConvertible {
52. public init? (_ description: String) {
53. var regularExpression: NSRegularExpression
54.
55. do {
56. regularExpression = try NSRegularExpression(pattern: "^Point\\(x:\\s*(.+),\\s*y:\\s*(.+)\\)$", options: [])
57. } catch {
58. preconditionFailure("\(error)")
59. }
60.
61. guard let match = regularExpression.matches(in: description, options: [], range: NSRange(location: 0, length: description.count)).first, match.numberOfRanges == 3, let x = Double((description as NSString).substring(with: match.range(at: 1))), let y = Double((description as NSString).substring(with: match.range(at: 2))) else {
62. return nil
63. }
64.
65. self.init(x: x, y: y)
66. }
67.

The second requirement, description, returns the representation by using String interpolation and calling the properties’ description properties:

 68.   public var description: String {
69. return "Point(x: \(self.x.description), y: \(self.y.description))"
70. }
71. }

We can now see that our custom, conforming type is working as intended by creating a textual value-preserving representation, converting it to a Point, converting it back into a representation and testing the original and converted representations for equality:

 72> "Point(x: 1.0, y: 2.0)"
$R45: String = "Point(x: 1.0, y: 2.0)"
73> Point($R45)
$R46: Point? = {
x = 1
y = 2
}
74> $R46!.description
$R47: String = "Point(x: 1.0, y: 2.0)"
75> $R45 == $R47
$R48: Bool = true

Conclusion

Make sure to take extra precaution when converting Strings to conforming numeric types from the Standard Library; not all numeric literals are accepted and underscores are not supported.

Also, when conforming your own types to LosslessStringConvertible, extra care needs to be put into documenting init?(_ description:), especially when description is far removed from the intuitive representation of the value.

Finally, as an aside, it’s worth mentioning that the Foundation framework is full of non-conforming types that would be ideal candidates for LosslessStringConvertible, such as Date, Data and URL. Someday.

--

--