2

I tried various things to do something quite simple that I would do using a decorator pattern in Java, but wanted to do with stackable modifications in Scala and failed.

Here's my use-case: I'm implementing some simple auto-completers. They all implement a base trait:

trait AutoCompleter {
  def topSuggestions(prefix: String): Iterator[String]
  def update(sentence: String*): Unit

  final def topSuggestions(prefix: String, n: Int): List[String] = topSuggestions(prefix).take(n).toList
}

Some of them are concrete implementations based on a custom implementation of a trie:

/**
  * This auto-completer learns from the user's input, and therefore does not require a preliminary dictionary.
  */
class ParrotAutoCompleter extends AutoCompleter {
  private val trie = new WeightedTrie[Char, String]()

  override def topSuggestions(prefix: String): Iterator[String] = trie.prefixedBy(prefix)
  override def update(sentence: String*): Unit = sentence.foreach(trie += _)
}

And some others are stackable modifications:

/**
  * This auto-completer will try returning some suggestions when the prefix did not match any known word by dropping the
  * last character until it finds a suggestion
  */
trait TolerantAutoCompleter extends AutoCompleter {
  def MaxRetries: Int

  abstract override def topSuggestions(prefix: String): Iterator[String] = {
    if (MaxRetries < 1) throw new IllegalArgumentException("Should allow 1 retry minimum, but max retries was: " + MaxRetries)
    for (attempt <- 0 to Math.min(prefix.length, MaxRetries)) {
      val suggestions = super.topSuggestions(prefix.substring(0, prefix.length - attempt))
      if (suggestions.hasNext) return suggestions
    }
    Iterator()
  }
}

And I use them like this:

val autoCompleter = new ParrotAutoCompleter with TolerantAutoCompleter { override val MaxRetries: Int = 5 }

This implementation works all fine, but it has a flaw: the sanity check performed on MaxRetries is done late, only when using the auto-completer rather than when creating it. More anecdotically, it is ran every time instead of just once.

The problem is that any code outside a method in a trait gets executed right away, even before MaxRetries has been overriden (independently of whether it is declared as a val or a def).

How could I perform my sanity check at construction time, after the override, and without losing the property of being a stackable modification ?

Dici
  • 25,226
  • 7
  • 41
  • 82

1 Answers1

2

When you override val MaxRetries = 5, you create a field that is only initialized in the constructor of the anonymous class. The flow of the constructor goes like this:

<jvm>: Initialize MaxRetries field to 0
<anon>: call super
TolerantAutoCompleter: call super
...
TolerantAutoCompleter: sanity check MaxRetries (still 0!)
TolerantAutoCompleter: return
<anon>: set MaxRetries to 5
<anon>: return

Use

new ParrotAutoCompleter with TolerantAutoCompleter { override def MaxRetries = 5 }

(with a def) instead.

HTNW
  • 27,182
  • 1
  • 32
  • 60
  • It seems that your answer is correct but the way you framed it I think the explanation remains unclear. I don't know exactly for which code you're describing this flow (although I imagine you're talking about my snippet except that you move the sanity check in the trait's body), and what you mean by `anon`. . Could you please clarify these two points ? – Dici Jan 31 '17 at 00:41
  • It's more standard to use `lazy val` to avoid recalculation (though in the specific case of a literal, `def` will be better). – Alexey Romanov Jan 31 '17 at 06:34
  • @AlexeyRomanov I actually tried using `lazy` in the trait declaration and it did not compile, but I realize I should have used it in the concrete usage of the trait. Would that fix the issue (can't try right now) ? If so, could you add an answer describing the rules of initialization order in this context ? – Dici Jan 31 '17 at 10:38
  • 1
    @Dici this would duplicate a lot of existing answers. See e.g. http://stackoverflow.com/questions/27781837/how-to-initialize-traits-vals-in-subtrait, http://stackoverflow.com/questions/15762650/how-to-initialize-the-value-from-trait-in-subtype, http://docs.scala-lang.org/tutorials/FAQ/initialization-order.html. – Alexey Romanov Jan 31 '17 at 11:07
  • @AlexeyRomanov fair enough ! Probably did not look hard enough before asking. I posted just before going to bed, I should have slept on it :p Thanks – Dici Jan 31 '17 at 14:49