racso
racso

Reputation: 149

Swift Implementation generic function

I'm a developer on Java and I'm trying to write in Swift the same solution that I have in Java code.

Is it possible to do this on Swift?

Example Java:

public interface Converter<S,T> {
    T convert(S in)
}

public class CarConverterToDTO implements Converter<Car, CarDTO> {
      @Override
      public CarDTO convert(Car in) {
      .....
      }
}

Example Swift:

protocol Converter {
   func convert<IN, OUT>(in: IN) -> OUT
}

How it would be the implementation?

Thanks!!!

Upvotes: 0

Views: 339

Answers (2)

rob mayoff
rob mayoff

Reputation: 385600

What appears to be a simple question is actually the tip of a rather large and unpleasant iceberg…

I'm going to start by giving you what is probably the real solution to your problem:

class Converter<Input, Output> {
    func convert(_ input: Input) -> Output {
        fatalError("subclass responsibility")
    }
}

struct Car { }
struct CarDTO { }

class DTOCarConverter: Converter<Car, CarDTO> {

    override func convert(_ input: Car) -> CarDTO {
        return CarDTO()
    }

}

Above, I've translated your Java Converter interface into a Swift class instead of a Swift protocol. That's probably what you want.

Now I'll explain why.

A programmer coming from Java to Swift might think that a Swift protocol is the equivalent of a Java interface. So you might write this:

protocol Converter {
    associatedtype Input
    associatedtype Output

    func convert(_ input: Input) -> Output
}

struct Car { }
struct CarDTO { }

class /* or struct */ DTOCarConverter: Converter {
    func convert(_ input: Car) -> CarDTO {
        return CarDTO()
    }
}

Okay, now you can create a converter and convert something:

let converter = DTOCarConverter()
let car = Car()
let dto = converter.convert(car)

But you're going to run into a problem as soon as you want to write a function that takes a Converter as an argument:

func useConverter(_ converter: Converter) { }
//                             ^
// error: protocol 'Converter' can only be used as a generic constraint because it has Self or associated type requirements

“Well, duh,” you say, “you forgot the type arguments!” But no, I didn't. Swift doesn't allow explicit type arguments after a protocol name:

func useConverter(_ converter: Converter<Car, CarDTO>) { }
//                             ^        ~~~~~~~~~~~~~
// error: cannot specialize non-generic type 'Converter'

I don't want to get into why you can't do this. Just accept that a Swift protocol is not generally equivalent to a Java interface.

A Swift protocol with no associated types and no mention of Self is, generally, equivalent to a non-generic Java interface. But a Swift protocol with associated types (or that mentions Self) is not really equivalent to any Java construct.

When discussing this problem, we often use the acronym “PAT”, which stands for “Protocol with Associated Types” (and includes protocols that mention Self). A PAT doesn't define a type that you can use as a function argument, return value, or property value. There's not much you can do with a PAT:

  • You can define a subprotocol. For example, Equatable is a PAT because it defines the == operator to take two arguments of type Self. Hashable is a subprotocol of Equatable.

  • You can use a PAT as a type constraint. For example, Set is a generic type. Set's type parameter is named Element. Set constrains its Element to be Hashable.

So you can't write a function that takes a plain Converter as an argument. But you can write a function that takes any implementation of Converter as an argument, by making the function generic:

func useConverter<MyConverter: Converter>(_ converter: MyConverter)
    where MyConverter.Input == Car, MyConverter.Output == CarDTO
{ }

That compiles just fine. But sometimes it's inconvenient to make your function generic.

And there's another problem that this doesn't solve. You might want a container that holds various Converters from Car to CarDTO. That is, you might want something like this:

var converters: [Converter<Car, CarDTO>] = []
//               ^        ~~~~~~~~~~~~~
// error: cannot specialize non-generic type 'Converter'

We can't fix this by making converters generic, like we did with the useConverter function.

What you end up needing is a “type-erased wrapper”. Note that “type-erased” here has a different meaning that Java's “type erasure”. In fact it's almost the opposite of Java's type erasure. Let me explain.

If you look in the Swift standard library, you'll find types whose names start with Any, like AnyCollection. These are mostly “type-erased wrappers” for PATs. An AnyCollection conforms to Collection (which is a PAT), and wraps any type that conforms to Collection. For example:

var carArray = Array<Car>()
let carDictionary = Dictionary<String, Car>()
let carValues = carDictionary.values
// carValues has type Dictionary<String, Car>.Values, which is not an array but conforms to Collection

// This doesn't compile:
carArray = carValues
//         ^~~~~~~~~
// error: cannot assign value of type 'Dictionary<String, Car>.Values' to type '[Car]'

