Scala 3, obviously. Scala 2's Tuple
set-up would have kept me from trying this.
I'm trying to write a generic parser library with pluggable algorithms. So, when you write your grammar, it gets converted into an abstract syntax tree, and then those abstract syntax trees get fed to whichever algorithm you want to use, and the algorithm decides how it's going to parse the input.
So, for example, I have these data classes that all extend ParseExpr[+T]
:
case class Term[T](t: T) extends ParseExpr[T] // a single terminal
case class NonTerm[T](name: String) extends ParseExpr[T] // a non-terminal
// I also store the type of the non-terminal, but it's ugly
case class Alt[T](ps: List[Pexpr[T]]) extends ParseExpr[T] // a list of alternate ParseExprs
case class Cat[?](ps: ?) extends ParseExpr[?] // a sequence of ParseExprs
Everything works fine except the last one. The way that Scala parser combinators deals with the equivalent Alt
and Cat
is that it binarizes them. In other words, you can only have two ParseExpr
s inside Alt
or Cat
, but these could also be Alt
and Cat
, so you can build up the equivalent of p ~ q ~ r
with Cat(Cat(p, q), r)
.
I'm implementing an algorithm that is much speedier when the parser is not binarized, so I'm trying to create a version of Cat
that takes any number of ParseExpr
s. The problem, of course, is the the result of a sequence of parsers is a (heterogeneous) sequence of values. So I thought I could make things completely general and use the new Scala 3 Tuple
.
Here's what I tried:
case class Cat[Ts <: Tuple](ps: Map[Ts, ParseExpr]) extends ParseExpr[Ts]
What I think this says is that Cat
takes a tuple of ParseExpr
s, each with its own type, and produces a tuple of the given types in order. This is great. Except I can't do anything with it.
First, I have to convert a Cat[Ts](pes: Map[Ts, ParseExpr])
into an actual parser. To do that, I have to convert each ParseExpr
into a Parser
. So I tried to map
over the Tuple
:
parseExpr match
case Cat(ps) => ps.map([t] => (pe: t) => parserFor(pe)) // should return Map[Ps, Parser]
But no matter how I do this, I can't get things to type-check.
Have I bitten off more than I can chew, and Tuple
s of higher-order types are just more trouble than they're worth, or am I missing something basic here?
Edit: After Dmytro rightly complained about my not providing code, I've banged away at this for the past couple of days, and I'm still stuck. What Dmytro provided does compile, but as soon as I try to write the actual parser, I get stuck. Here's what I have (also in scastie:
import scala.language.implicitConversions
import scala.Tuple.{Head, Tail, Map => TMap}
case class InputPos(in: IndexedSeq[Char], pos: Int):
def charAt: Char = in(pos)
def next: InputPos = copy(pos = pos + 1)
trait Pexpr[+P]
case class Term(t: Char) extends Pexpr[Char]
case class Cat[Ps <: Tuple](ps: TMap[Ps, Pexpr]) extends Pexpr[Ps]
trait Result[+P]:
val next: InputPos
def map[P2](f: P => P2): Result[P2]
case class Success[P](value: P, next: InputPos) extends Result[P]:
def map[P2](f: P => P2): Result[P2] = copy(value = f(value))
case class Failure(msg: String, next: InputPos) extends Result[Nothing]:
def map[P2](f: (Nothing) => P2): Result[P2] = this
trait Parser[+P] extends (InputPos => Result[P])
/* I tried to fake my own Tuple.Map with a wrapper, but couldn't
figure out how to write the map method.
trait TMap[Ts <: Tuple, M[_]]:
type Out <: Tuple
val instances: Out
object TMap:
given[M[_]]: TMap[EmptyTuple, M] with
type Out = EmptyTuple
val instances = EmptyTuple
given[M[_], H, T <: Tuple](using head: M[H], mt: TMap[T, M]): TMap[H *: T, M] with
type Out = M[H] *: mt.Out
val instances = head *: mt.instances
*/
type ParserFor[P <: Pexpr[?]] <: Parser[?] = P match
case Pexpr[p] => Parser[p]
def parserFor[P](pexpr: Pexpr[P]): Parser[P] = pexpr match
case Term(t) => elem(t)
case Cat(pexprs) => catHelper(pexprs)
def catHelper[Ps <: Tuple](pexprs: TMap[Ps, Pexpr])(
using TMap[TMap[Ps, Pexpr], ParserFor] =:= TMap[Ps, Parser]
): Parser[Ps] =
val parsers: TMap[Ps, Parser] = pexprs.map[ParserFor]([p] => (pexpr: p) => pexpr match
case pe: Pexpr[p] => parserFor[p](pe)
)
cat(parsers)
def elem(t: Char): Parser[Char] = new Parser[Char] {
def apply(input: InputPos): Result[Char] =
if (input.charAt == t) Success(t, input.next)
else Failure(s"expected $t, found ${input.charAt}", input)
}
def cat[Ps <: Tuple](ps: TMap[Ps, Parser]): Parser[Ps] = ps match
case _: EmptyTuple => new Parser[Ps] {
def apply(input: InputPos): Result[Ps] = Success[Ps](Tuple(), input)
}
case (h: Parser[Head[Ps]]) *: (t: TMap[Tail[Ps], Parser]) => new Parser[Ps] {
def apply(input: InputPos): Result[Ps] = h.apply(input) match
case Success(first, inputNext) =>
cat(t).apply(inputNext).map((rest: Tail[Ps]) => first *: rest)
case f@Failure(msg, inputNext) => f
}
In the middle is where I tried a trick from one of the links Dmytro provided, but then I had to write a map
method on a TMap[Tup, M[_]]
and got lost.
Any help greatly appreciated!