When do you use free functions?

Coming from languages like C# where there are no free functions.
I’m not sure when I’m supposed to use free functions and when I’m supposed to use OOP with classes. Maybe you can also include and example use case.

2 Likes

You will frequently see free functions that accept several arguments, where no argument is more important than the others, and the order of arguments is not really important:

max(1, 3)

The choice between a free function, a static method, or an instance method is generally a matter of good taste, clarity at the call site, and an intuition of the expected usage.

For example:

  • A free max(a, b) method is good because most programming languages provide such a free function. Many users expect this free function to exist.

  • max(a, b) is superior to the static method Int.max(a, b), because the static method has poor ergonomics. Forcing the user to type Int.max(...), Double.max(...), MyCustomComparableType.max(...) etc, is cumbersome.

  • max(a, b) is a better fit for Swift than the C# static methods Math.Max, because both languages do not have the same kind of generics. C# has to provide a fixed list of overloads for Int16, Int32, etc, when Swift can deal with all comparable types without any exception (including your own Comparable types which are not really numeric and don't naturally fall in the Math realm).

  • max(a, b) is superior to an instance method a.max(b), because max is not a good name for this instance method (and I could actually not find a good name for it).

  • max(a, b) is superior to the instance method, because it does not force me to choose between a.max(b) and b.max(a).

  • max(a, b) is superior to an instance method, because it can naturally be made to accept a variable number of arguments: max(a, b, c, ...).

Sometimes, you will provide both a free function, and a static/instance method. For example, Swift provides both max(a, b) and [a, b].max(): max(_:_:) / Sequence.max(). If you look closely, you will see that Sequence.max has to deal with empty sequences, and thus has to return an optional. In this regard, max(a, b), which returns a non-optional value, has better ergonomics in some contexts. max(a, b) has another advantage over [a, b].max(): the compiler can optimize it very efficiently. Both methods are closely related, but they address subtly different use cases, and this is why we have both.

As you can see, the choice can be quite subtle. This is why you should not be afraid to make a mistake. If your initial choice is poor, you will quickly notice it when using your api. And you will be able to refactor your code, and provide a more suitable api!

And if you have a specific question in mind, for a particular free function, you can ask the opinion of the community!

14 Likes

One thing I feel free functions would also be suitable for would be creational patterns? Like instead of having some factory class I could just:

createIRCClient(using server: ServerInfo, as user: User) -> IRCClient {
   ....
}

What do you think?

1 Like

It may be time that you look at the API Design Guidelines. It is an important document that drives the design of most Swift apis. When in doubt, refer to it.

For "creational patterns", the guidelines strongly favor initializers first, and factory methods / make methods second. You will rarely see a free function for this use case:

// Preferred, if possible
let client = IRCClient(using: server, as: user)

Consider for example:

// Conversions are provided as initializers
Int("123")

// A `make-` prefixed method
let iterator = mySequence.makeIterator()

There are very rare exceptions, such as the repeatElement(_:count:) free function:

let zeroes = repeatElement(0, count: 5)
for x in zeroes {
    print(x) // 0, 0, 0, 0, 0
}

But this is not generally advised. Free functions are only preferred when alternatives are inferior. If you want to play a game, try to provide an explanation why this repeatElement free function exists, instead of an initializer or a factory method. What was the design challenge, what were the other possible designs, and why was a free function preferred over those alternatives?

9 Likes

I actually use them quite a lot to separate smaller function "helpers" that don't require access to self (so more functional than OOP). I just put them at the end of the file and mark them private so nobody outside the file can see them. I found it a very useful technique.

3 Likes

This could be a great interview question for a frameworks team (Assuming there is actually a principled reason, I haven't thought it through myself yet :smiley:)

repeatElement(_:count:) could have been Repeated<T>.init(_:count:). this would have reduced the amount of private underscored APIs in the binary. i always just assumed it was spelled as a free function because it is perceived to be more “lightweight”.