19

I am trying to create a generic function that can take an optional argument. Here's what I have so far:

func somethingGeneric<T>(input: T?) {
    if (input != nil) {
        print(input!);
    }
}

somethingGeneric("Hello, World!") // Hello, World!
somethingGeneric(nil) // Errors!

It works with a String as shown, but not with nil. Using it with nil gives the following two errors:

error: cannot invoke 'somethingGeneric' with an argument list of type '(_?)'
note: expected an argument list of type '(T?)'

What am I doing wrong and how should I correctly declare/use this function? Also, I want to keep the usage of the function as simple as possible (I don't want do something like nil as String?).

Coder-256
  • 5,212
  • 2
  • 23
  • 51

7 Answers7

13

I guess the compiler can't figure out what T is just from nil.

The following works just fine though for example:

somethingGeneric(Optional<String>.None)

Mike Pollard
  • 10,195
  • 2
  • 37
  • 46
9

I believe you've overcomplicated the problem by requiring the ability to pass untyped nil (which doesn't really exist; even nil has a type). While the approach in your answer seems to work, it allows for the creation of ?? types due to Optional promotion. You often get lucky and that works, but I've seen it blow up in really frustrating ways and the wrong function is called. The problem is that String can be implicitly promoted to String? and String? can be implicitly promoted to String??. When ?? shows up implicitly, confusion almost always follows.

As MartinR points out, your approach is not very intuitive about which version gets called. UnsafePointer is also NilLiteralConvertible. So it's tricky to reason about which function will be called. "Tricky to reason about" makes it a likely source of confusing bugs.

The only time your problem exists is when you pass a literal nil. As @Valentin notes, if you pass a variable that happens to be nil, there is no issue; you don't need a special case. Why force the caller to pass an untyped nil? Just have the caller pass nothing.

I'm assuming that somethingGeneric does something actually interesting in the case that it is passed nil. If that's not the case; if the code you're showing is indicative of the real function (i.e. everything is wrapping in an if (input != nil) check), then this is a non-issue. Just don't call somethingGeneric(nil); it's a provable no-op. Just delete the line of code. But I'll assume there's some "other work."

func somethingGeneric<T>(input: T?) {
    somethingGeneric() // Call the base form
    if (input != nil) {
        print(input!);
    }
}

func somethingGeneric() {
   // Things you do either way
}

somethingGeneric(input: "Hello, World!") // Hello, World!
somethingGeneric() // Nothing
Rob Napier
  • 286,113
  • 34
  • 456
  • 610
  • Excellent response by Rob, commenting because this should definitely be the accepted answer – Ilias Karim Jul 24 '20 at 13:57
  • You're quite right, @RobNapier - I think I botched the copy and paste when I tested it out. I'm just going to delete the original comment. – GSnyder Oct 06 '21 at 00:35
5

Good question and answer. I have an Swift 4 update to contribute:

var str: String? = "Hello, playground"
var list: Array<String>? = ["Hello", "Coder256"]

func somethingGeneric<T>(_ input: T?) {
  if (input != nil) {
    print(input!);
  }
}

func somethingGeneric(_ input: ExpressibleByNilLiteral?) {}


somethingGeneric("Hello, World!")    // Hello, World!
somethingGeneric(nil)                // *nothing printed*
somethingGeneric(nil as String?)     // *nothing printed*
somethingGeneric(str)                // Hello, playground
str = nil
somethingGeneric(str)                // *nothing printed*
somethingGeneric(list)               // ["Hello", "Coder256"]
list = nil
somethingGeneric(list)               // *nothing printed*
stonecanyon
  • 119
  • 2
  • 6
2

I figured it out:

func somethingGeneric<T>(input: T?) {
    if (input != nil) {
        print(input!);
    }
}

func somethingGeneric(input: NilLiteralConvertible?) {}


