Использование отражения Scala для проверки метода объекта или поиска типа ключа на карте

Я портирую класс с Ruby на Scala 2.11, который реализует слияние переменных в сообщениях. Я хотел бы передать массив объектов методу слияния и заставить его искать в каждом объекте ключи, на которые есть ссылки в тексте сообщения.

Ядром этого является метод lookUp (key: String, obj: AnyRef), который получает один ключ для поиска и один объект для поиска ключа. Если объект представляет собой карту, а ключи карты являются либо символом, либо строкой, тогда он будет искать запрошенный ключ на карте. В противном случае он проверит, есть ли у объекта метод с тем же именем ключа, и если да, он вызовет этот метод.

В существующем коде Ruby это сделать тривиально просто:

def look_up(key, obj)
  if obj.respond_to?(:has_key?) && obj.has_key?(key)
    obj[key]
  elsif obj.respond_to?(key)
    obj.send(key)
  elsif obj.instance_variable_defined?(ivar = "@#{key}")
    obj.instance_variable_get(ivar)
  end
end

Поскольку это легко сделать, код Ruby также ищет переменную экземпляра с тем же именем. Мне не обязательно нужна эта функциональность в моей версии Scala.

Одна из проблем, с которыми я сталкиваюсь, заключается в том, что в найденных мною примерах требуется, чтобы я знал класс объекта заранее, чтобы я мог вызвать ru.typeOf[MyClass].declaration(ru.TermName("key")) (где ru это scala.reflect.runtime.universe).

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

ОБНОВЛЕНИЕ: я думал о чем-то подобном, но разве это излишество? Или нужно правильно фиксировать типы на Карте? Кроме того, это не компилируется. Map, Symbol и String не подходят для их контекста.

def lookUp[T](obj: T, key: String)(implicit tag: ru.TypeTag[T]): Option[String] = tag.tpe match {
  case ru.TypeRef(a, Map, List(Symbol, _)) => if (obj.contains(Symbol(key))) Some(obj(Symbol(key)).toString) else None
  case ru.TypeRef(a, Map, List(String, _)) => if (obj.contains(key)) Some(obj(key).toString) else None
  case _ =>
    if (/* obj.key() exists */)
      // Some(obj.key().toString)
    else
      None
}

ОБНОВЛЕНИЕ 2: Мне никогда не приходило в голову, что я могу использовать asInstanceOf с чем-то вроде Map[String, _]. Я использовал второй пример кода @johny, чтобы придумать свое решение. Я кэширую имена методов по классам в mutable.HashMap[Class[_], Set[String]].

def lookUp(obj: AnyRef, key: String): Option[String] = obj match {
  case m: Map[_, _] =>
    if (m.asInstanceOf[Map[String, _]].contains(key))
      extractValue(m.asInstanceOf[Map[String, _]](key))
    else if (m.asInstanceOf[Map[Symbol, _]].contains(Symbol(key)))
      extractValue(m.asInstanceOf[Map[Symbol, _]](Symbol(key)))
    else
      None
  case _ =>
    val klass = obj.getClass
    if (!methodsCache.contains(klass))
      methodsCache(klass) = klass.getMethods.toList.filter(_.getParameterCount == 0).map(_.getName).toSet
    val methodNames = methodsCache(klass)
    if (methodsCache(klass).contains(key))
      extractValue(klass.getDeclaredMethod(key).invoke(obj))
    else
      None
}

def extractValue(obj: Any): Option[String] = obj match {
  case null | None => None
  case Some(x) => Some(x.toString)
  case x => Some(x.toString)
}

person Jim Cain    schedule 28.11.2015    source источник


Ответы (1)


 def lookUp(key: String, obj: AnyRef) {
    obj match {
      case x: Map[String, _] => x(key)
      case _                 => obj.getClass.getDeclaredMethod(key).invoke(obj)
    }
  }

Изменить: чтобы убедиться, что ключ Map равен String или scala.Symbol

def lookUp(key: String, obj: AnyRef)= {
    obj match {
      case x: Map[_, _]  => 
        if(x.asInstanceOf[Map[String,_]].contains(key))
          x.asInstanceOf[Map[String,_]].get(key)
        else if(x.asInstanceOf[Map[scala.Symbol,_]].contains(scala.Symbol(key)))
          x.asInstanceOf[Map[scala.Symbol,_]].get(scala.Symbol(key))
        else
          None

      case _ => Some(obj.getClass.getDeclaredMethod(key).invoke(obj))
    }
  }

Вышеупомянутое также не гарантирует, что возвращаемый результат исходит из предполагаемого Map.

Изменить: следуя примеру @ JimCain

def lookUp[T:ru.TypeTag](obj: T, key: String): Option[Any] = ru.typeTag[T].tpe match {
    case ru.TypeRef(a, m, l) if(m.name.toString=="Map"&&l.head =:= ru.typeOf[java.lang.String])=> obj.asInstanceOf[Map[String,_]].get(key)
    case ru.TypeRef(a, m,l) if(m.name.toString=="Map"&&l.head =:= ru.typeOf[Symbol])=> obj.asInstanceOf[Map[Symbol,_]].get(scala.Symbol(key))
    case _ => Try(obj.getClass.getDeclaredMethod(key).invoke(obj)) match {
      case Success(x) => Some(x)
      case Failure(_) => None
    }
  }
person Johny T Koshy    schedule 28.11.2015
comment
Из REPL: warning: non-variable type argument String in type pattern scala.collection.immutable.Map[String,_] (the underlying of Map[String,_]) is unchecked since it is eliminated by erasure - person Jim Cain; 29.11.2015
comment
Из-за стирания типа параметры типа Map недоступны. Параметр String добавлен для удобства, чтобы избежать преобразования. Вы также можете использовать аннотацию @unchecked, например Map[String @unchecked, _], которая удалит предупреждение. Обратной стороной является то, что он будет соответствовать всем Map независимо от его параметров типа. - person Johny T Koshy; 29.11.2015
comment
Вот о чем я думал. Однако я хочу знать, являются ли ключи String или Symbol, чтобы я знал, как их надежно найти. Если ключи не относятся к типу, я хочу вообще пропустить его. - person Jim Cain; 29.11.2015
comment
Если строки и символы - единственные случаи, см. Редактирование. - person Johny T Koshy; 29.11.2015
comment
@JimCain, см. Это редактирование. Модификация вашего кода. - person Johny T Koshy; 29.11.2015