Singleton Objects - preferable to define it as a struct?

I am trying to simulate a puzzle. In it, there is going to be a light bulb; it will have a state throughout the program; its initial state will be off. It will have a togglemethod.

Since there is going to be only one such bulb (so it will be a singleton object), is it still preferable to define it as a struct? Is there anything wrong with the below code:

defmodule Bulb do
  
  defstruct [state: false]

  def new() do
    %Bulb{}
  end

  def toggle_bulb(bulb) do
    %Bulb{bulb | state: not bulb.state}
  end
end

In livebook, when I evaluate: bulb = Bulb.new(), I get: %Bulb{state: false}. And then when I evaluate, Bulb.toggle_bulb(bulb), I get, %Bulb{state: true}. But when I evaluate it again with another, Bulb.toggle_bulb(bulb), I still get, %Bulb{state: true}, so there must be something wrong with my code.

1 Like

object is only in OOP languages. struct is not object. All you need to do is reassign bulb variable, for example:

iex> bulb = Bulb.new()
%Bulb{state: false}
iex> bulb = Bulb.toggle_bulb(bulb)
%Bulb{state: true}
iex> bulb = Bulb.toggle_bulb(bulb)
%Bulb{state: false}
5 Likes

Yeah, there are no side effects in purely functional code, as Eiji very succinctly demonstrated.

Trying to add a little bit more color…

iex> bulb = Bulb.new()
%Bulb{state: false}

iex> Bulb.toggle_bulb(bulb)
%Bulb{state: true}

iex> Bulb.toggle_bulb(bulb)
%Bulb{state: true}

toggle_bulb cannot edit in place the bulb passed to it; all it can do is return a new bulb. So if you’re not capturing that (i.e. assigning it to a variable), then it’s lost.

This property of functional languages is very very good. Not only does it allow for easily parallelization, but it’s good from a code maintenance / reasoning standpoint.

2 Likes

As rest said, there is nothing wrong with the code, as Elixir structures are immutable, aka when you “modify” it then instead you create new copy with updated value.

But what you are trying to achieve is possible by having named process.

5 Likes

True, but I feel like my “functional awakening” was NOT trying to replicate OOP objects with processes.

One of the things I love about functional programming is that it really makes you think about state management. There’s some amount of friction to making an Agent or GenServer that makes you really question… can I do this another way?

4 Likes

Not going to repeat what the others said because it’s true.

What you’re looking for can be achieved with :ets. But I’d seriously consider if I can do it another way e.g. a GenServer seems like the best idea.

1 Like

Answer is - sometimes, but if you want to have global state without ugly hacks and using stuff that you probably shouldn’t use for that - not really. You could use ETS table, but that would be overkill and you still would need to have process to be parent of such table. You could (an)use :counters there or go totally nuts with :persistent_term, but last one would probably greatly impact performance

1 Like

Yes, absolutely. But you’re getting into a lot of subtle nuances.

It looks like he doesn’t really want global state. He just wants side effects…

bulb = Bulb.new()
Bulb.toggle(bulb)
bulb.state # Why isn't this true??

And that’s what I’m saying… functional programming points people in the right direction: Do you really want global state? Or do you just want side effects in local code? If it’s the latter, then no, don’t make global state just to do that.

1 Like

Side effects? Looks like old mate is expecting mutability.

At least now he has half a dozen different ways to toggle a bulb :bulb:

2 Likes

Sounds like it’s time for some lightbulb jokes.

I like this one:
Q: How many surrealists does it take to change a light bulb?
A: Two, one to hold the giraffe, and the other to fill the bathtub with brightly colored machine tools.

4 Likes

We all know the correct way to toggle a stateful bulb is the one that hasn’t been mentioned yet, which is: use postgres. If you have money, Amazon rds.

This sounds like nothing more than a Boolean variable to me.

bulb_on = false
bulb_on = not bulb_on

Or, at its most complicated:

def toggle_bulb(on), do: not on

I don’t see the need for wrapping it in a struct.

Regarding how to store the boolean, I think we need more context. How long does it need to persist? Does the user interact via HTTP requests? Websocket messages? Is the state separated per user, or global across all users?

5 Likes

This is somewhat related to an earlier question I asked. The details are not important; just trying to figure out why the second code does not work as I expected.

defmodule Bulb do
  defstruct [:state]

  def turn_on (bulb = %Bulb{}) do
    %{bulb | state: :on}
  end

  def turn_off (bulb = %Bulb{}) do
    %{bulb | state: :off}
  end

end

bulb = %Bulb{state: :on} # --> %Bulb{state: :on}
bulb |> Bulb.turn_off() # --> %Bulb{state: :off}
bulb |> Bulb.turn_on() # --> %Bulb{state: :on}
defmodule Bulb do
  defstruct [:state]

  def switch_bulb(bulb = %Bulb{}) do
      case bulb do
        %{state: :off} -> %{bulb | state: :on}
        %{state: :on} -> %{bulb | state: :off}
      end

  end