// But we can wrap both carArray and carValues in AnyCollection:
var anyCars: AnyCollection<Car> = AnyCollection(carArray)
anyCars = AnyCollection(carValues)

Note that we have to explicitly wrap our other collections in AnyCollection. The wrapping is not automatic.

Here's why I say this is almost the opposite of Java's type erasure:

  • Java preserves the generic type but erases the type parameter. A java.util.ArrayList<Car> in your source code turns into a java.util.ArrayList<_> at runtime, and a java.util.ArrayList<Truck> also becomes a java.util.ArrayList<_> at runtime. In both cases, we preserve the container type (ArrayList) but erase the element type (Car or Truck).

  • The Swift type-erasing wrapper erases the generic type but preserves the type parameter. We turn an Array<Car> into an AnyCollection<Car>. We also turn a Dictionary<String, Car>.Values into an AnyCollection<Car>. In both cases, we lose the original container type (Array or Dictionary.Values) but preserve the element type (Car).

So anyway, for your Converter type, one solution to storing Converters in a container is to write an AnyConverter type-erased wrapper. For example:

struct AnyConverter<Input, Output>: Converter {
    init<Wrapped: Converter>(_ wrapped: Wrapped) where Wrapped.Input == Input, Wrapped.Output == Output {
        self.convertFunction = { wrapped.convert($0) }
    }

    func convert(_ input: Input) -> Output { return convertFunction(input) }

    private let convertFunction: (Input) -> Output
}

(There are multiple ways to implement type-erased wrappers. That is just one way.)

You can then use AnyConverter in property types and function arguments, like this:

var converters: [AnyConverter<Car, CarDTO>] = [AnyConverter(converter)]

func useConverters(_ converters: [AnyConverter<Car, CarDTO>]) {
    let car = Car()
    for c in converters {
        print("dto = \(c.convert(car))")
    }
}

But now you should ask: what's the point? Why bother making Converter a protocol at all, if I'm going to have to use a type-erased wrapper? Why not just use a base class to define the interface, with subclasses implementing it? Or a struct with some closures provided at initialization (like the AnyConverter example above)?

Sometimes, there's not a good reason to use a protocol, and it's more sensible to just use a class hierarchy or a struct. So you should take a good look at how you're implementing and using your Converter type and see if a non-protocol approach is simpler. If it is, try a design like I showed at the top of this answer: a base class defining the interface, and subclasses implementing it.

Upvotes: 3

Nicklas Jensen
Nicklas Jensen

Reputation: 1474

The Swift equivalent to your Java code looks like this.

protocol Converter {
    associatedtype Input
    associatedtype Output

    func convert(input: Input) -> Output
}

class CarConverterToDTO: Converter {
    typealias Input = Car
    typealias Output = CarDTO

    func convert(input: Car) -> CarDTO {
        return CarDTO()
    }
}

Explanation

The equivalent to a generic Java interface in Swift, would be a protocol with associatedtypes.

protocol Converter {
    associatedtype Input
    associatedtype Output
}

To create an implementation of that protocol, the implementation must specify which types the associated types maps to, using typealias.

class CarConverterToDTO: Converter {
    typealias Input = Car
    typealias Output = CarDTO
}

Type Erasure

If you try to use to this approach, you may run into the issue of trying to store an instance of your generic protocol in a variable or property, in which case you will get the compiler error:

protocol 'Converter' can only be used as a generic constraint because it has Self or associated type requirements

The way to solve this issue in Swift, is by using type erasure, where you create a new implementation of your generic protocol, that itself is a generic type (struct or class), and uses a constructor accepting a generic argument, matching your protocol, like so:

struct AnyConverter<Input, Output>: Converter {

    // We don't need to specify type aliases for associated types, when the type
    // itself has generic parameters, whose name matches the associated types.

    /// A reference to the `convert(input:)` method of a converter.
    private let _convert: (Input) -> Output

    init<C>(_ converter: C) where C: Converter, C.Input == Input, C.Output == Output {
        self._convert = converter.convert(input:)
    }

    func convert(input: Input) -> Output {
        return self._convert(input)
    }
}

This is usually accompanied by an extension function on the generic protocol, that performs the type erasure by creating an instance of AnyConverter<Input, Output> using self, like so:

extension Converter {

    func asConverter() -> AnyConverter<Input, Output> {
        return AnyConverter(self)
    }
}

Using type erasure, you can now create code that accepts a generic Converter (by using AnyConverter<Input, Output>), that maps Car to CarDTO:

let car: Car = ...
let converter: AnyConverter<Car, CarDTO> = ...

let dto: CarDTO = converter.convert(input: car)

Upvotes: 0

Related Questions