How to do Object Overloading in Elixir?

What is the correct way to perform object overloading in Elixir? I am trying to currently overload the < operator, however, I keep getting an error.

Basically, I have the following object oriented Python code:

class Value:

    def __init__(self, a, b):
        self.a = a
        self.b = b

    def __lt__(self, other):
        return self.f1() > other.f1()

    def f1(self):
        return (self.a * self.b)

    def get_value(self, obj2):
        if self.f1() < obj2.f1():
            print("Done")
        else:
            print("Not Done")
        
v1 = Value(1, 3)
v2 = Value(2, 4)
v1.get_value(v2)

Now, my way of converting this in Elixir is the following:

defmodule Value do
  import Kernel, except: [<: 2]
  
  defstruct a: 0, b: 0

  def init(a, b), do: %Value{a: a, b: b}

  def v < other do
    v.a > other.a and v.b > other.b
  end

  def f1(v) do
    (v.a * v.b)
  end
  
  def get_value(obj1, obj2) do
    if f1(obj1) < f1(obj2), do: IO.puts "Done", else: IO.puts "Not Done"
  end
end

v1 = Value.init(1, 3)
v2 = Value.init(2, 4)
Value.get_value(v1, v2)

This, however, gives me the following error:

** (ArgumentError) you attempted to apply :a on 3. If you are using apply/3, make sure the module is an atom. If you are using the dot syntax, such as map.field or module.function, make sure the left side of the dot is an atom or a map
    :erlang.apply(3, :a, [])
    jdoodle.exs:9: Value.</2
    jdoodle.exs:17: Value.get_value/2

Can someone guide me as to what is wrong in my implementation and technically, whats the correct way to overload the < operator in Elixir?

You are trying to apply this v.a > other.a and v.b > other.b to a number returned from the f1(obj1).
If you are going to use f1/1 to multiply the values you dont need to override the operator.

And I also dont think its a really good idea to override this operator like that :sweat_smile:

2 Likes

When you say import Kernel, except: [<: 2], you are opting-out of the Kernel.</2 operator entirely. The code in get_value tries to call the overridden < but it expects a struct not a number.

One way to handle this would be to add additional clauses for < that also handle numbers by explicitly calling Kernel.<.

HOWEVER

I don’t think doing that will produce the kind of results you’d expect, based on the Python code.

In Python, overriding the comparison operators for a class means that other code that compares objects of that type will use the override code. For instance, passing a list of Value objects to sorted will call your function.

In Elixir, replacing an operator like Kernel.< means that the declaring code uses a different operator, but a Value struct in other code doesn’t behave differently unless you’ve imported Value.</2 explicitly. For instance, passing a list of Value structs to Enum.sort will compare them with Kernel.<.

The preferred approach for providing a custom comparison function is to define a compare/2 function; see the Enum.sort/2 docs for more details.

7 Likes

Modules are not objects, but a collection of functions.

There is a difference between object methods and functions.

get_value looks like a method.

IO.puts is a side effect. Functions have a tendencies to be pure.

Maybe like this.

defmodule Value do
  alias __MODULE__

  defstruct a: 0, b: 0

  def new(a, b), do: %Value{a: a, b: b}

  def f1(%Value{a: a, b: b}), do: a * b
  
  def compare(value1, value2), do: f1(value1) < f1(value2)
end

# Then

v1 = Value.new(1, 3)
v2 = Value.new(2, 4)

if Value.compare(v1, v2), do: IO.puts "Done", else: IO.puts "Not Done"

As a side note, object overloading with a functional language is complicate :slight_smile:

2 Likes

Thank you everyone for all your excellent responses! Will try to wrap my head around the suggestions given here

1 Like