end

bulb = %Bulb{state: :on}  # --> %Bulb{state: :on}
bulb |> Bulb.switch_bulb()  # --> %Bulb{state: :off}
bulb |> Bulb.switch_bulb()  # --> %Bulb{state: :off} why?

@blackened Do this:

bulb = %Bulb{state: :on}  # --> %Bulb{state: :on}
bulb2 = bulb |> Bulb.switch_bulb()  # --> %Bulb{state: :off}
bulb |> IO.inspect
bulb2 |> IO.inspect

Bulb is not a mutable value. When you call switch_bulb it returns a new datastructure, it doesn’t change the value that was passed in. You can re-bind the named variable if you want, but the underlying data is immutable.

3 Likes

I’m not sure if this is technically correct, but could one say that in functional languages, there is no such thing as “pass by reference”; everything is “pass by value”?

I mean, I know that’s selling immutability short, but maybe that could help people coming from non-functional world?

I’m not sure that’s a good way to think about it. Sure, it looks like things are passed by value but in reality they often are not. You get a reference to a immutable data structure. Because it’s immutable, multiple parts of your code may have a reference to the same data structure.

When you do something with the data structure, a new structure is created and you get a reference to the new structure. The old structure remains unchanged and other parts of the code that have the reference to the old structure see it as unchanged.

From the outside this looks a lot like pass by value, but with pass by value multiple code parts have a separate copy of the data structure vs a reference to the same data structure.

Still, because they look so alike (I’m not sure you can see the difference from a coding perspective) it might give some intuition on how to think about this.

4 Likes

Looking at this code:

bulb = %Bulb{state: :on}  # --> %Bulb{state: :on}
bulb |> Bulb.switch_bulb()  # --> %Bulb{state: :off}
bulb |> Bulb.switch_bulb()  # --> %Bulb{state: :off} why?

Others already answered the why.

Here is a snippet of code that might do what you want.

bulb = %Bulb{state: :on}  # --> %Bulb{state: :on}
bulb = bulb |> Bulb.switch_bulb()  # --> %Bulb{state: :off}
bulb |> Bulb.switch_bulb()  # --> %Bulb{state: :on} because we started with a switched bulb

On the 2nd line we re-bind the variable to the newly returned switched_off_bulb

Another fun way of debugging this would be

%Bulb{state: :on}
|> IO.inspect(label: "Initial Bulb should be on đź’ˇ")
|> Bulb.switch_bulb()
|> IO.inspect(label: "After 1st switch, bulb should be off ❌💡")
|> Bulb.switch_bulb()
|> IO.inspect(label: "After 2nd switch, bulb should be onđź’ˇ")

It is important that you understand the immutable behavior at this level. So keep asking here and playing with this code until you do.

If this behavior means that you can not solve the problem you are trying to solve, please ask the higher-level question of how you would do X with immutable data structures.

I am VERY hesitant to keep writing at this point because it could steer you in the wrong direction. If you are stuck in thinking in the concept of Objects, what I am about to recommend could seem to you like an Object, and it is not. That said, saving the persistent state of an application is a solved problem in Elixir.

If this puzzle can’t be solved with pure functions, it might be better to solve simpler puzzles for a while until you feel comfortable with Elixir and then come back to this one.

If there was a real light bulb, you would have a single GenServer that holds that state for the light bulb and is in charge of communicating with the light bulb. The interface to the GenServer would have a toggle or switch_bulb function which will return different results based on what the previous state was.

There is a concept of a Singleton GenServer. If the GenServer is named, only one GenServer can hold that name.

However, I beseech you always try to solve problems with pure functions that operate on data structures. As your codebase grows, this is always the easiest to reason about. This is one of Elixir’s main competitive advantages, that after the codebase has grown, productivity is not slowed down by complexity as much as it would be with complex objects that represent behavior and state.

2 Likes

Thank you, D, for a detailed answer. I will present the case in a new topic.

1 Like

Right, which is what I was going for.

Most OOP/imperative/procedural people understand this Ruby code…

pry> s = "FooFoo"
pry> s.gsub!("F", "B") # Pass by ref / mutate
pry> s
=> "BooBoo"

pry> s = "FooFoo"
pry> s.gsub("F", "B") # Pass by value / return new value
pry> s
=> "FooFoo"

pry> s = "FooFoo"
pry> s = s.gsub("F", "B") # Pass by value / return new value, but reassign
pry> s
=> "BooBoo"

Just something to help ease the transition… “everything is basically pass by value in functional languages, you cannot edit anything in place.” I guess kind of a way to make immutability more relatable, maybe?

1 Like