Actually you're asking what is the difference between x.m() and m(x). The difference is how method is resolved. There can be different methods with the same name m.
In the case x.m() method is resolved dynamically, at runtime (overriding, late binding, subtype polymorphism)
class Foo {
def m(): Unit = println("Foo")
}
class Bar extends Foo {
override def m(): Unit = println("Bar")
}
val x: Foo = new Bar
x.m() // Bar
In the case m(x) method is resolved statically, at compile time (overloading, early binding, ad hoc polymorphism)
class Foo
class Bar extends Foo
def m(x: Foo): Unit = println("Foo")
def m(x: Bar): Unit = println("Bar")
val x: Foo = new Bar
m(x) // Foo
One of classes Foo, Bar can be a case class.
The latter approach ("statical") can be implemented also with type classes rather than overloading
trait DoM[T] {
def m(t: T): Unit
}
def m[T](t: T)(implicit dm: DoM[T]): Unit = dm.m(t)
class Foo
class Bar extends Foo
implicit val fooDoesM: DoM[Foo] = _ => println("Foo")
implicit val barDoesM: DoM[Bar] = _ => println("Bar")
val x: Foo = new Bar
m(x) // Foo