The purpose of `ExpressibleByNilLiteral` as a public API

Background

From API docs:

ExpressibleByNilLiteral

A type that can be initialized using the nil literal, nil.

nil has a specific meaning in Swift—the absence of a value. Only the Optional type conforms to ExpressibleByNilLiteral. ExpressibleByNilLiteral conformance for types that use nil for other purposes is discouraged.

Facts

  • The description of this API doc does not discourage the use of ExpressibleByNilLiteral for the purpose of using nil to represent the absence of a value.
  • ExpressibleByNilLiteral is a public API without any underscore to discourage the use.
  • Pointer types used to be nil-convertible, and SE-0055 changed this and had discussions about removing ExpressibleByNilLiteral completely.
  • There are only two types in the standard library that conform to ExpressibleByNilLiteral: Optional and ImplicitlyUnwrappedOptional.

Problems

The Python interop library made PythonObject expressible by integer literal, float literal, boolean literal, string literal and array literal. This enables the following:

let x: PythonObject = true
// Python: x = [True]
let y: PythonObject = [true, false, 1, 2.1, []]
// Python: y = [True, False, 1, 2.1, []]

True and False are Python constant objects. PythonObject is made expressible by boolean literal because it is nice syntactic sugar, and True and true are semantically the same.

There's another common constant object: None. According to Python's official documentation, None represents the absence of a value. This is exactly the same as the meaning of nil in Swift, according to the Swift documentation. So, is it reasonable to make PythonObject conform to ExpressibleByNilLiteral? If so, this would enable the following:

let array: PythonObject = [1, nil, true, 2.1]
// Python: array = [1, None, True, 2.1]

However, this may introduce confusion, for example:

var dict: [String : PythonObject] = ...
dict["x"] = nil

In this case, it is possible that while the user is referring to Python's None, they are actually removing a value from the dictionary.

Some would argue that nil literal conversion should only be used for Optional, ImplicitlyUnwrappedOptional and _OptionalNilComparisonType, as this was also mentioned in SE-0055.

If so, why is ExpressibleByNilLiteral still a public API and should we consider removing it or prepending an underscore to it? If not, when does conforming to ExpressibleByNilLiteral make sense?

This purpose of this post is to kick off a discussion about the purpose of ExpressibleByNilLiteral as a public API.

4 Likes

I don't think the confusion in the example is really related to ExpressibleByNilLiteral, because people get similarly confused already when the dictionary Value is Optional, e.g.

var dict: [String: Int?] = [:]
dict["a"] = 1
dict["a"] = nil

I agree that conforming PythonObject to ExpressibleByNilLiteral certainly doesn't help, and I'm not sure it would be a good idea in practice, but I think the fundamental issue in the example is that assigning nil/.none etc. removes a value from a Dictionary. As I understand it, this only really exists because the subscript getter and setter are constrained to be the same type for various reasons, and I don't think this functionality would otherwise exist.

I agree. Dictionary's subscript setter is to blame here.

.none removes the entry.

dict["a"] = .none

I am not sure why it works this way. What if I wanted it to set an objects value to .none/nil like @rxwei mentioned.

You “simply” assign .some(nil) or .some(.none) to it.

2 Likes

Agreed, subscript requiring the same type for get and set, or subscript not allowing set-without-get is the source of confusion. It's a separate problem to discuss...

Back to the nil literal case, it seems PythonObject is one of few good cases to consider nil literal conversion for, because the purpose here is to map Swift literals to semantically equivalent constructs in a DSL / another language, like true -> True, false -> False and "foo" -> 'foo' in python interop.

If conforming PythonObject to ExpressibleByNilLiteral makes it worse and causes confusion with optionals (potentially in cases other than dictionary subscript), then I can't think of a case that doesn't. So the question becomes: should we remove ExpressibleByNilLiteral?

1 Like

I'm not sure if ExpressibleByNilLiteral should be deprecated.

But is it possible to add a warning for ambiguous nil literals?

SR-2176 is an open bug for ambiguous .some and .none lookups.

2 Likes

_OptionalNilComparisonType also conforms to ExpressibleByNilLiteral in the standard library, and it exists for an important reason.

-Chris

3 Likes

I understand ExpressibleByNilLiteral is necessary for standard library API implementations.

If uses of it are discouraged for all cases outside the standard library, then it is reasonable to make ExpressibleByNilLiteral underscored.

If not, like today's API description where only uses for purposes other than representing the absence of value are discouraged, then it is worth figuring out some cases outside the standard library where a nil literal conversion is suitable. As a result, the documentation can be improved to convey what's recommended and what's not.

1 Like

People use it for JSON-like use cases as well. Whether or not that's a good use I won't offer an opinion on.

indirect enum JSON {
  case string(String)
  case number(Double)
  case boolean(Bool)
  case array([JSON])
  case object([String: JSON])
  case null
}
extension JSON: NilLiteralConvertible { … }
2 Likes

I use it in a GraphQL library I've been working on. Optional Input Types in GraphQL can be a value, null, or ignored / not sent / undefined. This library is not open source yet so the rest of the code is not available, but this should be enough to demonstrate.

public enum OptionalInputValue<T>: ExpressibleByNilLiteral {
    case val(T), null, ignored
    public init(nilLiteral: ()) { self = .null }
}

However, now that I've seen this

nil has a specific meaning in Swift—the absence of a value.

I'm not sure if .null or .ignored should be the case represented by nil. Is nil the value that represents the the absence of a value or is it truly the absence of a value?

This seems to fall in line with the confusion presented by the dictionary example plus this will likely turn up again if Swift adds bindings to JavaScript which has null and undefined. Maybe the documentation needs clarification on the meaning of nil in such trinary cases?

1 Like

@dabrahams @Ben_Cohen I'm curious about your thoughts on this.