Классы значений вводят нежелательные общедоступные методы

Посмотрев на некоторые scala -docs моих библиотек, мне показалось, что из классов значений возникает какой-то нежелательный шум. Например:

implicit class RichInt(val i: Int) extends AnyVal {
  def squared = i * i
}

Это вводит нежелательный символ i:

4.i   // arghh....

Этот материал появляется как в документах scala, так и в автоматическом завершении IDE, что действительно не хорошо.

Итак... любые идеи о том, как смягчить эту проблему? Я имею в виду, что вы можете использовать RichInt(val self: Int), но это не делает его лучше (4.self, wth?)


ИЗМЕНИТЬ

В следующем примере компилятор удаляет промежуточный объект или нет?

import language.implicitConversions

object Definition {
  trait IntOps extends Any { def squared: Int }
  implicit private class IntOpsImpl(val i: Int) extends AnyVal with IntOps {
    def squared = i * i
  }
  implicit def IntOps(i: Int): IntOps = new IntOpsImpl(i)  // optimised or not?
}

object Application {
  import Definition._
  // 4.i  -- forbidden
  4.squared
}

Ответы

Ответ 1

В Scala 2.11 вы можете сделать val приватным, что устраняет эту проблему:

implicit class RichInt(private val i: Int) extends AnyVal {
  def squared = i * i
}

Ответ 2

Он вводит шум (примечание: в 2.10, в 2.11, а за пределами вас просто объявляет val приватным). Вы не всегда хотите. Но так, как сейчас.

Вы не можете обойти проблему, следуя шаблону private-value-class, потому что компилятор не может видеть, что это класс значений в конце его, поэтому он проходит через общий маршрут. Здесь байт-код:

   12: invokevirtual #24;
          //Method Definition$.IntOps:(I)LDefinition$IntOps;
   15: invokeinterface #30,  1;
          //InterfaceMethod Definition$IntOps.squared:()I

Посмотрите, как первая возвращает копию класса Definition$IntOps? Он в коробке.

Но эти два шаблона работают, вроде:

(1) Общий шаблон имен.

implicit class RichInt(val repr: Int) extends AnyVal { ... }
implicit class RichInt(val underlying: Int) extends AnyVal { ... }

Используйте один из них. Добавление i в качестве метода раздражает. Добавление underlying, когда нет ничего основополагающего, не так уж плохо - вы попадете только в него, если вы все равно попытаетесь получить базовое значение. И если вы продолжаете использовать одно и то же имя снова и снова:

implicit class RicherInt(val repr: Int) extends AnyVal { def sq = repr * repr }
implicit class RichestInt(val repr: Int) extends AnyVal { def cu = repr * repr * repr }

scala> scala> 3.cu
res5: Int = 27

scala> 3.repr
<console>:10: error: type mismatch;
 found   : Int(3)
 required: ?{def repr: ?}
Note that implicit conversions are not applicable because they are ambiguous:
 both method RicherInt of type (repr: Int)RicherInt
 and method RichestInt of type (repr: Int)RichestInt

имя collade sorta все равно заботится о вашей проблеме. Если вы действительно этого хотите, вы можете создать пустой класс значений, который существует только для столкновения с repr.

(2) Явный неявный шаблон

Иногда вы внутренне хотите, чтобы ваше значение называлось чем-то более коротким или более мнемоническим, чем repr или underlying, не делая его доступным для исходного типа. Один из вариантов заключается в том, чтобы создать переадресацию как таковую:

class IntWithPowers(val i: Int) extends AnyVal {
  def sq = i*i
  def cu = i*i*i 
}
implicit class EnableIntPowers(val repr: Int) extends AnyVal { 
  def pow = new IntWithPowers(repr)
}

Теперь вам нужно называть 3.pow.sq вместо 3.sq - это может быть хорошим способом вырезать пространство имен! - и вам не нужно беспокоиться о загрязнении пространства имен за пределами оригинала repr.

Ответ 3

Возможно, проблема заключается в разнородных сценариях, для которых были построены классы значений. Из SIP:

• Вложенные неявные обертки. Методы этих оберток будут переведены в методы расширения.

• Новые числовые классы, такие как unsigned ints. Для таких классов больше не нужно будет бокс-накладных расходов. Таким образом, это похоже на классы значений в .NET.

• Классы, представляющие единицы измерения. Опять же, для этих классов не было бы расходов на бокс.

Я думаю, что есть разница между первым и вторым. В первом случае класс значения должен быть прозрачным. Вы не ожидали бы какого-либо типа RichInt, но на самом деле вы работаете только с Int. Во втором случае, например, 4.meters, я понимаю, что получение фактического "значения" имеет смысл, поэтому требуется val.

Это разделение снова отражается в определении класса значений:

1. C должен иметь ровно один параметр, который помечен значком val и который имеет общедоступную доступность.

...

7. C должен быть эфемерным.

Последнее означает, что у него нет других полей и т.д., что противоречит № 1.

С

class C(val u: U) extends AnyVal

единственное место в SIP, где используется u, - это примеры реализации (например, def extension$plus($this: Meter, other: Meter) = new Meter($this.underlying + other.underlying)); а затем в промежуточных представлениях, только для окончательного удаления:

new C(e).u ⇒ e

Промежуточное представление, доступное для синтетических методов IMO, является тем, что также может быть выполнено компилятором, но не должно быть видимым в написанном пользователем коде. (I.e., вы можете использовать val, если хотите получить доступ к одноранговым узлам, но не обязательно).

Ответ 4

Возможность использовать имя, которое затенено:

implicit class IntOps(val toInt: Int) extends AnyVal {
  def squared = toInt * toInt
}

или

implicit class IntOps(val toInt: Int) extends AnyVal { ops =>
  import ops.{toInt => value}
  def squared = value * value
}

Это все равно окажется в scala -docs, но, по крайней мере, вызов 4.toInt не путает, фактически не запускает IntOps.

Ответ 5

Я не уверен, что это "нежелательный шум", поскольку, по-моему, почти всегда вам нужно получить доступ к базовым значениям при использовании RichInt. Рассмотрим это:

// writing ${r} we use a RichInt where an Int is required
scala> def squareMe(r: RichInt) = s"${r} squared is ${r.squared}"
squareMe: (r: RichInt)String

// results are not what we hoped, we wanted "2", not "[email protected]"
scala> squareMe(2)
res1: String = [email protected] squared is 4

// we actually need to access the underlying i
scala> def squareMeRight(r: RichInt) = s"${r.i} squared is ${r.squared}"
squareMe: (r: RichInt)String

Кроме того, если у вас есть метод, который добавляет два RichInt, вам нужно будет снова получить доступ к базовому значению:

scala> implicit class ImplRichInt(val i: Int) extends AnyVal {
     |   def Add(that: ImplRichInt) = new ImplRichInt(i + that) // nope...
     | }
<console>:12: error: overloaded method value + with alternatives:
  (x: Int)Int <and>
  (x: Char)Int <and>
  (x: Short)Int <and>
  (x: Byte)Int
 cannot be applied to (ImplRichInt)
         def Add(that: ImplRichInt) = new ImplRichInt(i + that)
                                                        ^

scala> implicit class ImplRichInt(val i: Int) extends AnyVal {
     |   def Add(that: ImplRichInt) = new ImplRichInt(i + that.i)
     | }
defined class ImplRichInt

scala> 2.Add(4)
res7: ImplRichInt = [email protected]