weaver-test icon indicating copy to clipboard operation
weaver-test copied to clipboard

Suggestion: partial pattern match syntax

Open mcanlas opened this issue 3 years ago • 11 comments

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))

mcanlas avatar Nov 27 '22 12:11 mcanlas

Tangent Q: thoughts on a matcher for something like MonadError and dump the error channel on failure?

mcanlas avatar Nov 27 '22 18:11 mcanlas

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.

Baccata avatar Nov 27 '22 21:11 Baccata

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.

mcanlas avatar Nov 28 '22 00:11 mcanlas

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.

keynmol avatar Nov 28 '22 09:11 keynmol

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 ?

Baccata avatar Nov 28 '22 09:11 Baccata

@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 => ... }

mcanlas avatar Nov 28 '22 13:11 mcanlas

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

Baccata avatar Nov 28 '22 13:11 Baccata

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)?

keynmol avatar Nov 28 '22 14:11 keynmol

forSome is a keyword in Scala 2, and also the Some suffix is tightly coupled to Option imho. How about expectMatch ?

Baccata avatar Nov 28 '22 14:11 Baccata

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.

mcanlas avatar Nov 28 '22 15:11 mcanlas

Sure go ahead

Baccata avatar Nov 28 '22 16:11 Baccata

Merged, but not yet released

mcanlas avatar Feb 21 '23 21:02 mcanlas

Released in 0.8.2! 🎉

mcanlas avatar Mar 25 '23 09:03 mcanlas