Suggestion: partial pattern match syntax
What do we think? Tried to provide more vibrant information by using Show
Inspired by https://www.scalatest.org/user_guide/using_inside
def matches[A](x: A)(
f: PartialFunction[A, Expectations]
)(implicit pos: SourceLocation, A: Show[A] = Show.fromToString[A]): Expectations =
if (f.isDefinedAt(x))
f(x)
else
failure("Pattern did not match, got: " + A.show(x))
Tangent Q: thoughts on a matcher for something like MonadError and dump the error channel on failure?
What do we think?
I'm not certain I want it, not certain I don't want it. @keynmol wdyt ?
Tangent Q: thoughts on a matcher for something like MonadError and dump the error channel on failure?
You can just lift your monad to the effect type and gain this behaviour.
Re: the MonadError case, I have a suite that is pure and one of the return values happens to be Either or ValidatedNec. So it's inconvenient to hoist up the suite's effect type just for singular cases.
I'm not certain I want it, not certain I don't want it. @keynmol wdyt ?
Funny that Mark mentioned inside specifically - I've been using it a ton in my current scalatest codebase, and IMO it's an excellent combinator. I'm using it to replace a lot of the usages of .get/.fold on Eithers.
It mostly shows up in doing tons of this:
enum Test:
case Yo(i: Int)
case Noyo(b: String)
exists(Option(Test.Yo(15))) {
case Test.Yo(i) => expect(i == 25)
case Test.Noyo(b) => failure("expected Yo")
}.run.tap(println)
having to handle the "rest" branch and give it a meaningful message is annoying.
A generic case of this is asserting on the Left part of either if you do attempt or something.
Currently our forEach and exists combinators are not sufficient:
val x = Try[Int](throw NoSuchMethodException("yo"))
forEach(x) {i => expect(i == 25)}.run.tap(println)
forEach(x.toEither) {i => expect(i == 25)}.run.tap(println)
exists(x) {i => expect(i == 25)}.run.tap(println)
exists(x.toEither) {i => expect(i == 25)}.run.tap(println)
output:
Valid(())
Valid(())
Invalid(NonEmptyList(weaver.AssertionException: empty))
Invalid(NonEmptyList(weaver.AssertionException: empty))
After just writing it, I found forEach to be super surprising - even though it makes sense that "container" is empty.
It's the classic Option#forall with empty Option pitfall.
exists does better, but an error message could be improved - and it depends on having Foldableinstance for the type.
I imagine myself doing a lot of inside(value: Test) { case Test.Yo(b: Int) => ... } just to get a good error message when value is not Test.Yo. Lazy, yes, but very convenient.
If you define matches, the you get a nicer error:
matches(x) {case Success(i) => expect(i == 25)}.run.tap(println)
matches(x.toEither) {case Right(i) => expect(i == 25)}.run.tap(println)
// Invalid(NonEmptyList(weaver.AssertionException: Pattern did not match, got: Failure(java.lang.NoSuchMethodException: yo)))
// Invalid(NonEmptyList(weaver.AssertionException: Pattern did not match, got: Left(java.lang.NoSuchMethodException: yo)))
I just wonder if we can do anything better with the error message.
exists does better
It is correct that forEach is classically called forall. It is also worth noting that exists and forall are the exact dual of each other, folding the data structure using boolean sums and products respectively. It's not like one is better, it may be that the terminology doesn't match your own intuition (arguably there's no perfect terminology for this)
I've been using it a ton in my current scalatest codebase, and IMO it's an excellent combinator
That's sufficient for me to consider the addition, but I don't like the name inside because it's not actually peeking into the data structure, it's matching against the whole thing. I'm cool with using matches as suggested by Mark. Any other suggestion on your end, @keynmol ?
@keynmol Your extremely similar experience is very validating, thank you for the detailed write up! Get outta my brain! 😵💫
Re: Error channel messages, my vibe is similar, demand Show[E]
My confusion was these are two slightly similar but different use cases. The proposed matches would work on ANY expression. But then this other, unnamed combinator has a clunky name. applicativeErrorWasGud(ae) { x => ... }
You won't be able to write a generic MonadError integration, because all combinators would be working against the context of the MonadError, and does not give you the necessary abstraction to be able to deconstruct it. You need something like BiFoldable
I'm cool with using matches as suggested by Mark. Any other suggestion on your end, @keynmol ?
Yeah I'm okay with matches, but may be a version of for* can be explored - i.e. forSome, indicating a presence of partial function (and not Some)?
forSome is a keyword in Scala 2, and also the Some suffix is tightly coupled to Option imho. How about expectMatch ?
I don't think I'm following as closely now with the BiFoldable bit but I'll try to make a PR (or two?) tonight to help drive discussion.
Sure go ahead
Merged, but not yet released
Released in 0.8.2! 🎉