rchillyard / matchers   1.0.5

MIT License GitHub

A library of composable matchers

Scala versions: 2.13

Codacy Badge Maven Central CircleCI GitHub Top Languages GitHub GitHub last commit GitHub issues GitHub issues by-label

Matchers

A library of composable matchers. If you're familiar with the Parsers class from the Scala parser-combinators library, you should be fairly comfortable with the Matchers.

The chief difference between Parsers and Matchers is that, whereas Parsers defines two parametric types via the type alias mechanism and one parametric type of the ParseResult type. In Matchers, there are just the two parametric types on the MatchResult type.

For the most part, the methods in Matchers result in a Matcher[T, R] where T and R are the input and result types respectively. A Matcher[T, R] is a function which takes a value of T and returns a MatchResult[R]. There are three subclasses of MatchResult[R]:

  • case class Match[R](r: R)
  • case class Miss[T, R](msg: String, t: T)
  • case class Error[R](e: Throwable)

Composition of MatchResult

Perhaps the simplest place to start is with the || method:

def ||[S >: R](sm: => MatchResult[S]): MatchResult[S]

If this is successful, then it is returned as is. Otherwise, sm will be returned. Note that MatchResult[R] is a subtype of MatchResult[S] because the parametric type of MatchResult is covariant, and because R is a subtype of S.

There is a similar method defined as |:

def |[S >: R](sm: => Matcher[Any, S]): MatchResult[S]

It behaves as || if this is successful, but otherwise, matcher sm is invoked on the failed input value for this Miss. An Error always returns itself.

The corresponding "and" methods are somewhat different because the return type must include two values of two disparate types:

def &&[S](sm: => MatchResult[S]): MatchResult[R ~ S]

The result of the && method will be successful only if sm is also successful. In this case, the result will a tuple of the two results.

Parsing

Any Matcher whose input is a String can be referred to as a Parser (a type alias).

There are simple numeric parsers, for example:

val p = m.parserInt
p("12345") shouldBe m.Match(12345)

It is easy to create matchers which parse regular expressions (without having to depend on Scala Parser Combinators). For example:

import matchers._
val m: matchers.Parser[List[String]] = """(\w+)\s(\d+)""".regexGroups
m("Hello 12345") shouldBe matchers.Match(List("Hello", "12345"))

This utilizes the regexGroups method of implicit class ParserOps.

It is also easy to parse strings as instances of case classes, even with optional parameters. See, for example,

case class Rating(code: String, age: Option[Int])
val p: m.Parser[Rating] = m.parser2("""(\w+)(-(\d+))?""", 1, 3)(m.always, m.opt(m.parserInt))(Rating)
p("PG-13") shouldBe m.Match(Rating("PG", Some(13)))
p("R") shouldBe m.Match(Rating("R", None))

Tilde

As in the Scala Parser Combinators, there is a ~ case class. It is essentially just a tuple of two elements. However, if we use the ~ operator on two Matchers for example as follows:

import matchers._
val m: matchers.Matcher[String ~ String, Int] = "1".m ~ "2".m ^^ {
  case x ~ y => x.toInt + y.toInt
}

... you can use the ^^ (or map) method to transform the MatchResult from String ~ String to some other type, in this case an Int.

In this case, the m method on a String is provided by an implicit class MatcherStringOps.

Usage

Typical examples of the use of Matchers would be something such as the following (from an application which deals with lazy expressions of numeric quantities):

def biFunctionSimplifier: Matcher[Expression, Expression] = matchBiFunction & (matchSimplifyPlus | matchSimplifyTimes) :| "biFunctionSimplifier"

This can be interpreted as defining a matcher which takes an Expression and returns an Expression. If matchBiFunction succeeds, AND if one of the following matchers succeeds (tried in sequence), then a Match[Expression] will result. The intermediate result is a Tuple3. The final :| "biFunctionSimplifier" is a logging matcher which can be turned on for easier debugging but otherwise does not affect the result.

def matchSimplifyPlus: Matcher[(ExpressionBiFunction, Expression, Expression), Expression] =
  matchDyadicBranches(Sum) & *(matchBiFunctionConstantResult(Product, Number(-1), Number.zero)) :| "matchSimplifyPlus"

This can be interpreted as defining a matcher which takes the tuple returned by the matchBiFunction above, and returns an Expression. If matchDyadicBranches(Sum) succeeds (where Sum is basically the "plus" operator), AND if the following matcher succeeds, then a Match[Expression] will result. The intermediate result is a tuple of Expressions. Since these are in arbitrary order, the "*" matcher will try matchBiFunctionConstantResult(Product, Number(-1), Number.zero) with the tuple straight or inverted as necessary.

Debugging/Logging

By default, matchers are not named (the default name is ""). it is easy to name matchers, either by using the Matchers method (with name parameter) or the following mechanism: After any matcher reference, you can invoke the :| operator with a String representing the name and this will turn on logging for just that . For example,

import m.MatcherOps
val p = m.success(1) :| "success(1)"

Note that somewhere you will have to import the MatcherOps in order to make the :| operator available.

The second change you need to make to enable debugging/logging is to set the LogLevel by defining an implicit val of that type. The default value is LogOff, but you also have LogDebug and LogInfo available.

The default MatchLogger results in logging information going to the console via println. You can easily set up your own implicit value of MatchLogger which is simply a String => Unit function.

Version

  • 1.0.5 Added combine and accumulate methods to MatchResult...
  • 1.0.4 Make logging more consistent and easier to use.
  • 1.0.3 Replaced most tuples with tildes.
  • 1.0.2 Support regex parsers
  • 1.0.1 Added ~
  • 1.0.0 Original Version