Scalaのオペレーターの過負荷

少し前に、 Scalaコースを発表しました 。 彼はUDEMY MOOCプラットフォーム- 「Scala for Java Developers」に着手しました。 コースの詳細については、記事の最後をご覧ください



次に、コースのトピックの1つであるScalaでの演算子のオーバーロードに関する資料を提示したいと思います。











はじめに



(メソッド以外のエンティティとして)演算子がないため、Scalaは演算子をオーバーロードしません。 「+」、「/」、「::」、「<〜」の形式の記号(演算子)名、および記述の接頭辞/中置/後置形式のメソッドがあります。 ただし、便宜上、以下では演算子という用語を使用します。





中置演算子





Scalaでは、単一引数のメソッドは、いわゆる中置演算で記述できます。 すなわち





例:

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) }
      
      





さらに例では、case-class Iが表示され、それぞれの場合に異なるメソッドを付与します。 簡潔にするためだけに作成されました(フィールドはプライマリコンストラクターによって自動的に生成および初期化されます+コンパニオンオブジェクトは、プライマリコンストラクターと同じシグネチャを持つapplyメソッドで自動的に生成されます。これにより、新しいI(k)ではなくI(k)を介してインスタンスを作成できます。 I(k)はI.apply(k)と同等であり、Scalaのapplyメソッドは省略できます)。 クラスIは1つのIntの「ラッパー」であり、複素数、多項式、行列の完全なクラスのプロトタイプと見なすことができます。



メソッドに「シンボリック」/「演算子」の名前を付けると、すべてがより興味深いものになります

 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) }
      
      





JVM(クラスファイル形式)は「演算子文字」からの名前をサポートしないため、コンパイル中に合成名が生成されます。



クラスで実行

 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リフレクション

 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)
      
      







はい、Javaメソッドはそのような名前で表示されます(クラスファイルなど)

 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()); } }
      
      





JVMの下でコンパイルされたすべての言語の透過的な統合を覚えていますか?



はい、一般的に、Scalaの「構文上のトリック」の半分は、中置表記法と暗黙の変換の混合で構成されています。



例#1

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







