2
votes

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 ?

1

1 Answers

2
votes

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.