Revision History |
---|
None so far |
1A Here is one recursive solution:
def removeOne[A](x: A, l: List[A]): List[A] = l match { case Nil => Nil case y :: ys => if (y == x) then ys else y :: removeOne(x, ys) }
1B Here is how to
define removeAll
using foldRight
:
def removeAll[A](x: A, l: List[A]): List[A] = { l.foldRight(Nil)((y: A, r: List[A]) => // r is the result from the right, a list with no occurrences // of x if (x == y) then r else y :: r) }
It's the natural recursive solution, with the "natural recursion"
part abstracted away into the definition of foldRight
.
(The Nil
part is the base case, and the rest is what to do
in the recursion case, after you've called the thing
recursively to get r
.)
This won't seem like progress to you until you've written out that
recursion a few dozen times.
1C There are many ways to answer question 1C. The answer I sort of had in mind was —
def removeOneFunctional[A](x: A, l: List[A]): List[A] = { val (prefixWithNoXs, suffixStartingWithX) = l.span(_ != x) (suffixStartingWithX match { case Nil => l case y :: ys => prefixWithNoXs ::: ys })}
(See the problem set for an explanation of span
. Or
check the Scala library.)
This solution is not as nice as I visualized at the outset. The check for
whether suffixStartingWithX
is Nil
is
clunky, and a call to ":::
" is something one should
always think about when using linked lists. It requires copying its
first argument, an overhead we avoided with the recursive solution.
Sometime after releasing the problem set, I realized that the
built-in function diff
would solve the problem much quicker:
def removeOneWithDiff[A](x: A, l: List[A]): List[A] = { l.diff(List(x)) }
l1.diff(
l2)
finds the
complement of l1 and l2, the elements of l1
that aren't in l2. But this is no ordinary complement; if an
element e appears k times in l2, the first
k elements of l1 will not appear in the result.
For k=1, this is exactly what we want!
After all this trickery, one may feel that we've missed an
opportunity. foldRight
abstracts the recursion pattern
in functions like removeAll
. If we abstract the
recursion pattern found in removeOne
, maybe we'll have
other opportunities to use it and avoid repeating ourselves.
Here's one way to do that:
/** Like foldRight, except (a) it's not a member of List, so we have to pass * the list as the first argument (parameter `l`); (b) there's a `maybeStop` * parameter, a function of an element of the list (*x*) and the * tail after it (*xs*) that returns None or the value to * return. `maybeStop` is called first; as long as the result is * None, the recursion happens, and `combine` is called with two * arguments: *x* and the value obtained from the recursion, just * as for `foldRight`. When 'maybeStop` returns Some(r), then * the recursion stops and r is returned. */ def foldRightUntil[A,B](l: List[A], z: B)(maybeStop: (A, List[A]) => Option[B], combine: (A,B) => B): B = { def doTheRecursion(remaining: List[A]): B = remaining match { case Nil => z case x :: xs => maybeStop(x, xs) match { case Some(r) => r // Done early case None => { // Keep going val r = doTheRecursion(xs) combine(x, r) }}} doTheRecursion(l) }
Armed with this new tool, we can define removeOne
one
last way:
def removeOneWithFoldRightUntil[A](x: A, l: List[A]): List[A] = { foldRightUntil(l, List[A]())(((y: A, r: List[A]) => if (y == x) Some(r) else None), ((y: A, r: List[A]) => y :: r)) }
The reason we have to say List[A]()
instead
of Nil
as the value of z
is that
otherwise the type checker decides A
, the type variable
in foldRightUntil
, =
Nothing
and then can't live with the consequences.Check
2A Here is an object containing solutions
for problems 2A and 2B, to wit, definitions
of leftmostTip
and expandPath
:
object PS3Q2 { type Path = List[Int] def leftmostTip(tr: CFGsyntaxTree): Option[Path] = tr match { // First case only at top level case CFGtreeEmpty(_) => None case CFGtreeLeaf(term, _) => None case CFGtreeTip(term, _) => Some(Nil) case CFGexpansion(nonterm, subTrees, _, _, _) => { def searchThroughSubTrees(subTreesRemaining: List[CFGsyntaxTree], index: Int): Option[Path] = subTreesRemaining match { case Nil => None case subTree :: moreSubs => { leftmostTip(subTree) match { case None => searchThroughSubTrees(moreSubs, index + 1) // Success! – case Some(path) => Some(index :: path) } } } // Body of "case CFGexpansion(…) => …" begins here – searchThroughSubTrees(subTrees, 0) } }
But wait! Doesn't that
subroutine searchThroughSubTrees
fit the pattern
that foldRightUntil
captures? There's one discrepancy:
that extra index
argument. No problem: we can
use zipWithIndex
another function from the Scala
collections API, to associate every element with its index up
front. The result looks like this:
def leftmostTip(tr: CFGsyntaxTree): Option[Path] = tr match { // First case only at top level case CFGtreeEmpty(_) => None case CFGtreeLeaf(term, _) => None case CFGtreeTip(term, _) => Some(Nil) case CFGexpansion(nonterm, subTrees, _, _, _) => { type IndexedSynTree = (CFGsyntaxTree, Int) foldRightUntil(subTrees.zipWithIndex, None: Option[Path])( // maybeStop: ((subTreePlusIndex: IndexedSynTree, remainingTrees: List[IndexedSynTree]) => subTreePlusIndex match { case (subTree, index) => leftmostTip(subTree).map((path) => Some(index :: path)) }), // combine: ((subTreePlusIndex: IndexedSynTree, resultFromRight: Option[Path]) => resultFromRight) ) } }
Not quite as nice, but maybe we'll see a more general way of
abstracting the tool we want. (The reason we have to
say None: Option[Path]
is analogous to the reason we
couldn't use z=Nil
in removeOneWithFoldRightUntil
. We could have
said Nil: List[A]
instead of List[A]()
, if
that makes the analogy clearer.)
One piece of this version that may be puzzling is this bit of code
from the combiner
argument to foldRightUntil
:
leftmostTip(subTree).map((path) => Some(index :: path))
Does it make sense to use map
on the result returned
by leftmostTip
? Yes, because (a) if you look it up,
it's there in the Option
API; and (b) for many
purposes, an Option[A]
behaves like
a List[A]
with either 0 elements or 1. That is,
None.map(
f)
= None
and
Some(
x).map(
f)
=
Some(
f(
x)
. What's weird
in this case is that the x in question is the result of
leftmostTip
, so it's of type Option[Path]
.
So if the result returned by leftmostTip
is
Some(List(
p0, p1,
…))
, the result of the map
is
Some(Some(List(index
, p0, p1,
…)))
, of
type Option(Option(Path))
. This seems at first to defy
the laws of physics, or at least of good taste. But it's perfectly
legal, and actually makes sense. The outer layer of Some
is a signal to foldRightUntil
. It's peeled off before
the value is returned, the value being itself of type Option[Path]
.
2B But let's get back to business, and back to our object PS3Q2:
def expandPath(tr: CFGsyntaxTree, path: Path, grammar: CFG): List[CFGsyntaxTree] = (path, tr) match { case (Nil, CFGtreeTip(nontrm, d)) => { def expandUsingRule(lhs: CFGnonterminal, rule: CFGrule, depth: Int): CFGexpansion = { val subTrees = for { sym <- rule.rhs } yield sym match { case term: CFGterminal => CFGtreeLeaf(term, d+1) case nt: CFGnonterminal => CFGtreeTip(nt, d+1) case CFGempty => throw new Exception("CFGempty found in unexpected place") } CFGexpansion(nontrm, subTrees, rule, d, // The expansion is one level deeper except // in the case where the rhs is empty – if (rule.rhs.isEmpty) d else d+1) } // Body of "case (Nil, CFGtreeTip(…)) => …" starts here – val rules = grammar.lookupLeft(nontrm) rules.map(r => expandUsingRule(nontrm, r, d)) } case (i :: is, CFGexpansion(nontrm, expansion, rule, depth, maxDepth)) => { if (i < 0 || i >= expansion.length) throw new Exception( "Bogus index " + i + " in path (expansion length = " + expansion.length + ", nontrm = " + nontrm + ")" ) val subTree = expansion(i) // Stuff before the path to the tip – val beforeStuff = expansion.take(i) // Stuff after the path to thetip val afterStuff = expansion.drop(i+1) // For each way of expanding the path to the subtree, substitute // it for the `i`th element of `expansion` – expandPath(subTree, is, grammar).map(newTree => CFGexpansion(nontrm, beforeStuff ++ (newTree :: afterStuff), rule, depth, // The only way the new tree can provide a higher // maxDepth than before is if the new subTree // provides it – Math.max(maxDepth, newTree.maxDepth)) ) } case _ => throw new Exception( "Path and tree out of synch: path = " + path + "\n Tree = " + tr ) } }
Note Check
Here are the gory details (I think):
The type checker tries to find values for all the type variables that
make the call to foldRightUntil
match its header.
To avoid confusion, we'll call the A
from
foldRightUntil
Af
and
the A
from removeOneWithFoldRightUntil
Ar
.
When the type checker matches the types in the first argument lists it concludes
Af
=
Ar
by looking at the type of the l
arguments.
But then it matches z
againt Nil
and decides
B
= List[Nothing]
. Having a type of
List[Nothing]
is not the end of the world in some cases
because if, say, you later require it to = List[Int]
,
then it takes the greatest upper bound of Nothing
and Int
, i.e., Int
, and B
gets
type List[Int]
. Unfortunately, we never get anything as
concrete as Int
to match against Nothing
;
all we have is Ar
. The type checker just
infers that Ar
=Nothing
, and it
can't recover from that.
[Back]