Infix Chain Combinators

The parsley.expr.infix module contains combinators for the strongly-typed abstraction of operators to values in expressions. This allows parsley to handle left recursion idiomatically. To distinguish between these chains and those found in parsley.expr.chain, it is recommended to always import this module qualified as import parsley.expr.infix.

The Scaladoc for this page can be found at parsley.expr.infix.

Binary Chains

The stronger types implied by infix means that only binary chains are useful to define. Both infix.left1 and infix.right1 behave the same as chain.left1 and chain.right1. The difference is in the types:

def left1[A, B](p: Parsley[A], op: Parsley[(B, A) => B])(implicit wrap: A => B): Parsley[B]
def right1[A, B](p: Parsley[A], op: Parsley[(A, B) => B])(implicit wrap: A => B): Parsley[B]

As both combinators return type B, the B denotes where recursion can appear. The wrap function is a way of converting the final p value in either bracketing to be a value of type B to fit into the operator. When A <: B, the A <:< B instance, which is of type A => B will be summoned, meaning the terminal p is upcast into a B. When A and B are the same type, A =:= B is summoned, which acts as the identity function, and they behave the same as chain.left1 or chain.right1.

Ultimately, these chains are useful when the parser writer wants further guarantees that the parser adheres to the grammar precisely: when strong types are employed, infix.left1 and infix.right1 cannot be substituted for each other.

As an example:

import parsley.character.digit
import parsley.expr.infix
import parsley.syntax.character.stringLift

sealed trait Expr
case class Add(x: Expr, y: Num) extends Expr
case class Num(n: Int) extends Expr

lazy val expr = infix.left1(num, "+".as(Add(_, _)))
lazy val num = digit.foldLeft1(0)((n, d) => n * 10 + d.asDigit).map(Num(_))
expr.parse("56+43+123")
// res0: parsley.Result[String, Expr] = Success(Add(Add(Num(56),Num(43)),Num(123)))

In the above example, the Add constructor is recursive on the left, but Num must appear on the right. As Num and Add share a common supertype Expr, this is what the chains will return. To illustrate what happens if right1 was used instead:

lazy val badExpr = infix.right1(num, "+".as(Add(_, _)))
// error: inferred type arguments [Num,Add,Num] do not conform to method right1's type parameter bounds [A,B,C >: B]
// lazy val badExpr = infix.right1(num, "+".as(Add(_, _)))
//                    ^^^^^^^^^^^^
// error: type mismatch;
//  found   : parsley.Parsley[Num]
//  required: parsley.Parsley[A]
// lazy val badExpr = infix.right1(num, "+".as(Add(_, _)))
//                                 ^^^
// error: type mismatch;
//  found   : parsley.Parsley[(Expr, Num) => Add]
//  required: parsley.Parsley[(A, C) => B]
// lazy val badExpr = infix.right1(num, "+".as(Add(_, _)))
//                                      ^^^^^^^^^^^^^^^^^

Notice that the error message here refers to the type (A, C) => B. In practice, to help reduce type ascriptions, the explicit C >: B is used as well -- in the working example, C is Expr, A is Num, and B is Add. The wrap is actually A => C, which in this case is provided by Num <:< Expr.