Skip to content

Commit 50a556c

Browse files
authored
Merge pull request #1864 from Bhashit/issue-1856-box-combinator-and-other-improvements
Add transform and flip methods. transform allows callers to take a Box and, via PartialFunction, turn it into any other Box. If the PartialFunction fails, the original Box is returned. flip allows callers to take a Box and, if it is an EmptyBox, flip it into a Full box with a specific type. If it is a Full, an Empty is returned, flipping the box from full to empty or vice versa.
2 parents aa24c2d + 783bf6e commit 50a556c

2 files changed

Lines changed: 128 additions & 19 deletions

File tree

core/common/src/main/scala/net/liftweb/common/Box.scala

Lines changed: 55 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -167,8 +167,9 @@ sealed trait BoxTrait {
167167
* @return A `Full` containing the transformed value if
168168
* `pf.isDefinedAt(value)` and `Empty` otherwise.
169169
*/
170-
def apply[InType, OutType](pf: PartialFunction[InType, OutType])(value: InType): Box[OutType] =
171-
if (pf.isDefinedAt(value)) Full(pf(value)) else Empty
170+
def apply[InType, OutType](pf: PartialFunction[InType, OutType])(value: InType): Box[OutType] = {
171+
apply(value)(pf)
172+
}
172173

173174
/**
174175
* Apply the specified `PartialFunction` to the specified `value` and return
@@ -180,8 +181,9 @@ sealed trait BoxTrait {
180181
* @return A `Full` containing the transformed value if
181182
* `pf.isDefinedAt(value)` and `Empty` otherwise.
182183
*/
183-
def apply[InType, OutType](value: InType)(pf: PartialFunction[InType, OutType]): Box[OutType] =
184-
if (pf.isDefinedAt(value)) Full(pf(value)) else Empty
184+
def apply[InType, OutType](value: InType)(pf: PartialFunction[InType, OutType]): Box[OutType] = {
185+
pf.andThen(Full.apply).applyOrElse(value, (_: InType) => Empty)
186+
}
185187

186188
/**
187189
* This implicit transformation allows one to use a `Box` as an `Iterable` of
@@ -549,18 +551,18 @@ sealed abstract class Box[+A] extends Product with Serializable{
549551
*/
550552
def asA[B](implicit m: Manifest[B]): Box[B] = Empty
551553

552-
/**
553-
* Return this Box if `Full`, or the specified alternative if it is
554-
* empty. Equivalent to `Option`'s `[[scala.Option.orElse orElse]]`.
555-
*/
556-
def or[B >: A](alternative: => Box[B]): Box[B] = alternative
557-
558554
/**
559555
* Returns an `[[scala.collection.Iterator Iterator]]` over the value
560556
* contained in this `Box`, if any.
561557
*/
562558
def elements: Iterator[A] = Iterator.empty
563559

560+
/**
561+
* Return this Box if `Full`, or the specified alternative if it is
562+
* empty. Equivalent to `Option`'s `[[scala.Option.orElse orElse]]`.
563+
*/
564+
def or[B >: A](alternative: => Box[B]): Box[B]
565+
564566
/**
565567
* Get a `java.util.Iterator` from the Box.
566568
*/
@@ -773,10 +775,8 @@ sealed abstract class Box[+A] extends Product with Serializable{
773775
* If the partial function is defined at the current Box's value, apply the
774776
* partial function.
775777
*/
776-
final def collect[B](pf: PartialFunction[A, B]): Box[B] = {
777-
flatMap(value =>
778-
if (pf.isDefinedAt(value)) Full(pf(value))
779-
else Empty)
778+
final def collect[B](pf: PartialFunction[A, B]): Box[B] = flatMap { value =>
779+
Box(value)(pf)
780780
}
781781

782782
/**
@@ -788,6 +788,47 @@ sealed abstract class Box[+A] extends Product with Serializable{
788788
final def collectFirst[B](pf: PartialFunction[A, B]): Box[B] = {
789789
collect(pf)
790790
}
791+
792+
/**
793+
* Transforms this box using the `transformFn`. If `transformFn` is defined for this box,
794+
* returns the result of applying `transformFn` to it. Otherwise, returns this box unchanged.
795+
*
796+
* If you want to change the content of a `Full` box, using `[[map]]` or `[[collect]]` might be better
797+
* suited to that purpose. If you want to convert an `Empty`, `Failure` or a `ParamFailure` into a
798+
* `Full` box, you should use `[[flip]]`.
799+
*
800+
* @example {{{
801+
*
802+
* // Returns Full("alternative") because the partial function covers the case.
803+
* Full("error") transform { case Full("error") => Full("alternative") }
804+
*
805+
* // Returns Full(1), this Full box unchanged, because the partial function doesn't cover the case.
806+
* Full(1) transform { case Full(2) => Failure("error") }
807+
*
808+
* // Returns this Failure("another-error") unchanged because the partial function doesn't cover the case.
809+
* Failure("another-error") transform { case Failure("error", Empty, Empty) => Full("alternative") }
810+
*
811+
* // Returns Full("alternative") for an Empty box since `partialFn` is defined for Empty
812+
* Empty transform { case Empty => Full("alternative") }
813+
*
814+
* // Returns Empty because the partial function is not defined for Empty
815+
* Empty transform { case Failure("error", Empty, Empty) => Full("alternative") }
816+
*
817+
* }}}
818+
*/
819+
def transform[B >: A](transformFn: PartialFunction[Box[A], Box[B]]): Box[B] = {
820+
transformFn.applyOrElse(this, (thisBox: Box[A]) => thisBox)
821+
}
822+
823+
/**
824+
* Returns a `Full` box containing the results of applying `flipFn` to this box if it is a `Failure`,
825+
* `ParamFailure` or `Empty`. Returns `Empty` if this box is `Full`. In other words, it "flips" the
826+
* full/empty status of this Box.
827+
*/
828+
def flip[B](flipFn: EmptyBox => B): Box[B] = this match {
829+
case e: EmptyBox => Full(flipFn(e))
830+
case _ => Empty
831+
}
791832
}
792833

793834
/**

core/common/src/test/scala/net/liftweb/common/BoxSpec.scala

Lines changed: 73 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -67,8 +67,8 @@ class BoxSpec extends Specification with ScalaCheck with BoxGenerator {
6767
Full(1) reduceLeft {(x: Int, y: Int) => x + y} must_== 1
6868
}
6969
"be used as an Option" in {
70-
Full(1) orElse Some(2) must_== Some(1)
71-
Empty orElse Some(2) must_== Some(2)
70+
Full(1) orElse Some(2) must beSome(1)
71+
Empty orElse Some(2) must beSome(2)
7272
}
7373
"be implicitly defined from an Option. The openOrThrowException method can be used on an Option for example" in {
7474
Some(1).openOrThrowException("This is a test") must_== 1
@@ -152,14 +152,38 @@ class BoxSpec extends Specification with ScalaCheck with BoxGenerator {
152152
Full(Empty).flatten must_== Empty
153153
}
154154
}
155+
"define a 'collect' method that takes a PartialFunction to transform its contents" in {
156+
"If the partial-function is defined for the contents of this box, returns a full box containing the result of applying that partial function to this Box's contents" in {
157+
Full("Albus") collect { case "Albus" => "Dumbledore"} must_== Full("Dumbledore")
158+
}
159+
"If the partial-function is not defined for the contents of this box, returns Empty" in {
160+
Full("Hermione") collect { case "Albus" => "Dumbledore"} must beEmpty
161+
}
162+
}
163+
"define a 'transform' method that takes a PartialFunction to transform this box into another box" in {
164+
"If the partial-function is defined for this box, returns the result of applying the partial function to it" in {
165+
Full(404) transform {
166+
case Full(x: Int) if x != 200 => Failure("Server error")
167+
} must_== Failure("Server error")
168+
}
169+
"If the partial-function is not defined for this box, returns itself unchanged" in {
170+
Full("Intended Result") transform {
171+
case _: EmptyBox => Full("Alternative")
172+
case Full("Unexpected Result") => Full("Alternative")
173+
} must_== Full("Intended Result")
174+
}
175+
}
176+
"define a 'flip' method returning Empty" in {
177+
Full(1) flip { _ => "No data found" } mustEqual Empty
178+
}
155179
"define an 'elements' method returning an iterator containing its value" in {
156180
Full(1).elements.next must_== 1
157181
}
158182
"define a 'toList' method returning a List containing its value" in {
159183
Full(1).toList must_== List(1)
160184
}
161185
"define a 'toOption' method returning a Some object containing its value" in {
162-
Full(1).toOption must_== Some(1)
186+
Full(1).toOption must beSome(1)
163187
}
164188
"return itself if asked for its status with the operator ?~" in {
165189
Full(1) ?~ "error" must_== Full(1)
@@ -298,14 +322,34 @@ class BoxSpec extends Specification with ScalaCheck with BoxGenerator {
298322
"define a 'flatten' method returning Empty" in {
299323
Empty.flatten must beEmpty
300324
}
325+
"define a 'collect' method returning Empty" in {
326+
Empty collect { case _ => "Some Value" } must beEmpty
327+
}
328+
"define a 'transform' method that takes a PartialFunction to transform this Empty box into another box" in {
329+
"If the partial-function is defined for Empty, returns the result of applying the partial function to it" in {
330+
Empty transform {
331+
case Failure("error", Empty, Empty) => Full("failure-alternative")
332+
case Empty => Full("alternative")
333+
} must_== Full("alternative")
334+
}
335+
"If the partial-function is not defined for Empty, returns Empty" in {
336+
Empty transform { case Failure("The Phantom Menace", Empty, Empty) => Full("Return Of The Jedi") } must_== Empty
337+
}
338+
}
339+
"define a 'flip' method returning a Full box" in {
340+
Empty flip {
341+
case Empty => "flipped-empty"
342+
case _ => "flipped-failure"
343+
} mustEqual Full("flipped-empty")
344+
}
301345
"define an 'elements' method returning an empty iterator" in {
302346
Empty.elements.hasNext must beFalse
303347
}
304348
"define a 'toList' method returning Nil" in {
305349
Empty.toList must_== Nil
306350
}
307351
"define a 'toOption' method returning None" in {
308-
Empty.toOption must_== None
352+
Empty.toOption must beNone
309353
}
310354
"return a failure with a message if asked for its status with the operator ?~" in {
311355
Empty ?~ "nothing" must_== Failure("nothing", Empty, Empty)
@@ -358,7 +402,31 @@ class BoxSpec extends Specification with ScalaCheck with BoxGenerator {
358402
Failure("error", Empty, Empty) flatMap {x: String => Full(x.toString)} must_== Failure("error", Empty, Empty)
359403
Failure("error", Empty, Empty).flatten must_== Failure("error", Empty, Empty)
360404
}
361-
"return a itself when asked for its status with the operator ?~" in {
405+
"define a 'collect' method returning itself" in {
406+
Failure("error", Empty, Empty) collect { case _ => "Some Value" } must_== Failure("error", Empty, Empty)
407+
}
408+
"define a 'transform' method that takes a PartialFunction to transform this Failure into another box" in {
409+
"If the partial-function is defined for this Failure, returns the result of applying the partial function to it" in {
410+
Failure("The Phantom Menace") transform {
411+
case Failure("The Phantom Menace", Empty, Empty) => Full("Return Of The Jedi")
412+
} must_== Full("Return Of The Jedi")
413+
414+
Failure("The Phantom Menace") transform {
415+
case Failure("The Phantom Menace", Empty, Empty) => Failure("Clones")
416+
case _ => Full("Jedi")
417+
} must_== Failure("Clones")
418+
}
419+
"If the partial-function is not defined for this Failure, returns itself unchanged" in {
420+
Failure("Clones") transform { case Failure("The Phantom Menace", Empty, Empty) => Full("Jedi") } must_== Failure("Clones")
421+
}
422+
}
423+
"define a 'flip' method returning a Full box" in {
424+
Failure("error", Empty, Empty) flip {
425+
case Empty => "flipped-empty"
426+
case _: Failure => "flipped-failure"
427+
} must_== Full("flipped-failure")
428+
}
429+
"return itself when asked for its status with the operator ?~" in {
362430
Failure("error", Empty, Empty) ?~ "nothing" must_== Failure("error", Empty, Empty)
363431
}
364432
"create a new failure with a chained message if asked for its status with the operator ?~!" in {

0 commit comments

Comments
 (0)