📜 ⬆️ ⬇️

"Operator Overloading" in Scala

Some time ago I announced a course on Scala . He started and laid out on the UDEMY MOOC platform - “Scala for Java Developers” . You can read more about the course at the end of the article .

Now I would like to present material on one of the topics of the course - operator overloading in Scala.


')


Introduction


in Scala there is no operator overload, as there are no operators (as entities other than methods). There are methods with symbolic (operator) names of the form '+', '/', '::', '<~' and the prefix / infix / posfix form of the record. However, for convenience, the term operator will be used further.


Infix operators



In Scala, the methods of a single argument can be written in the so-called infix form ( infix operations ). Namely


Example:
object Demo { // "normal" notation val x0 = I(1).add(I(2)) // infix notation val x1 = I(1) add I(2) } case class I(k: Int) { def add(that: I): I = I(this.k + that.k) } 

In the examples below, case-class I will appear, which I will endow in each case with different methods. It is made case exclusively for short code (fields are automatically generated and initialized by the primary constructor + automatically generated by the companion object with the apply method with a signature identical to the primary constructor, which allows you to create instances through I (k) and not new I (k). I recall that I (k) is equivalent to I.apply (k), and the apply method in Scala can be omitted). Class I is a “wrapper” around one Int and can be considered as a prototype for a full-fledged class of complex numbers, polynomials, matrices.

Everything becomes more interesting if the method is given a "symbolic" / "operator" name
 object Demo { // "normal" notation val x0 = I(1).+(I(2)) // infix notation val x1 = I(1) + I(2) } case class I(k: Int) { def +(that: I): I = I(this.k + that.k) } 

The JVM (class file format) does not support names from “operator characters”, therefore, synthetic names are generated during compilation.

Launch the class
 class I { def +(that: I): I = new I def -(that: I): I = new I def *(that: I): I = new I def /(that: I): I = new I def \(that: I): I = new I def ::(that: I): I = new I def ->(that: I): I = new I def <~(that: I): I = new I } 


java reflection
 import java.lang.reflect.Method; public class Demo { public static void main(String[] args) { for (Method m: I.class.getDeclaredMethods()) { System.out.println(m); } } } >> public I.$plus(I) >> public I.$minus(I) >> public I.$times(I) >> public I.$div(I) >> public I.$bslash(I) >> public I.$colon$colon(I) >> public I.$minus$greater(I) >> public I.$less$tilde(I) 


Yes, Java methods are visible with such names (as in the class file)
 public class Demo { public static void main(String[] args) { new I().$plus(new I()); new I().$minus(new I()); new I().$times(new I()); new I().$div(new I()); new I().$bslash(new I()); new I().$colon$colon(new I()); new I().$minus$greater(new I()); new I().$less$tilde(new I()); } } 

You remember about the transparent integration of all languages ​​compiled for JVM?

In general, half of Scala's “syntactic tricks” consist of a mixture of infix notation and implicit conversions.

Example # 1 :
 object Demo { for (k <- 1 to 10) { println(k) } } 


Infix notation is converted to normal
 object Demo { for (k <- 1.to(10)) { println(k) } 


Int does not have a 'to' method, so an implicit conversion is sought that allows Int to be converted to some type with a 'to' method and a suitable signature.

And it is located in Predef.scala (I remind you that java.lang. * + Scala. * + Predef. * Are implicitly imported into each file before compiling)

