In functional programming, are local mutable variables with no side effects still considered “bad practice”?

Hi everyone,
I search some data about mutable state/shared memory.

the topic maybe cloud explain what i try to recognize.

my question is:

  1. are local mutable variables with no side effects is still bad?
  2. If it is bad, what is the big issue?
  3. If it is good, Elixir could reach it in the future or it is difficult? (In some condition we could prove it is in order of execution?)

Hope to get someone’s advise, thank you.

or for the readability of code, we always could make this a pure function and keep the state passed
and it is still efficient.

In FP the important concept is referential transparency, where a function call may be replaced by its definition, allowing you to reason about code like algebra.

Local state allows you to preserve RT while having good performance for some operations.

Immutability seems to be enforced at the BEAM level, so unlikely for Elixir to ever have local mutable variables.

However, since elixir has macros which can do almost anything, you could create a DSL in elixir that compiled to a NIF, perhaps using the LLVM JIT compiler. I’d love to see something like that used to offload number crunching to GPUs :grinning:

5 Likes

Shared mutable variables are trivial to add using genservers (or more directly, Agents). A mutable variable is just a genserver after all.

You can use some macro magic to do thing like these:

MutableVars.defmodule MyModule do
  var mut_var = 15

  def counter() do
    mut_var = mut_var + 1
    mut_var
  end
end

This basically requires injecting a var macro which defines a genserver upon use and rewriting the AST in a way that the match operator behaves differently when the left hand side is a mutable variable defined with var.

2 Likes

Is it not rebinding?

Could you elaborate further? I don’t understand your question.

some algorithms do not permit an efficient implementation with such an approach. Eg most linear algebra operations are orders of magnitude slower without mutable arrays.

Matlab is an example of a language with efficient local mutation, and copy-on-write semantics.

3 Likes

Sorry about that
I mean in your example:

MutableVars.defmodule MyModule do
  var mut_var = 15

  def counter() do
    mut_var = mut_var + 1
    mut_var
  end
end

“is mut_var change it’s value by rebinding?”

and i see about marco hygiene, i guess you mean this one:

It is more like magic and maybe not suitable for calculate.
but thank you for the tip.

:slight_smile:

Of course. That’s why you shouldn’t use those algorithms on the BEAM. I was not sugggessung that using Agents for this purpose was efficient

1 Like

So,

There are no local mutable variables in Elixir. You can simulate one with GenServer/agent state of course. But within functions you can re-bind the varibable name, think of it as a label.

The issue with mutable local variables is that when you branch off with execution, something might change the variable when you don’t expect and then you have a bug in your code. This is not goingn to happen in Elixir. The only way to reference this variable later on when you branch out, is if you did that from lambda, i.e. function referencing the variable of the scope it was defined in. And in Elixir, even if you reference the variable a that was later on re-bound, in a closure you will see the old value for a.

I have described that more in details in this blog post couple of years ago :slight_smile:

2 Likes

The goal here is to break macro hygiene and much more! In this example, mut_var should become a key in a global datastore (genserver, ets, whatever), protected by a process that acts as a mutex if you really want to get fancy.

I was talking about rewriting: mut_var = value into something like MutableVars.set(:mut_var, value) and other references to mut_var by MutableVars.get(:mut_var), where these functions set and get values in a global keystore.

2 Likes

Algebraic Effects also allow you to make ‘mutable’ variables in languages without mutable variables. ^.^

Here’s a playground of Algebraic Effects I made in Elixir:

And here’s an implementation of a ‘mutable’ variable in that effects system:

defmodule State do
  import ElixirEffects

  defmodule Get do
    ElixirEffects.defeffect
  end

  defmodule Set do
    ElixirEffects.defeffect [:value]
  end

  def get, do: perform %Get{}
  def set(value), do: perform %Set{value: value}

  def run(init, fun) do
    run_effect init, fun.() do
      state, %Get{} -> {:return, state}
      state, %Set{value: new_state} -> {:return, new_state, state} # Return old state
    end
  end
end

So this defines a State effect, which can then be used like:

State.run 0, fn ->
  import State
  assert 0 = get()
  assert 0 = set(42)
  assert 42 = get()
  6.28
end

The first argument to State.run is the initial value of the ‘variable’, at which point you can then get and set it all you want. You could even ‘key’ it based on a name or something and get multiples too!

For note, any mutable variable system can be emulated with immutable variables. look at even C/C++, they are as mutable as you can get, but they lower down to (in clang) the LLVM Assembly language, which is entirely immutable (but it’s optimizers optimize into mutable machine code, but it itself is immutable to simplify optimizations and implementations). :slight_smile:

As an aside, the effects system is able to handle a lot more than just fake mutation, they are an entirely powerful construct. Sadly mine is not fully capable because of BEAM VM limitations (no continuations so to compensate I’d have to recompile effectful code as CPS or wrap everything up into a new process, which is entirely doable, just a lot more work that I don’t have time to do in such a simple playground).

3 Likes

loooooooooool

2 Likes

My impression over three years is that it’s considered bad practice. There’s no need for mutable local variables almost all of the time though I have seen (rare) cases where mutability helps with readability. When we come to functional languages we shun mutability. The question, like you ask in point 2, is why? It’s easy to miss the bigger picture all together. A well designed “functional” program with small but meaningful functions naturally rules out mutability. That’s because all we’re doing is passing parameters and returning a value.

1 Like

I sort of agree. In languages where you have all the tools at your disposal, most functions will be compositions of a fairly limited set of other functions and sometimes a where/let ... in set of bindings to bind sub-expressions to more convenient formulations, but it’s less usual to actually need mutable variables.

Then again, the important part (as people said) is the interface. If you give me a pure interface I don’t particularly care what goes on inside the black box, until it misbehaves.

1 Like

It’s not a bad practice, but it depends on what you want to achieve.

  1. Many functional programming languages actually are internally mutable for optimizing the performance, but the API are still referential transparent.

  2. Above the API level. It’s nice to have a way to mutate the state since syntax because sometimes it’s would be a little bit easier to write code. (Someone don’t agree but I still believe there are a lot of times functional update are way too verbose, especially you don’t have something like comprehension and yield)

Either way, it seems to be hard for Elixir to have it because on BEAM memory of processes are not shared. And shared state breaks the assumption where it can safely distribute the computation to get such a high performance.
For example, there are schedulers to manage processes for each core(thread). When some core are free from work, sometimes they steal work from other thread, and give the result back to the target. If the memory were shared, then referential transparency would be break. And this mechanism could not be achieved or extremely complicated. Actually there are many other places in BEAM are designed with immutable (not shared) data structures in mind, and only then it can achieve current behavior.

But the syntax concern might be solved or improved by other ways like the state monad has been shown above, and update_in(path, fun) is also a good example. There would be many more great ways to deal with this issue since there are macros in Elixir.

I think this is not bad if outside function behaves like pure function but inside you mutate variables due to some performance issues.
But this should be hidden / encapsulated inside some library.
I think Scala inside do a lot of these tricks inside libraries.