Avoiding standalone functions as a programmer style?

I'm not sure whether or not this was ever a formal convention or "best practice"… but I usually avoid defining standalone (free) functions whenever possible. I would prefer to create a trivial struct or enum type and then define the free function as a static method on that type.

The only source I have so far backing me up on this style pattern is How to Mock Standalone Functions in Swift, which walks through all the work needed to stub out a standalone function (and my take-away is to prefer typed methods to avoid having to do all this work or pass this work on to another engineer in the future). I would be interested if anyone else has written on this topic or you have more ideas or thoughts to share. Thanks!

I don’t think ultimately avoiding freestanding functions at all cost is necessary. They can be a valid representation of logic of any kind. If you need to mock such function, there is something fundamentally wrong design in place, not that such functions are bad. There can be pretty legitimate cases to have just freestanding functions.

As for personal preferences, I tend to have functions be part of some type by relation to that type, so it effectively manipulates the data on it. With procedural style you’ll have the same: data and functions to manipulate the data, yet the language gives us ability to unite them in a better way. Yet if something is just an action (hey command pattern), it can be either a struct with one method, or just the method - both options are fine, yet in some cases wrapping in a type can make code clearer, as well as align with overall approach in the codebase.

As conclusion, having freestanding functions are OK if that makes sense from design perspective, like it is really something that belongs on its own, but having much of them can create problems if you won’t follow any other approach to structure them, as well as lead to confusion if another part of the project uses them significantly less.

2 Likes

To me, the only "problem" with standalone functions is that they reside in the global namespace. Otherwise, I personally write a lot of top-level private funcs (which essentially makes them fileprivate) because I'm trying to keep the actual methods on a type only related to its inner state.

Otherwise, I namespace freestanding function with a caseless enum.

8 Likes

Ahh… yes… my original question was more going to be focused on public functions of a module (and should those public functions be converted to a typed method). I am neutral on whether or not I would advocate for private helper functions to be made to live on a type to enforce a style… that I would see more as a matter of personal engineering preference.

I think attempting to test code by replacing top-level functions is going to cause more problems than it solves. If you have something like

func f() {
  … g() …
}

and you find yourself wanting to replace g() with some kind of stub implementation to test f(), it’s almost always better to refactor f() and either split off the part “after g()” so you can test it directly on various inputs, or failing that, have f() take a closure for the g() operation.

Standalone functions are great, especially if they don’t touch any mutable global state. Highly recommended.

7 Likes

The API Design Guidelines do have “Prefer methods and properties to free functions”, but don’t really spend time to justify it. I personally think the biggest reason to do this is organization and code completion: if you have a value already, its members describe a bunch of things you can do with it. It’s not everything you can do with a value, it might be an argument somewhere else, but it’s a good starting point. Even for static methods, someone might know what type they’re generally working with, and start looking for methods on that type.

But that only works if there is a type where people would go looking for operations. If there isn’t, I’m not sure making people look under a type is clearer than just using a free function. They’re still going to be in your module regardless. Having a wrapper “namespace” enum could still be useful for grouping, sure, but I wouldn’t say it’s a strict requirement or anything.

10 Likes

As an example: the sequence(first:next:) function is super useful but almost nobody knows that it exists because it’s defined at the top level.

4 Likes

It doesn't seem methods are any easier to "mock" than standalone functions. They both require shenanigans to swap out details at runtime, whether that is done by subclassing+overloads of helpers you want to override, or having a global+mutable version of the helper that you can swap out. So I wouldn't consider that a knock against free functions.

And it's worth mentioning that there are techniques to make all functions testable immediately, regardless of where they live (free/static/instance). With a little bit of work you can implement a "dependency management" micro library that allows you to easily and safely provide dependencies from the outside to any function that is also overridable.

You can even have a "precondition" dependency that will perform a real precondition in production but in tests it will skip the precondition and just record a testing issue, instantly making it testable:

func operate(_ values: [Double]) -> Double {
  // Some mechanism for providing dependencies that can be
  // controlled from the outside in a safe manner:
  @Dependency(\.precondition) var precondition

  precondition(!values.count.isEmpty, "'values' must not be empty.")
  return …
}

@Test
func operatePrecondition() {
  // Test that invoking 'operate' with an empty array raises 
  // a precondition.
  withKnownIssue {
    _ = operate([])
  } matching: {
    $0.comments[0].rawValue == "'values' must not be empty."
  }
}

@Test
func operateSuccessfully() {
  // Test that invoking 'operate' with a non-empty array does 
  // not raise a precondition.
  #expect(operate([1, 2, 3]) == …)
}
4 Likes

This is a very opinionated subject, so it's basically up to you to decide if you want them or not, but personally I sporadically use freestanding functions, for example requiredCast that replaces as! with some additional logging.