A recommendation engine

A recommendation engine can be thought of as a software system that gives you the most relevant answer based on a list of objects:

protocol RecommendationEngine {
associatedtype Model
var models: [Model] { get }
func filter(elements: [Model]) -> [Model]
func sort(elements: [Model]) -> [Model]
}

We build our recommendation engine around two core methods—one for filtering out the undesired objects and the other around sorting; the most relevant results are always first.

Thanks to protocol extensions, we're able to provide the default implementation for the matching algorithm:

extension RecommendationEngine {
func match() -> Model? {
// If we only have 0 or 1 models, no need to run the algorithm
guard models.count > 1 else { return models.first }

return sort(elements: filter(elements: models)).first
}
}

Now we have a fully functional protocol with associated types, with our killer and secret algorithm that's hidden from the recommendation engine's implementations.

Let's first make recommendations for restaurants and, to begin with, we want to show the user it's an all time favorite:

let restaurants = [
Restaurant(name: "Tony's Pizza", beenThere: true, score: 2.0),
Restaurant(name: "Krusty's", beenThere: true, score: 3.0),
Restaurant(name: "Bob's burger", beenThere: false, score: 4.9)]

We added three restaurants into the database and have some information on them:

See the following code:

struct FavoriteEngine: RecommendationEngine {
var models: [Restaurant]

// Filter only the restaurants where you've been
func filter(elements: [Restaurant]) -> [Restaurant] {
return elements.filter { $0.beenThere }
}

// Sort by the best score
func sort(elements: [Restaurant]) -> [Restaurant] {
return elements.sorted { $0.score > $1.score }
}
}

let engine = FavoriteEngine(models: restaurants)
let match = engine.match() // Restaurant(name: "Krusty's"... )

Now that we can pull out the favorite restaurant, our users also want to get the best restaurant in town that you haven't visited yet, the one with the best score:

struct BestEngine: RecommendationEngine {
var models: [Restaurant]

func filter(elements: [Restaurant]) -> [Restaurant] {
return elements.filter { !$0.beenThere }
}

func sort(elements: [Restaurant]) -> [Restaurant] {
return elements.sorted { $0.score > $1.score }
}
}

let engine = BestEngine(models: restaurants)
let match = engine.match() // Restaurant(name: "Bob's burger"... )

With the same base matching algorithm, we were able to provides two recommendation engines for restaurants.

You may also have noticed the duplication of the sorting algorithm: restaurants have a score property, and we can use it for the sorting. We can generalize it at protocol level as well, through conditional conformance, and remove the specific implementations in the recommendation engine implementations:

extension RecommendationEngine where Model == Restaurant {
func sort(elements: [Model]) -> [Model] {
return elements.sorted { $0.score > $1.score }
}
}

With conditional conformance, remember you can conform to any type. As an exercise, you can rewrite part of this algorithm with protocols that expose the filtering or scoring primitives.