cover image

In general, each function has a namespace of its own. Variables defined inside a function are scoped to that function and should not live after the function execution is done, that’s why we have return value.

A lot of languages are supporting global variables or some kind of mechanism to reach variables outside of the scope of the function. Is it bad? Short answer: mostly yes.

In this context we have two kind of function, “pure” and “impure” function. Pure functions are operating in their own scope, and they have no side effects.

What is “side effect”?

Let’s see a go function with side effect.

func generateName(server *Server) error {
  if server == nil {
    return NilReferenceError
  }

  if server.Name == "" {
    server.Name = fmt.Sprintf("server-%d", server.ID)
  }

  return nil
}

This function seems silly, but simple enough to show what is a side effect. When we call this function, it changes the object. It changes something that’s not it the scope of the function, that’s the side effect of the function. Because it has a side effect, it’s an impure function. The pure version would be something like this:

func generateName(server *Server) (string, error) {
  if server == nil {
    return "", NilReferenceError
  }

  if server.Name != "" {
    return server.Name, nil
  }

  return fmt.Sprintf("server-%d", server.ID), nil
}

Even better if we remove that * and don’t use as a reference (pointer).

func generateName(server Server) string {
  if server.Name != "" {
    return server.Name
  }

  fmt.Sprintf("server-%d", server.ID)
}

Why is it bad?

Imagine you call a function like func validate(server *Server) error. Your assumption is, it will validate the server and returns with an error if something is wrong. Now imagine someone generates a name during validation if it’s not provided, because why not, it’s not an error and we can generate it.

Side effects can cause serious issues, it is possible to change something and the program explodes on a totally different place and to figure out what changes a value or a global variable, you have to dig deep into functions.

Without side effects, it’s pretty straightforward what changed a given value.

Let’s see a different example, now use Python.

options = ["Alpha", "Omega", "Midgardsormr"]

def is_valid(name: str) -> bool:
    return name in options

print(options)
print(is_valid("Omega"))
print(options)

The output is straightforward:

['Alpha', 'Omega', 'Midgardsormr']
True
['Alpha', 'Omega', 'Midgardsormr']

Now someone wants to add a single value, but only in validation (assuming options) is used in other places too. A new inter tries to make it easy and adds the value to the list in the function:

options = ["Alpha", "Omega", "Midgardsormr"]

def is_valid(name: str) -> bool:
    options.append("Chaos")

    return name in options

print(options)
print(is_valid("Omega"))
print(is_valid("Chaos"))
print(options)

With this simple addition, the output changed a lot:

['Alpha', 'Omega', 'Midgardsormr']
True
True
['Alpha', 'Omega', 'Midgardsormr', 'Chaos', 'Chaos']

From now on, the options list will be longer and longer each time we call the is_valid function.

We can fix this in two ways, make a function that returns with the list, or pass the list as an argument. The function approach:

def options():
    return ["Alpha", "Omega", "Midgardsormr"]

def is_valid(name: str) -> bool:
    options().append("Chaos")

    return name in options()

print(options())
print(is_valid("Omega"))
print(is_valid("Chaos"))
print(options())

In this case the inter will see this approach is not working, so they have to find a different way.

['Alpha', 'Omega', 'Midgardsormr']
True
False
['Alpha', 'Omega', 'Midgardsormr']

Pure and impure functions

In general, if a function can be pure, it’s better if it’s pure and removes all possibilities for any future contributor to mess it up.

When I see a function like this:

func createNetworkIface(
  iface *models.NetworkInterface,
  status *models.NetworkInterfaceStatus,
) *NetworkInterfaceConfig {
  // ...
}

// or
func generateNetworkConfig(vm *models.MicroVM) (string, error) {
  // ...
}

I always check if this function makes any changes on iface, status, or vm. It shouldn’t do that, but it is possible to add a line in the function to change those instances and that can lead to serious issues later. If it generates or creates something, easier to not change anything and return with a value.

Even if your output is the same as your input, it can be still pure function. The formula is easy, you get a World (domain, struct) and return with a new World (domain, struct) and not the same one, reconstruct and create the whole World.

type Plane struct {
  Width int
  Density int
}

func PrepareForTrouble(width, density int) Plane {
  return Plane{width: width, density: density}
}

func MakeItDouble(plane Plane) Plane {
  return PrepareForTrouble(plane.Width * 2, plane.Density * 2)
}

That way we have the original and the new one as well.

But what if I want side effects?

Pure functions are not always the right way, it would make things harder than they should be, but it’s important to know the differences and use the right tool in the right place. Sometimes it’s easier and logical to write a function with side effects, sometimes it’s not.