Optional function ?

Hi,

I have a naive question: how to declare an optional function ?

Regards,

You mean a protocol with one or more optional methods, à la Objective-C? You can mark a method with optional, but only for @objc protocols and methods. For pure Swift code, you can instead use a property whose type is the same as the type of the function you want be optional (but wrapped into an Optional), and assign it to nil in implementing types that you don't want to implement that function (unfortunately, you won't have named arguments and generics). Take a look at the following code:

/// uses objc runtime
@objc protocol ExampleObjc {
  func required(withValue: Int) -> String
  @objc optional func nonRequired(withValue: Int) -> String
}

@objc class WithBothObjcImpl: NSObject, ExampleObjc {
  func required(withValue value: Int) -> String {
    ""
  }

  func nonRequired(withValue value: Int) -> String {
    ""
  }
}

@objc class WithRequiredOnlyObjcImpl: NSObject, ExampleObjc {
  func required(withValue value: Int) -> String {
    ""
  }
}

/// uses pure swift
protocol Example {
  func required(withValue: Int) -> String
  var nonRequired: ((Int) -> String)? { get }
}

class WithBothImpl: Example {
  func required(withValue value: Int) -> String {
    ""
  }

  let nonRequired: ((Int) -> String)? = { value in
    ""
  }
}

class WithRequiredOnlyImpl: Example {
  func required(withValue value: Int) -> String {
    ""
  }

  let nonRequired: ((Int) -> String)? = nil
}
2 Likes

I think a better pattern is to add default, no-op methods for optional protocol requirements in an extension.

2 Likes

In pure Swift I find preferable to have a sub-protocol for the additional "optional" requirements. You can cast to that (or pass it directly) in the cases where the other requirement is relevant.

1 Like

Closures, in theory, support this, but they don't support argument labels. :frowning_face:

You can annotate them, but you can't use those at call site (anymore).

struct Type {
  typealias Method = (
    _ parameter0: Bool,
    _ parameter1: Int
  ) -> String

  init(method: Method? = nil) {
    self.method = method
  }

  private let method: Method?
}

If the method doesn't throw, this solution is pretty decent, to get the labels back:

struct MethodNotAssignedError: Error { }

extension Type {
  func method(
    argument0 parameter0: Bool,
    argument1 parameter1: Int
  ) throws -> String {
    guard let method = method
    else { throw MethodNotAssignedError() }

    return method(parameter0, parameter1)
  }
}
try Type().method(argument0: false, argument1: 1) // MethodNotAssignedError()
try Type { _, _ in "🧵" } .method(argument0: false, argument1: 1) // 🧵

If it does, you have to figure out why, via catch. Or, you can optionally return a curried closure.

extension Type {
  func getMethod(
    argument0 parameter0: Bool,
    argument1 parameter1: Int
  ) -> ( () throws -> String )? {
    method.map { method in
      { try method(parameter0, parameter1) }
    }
  }
}
try Type().getMethod(argument0: false, argument1: 1)?() // nil
try Type { _, _ in "🧵" } .getMethod(argument0: false, argument1: 1)?() // 🧵
1 Like