case class DNA(strand: String) {
def nucleotideCounts: Either[String, Map[Char, Int]] = {
countRecur(strand, Map('A' -> 0, 'C' -> 0, 'G' -> 0, 'T' -> 0))
}
@scala.annotation.tailrec
private def countRecur(
strand: String,
output: Map[Char, Int]
): Either[String, Map[Char, Int]] = {
if (strand.isEmpty) return Right(output)
strand.head match {
case chr if output.contains(chr) =>
countRecur(strand.tail, output + (chr -> (output(chr) + 1)))
case chr => Left(s"invalid nucleotide '$chr'")
}
}
}
The nucleotideCounts() method returns the result of calling the private recursive method, passing in the
input String and a Map.
The recursive method is annotated with the @tailrec annotation to verify that the method can be compiled
with tail call optimization.
A tail call is a particular form of recursion where the last call in the method is a call to the same method and nothing else.
In other words, if the last call in recurMe() is recurMe(arg1, arg2) + 1, the + 1 makes the recursion non-tail recursive.
If the last call in recurMe() is recurMe(arg1, arg2, acc + 1), then the recursion is a tail call, because only the method is being called
with no other operation being peformed on it.
If the input String is empty, then the method returns the Map wrapped as a Right value.
Otherwise, the head method is used to get the first character in the String, which is passed to the match.
The pattern matching checks to see if the character is valid.
If the Map contains() the character as a key, then the recursive method calls itself, passing the
tail of the input String and a new Map created by using the + alias for the updated() method.
Recreating an immutable Map instead of changing a mutable Map is how the approach maintains immutability.
If the Map does not contain the character, then an error String is created and wrapped as a Left value,
which is immediately returned from the recursive method.