somethingGeneric("Hello, World!") // Hello, World!
somethingGeneric(nil) // *nothing printed*
somethingGeneric(nil as String?) // *nothing printed*
Coder-256
  • 5,212
  • 2
  • 23
  • 51
  • 2
    Your `somethingGeneric(input:T?)` func still doesn't accept nil as a value though. It is just going through the empty function and isn't printing anything because that function doesn't do anything. With this, if you call the func with nil, it will go through the func with `input: NilLiteralConvertible` as a parameter, but if you call it with a nil Optional, such as saying let x: String?, it will go through the func with `input: T?` as a parameter, so you will have to duplicate your nil-handling logic. – Will M. Mar 16 '16 at 15:34
  • @WillM. `somethingGeneric(input: T?)` doesn't take `nil`; `somethingGeneric(input: NilLiteralConvertible?)` does. I chose this way because it works for all three, but you are right in that I could convert `somethingGeneric(input: T?)` to not take an optional, but it would break `nil as String?` (I just didn't want it to be a requirement that the function was called that way). – Coder-256 Mar 16 '16 at 15:36
  • 2
    Note that – for example – `let ptr : UnsafePointer? = ... ; somethingGeneric(ptr)` would call the *second* function, because `UnsafePointer` is `NilLiteralConvertible` and the second generic function is more specific. – Martin R Mar 16 '16 at 15:36
  • @MartinR Good point. It doesn't matter in this case, but I *could* use `if (input != nil)` to check that `input` is actually `nil`. – Coder-256 Mar 16 '16 at 15:38
  • 1
    @MartinR All optionals are NilLiteralConvertible, and yet an optional String still goes to the first function though, regardless of if it is Optional.Some or Optional.None – Will M. Mar 16 '16 at 15:49
  • 2
    @WillM.: The second function takes an `NilLiteralConvertible?`, and `String?` does not match that type (because `String` is not `NilLiteralConvertible`). – Martin R Mar 16 '16 at 16:02
  • @MartinR String? is NilLiteralConvertible though, even if String isn't. If you get rid of the first func, the second func will accept somethingGeneric(optionalString) Maybe since, as you said, it doesn't quite fit the optionality, the compiler thinks that the first func is a better fit. – Will M. Mar 16 '16 at 16:04
  • 2
    @WillM. This is optional promotion. In the case you're describing, the compiler is promoting to `String??` which matches `NilLiteralConvertible?`. My experience is that this kind of use of generics is very fragile, and that optional promotion will burn you in surprising ways, creating `??` types. I would recommend constraining `T` is some way. – Rob Napier Mar 16 '16 at 16:13
  • @RobNapier good to know. I've never heard that term before. I'm not surprised it will burn you, considering if you take an optional string and check if its NilLiteralConvertible? it returns false, but it goes in to the function anyways. Now I know something new about Swift. – Will M. Mar 16 '16 at 16:22
  • @Coder-256 What if T conforms to encodable? and if the input is class object it will work ... but not for struct based encodable – Satyam Nov 30 '21 at 17:29
2

I think that you will never call somethingGeneric(nil) but mostly somethingGeneric(value) or somethingGeneric(function()) for which the compiler has enough info not to be stucked trying to guess the type:

func somethingGeneric<T>(input: T?) {
    if let input = input {
        print(input);
    }
}

func neverString() -> String? {
    return nil
}

let a: String? = nil

somethingGeneric("Hello, World!") // Hello, World!
somethingGeneric(a) // Nothing and no error
somethingGeneric(neverString()) // Nothing and no error

Also, I would use the if let syntax instead of if(value != nil).

Valentin
  • 10,769
  • 2
  • 17
  • 27
1

Here is the solution I came up with that compiles on Swift 5, as many of the solutions here did not compile for me. It might be considered hacky as I use a stored variable to help things along. I was unable to come up with a Swift 5 version of the nil parameters that resolve to type T.

class MyClass {
    func somethingGeneric<T>(input: T?) {
        if let input = input {
            print(input)
        }
    }

    func somethingGeneric() {
        somethingGeneric(Object.Nil)
    }

}

final class Object {
    static var Nil: Object? //this should never be set
}
Dylan Reich
  • 1,400
  • 2
  • 11
  • 14
1

Actually there is a way to do this, inspired by Alamofire's internal code.

You do not have to install Alamofire to use this solution.

Usage

Your problematic method definition

func someMethod<SomeGenericOptionalCodableType: Codable>(with someParam: SomeGenericOptionalCodableType? = nil) {
        // your awesome code goes here
}

What works ✅

// invoke `someMethod` correctly 
let someGoodParam1 = Alamofire.Empty.value
someMethod(with: someGoodParam1)

I think it is possible to use Alamofire.Empty.value as a default value in someMethod definition as a parameter.

What does not work ❌

// invoke `someMethod` incorrectly
let someBadParam1: Codable? = nil
let someBadParam2 = nil
someMethod(with: someBadParam1)
someMethod(with: someBadParam2)

Solution definition (source)

/// Type representing an empty value. Use `Empty.value` to get the static instance.
public struct Empty: Codable {
    /// Static `Empty` instance used for all `Empty` responses.
    public static let value = Empty()
}
om-ha
  • 3,102
  • 24
  • 37