 //   Predef.scala package scala object Predef extends LowPriorityImplicits with DeprecatedPredef {...} private[scala] trait DeprecatedPredef {...} private[scala] abstract class LowPriorityImplicits { ... @inline implicit def byteWrapper(x: Byte) = new runtime.RichByte(x) @inline implicit def shortWrapper(x: Short) = new runtime.RichShort(x) @inline implicit def intWrapper(x: Int) = new runtime.RichInt(x) @inline implicit def charWrapper(c: Char) = new runtime.RichChar(c) @inline implicit def longWrapper(x: Long) = new runtime.RichLong(x) @inline implicit def floatWrapper(x: Float) = new runtime.RichFloat(x) @inline implicit def doubleWrapper(x: Double) = new runtime.RichDouble(x) @inline implicit def booleanWrapper(x: Boolean) = new runtime.RichBoolean(x) ... } 


RichInt already has a 'to' method with one argument of type Int.
 //   scala.runtime.RichInt package scala.runtime import scala.collection.immutable.Range final class RichInt(val self: Int) ... { ... def to(end: Int): Range.Inclusive = Range.inclusive(self, end) ... } 


And therefore, when compiled, it "spins" into something like
 import scala.runtime.RichInt object Demo { val tmp: Range = new RichInt(1).to(10) for (k <- tmp) { println(k) } } 


After “promotion” for in map / flatMap / foreach we have
 import scala.runtime.RichInt object Demo { val tmp: Range = new RichInt(1).to(10) tmp.foreach(elem => println(elem)) } 


Example # 2 :
 object Demo { var map = Map("France" -> "Paris") map += "Japan" -> "Tokyo" } 


After the transition from the infix form of calling the '->' and '+' methods to normal
 object Demo { var map = Map("France".->("Paris")) map = map.+("Japan".->("Tokyo")) } 


and searching for a suitable implicit conversion String to some type with the '->' method (again found in Predef.scala) gets a “desaccharized form” (String in Scala is, in fact, java.lang.String and it doesn't have method '->')
 object Demo { var map: Map[String, String] = Map.apply(new ArrowAssoc("France").->("Paris")) map = map.+((new ArrowAssoc("Japan").->("Tokyo"))) } 


From the funny: here is the source code (abbreviated) of the ArrowAssoc class from Predef.scala
  implicit class ArrowAssoc[A](private val self: A) extends AnyVal { def -> [B](y: B): Tuple2[A, B] = Tuple2(self, y) } 

thanks to generics, we can put an arrow between representatives of ANY TWO TYPES! If you make 1 -> true, then the type variable A will be taken as Int, and the type variable B - as Boolean!


"Pointless style" (infix notation) is not "point-free style" (tacit programming)



Do not confuse the pointless style (infix notation), which we are considering, with the so-called point-free style or otherwise - tacit programming .

Point-free style assumes that you build new functions from certain primitives and other functions without explicitly specifying arguments, without entering formal parameter names. The name comes from topology, where thoughts often are conducted in terms of neighborhoods and not specific points.

Consider a simple example: the function Int => Int, which returns an argument increased by 1.

Here is NOT pointless and NOT point-free style
 object Demo { val f: Int => Int = x => 1.+(x) } 

Let me remind you that in Scala '+' is a method belonging to the type Int, and not an operator. Although compiled under JVM is converted to the same operator '+' over the primitive int.

Here is pointless and NOT point-free style.
 object Demo { val f: Int => Int = x => 1 + x } 


Here is NOT pointless and point-free style (f - with placeholder, g - without placeholder)
 object Demo extends App { val f: Int => Int = 1.+(_) val g: Int => Int = 1.+ } 


That's pointless and point-free style (f - with placeholder, g - without placeholder)
 object Demo { val f: Int => Int = 1 + _ val g: Int => Int = 1 + } 


Further, we will not consider point-free / tacit-programming, this can be the subject of a separate article.


Operators priority



If we start defining our "operators", we may face a lack of priorities.
 object Demo extends App { println(I(1) add I(2) mul I(3)) } case class I(k: Int) { def add(that: I): I = I(this.k + that.k) def mul(that: I): I = I(this.k * that.k) } >> 9 


we would like the multiplication (mul) to have priority over addition (add) (that is, we want 1 + (2 * 3) = 7, not (1 + 2) * 3 = 9). However, the record type
 I(1) add I(2) mul I(3) 


Equivalent to the following
 I(1).add.(I(2)).mul(I(3)) 


Which is equivalent to such
 ( I(1).add.(I(2)) ).mul(I(3)) 


But not
 I(1).add( I(2).mul(I(3)) ) 


Since the method call is a left-associative operation , that is, there is an arrangement of brackets (convolution) from left to right.

This can be fixed by explicitly setting parentheses.
 object Demo extends App { println(I(1) add ( I(2) mul I(3) )) } case class I(k: Int) { def add(that: I): I = I(this.k + that.k) def mul(that: I): I = I(this.k * that.k) } >> 7 


or using the priority of normal calls before infix calls (not recommended style, do not mix infix calls and normal call forms, brackets are better)
 object Demo extends App { println(I(1) add I(2).mul(I(3))) } case class I(k: Int) { def add(that: I): I = I(this.k + that.k) def mul(that: I): I = I(this.k * that.k) } >> 7 


However, if we rename the methods ('mul' -> '*', 'add' -> '+'), then a bit of magic will happen without any indication of the priority '*' over '+'!
 object Demo extends App { println(I(1) + I(2) * I(3)) } case class I(k: Int) { def +(that: I): I = I(this.k + that.k) def *(that: I): I = I(this.k * that.k) } >> 7 


Open the Holy Book on the “6.12.3 Infix Operations” section and read:
The operator is determined by the operator's first character. The characters are in the same line.
 (all letters)
 |
 ^
 &
 =!
 <>
 :
 + -
 * /%
 (all other special characters)



So, if our method starts with '*', then it takes precedence over the method starting with '+'. Which, in turn, takes precedence over any name starting with a "normal letter."

So this will also work like this (it is not recommended to call similar operators (multiplication, addition) both string and operator names)
 object Demo extends App { println(I(1) add I(2) * I(3)) } case class I(k: Int) { def add(that: I): I = I(this.k + that.k) def *(that: I): I = I(this.k * that.k) } >> 7 


Consider the following expression: 1 * 2 * 3 + 4 * 5 * 6 + 7 * 8 * 9.

If operators "add" and "multiply" give string names add and mul
 object Demo extends App { println(I(1) mul I(2) mul I(3) add I(4) mul I(5) mul I(6) add I(7) mul I(8) mul I(9)) } case class I(k: Int) { def add(that: I): I = I(this.k + that.k) def mul(that: I): I = I(this.k * that.k) } 

then everything will collapse on the left
1 * 2 * 3 + 4 * 5 * 6 + 7 * 8 * 9 -> (((((((1 * 2) * 3) + 4) * 5) * 6) + 7) * 8) * 9

But in the case of the names '+' and '*'
 object Demo extends App { println(I(1) * I(2) * I(3) + I(4) * I(5) * I(6) + I(7) * I(8) * I(9)) } case class I(k: Int) { def +(that: I): I = I(this.k + that.k) def *(that: I): I = I(this.k * that.k) } 

the line will be divided into groups by equal priorities
 1 * 2 * 3 + 4 * 5 * 6 + 7 * 8 * 9 -> 
 (1 * 2 * 3) + (4 * 5 * 6) + (7 * 8 * 9)


inside each group (groups are taken from left to right) there will be a convolution from left to right
 (1 * 2 * 3) + (4 * 5 * 6) + (7 * 8 * 9) ->
 ((1 * 2) * 3) + ((4 * 5) * 6) + ((7 * 8) * 9)


after which, addition operands will be folded from left to right
 ((1 * 2) * 3) + ((4 * 5) * 6) + ((7 * 8) * 9) ->
 (((1 * 2) * 3) + ((4 * 5) * 6)) + ((7 * 8) * 9)



Operators Associativity



We read the Sacred Book , section “6.12.3 Infix Operations” further:
The associativity of an operator is determined by the operator's last character. Operators ending in a colon `: 'are right-associative. All other operators are left-associative.
...
The right-hand operand of a left-associative operator may consist of several arguments in parentheses, eg e; op; (e1, ..., en). This expression is then interpreted as e.op (e1, ..., en).

A left-associative binary operation e1; op; e2 is interpreted as e1.op (e2). If op is right associative, the same operation is interpreted as {val x = e1; e2.op (x)}, where x is a fresh name.


What does this mean in practice? This means that left-associative convolutions are by default, but for methods in the infix form, ending in a colon works - right associative. Moreover, the arguments of the operator are reversed.

This means that in the following code
 object Demo { println(I(1) ++ I(2) ++ I(3) ++ I(4)) println(I(1) +: I(2) +: I(3) +: I(4)) } case class I(k: Int) { def ++(that: I): I = I(this.k + that.k) def +:(that: I): I = I(this.k + that.k) } 

line
 I (1) ++ I (2) ++ I (3) ++ I (4)

minimized to (left associative)
 ((I (1) ++ I (2)) ++ I (3)) ++ I (4)

and then to
 ((((I (1). ++ (I (2))). ++ (I (3)) ++ I (4)


and string
 I (1) +: I (2) +: I (3) +: I (4)

collapses to (right associative)
 I (1) +: (I (2) +: (I (3) +: I (4)))

and when passing from the infix form to the usual one, inversion of the operator's arguments occurs (magic ':' at the end of the operator's name)
 I (1) +: (I (2) +: (I (3) +: I (4))) -> 
 I (1) +: (I (2) +: (I (4). + :( I (3)))) ->
 I (1) +: ((I (4). + :( I (3))). + :( I (2))) ->
 ((I (4). + :( I (3))). + :( I (2))). + :( I (1))


Question: What sick mind can this be useful for?

Well ... here's an example from the standard library (List creation)
 object Demo { val list = 0 :: 1 :: 2 :: Nil } 

Question: how did this magic happen? And why is there an empty Nil list at the end?
Everything is extremely simple: '::' is a method of the List class! With the right associativity and reverse operands.

List is defined like this (abbreviated and modified version)
 sealed abstract class List[+A] { def head: A def tail: MyList[A] def isEmpty: Boolean def ::[B >: A](x: B): List[B] = new Node(x, this) } final case class Node[A](head: A, tail: List[A]) extends List[A] { override def isEmpty: Boolean = false } object Nil extends List[Nothing] { override def head: Nothing = throw new Error override def tail: MyList[Nothing] = throw new Error override def isEmpty: Boolean = true } 


And code
 object Demo { val list = 0 :: 1 :: 2 :: Nil } 


untapped by the compiler in
 object Demo { val list = ( ( Nil.::(2) ).::(1) ).::(1) } 

That is, we simply fill the elements into a single-linked list (stack), starting with an empty list (Nil).


Infix types



Infix types (infix types) is simply a record of type constructors from two arguments in infix form.

So, in order. What is a type constructor from two arguments? This is just a generic class / trait with two type variable. Having such a class (let's call it 'ab'), we give it two types, for example, Int and String, and we get (construct) the type ab [Int, String]

We look
 object Demo extends App { val x0: ab[Int, String] = null val x1: Int ab String = null } case class ab[A, B](a: A, b: B) 

The type ab [Int, String] can simply be written in infix form as an Int ab String.

Everything becomes more fun if we call the type constructor not trivially 'ab' but magically, for example, '++'.
 object Demo extends App { val x0: ++[Int, String] = null val x1: Int ++ String = null val x2: List[Int ++ String] = null val f: Int ++ String => String ++ Int = null } case class ++[A, B](a: A, b: B) 


If you meet somewhere the magic of the form
 def f[A, B](x: A <:< B) 

or
 def f[A, B](x: A =:= B) 


Just know that there are a couple of classes in Predef.scala with the names '=: =' and '<: <'
 object Predef extends ... { .. ... class <:<[-From, +To] extends ... ... class =:=[From, To] extends ... .. } 



Prefix operators



From Scala specification
A prefix operation op; it is a prefix operator op; it is a '+', '-', '!' or '~'. The expression op; e is equivalent to the postfix method application e.unary_op.

Prefix operators must be atomic. For example, the sync signal is read as - (sin (x)), and it should be noted. .


In Scala, a programmer can define only 4 prefix operators with the names '+', '-', '!', '~'. They are defined as methods without arguments with the names 'unary_ +', 'unary_-', 'unary_!', 'Unary_ ~'.

 object Demo { val x0 = +I(0) val x1 = -I(0) val x2 = !I(0) val x3 = ~I(0) } case class I(k: Int) { //      def unary_+(): I = I(2 * this.k) def unary_-(): I = I(3 * this.k) def unary_!(): I = I(4 * this.k) def unary_~(): I = I(5 * this.k) } 


If we look through the Java Redflection API, we will see what these methods are compiled into.
 import java.lang.reflect.Method; public class Demo { public static void main(String[] args) { Class clazz = I.class; for (Method m : clazz.getDeclaredMethods()) { System.out.println(m); } } } >> public I I.unary_$plus() >> public I I.unary_$minus() >> public I I.unary_$bang() >> public I I.unary_$tilde() ... 


It should be noted that along with the prefix form, the original names are preserved (that is, the short form '+' / '-' / '!' / '~' Is just syntactic sugar to the existing one after compiling the full form of 'unary _ +' / ' unary _- '/' unary _! '/' unary_ ~ ')
 object Demo extends App { val x0 = +I(0) val x1 = -I(0) val x2 = !I(0) val x3 = ~I(0) //    val y0 = I(0).unary_+() val y1 = I(0).unary_-() val y2 = I(0).unary_!() val y3 = I(0).unary_~() } case class I(k: Int) { //      def unary_+(): I = I(2 * this.k) def unary_-(): I = I(3 * this.k) def unary_!(): I = I(4 * this.k) def unary_~(): I = I(5 * this.k) } 



Postfix operators



Methods in the postfix form are methods without an argument that were called without a dot. For a number of reasons, methods in the postfix notation are the cause of many errors (see for a start here and here ).
 object Demo { val tailList0 = List(0, 1, 2).tail // "normal" notation val tailList1 = List(0, 1, 2) tail // postfix/suffix notation } 


Let's try to determine factorial on integers (def!).
First, let's turn our attention to 100,500 ways to call a method in Scala
 object Demo extends App { val a = I(0).!() val b = I(0).! val c = I(0) !() val d = I(0) ! // postfix notation } case class I(k: Int) { def !(): I = I(2 * this.k) //    ,    } 


Make the method '!' on the wrapper class
 object Demo extends App { val x: I = I(5)!; println(x) } case class I(k: Int) { def !(): I = if (k == 0) I(1) else I(k) * (I(k - 1)!) def *(that: I): I = I(this.k * that.k) } >> I(120) 

Note that the semicolon is obligatory at the end of the first line, otherwise it is NOT COMPILATED (postfix is ​​pain, yes)!

Hide the explicit presence of a wrapper class under implicit (Int -> I, I -> Int)
 object Demo extends App { implicit class I(val k: Int) { def !(): I = if (k == 0) I(1) else I(k) * (I(k - 1)!) def *(that: I): I = I(this.k * that.k) } implicit def toInt(x: I): Int = xk val x: Int = 5!; println(x) } >> 120 


Now let's hide the implicit ones
 object Demo extends App { import MathLib._ val x: Int = 5!; println(x) } object MathLib { implicit class I(val k: Int) { def !(): I = if (k == 0) I(1) else I(k) * (I(k - 1)!) def *(that: I): I = I(this.k * that.k) } implicit def toInt(x: I): Int = xk } >> 120 



About the course



Announced a course on Scala is laid out on the UDEMY MOOC platform - "Scala for Java Developers" . The initial idea of ​​writing a comprehensive course on all aspects of the language and the most popular type acrobatic libraries (scalaz, shapeless) has been preserved, but has undergone minor changes.
It was decided to cut the original 32-hour large course for $ 399 into two 16-hour courses for $ 199 each (if you enter the coupon code HABR-OPERATOR on UDEMY or simply follow the link udemy.com/scala-for-java-developers- ru /? couponCode = HABR-OPERATOR , then the price with a discount will be $ 179, the number and duration of coupon codes is limited). It was decided to saturate the course with tests (there will be more than 50 tests for 5-15 questions with code examples for each part of the course).
The first course was shot at 75% (12 hours out of 16) and uploaded to UDEMY by 50% (8 hours out of 16), as part of the video is being processed.

The first part includes such topics.


The second part (while at the stage of elaboration) includes such topics.


Note # 1 : a number of topics (OOP, Generics, Scala types, ...) were decided to be divided into 2 or even 3 parts because of the complexity and importance of the issue (the first parts are located in the first part of the course, the last - in the second part of the course (“OOP -III: inheritance, Cake Pattern ”,“ Generics-II: existential types, higher-king types ”, ...)).

Note # 2 : in view of the fact that many programmers have certain problems with mathematics (we teach 3 semesters "matan", but no more useful for a programmer "discrete disciplines" - set theory, discrete mathematics, mathematical logic, algebra, combinatorics, theory categories, formal languages ​​/ grammars, ...) and because functional programming is strongly using mathematical concepts, several sections of mathematics are introduced into the course (all mathematics are “encoded in Scala”, so without university “spherical codes s in vacuo ").

PS I will answer all questions in the comments / messages in the "lichku" or by contact
skype: GolovachCourses
email: GolovachCourses@gmail.com

PPS Simultaneously with the development of the Scala course, the author conducts internal trainings on Scala in IT companies, trainings at conferences , gives reports on Scala and provides consulting services when translating projects from Java to Scala.

Source: https://habr.com/ru/post/255631/


All Articles