Your Scala code can extend existing classes with implicit conversions

If you have been working for a while in a reasonable large Java project you have probably seen quite a lot of utility classes with static methods, for example some StringUtil class with methods you wish would have existed in the String class instead.

When you write code with Scala, you can actually make the client code look like you have extended the String class with new methods. In other words, it is possible to write code in such a way that the following kind of test code will pass

val sentenceWithWords = "My name is Tomas. What is your name?"
assertEquals(classOf[java.lang.String], sentenceWithWords.getClass())// proves that the above String indeed became typed as a java.lang.String
val firstTwoWordsWithUppercasedFirstLetter = sentenceWithWords.getFirstWordsIncludingIntermediateCharacters(2).getWithFirstLetterInEachWordUppercasedAndTheRestLowercased()
assertEquals("My Name", firstTwoWordsWithUppercasedFirstLetter)

(By the way, the above kind of code, i.e. extending existing classes, is possible to write with C#.NET, and there it is called Extension methods)

The above kind of code is possible to implement in Scala, by using so called implicit conversions. It means that when you invoke a method of a object, which is not defined in a class (e.g. String) then the compiler will search for methods with implicit conversions from String into some other class which does implement the invoked method.

The Scala implicit conversion will be illustrated further down, but first an example below which will show some Java test code, and the Java implementation of a utility class. It will later be reused from some Scala code which will make it seem available directly through the String class. The tested Java utility class below defines two methods for retrieval/transformation of parts of a sentence with words. Therefore the class has been named to 'SentenceUtility'. One of its method will receive the first x words, where x is a parameter to the method. The other method will make the first letter of each word uppercased and the rest lowercased.

package implicitConversionExample;
import static junit.framework.TestCase.assertEquals;
import org.junit.Test;
import static implicitConversionExample.SentenceUtility.getWithFirstLetterInEachWordUppercasedAndTheRestLowercased;
import static implicitConversionExample.SentenceUtility.getFirstWordsIncludingIntermediateCharacters;
public class SentenceUtilityTest {

    private static final String sentenceWithWords = "My name is Tomas. What is your name?";

    @Test
    public void verify_getFirstWordsIncludingIntermediateCharacters() {
        final String  firstThreeWords = SentenceUtility.getFirstWordsIncludingIntermediateCharacters(sentenceWithWords, 3);
        assertEquals(firstThreeWords, "My name is", firstThreeWords);

    }

    @Test
    public void verify_getWithFirstLetterInEachWordUppercasedAndTheRestLowercased() {
        final String  firstLetterInEachWordUppercasedAndTheRestLowercased
                = SentenceUtility.getWithFirstLetterInEachWordUppercasedAndTheRestLowercased(sentenceWithWords);
        assertEquals("My Name Is Tomas. What Is Your Name?", firstLetterInEachWordUppercasedAndTheRestLowercased);
    }

    @Test
    public void exampleCombiningBothUtilityMethods() {
        assertEquals(
                "My Name",
                // the two methods below can be invoked without the class name
                // because of a static import from this test class 
                getWithFirstLetterInEachWordUppercasedAndTheRestLowercased(
                        getFirstWordsIncludingIntermediateCharacters(sentenceWithWords, 2)
                )
        );
        // of course, if you often would like to combine these utility methods as above,
        // you might provide another utility method for that, i.e. a method like this:
        // getFirstWordsIncludingIntermediateCharactersAndWithFirstLetterInEachWordUppercasedAndTheRestLowercased   :-)
    }
}

package implicitConversionExample;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
public class SentenceUtility {

    private final static String WORD_REG_EXP = "(\\w+)";
    private final static String NON_WORD_REG_EXP = "(\\W+)";
    private final static Pattern REGULAR_EXPRESSION_SEPARATING_WORDS_AND_NON_WORDS = Pattern.compile(WORD_REG_EXP + NON_WORD_REG_EXP);

    /**
     * Reused method, with an algorithm that initially was duplicated into the two other methods in this class,
     * but then was refactored into this common code.
     */
    private static String getSentenceWithFilteredOrTransformedWords(
        final String sentenceWithWords,
        final SentenceWordsFilterOrTransformer sentenceWordsFilterOrTransformer
    ) {
        final StringBuilder wordsIncludingIntermediateNonWordCharacters = new StringBuilder();
        final Matcher matcherForWordsAndNonWords = REGULAR_EXPRESSION_SEPARATING_WORDS_AND_NON_WORDS.matcher(sentenceWithWords + " ");
        while(matcherForWordsAndNonWords.find()) {
            final String word = matcherForWordsAndNonWords.group(1);
            final String nonWord = matcherForWordsAndNonWords.group(2);
            sentenceWordsFilterOrTransformer.populateStringBuilderWithPotentiallyTransformedWordsUnlessShouldBeIgnored(
                wordsIncludingIntermediateNonWordCharacters, word, nonWord
            );
        }
        return wordsIncludingIntermediateNonWordCharacters.toString().trim();
    }

    interface SentenceWordsFilterOrTransformer {
        void populateStringBuilderWithPotentiallyTransformedWordsUnlessShouldBeIgnored(
            StringBuilder stringBuilder,
            String word,
            String nonWord
        );
    }
    
    public static String getFirstWordsIncludingIntermediateCharacters(
        final String sentenceWithWords,
        final int numberOfWordsToReturn
    ) {
        return getSentenceWithFilteredOrTransformedWords(
            sentenceWithWords,
            new SentenceWordsFilterOrTransformer() {
                int numberOfWordsIncludedSoFar = 0;
                public void populateStringBuilderWithPotentiallyTransformedWordsUnlessShouldBeIgnored(
                    final StringBuilder wordsIncludingIntermediateCharacters,
                    final String word,
                    final String nonWord
                ) {
                    if(numberOfWordsIncludedSoFar >= numberOfWordsToReturn) {
                        // ignore the rest of the words, i.e. this filtering is the purpose of this implementation
                        return;
                    }
                    wordsIncludingIntermediateCharacters.append(word);
                    wordsIncludingIntermediateCharacters.append(nonWord);
                    numberOfWordsIncludedSoFar++;
                }
            }
        );
    }


    public static String getWithFirstLetterInEachWordUppercasedAndTheRestLowercased(final String sentenceWithWords) {
        return getSentenceWithFilteredOrTransformedWords(
            sentenceWithWords,
            new SentenceWordsFilterOrTransformer() {
                public void populateStringBuilderWithPotentiallyTransformedWordsUnlessShouldBeIgnored(
                    final StringBuilder wordsIncludingIntermediateCharacters,
                    final String word,
                    final String nonWord
                ) {
                    // this implementation of the interface will transform each word by making the first letter
                    // uppercased and the rest of the letters lowercased
                    wordsIncludingIntermediateCharacters.append(word.substring(0, 1).toUpperCase());
                    wordsIncludingIntermediateCharacters.append(word.substring(1).toLowerCase());
                    wordsIncludingIntermediateCharacters.append(nonWord);
                }
            }
        );

    }
}

Now the Scala code below will make it possible to use the above Java methods directly as if they were defined in the String class, in an object-oriented invocation instead of a procedural invocation that provides a String object as parameter to a static utility method.

package implicitConversionExample
object StringExtensionWithSentenceMethods {
  /**
   * This 'implicit' method below will become invoked when the Scala code
   * invokes a String method which does not exist.
   * Then it will try to instead find the method in the return class
   * below, and if it exists, then the code will indeed compile.
   * Note that this is not runtime magic, but the compiler will
   * try to do the implicit conversions and find the method invoked
   * in the source code. 
   */
  implicit def stringToStringExtensionWithSentenceMethods(string: String) = {
    new StringExtensionWithSentenceMethods(string)
  }
}
class StringExtensionWithSentenceMethods(private val sentenceWithWords: String) 
{
  def getFirstWordsIncludingIntermediateCharacters(numberOfWords: Int): String = {
    // here we simply implement the method by reusing the code in the Java utility class below
    SentenceUtility.getFirstWordsIncludingIntermediateCharacters(sentenceWithWords, numberOfWords)

  }
  def getWithFirstLetterInEachWordUppercasedAndTheRestLowercased(): String = {
    // here we simply implement the method by reusing the code in the Java utility class below
    SentenceUtility.getWithFirstLetterInEachWordUppercasedAndTheRestLowercased(sentenceWithWords)
  }
}

To be able to actually use the method as in the below Scala test class, it is also important to bring the above implicit conversion method into scope, which you can do with the below import of the method.

 

package implicitConversionExample
import implicitConversionExample.StringExtensionWithSentenceMethods.stringToStringExtensionWithSentenceMethods // Important import ! 
import org.scalatest.junit.AssertionsForJUnit
import org.junit.Assert.assertEquals
import org.junit.{Before, BeforeClass, Test}

class StringExtensionTest extends AssertionsForJUnit {

  private val sentenceWithWords = "My name is Tomas. What is your name?" // java.lang.String

  @Before
  def before() {
    // prove that the above String indeed is typed as a java.lang.String
    assertEquals(classOf[java.lang.String], sentenceWithWords.getClass())
  }

  @Test 
  def getFirstWordsIncludingIntermediateCharacters() {
    // As asserted above, the string used below is indeed a java.lang.String,
    // but still this code will work, since the implicit conversion into the class
    // StringExtensionWithSentenceMethods will occur (in COMPILE time!).
    // In other words, when the compiler (and NOT the runtime!) will not found
    // the method below, it will instead try to find a method which makes
    // an implicit conversion from String into a class which defines the method below 
    val firstThreeWords = sentenceWithWords.getFirstWordsIncludingIntermediateCharacters(3)
    assertEquals("My name is", firstThreeWords)
  }


  @Test
  def getWithFirstLetterInEachWordUppercasedAndTheRaestLowercased() {
    val firstLetterInEachWordUppercasedAndTheRestLowercased = sentenceWithWords.getWithFirstLetterInEachWordUppercasedAndTheRestLowercased()
    assertEquals("My Name Is Tomas. What Is Your Name?", firstLetterInEachWordUppercasedAndTheRestLowercased)
  }

  @Test
  def exampleCombiningBothUtilityMethods() {
    val firstTwoWordsWithUppercasedFirstLetter =
    sentenceWithWords.getFirstWordsIncludingIntermediateCharacters(2).getWithFirstLetterInEachWordUppercasedAndTheRestLowercased()
    assertEquals("My Name", firstTwoWordsWithUppercasedFirstLetter)

    // The above Scala code should be compared with the corresponding procedural Java code in statically imported static utility methods as below: 
    // (and as in the Java class 'SentenceUtilityTest' further up in this webpage)
    //    getWithFirstLetterInEachWordUppercasedAndTheRestLowercased(
    //            getFirstWordsIncludingIntermediateCharacters(sentenceWithWords, 2)
    //    )
  }
}



/ Tomas Johansson, Stockholm, Sweden, 2010-01-24