Consider this (very ugly code):
object ExternalReferences2 {
import java.util.regex._
implicit def symbol2string(sym: Symbol) = sym.name
object Mapping {
def fromXml(mapping: scala.xml.NodeSeq) = {
new Mapping(mapping \ 'vendor text,
mapping \ 'match text,
mapping \ 'format text)
}
}
case class Mapping(vendor: String,
matches: String,
format: String) extends PartialFunction[String, String] {
private val pattern = Pattern.compile(matches)
private var _currentMatcher: Matcher = null
private def currentMatcher =
{ println("Getting matcher: " + _currentMatcher); _currentMatcher }
private def currentMatcher_=(matcher: Matcher) =
{ println("Setting matcher: " + matcher); _currentMatcher = matcher }
def isDefinedAt(entity: String) =
{ currentMatcher = pattern.matcher(entity); currentMatcher.matches }
def apply(entity: String) = apply
def apply = {
val range = 0 until currentMatcher.groupCount()
val groups = range
map (currentMatcher.group(_))
filterNot (_ == null)
map (_.replace('.', '/'))
format.format(groups: _*)
}
}
val config =
<external-links>
<mapping>
<vendor>OpenJDK</vendor>
<match>{ """^(javax?|sunw?|com.sun|org\.(ietf\.jgss|omg|w3c\.dom|xml\.sax))(\.[^.]+)+$""" }</match>
<format>{ "http://download.oracle.com/javase/7/docs/api/%s.html" }</format>
</mapping>
</external-links>
def getLinkNew(entity: String) =
(config \ 'mapping)
collectFirst({ case m => Mapping.fromXml(m)})
map(_.apply)
def getLinkOld(entity: String) =
(config \ 'mapping).view
map(m => Mapping.fromXml(m))
find(_.isDefinedAt(entity))
map(_.apply)
}
I tried to improve the getLinkOld method by using collectFirst as shown in getLinkNew, but I always get a NullPointerException because _currentMatcher is still set to null
scala> ExternalReferences2.getLinkNew("java.util.Date")
Getting matcher: null
java.lang.NullPointerException
at ExternalReferences2$Mapping.apply(<console>:32)
at ExternalReferences2$$anonfun$getLinkNew$2.apply(<console>:58)
at ExternalReferences2$$anonfun$getLinkNew$2.apply(<console>:58)
at scala.Option.map(Option.scala:131)
at ExternalReferences2$.getLinkNew(<console>:58)
at .<init>(<console>:13)
at .<clinit>(<console>)
at .<init>(<console>:11)
at .<clinit>(<console>)
while it works perfectly with getLinkOld.
What is the problem here?
Your matcher is created as a side-effect in
isDefined. Passing side effecting functions to routine such asmapis usually a recipe for disaster, but this is not even what happens here. Your code requiresisDefinedto have been called just beforeapplyis, with the same argument. That makes your code very fragile, and that is what you should change.Clients of
PartialFunctiondo not have to do follow that protocol in general. Imagine for instanceAnd here the code that calls
applyis not even yours, but the collection classes’, so you do not control what happens.Your specific problem in
getLinkNewis thatisDefinedis simply never called.ThePartialFunctionargument ofcollectFirstis{case m => ...}. TheisDefinedthat is called is theisDefinedof this function. Asmis an irrefutable pattern, it is allways true, and collectFirst will always return the first element if there is one. That the partial function returns another partial function (theMapping) which happens not to be defined atm, is irrelevant.Edit – Possible workaround
A very light change would be to check whether a
matcheris available and create it if it is not. Better, keep theentitystring that has been used to create it too, so that you can check it is the proper one. This should make the side effect benign as long as there is no multithreading. But the way, do not usenull, useOption, so the compiler will not let you ignore the possibility that it may beNone.Edit again. Stupid me
Sorry, the so called workaround indeed makes the class safer, but it does not make the
collectFirstsolution work. Again, thecase m =>partial function is always defined (note:entitydoes not even appears in yourgetLinkNewcode, which should be worrying). The problem is that one would need a PartialFunction of a NodeSeq (not of entity, which will be known to the function, but not passed as argument). isDefined will be called, then apply. The pattern and the matcher depends on the NodeSeq, so they cannnot be created beforehand, but only in isDefined and/or apply. In the same spirit, you can cache what is computed in isDefined to reuse in Apply. This is definitely not prettyYou use that with
(config \ mapping).collectFirst(linkFor(entity))