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

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

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

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

4.i   // arghh....

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

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


ИЗМЕНИТЬ:

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

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
}

person 0__    schedule 30.07.2013    source источник
comment
Он собирался сказать, сделайте его private или потеряйте квалификатор, но, видимо, это не разрешено для классов значений. Поэтому я думаю, что ответ таков: вы не можете.   -  person Tobias Brandt    schedule 30.07.2013
comment
Или еще лучше: 4.i.i.i.i.i.i   -  person nilskp    schedule 30.07.2013


Ответы (5)


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

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

Это вносит шум (примечание: в 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

коллизия имен решает вашу проблему в любом случае. Если вы действительно хотите, вы можете создать пустой класс значений, который существует только для того, чтобы сталкиваться с 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.

person Rex Kerr    schedule 30.07.2013
comment
Хорошие моменты. Что касается (2), см. мой краткий ответ для переименования на основе импорта. - person 0__; 31.07.2013

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

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

• Новые числовые классы, такие как целые без знака. Для таких классов больше не нужно было бы накладных расходов на бокс. Так что это похоже на классы значений в .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, также может быть выполнено компилятором, но не должно быть видно в коде, написанном пользователем. (Т.е. вы можете использовать val, если хотите получить доступ к одноранговому узлу, но не обязаны).

person 0__    schedule 30.07.2013

Возможно использование затененного имени:

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

Or

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

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

person 0__    schedule 30.07.2013

Я не уверен, что это «нежелательный шум», так как я думаю, что вам почти всегда потребуется доступ к базовым значениям при использовании вашего 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 "RichInt@2"
scala> squareMe(2)
res1: String = RichInt@2 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 = ImplRichInt@6
person Paolo Falabella    schedule 30.07.2013
comment
Это как бы показывает шизофреническую природу классов ценности. С одной стороны, идея состоит в том, чтобы разрешить такие вещи, как маркировка юнитов (ваш первый пример). В этом случае вам не обязательно думать о неявных классах. С другой стороны, это механизм для получения бесплатных методов расширения. В этом случае вы хотите, чтобы класс был прозрачным, никогда не возвращал тип RichInt, поэтому требование val не имеет смысла. - person 0__; 30.07.2013
comment
@0__ Думаю, я согласен: насколько я их понимаю, классы значений не предназначены для инкапсуляции или скрытия того факта, что они представляют собой тонкий слой над типом значения, который они оборачивают. С другой стороны, неявные классы предназначены для того, чтобы позволить компилятору заменять один тип другим (и не заботиться о базовом типе). Неявные классы значений, смешивая эти два свойства, имеют тенденцию выглядеть немного неуклюжими... - person Paolo Falabella; 30.07.2013