中置記法は通常に変換されます

 object Demo { for (k <- 1.to(10)) { println(k) }
      
      







Intには「to」メソッドがないため、「to」メソッドと適切な署名を使用してIntを何らかの型に変換できる暗黙的な変換が求められます。



そして、それはPredef.scalaにあります(java.lang。* + Scala。* + Predef。*コンパイル前に各ファイルに暗黙的にインポートされます。)



 //   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には、Int型の1つの引数を持つ 'to'メソッドが既にあります。

 //   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) ... }
      
      







したがって、コンパイル時に、次のような「巻き戻し」が行われます。

 import scala.runtime.RichInt object Demo { val tmp: Range = new RichInt(1).to(10) for (k <- tmp) { println(k) } }
      
      







for in map / flatMap / foreachの「プロモーション」の後、

 import scala.runtime.RichInt object Demo { val tmp: Range = new RichInt(1).to(10) tmp.foreach(elem => println(elem)) }
      
      







例#2

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







「->」および「+」メソッドを呼び出す中置形式から通常への移行後

 object Demo { var map = Map("France".->("Paris")) map = map.+("Japan".->("Tokyo")) }
      
      







メソッド '->'(再びPredef.scalaにあります)を使用して適切な暗黙の変換文字列を検索し、「非糖化形式」を取得します(Scalaの文字列は基本的にjava.lang.Stringであり、メソッド '->')

 object Demo { var map: Map[String, String] = Map.apply(new ArrowAssoc("France").->("Paris")) map = map.+((new ArrowAssoc("Japan").->("Tokyo"))) }
      
      







面白いから:Predef.scalaのArrowAssocクラスのソースコード(短縮)です。

  implicit class ArrowAssoc[A](private val self: A) extends AnyVal { def -> [B](y: B): Tuple2[A, B] = Tuple2(self, y) }
      
      





ジェネリックのおかげで、2つのタイプの代表者の間に矢印を置くことができます! 1-> trueを実行すると、Intに型変数Aが、ブール型に変数Bが入力されます!





「無意味なスタイル」(中置記法)は「無ポイントなスタイル」(暗黙のプログラミング)ではありません





私たちが検討している無意味なスタイル(中置記法)を、いわゆるポイントフリースタイルまたは別の方法で暗黙のプログラミングと混同しないでください。



ポイントフリースタイルでは、引数を明示的に指定せずに、正式なパラメーター名を入力せずに、特定のプリミティブや他の関数から新しい関数を作成することを想定しています。 名前は、特定のポイントではなく近隣の観点で反映されることが多いトポロジに由来します。



単純な例を考えてみましょう。関数Int => Intは、1ずつ増加した引数を返します。



これは無意味でも無意味でもないスタイルです

 object Demo { val f: Int => Int = x => 1.+(x) }
      
      





Scalaでは、「+」はInt型のメソッドであり、演算子ではないことを思い出させてください。 JVMでコンパイルする場合、intプリミティブの「+」演算子に変換されますが。



これは無意味で、無意味なスタイルではありません

 object Demo { val f: Int => Int = x => 1 + x }
      
      







これは無意味で無意味なスタイルではありません(f-プレースホルダーあり、g-プレースホルダーなし)

 object Demo extends App { val f: Int => Int = 1.+(_) val g: Int => Int = 1.+ }
      
      







無意味と無意味の両方のスタイルがあります(f-プレースホルダーあり、g-プレースホルダーなし)

 object Demo { val f: Int => Int = 1 + _ val g: Int => Int = 1 + }
      
      







さらに、ポイントフリー/暗黙のプログラミングについては考慮しません。これは別の記事の主題になる場合があります。





オペレーターの優先度





「演算子」を定義し始めると、優先順位の欠如に直面する可能性があります

 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
      
      







乗算(mul)が加算(add)よりも優先されるようにします(つまり、(1 + 2)* 3 = 9ではなく1 +(2 * 3)= 7にします)。 ただし、ビューレコード

 I(1) add I(2) mul I(3)
      
      







以下と同等

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







これは同等です

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







でもない

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







メソッド呼び出しは左結合操作であるため 、つまり、括弧は左から右に配置されます(畳み込み)。



これは、明示的な括弧で修正できます。

 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
      
      







または、通常の呼び出しを中置呼び出しよりも優先します (推奨されないスタイル、 中置呼び出しと通常形式の呼び出しを混在させないでください、括弧はより良いです)

 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
      
      







ただし、メソッドの名前を変更すると(「mul」->「*」、「add」->「+」)、「+」よりも「*」の優先順位を示すことなく、少し魔法が発生します!

 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
      
      







「6.12.3 Infix Operations」セクションの聖典を開き、次を読みます。

中置演算子の優先順位は、演算子の最初の文字によって決まります。 同じ優先順位の同じ行の文字を使用して、文字を優先順位の高い順に以下にリストします。

 (すべての文字)
 |
 ^
 &
 =!
 <>
 :
 +-
 * /%
 (他のすべての特殊文字)






したがって、メソッドが「*」で始まる場合、「+」で始まるメソッドよりも優先されます。 これは、「通常の文字」で始まる名前よりも優先されます。



したがって、これもこのように機能します(文字列と演算子名で同様の演算子(乗算、加算)を呼び出すことは推奨されません)

 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
      
      







次の式を検討してください:1 * 2 * 3 + 4 * 5 * 6 + 7 * 8 * 9。



演算子「add」および「multiply」がストリング名にaddおよび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) }
      
      





その後、すべてが左側の畳み込みになります

1 * 2 * 3 + 4 * 5 * 6 + 7 * 8 * 9->(((((((((1 * 2)* 3)+ 4)* 5)* 6)+ 7)* 8)* 9



しかし、名前「+」および「*」の場合

 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) }
      
      





