It seems to me that the algorithm does not meet the single responsibility principle, it is doing more than one thing.
Right. This is one of the reasons why for logging, auditing, security checks, performance monitoring, exception handling, caching, transaction management, persistence, validation etc. i.e. for different kinds of additional orthogonal behavior people use instrumentation of their code
What are the possible AOP use cases?
Instrumentation can be runtime (runtime reflection, runtime annotations, aspect-oriented programming, java agents, bytecode manipulation), compile-time (macros, compile-time annotation processors, compiler plugins), pre-compile-time/build-time (source generation, Scalameta/SemanticDB, sbt source generators, boilerplate templating) etc.
For example you can instrument your code at compile time with a macro annotation logging branching ("if-else")
import scala.annotation.{StaticAnnotation, compileTimeOnly}
import scala.language.experimental.macros
import scala.reflect.macros.blackbox
@compileTimeOnly("enable macro annotations")
class logBranching extends StaticAnnotation {
def macroTransform(annottees: Any*): Any = macro LogBranchingMacro.impl
}
class LogBranchingMacro(val c: blackbox.Context) {
import c.universe._
val printlnT = q"_root_.scala.Predef.println"
def freshName(prefix: String) = TermName(c.freshName(prefix))
val branchTransformer = new Transformer {
override def transform(tree: Tree): Tree = tree match {
case q"if ($cond) $thenExpr else $elseExpr" =>
val condStr = showCode(cond)
val cond2 = freshName("cond")
val left2 = freshName("left")
val right2 = freshName("right")
val (optLeft1, optRight1, cond1, explanation) = cond match {
case q"$left == $right" =>
(
Some(this.transform(left)),
Some(this.transform(right)),
q"$left2 == $right2",
q""" ", i.e. " + $left2 + "==" + $right2 """
)
case _ =>
(
None,
None,
this.transform(cond),
q""" "" """
)
}
val backups = (cond, optLeft1, optRight1) match {
case (q"$_ == $_", Some(left1), Some(right1)) =>
Seq(
q"val $left2 = $left1",
q"val $right2 = $right1"
)
case _ => Seq()
}
val thenExpr1 = this.transform(thenExpr)
val elseExpr1 = this.transform(elseExpr)
q"""
..$backups
val $cond2 = $cond1
$printlnT("checking condition: " + $condStr + $explanation + ", result is " + $cond2)
if ($cond2) $thenExpr1 else $elseExpr1
"""
case _ => super.transform(tree)
}
}
def impl(annottees: Tree*): Tree = annottees match {
case q"$mods def $tname[..$tparams](...$paramss): $tpt = $expr" :: Nil =>
val expr1 = branchTransformer.transform(expr)
q"$mods def $tname[..$tparams](...$paramss): $tpt = $expr1"
case _ => c.abort(c.enclosingPosition, "@logBranching can annotate methods only")
}
}
// in a different subproject
@logBranching
def betterContains(list: List[String], element: String): Boolean = {
if (list.isEmpty) false
else if (list.head == element) true
else betterContains(list.tail, element)
}
// scalacOptions += "-Ymacro-debug-lite"
//scalac: def betterContains(list: List[String], element: String): Boolean = {
// val cond$macro$1 = list.isEmpty;
// _root_.scala.Predef.println("checking condition: ".$plus("list.isEmpty").$plus("").$plus(", result is ").$plus(cond$macro$1));
// if (cond$macro$1)
// false
// else
// {
// val left$macro$5 = list.head;
// val right$macro$6 = element;
// val cond$macro$4 = left$macro$5.$eq$eq(right$macro$6);
// _root_.scala.Predef.println("checking condition: ".$plus("list.head.==(element)").$plus(", i.e. ".$plus(left$macro$5).$plus("==").$plus(right$macro$6)).$plus(", result is ").$plus(cond$macro$4));
// if (cond$macro$4)
// true
// else
// betterContains(list.tail, element)
// }
//}
betterContains(List("a", "b", "c"), "c")
//checking condition: list.isEmpty, result is false
//checking condition: list.head.==(element), i.e. a==c, result is false
//checking condition: list.isEmpty, result is false
//checking condition: list.head.==(element), i.e. b==c, result is false
//checking condition: list.isEmpty, result is false
//checking condition: list.head.==(element), i.e. c==c, result is true
For example Scastie instruments user-entered code with Scalameta
https://github.com/scalacenter/scastie/tree/master/instrumentation/src/main/scala/com.olegych.scastie.instrumentation
Another approach to add additional behavior in functional programming is effects, e.g. monads. Read about logging monad, Writer
monad, logging with free monads etc.