Scala Extractor не применяется дважды

Я только что обнаружил, что unapply в моем экстракторе по какой-то причине вызывается дважды. Кто знает почему и как этого избежать?

val data = List("a","b","c","d","e")

object Uap {
  def unapply( s:String ) = {
    println("S: "+s)
    Some(s+"!")
  }             
}

println( data.collect{ case Uap(x) => x } )

Это производит вывод:

S: a
S: a
S: b
S: b
S: c
S: c
S: d
S: d
S: e
S: e
List(a!, b!, c!, d!, e!)

Окончательный результат прекрасен, но в моей реальной программе неприменение нетривиально, поэтому я, конечно, не хочу вызывать его дважды!


person Greg    schedule 11.06.2013    source источник


Ответы (4)


collect принимает PartialFunction в качестве входных данных. PartialFunction определяет два ключевых члена: isDefinedAt и apply. Когда функция collect запускает вашу функцию, она запускает ваш экстрактор один раз, чтобы определить, есть ли у вашей функции isDefinedAt какие-то конкретные входные данные, и если это так, то он снова запускает экстрактор как часть apply для извлечения значения.

Если есть тривиальный способ правильной реализации isDefinedAt, вы можете реализовать его самостоятельно, явно реализуя свою собственную PartialFunction вместо использования синтаксиса case. или вы можете сделать filter, а затем map с общей функцией в коллекции (что, по сути, и делает сбор, вызывая isDefinedAt, затем apply)

Другим вариантом было бы lift частичной функции в общую функцию. PartialFunction определяет lift, который превращает PartialFunction[A,B] в A=>Option[B]. Вы можете использовать эту расширенную функцию (назовем ее fun) для выполнения: data.map(fun).collect { case Some(x) => x }

person stew    schedule 11.06.2013
comment
Другой вариант — кэшировать последнее значение в объекте частичной функции. - person ziggystar; 12.06.2013
comment
@Greg Это правда, но это своего рода старая школа, поскольку это деталь реализации, которая не соответствует действительности в 2.11, и имеет несколько нормальное исправление в 2.10. - person som-snytt; 12.06.2013

На самом деле, это было исправлено в версии 2.11 как ошибка производительности:

$ skala
Welcome to Scala version 2.11.0-20130423-194141-5ec9dbd6a9 (Java HotSpot(TM) 64-Bit Server VM, Java 1.7.0_06).
Type in expressions to have them evaluated.
Type :help for more information.

scala> val data = List("a","b","c","d","e")
data: List[String] = List(a, b, c, d, e)

scala> 

scala> object Uap {
     |   def unapply( s:String ) = {
     |     println("S: "+s)
     |     Some(s+"!")
     |   }             
     | }
defined object Uap

scala> 

scala> println( data.collect{ case Uap(x) => x } )
S: a
S: b
S: c
S: d
S: e
List(a!, b!, c!, d!, e!)

См. примечания по эффективности на применить или иначе.

Вот версия для 2.10, где проблема легко решается расширением:

object Test extends App {
  import scala.collection.TraversableLike
  import scala.collection.generic.CanBuildFrom
  import scala.collection.immutable.StringLike

  implicit class Collector[A, Repr, C <: TraversableLike[A, Repr]](val c: C) extends AnyVal {
    def collecting[B, That](pf: PartialFunction[A, B])(implicit bf: CanBuildFrom[Repr, B, That]): That = {
      val b = bf(c.repr)
      c.foreach(pf.runWith(b += _))
      b.result
    }
  }

  val data = List("a","b","c","d","e")

  object Uap {
    def unapply( s:String ) = {
      println("S: "+s)
      s match {
        case "foo" => None
        case _     => Some(s+"!")
      }
    }
  }
  val c = Collector[String, List[String], List[String]](data)
  Console println c.collecting { case Uap(x) => x }
}

Результат:

$ scalac -version
Scala compiler version 2.10.1 -- Copyright 2002-2013, LAMP/EPFL

apm@halyard ~/tmp
$ scalac applyorelse.scala ; scala applyorelse.Test
S: a
S: b
S: c
S: d
S: e
List(a!, b!, c!, d!, e!)

Обратите внимание, что эта версия Uap является частичной:

scala> val data = List("a","b","c","d","e", "foo")
data: List[String] = List(a, b, c, d, e, foo)

scala> data.map{ case Uap(x) => x }
S: a
S: b
S: c
S: d
S: e
S: foo
scala.MatchError: foo (of class java.lang.String)

Я думаю, что если вариант использования — PF, код должен быть частичным.

person som-snytt    schedule 11.06.2013

Добавляя к ответу @stew, collect реализуется как:

def collect[B, That](pf: PartialFunction[A, B])(implicit bf: CanBuildFrom[Repr, B, That]): That = {
val b = bf(repr)
for (x <- this) if (pf.isDefinedAt(x)) b += pf(x)
b.result
}

Он использует pf.isDefinedAt(x). Выполнение scalac -Xprint:typer check.scala (check.scala содержит ваш код). Он печатает:

....
final def isDefinedAt(x1: String): Boolean = ((x1.asInstanceOf[String]:String): String @unchecked) match {
      case check.this.Uap.unapply(<unapply-selector>) <unapply> ((x @ _)) => true
      case (defaultCase$ @ _) => false
    }

Итак, как вы видите, здесь снова вызывается unapply. Это объясняет, почему он печатает дважды, то есть один раз, чтобы проверить, определен ли он, а затем следующий, когда он уже вызывается в `pf(x).

@som-snytt прав. Начиная со Scala 2.11, функция collect в TraversableLike изменена на:

def collect[B, That](pf: PartialFunction[A, B])(implicit bf: CanBuildFrom[Repr, B, That]): That = {
val b = bf(repr)
foreach(pf.runWith(b += _))
b.result
}

Причина, по которой он печатается только один раз, заключается в том, что внутри он вызывает applyOrElse, который проверяет, определен ли он. Если да, функция применяется там же (в приведенном выше случае (b += _)). Следовательно, он печатается только один раз.

person Jatin    schedule 12.06.2013

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

scala>    println( data.map{ case Uap(x) => x } )
S: a
S: b
S: c
S: d
S: e
List(a!, b!, c!, d!, e!)

Не знаю, почему это работает, почему.

person Jakozaur    schedule 11.06.2013
comment
Не думай, что я могу. В моем реальном приложении я фактически выполняю логику выбора в неприменении, поэтому оно не всегда возвращает Some (что-то). Карте всегда нужен результат 1 к 1. - person Greg; 11.06.2013
comment
Поскольку совпадение с шаблоном OP не является частичным, я думаю, что было бы несправедливо голосовать против. Варианты использования имеют значение. - person som-snytt; 12.06.2013