zio / zio-prelude

A lightweight, distinctly Scala take on functional abstractions, with tight ZIO integration

Home Page:https://zio.dev/zio-prelude

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

Add Gen instances for Newtype assertions

err0r500 opened this issue · comments

I would be great to be able to get a Gen for a Newtype (based on the assertions).
If it's already feasible, I haven't been able to figure out how...

Something like that :

import zio.test.*
import zio.prelude.Newtype
import zio.prelude.Assertion.{isEmptyString, hasLength, lessThan}

object Title extends Newtype[String]:
  override inline def assertion = !isEmptyString && hasLength(lessThan(10))
type Title = Title.Type

object PreludeSpec extends ZIOSpecDefault {
  import zio.test.magnolia.DeriveGen

  def spec = suite("Prelude")(
    test("generates valid values") {
      check(DeriveGen[Title]) { (t) =>
        assertTrue(Title.make(Title.unwrap(t)).toEither.isRight)
      }
    }
  )
}

@err0r500 You can derive a generator for the underlying type and collect the values that satisfy the assertion.

Sure, but doing this (chaining filters) usually (in other property-based testing frameworks I work with) quickly leads to being unable to generate valid values. I'll give a try though, thanks!

That is definitely true. I don't think there is enough information to derive a generator though. You can think of an assertion as a function A => Boolean. So with that information there is no way in general to generate the subset of values satisfying the predicate without filtering them. Obviously there is in specific cases such as multiplying a number by two to get even integers but that information is definitely not provided by the interface of an assertion.

I'm new to scala but from my shallow understanding of this code :

private[prelude] case class And[A](left: Assertion[A], right: Assertion[A]) extends Assertion[A] {

I was hoping that providing Gen (or something similar) instances based on type-level Assertions would allow to build up a Gen for a Newtype...

I still don't understand how you think this would work. We have a new type B which has some underlying representation as a type A. The only thing that the user needs to do to implement a new type is define a function A => Boolean which specifies whether an A is a valid instance of B. Assume we know how to generate values of type A. How do we use that information to generate B values?

Sorry I updated my previous comment, I move the relevant content here.
Not sure I get your point, the idea is to provide instances of each predicate and combine them (which would actually imply some filtering at some point) so the probability to generate valid values is high enough.

For instance, there are lots of cases where these instances can be more efficient than just filtering out.
Take strings :

  • hasLength(greaterThan(x)) -> generate a list of chars of size x, then append a random string
  • hasLength(lessThan(x)) -> truncate random string to length x
  • endsWith(x) -> generate a random string then append the char x
    etc.

Right. My point is that an assertion is an open domain. If you are given an Assertion[A] the only thing you know about it is that you have a function A => Boolean. You can certainly pattern match on it and if it is HasLength(GreaterThan(10)) you can do something more efficient. But a user can define an assertion with an arbitrary function A => Boolean, so how are you going to generate values for that?

I see, I hadn't in mind user defined predicates !
Sure, in this case we obviously won't be able to provide anything relevant.