4

The code explains itself.

val s = Seq(1,1,1)
val res: Seq[Int] = s.map(...)
                     .check(count how many 1s, if > 2 throw Exception)
                     .map(...)

I am searching the simple solution to this check function .

  • I can use map and closure to count and throw, but I want pure function.
  • I can use filter and size or reduce, but it return a value and not resumable with following maps.

How do I make a pure and stateful checking-pipe to the pipeline ?

WeiChing 林煒清
  • 4,452
  • 3
  • 30
  • 65
  • Write a simple function, which will take whole collection as input, do the check inside it using reduce, and will return Some(collection) if check was ok, and None otherwise. Then it will be pure. You can inject check as lambda. – michaJlS May 28 '16 at 08:35

3 Answers3

5

Throwing an exception is arguably not pure. If you instead were using a monadic form of error handling, you'd do something like this:

Option(s.map(foo)).
  filter(m => m.count(_ == 1) < 2).
  map{ s =>
    s.map(bar)
     .filter(baz)
     ...
  }

As it is, if you want to compose it within the pipeline, and you don't want to add extra parentheses as is necessary with match, you can use the commonly-enriched tap method:

implicit class TapAnything[A](private val a: A) extends AnyVal {
  def tap[U](f: A => U): A = { f(a); a }
}

Now you can

s.map(...)
 .tap(self => if (self.count(_ == 1) > 1) throw new Exception)
 .map(...)
 ...

(note: the private val + extends AnyVal stuff is just to indicate to the compiler that it's supposed to try to avoid creating an extra object to make the call).

Rex Kerr
  • 166,841
  • 26
  • 322
  • 407
  • I guess you could make a similar `tapOption` instead of throwing :) (great answer!) – Andy Hayden May 28 '16 at 03:14
  • 1
    @AndyHayden - It's not really a tap if it's changing the type. Otherwise it's a pipe, usually written `|>` and done in general, but you could of course do `s |> {x => if (x.count(_ == 1) < 2) Some(x) else None}` inline. (Possibly needing a `.` after the `s` to get the precedence of everything correct.) But, yes, one can make a validating analog (maybe `optionIf` would be a better name than `tapOption`). – Rex Kerr May 28 '16 at 03:16
4

One solution is to pattern match, so check would become:

> Seq(1, 1) match {
    case ls if (ls.count(_ == 1) <= 2) => ls
    case _ => throw new Exception("oh no!")
  }
List(1, 1): Seq[Int]


> Seq(1, 1, 1) match {
    case ls if (ls.count(_ == 1) <= 2) => ls
    case _ => throw new Exception("oh no!")
  }
java.lang.Exception: oh no!
  $.<init>(Main.scala:177)
  $.<clinit>(Main.scala:-1)

It may be preferable to return an Option type (instead of throwing):

> Seq(1, 1, 1) match {
    case ss if (ss.count(_ == 1) <= 2) => Option(ss)
    case _ => None
  }
None: Option[Seq[Int]]
Andy Hayden
  • 359,921
  • 101
  • 625
  • 535
1

If you want to pass along an error message without throwing an exception, a good candidate would be Either[A, B]. This would require some more work (working with left or right operands) down the pipeline, but allows you to pass a long a more descriptive error message, where for example, Option[T] can't convey:

val x = Seq(1,1,0)
        .map(_ * 3)
        .map(i => if (i > 2) Right(i) else Left("Was smaller than 2"))
        .map(_.right.map(_ * 3))

x.foreach {
  case Right(i) => println("Yay, right")
  case Left(error) => println(error)
}

scala> :paste
// Entering paste mode (ctrl-D to finish)

  val x = Seq(1,1,0)
    .map(_ * 3)
    .map(i => if (i > 2) Right(i) else Left("Was smaller than 2"))
    .map(_.right.map(_ * 3))

  x.foreach {
    case Right(i) => println("Yay, right")
    case Left(error) => println(error)
  }

// Exiting paste mode, now interpreting.

Yay, right
Yay, right
Was smaller than 2

This can be slightly inconvenient as Either is unbiased, but it does allow you to flow the behavior.

Yuval Itzchakov
  • 146,575
  • 32
  • 257
  • 321
  • The convention is that `Right` is correct and `Left` is wrong, when using `Either` for error handling. – Rex Kerr May 28 '16 at 14:54