ラインは、同じ優先度に従ってグループに分割されます

 1 * 2 * 3 + 4 * 5 * 6 + 7 * 8 * 9-> 
 (1 * 2 * 3)+(4 * 5 * 6)+(7 * 8 * 9)




各グループ内(グループは左から右に取られます)で、左から右への畳み込みがあります

 (1 * 2 * 3)+(4 * 5 * 6)+(7 * 8 * 9)->
 ((1 * 2)* 3)+((4 * 5)* 6)+((7 * 8)* 9)




その後、追加オペランドが左から右に折り返されます

 ((1 * 2)* 3)+((4 * 5)* 6)+((7 * 8)* 9)->
 (((1 * 2)* 3)+((4 * 5)* 6))+((7 * 8)* 9)






演算子の結合性





聖典のセクション「6.12.3 Infix Operations」をさらに読みます。

演算子の結合性は、演算子の最後の文字によって決まります。 コロン `: 'で終わる演算子は右結合です。 他のすべての演算子は左結合です。

...

左結合演算子の右オペランドは、e; op;(e1、...、en)などの括弧で囲まれたいくつかの引数で構成されます。 この式は、e.op(e1、...、en)として解釈されます。



左結合二項演算e1; op; e2は、e1.op(e2)として解釈されます。 opが右結合の場合、同じ演算は{val x = e1; e2.op(x)}、ここでxは新しい名前です。





これは実際にはどういう意味ですか? これは、左結合畳み込みがデフォルトであることを意味しますが、コロンで終わる中置形式のメソッドでは機能します-右結合。 さらに、演算子の引数が交換されます。



これは、次のコードで

 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) }
      
      





ひも

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


折りたたみ(左結合)

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


そしてその後まで

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




そしてライン

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


崩壊(連想権)

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


そして、中置形式から通常の形式への移行中に、演算子の引数が反転されます (演算子名の最後に魔法の「:」)

 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))




質問:これはどの病人にとって有用ですか?



さて...ここに標準ライブラリの例があります(リストの作成)

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





質問:この魔法はどのように発生しましたか? そして、最後に空のNilリストがあるのはなぜですか?

すべてが非常に簡単です: '::'はListクラスのメソッドです! 適切な連想オペランドと逆オペランドを使用します。



リストは次のように定義されます(短縮および変更されたバージョン)

 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 }
      
      







そしてコード

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







コンパイラーにより

 object Demo { val list = ( ( Nil.::(2) ).::(1) ).::(1) }
      
      





つまり、空のリスト(Nil)で始まる単純なリンクリスト(スタック)に要素を単純に詰め込みます。





中置型





中置型は、中置形式の2つの引数からの型構築子の単なる記録です。



だから、順番に。 2つの引数からの型コンストラクタとは何ですか? これは、2つの型変数を持つ単なる汎用クラス/特性です。 このようなクラスを作成し(「ab」と呼びましょう)、2つの型(たとえば、IntとString)を指定し、型abを取得(構築)します[Int、String]



見て

 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)
      
      





タイプab [Int、String]は、Int ab Stringとして中置形式で簡単に記述できます。



型コンストラクタを単純な「ab」ではなく、「++」などの魔法のように呼び出すと、すべてがより楽しくなります。

 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)
      
      







ある種の魔法に出会ったら

 def f[A, B](x: A <:< B)
      
      





または

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







Predef.scalaには、「=:=」および「<:<」という名前のクラスがいくつかあることを知ってください。

 object Predef extends ... { .. ... class <:<[-From, +To] extends ... ... class =:=[From, To] extends ... .. }
      
      









プレフィックス演算子





Scala仕様から

プレフィックス操作op; eは、プレフィックス演算子opで構成され、識別子「+」、「-」、「!」のいずれかでなければなりません または「〜」。 式op; eは、後置法アプリケーションe.unary_opと同等です。



プレフィックス演算子は、オペランド式がアトミックである必要がないという点で、通常の関数アプリケーションとは異なります。 たとえば、入力シーケンス-sin(x)は-(sin(x))として読み取られますが、関数適用否定sin(x)は、オペランド否定および(x)に対する中置演算子sinの適用として解析されます。 。




Scalaでは、プログラマーは「+」、「-」、「!」、「〜」という名前のプレフィックス演算子を4つだけ定義できます。 これらは、名前が「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) }
      
      







Java Redflection APIを使用して見ると、これらのメソッドが何にコンパイルされているかがわかります

 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() ...
      
      







接頭辞形式とともに、元の名前が保存されていることに注意する必要があります(つまり、短い形式「+」/「-」/「!」/「〜」は、既存の完全な形式「単項_ +」/「単項_- '/'単項_! '/'単項_〜 ')

 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メソッドは、ピリオドなしで呼び出される引数のないメソッドです。 いくつかの理由から、接尾辞表記法のメソッドが多くのエラーの原因です( ここと最初を見てください )。

 object Demo { val tailList0 = List(0, 1, 2).tail // "normal" notation val tailList1 = List(0, 1, 2) tail // postfix/suffix notation }
      
      







整数の階乗を決定してみましょう(def!)。

まず、Scalaでメソッドを呼び出す100500の方法に注目しましょう

 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) //    ,    }
      
      







「!」メソッドを作成しましょう ラッパークラス

 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)
      
      





最初の行の最後にはセミコロンが必須であることに注意してください。そうでない場合はコンパイルしません(後置は苦痛です、はい)。



暗黙的なsの下でラッパークラスの明示的な存在を隠します(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
      
      







暗黙のものを隠してみましょう

 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
      
      









コースについて





発表されたScalaコースは UDEMY MOOCプラットフォーム- 「Scala for Java Developers」 アップロードされています。 言語のすべての側面と最も人気のある「タイプアクロバティック」ライブラリ(scalaz、shapeless)に関する包括的なコースを作成するという元のアイデアは保持されていますが、小さな変更が加えられています。

元の大きな399ドルの32時間コースは、199ドルで2つの16時間コースに「カット」されることになりました(UDEMYでHABR-OPERATORクーポンコードを入力するか、単にudemy.com/scala-for-java-developers- com /?ticketCode = HABR-OPERATORの場合、割引価格は179ドルになり、割引クーポンの数量と有効性は制限されます)。 テストでコースを飽和させることが決定されました(コースの各部分のコード例で5〜15の質問の50以上のテストがあります)。

ビデオの一部が処理されているため、最初のコースは75%(16時間のうち12時間)で撮影され、50%(16時間のうち8時間)でUDEMYにレイアウトされました。



最初の部分にはそのようなトピックが含まれています





2番目の部分(まだ開発中)には、このようなトピックが含まれています





注#1 :問題の複雑さと重要性のために、いくつかのトピック(OOP、Generics、Scalaタイプなど)を2つまたは3つの部分に分割することが決定されました(最初の部分はコースの最初の部分にあり、最後はコースの2番目の部分にあります(「OOP -III:継承、Cake Pattern ''、“ Generics-II:存在型、上位王型”、...))。



注#2 :多くのプログラマーが数学に特定の問題を抱えているという事実のため(「マタン」の3学期を教えていますが、プログラマーにとってもはや有用ではない「離散した分野」-セット理論、離散数学、数学論理、代数、組み合わせ論、理論カテゴリ、形式言語/文法など)、および関数型プログラミングは数学の概念を強力に使用しているため、数学のいくつかのセクションがコースに導入されます(すべての数学は「Scalaでエンコード」され、大学はありません )」真空中でです。



PS私は「PM」または連絡先のコメント/メッセージのすべての質問に答えます

スカイプ:GolovachCourses

メール:GolovachCourses@gmail.com



PPS Scalaコースの開発に加えて、著者はIT企業でScalaの内部トレーニング、会議でのトレーニングを行い 、Scalaでプレゼンテーションを行い 、プロジェクトをJavaからScalaに翻訳する際にコンサルティングサービスを提供します。



All Articles