Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Simplifying property configuration #346

Draft
wants to merge 1 commit into
base: master
Choose a base branch
from

Conversation

dharmaturtle
Copy link
Member

Closes #343

Okay, I got sucked into this.

Marking as a draft cause I still need to

  • update tutorial/documentation
  • update changelog

Buuuuut for the most part I think it's mostly done.

@@ -71,91 +71,46 @@ type PropertyExtensions private () =
let (Property property) = property
Property.report property

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It might actually be nice to keep the config overloads. Thoughts?

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we should only expose a single way to configure properties; this new API should be the only API.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am not exactly sure what is being discussed here. However, I am not overly concerned with the C# API.

let ofGen (x : Gen<Journal * Outcome<'a>>) : Property<'a> =
Property x
let ofGen (gen : Gen<Journal * Outcome<'a>>) : Property<'a> =
Property (PropertyConfig.defaultConfig, gen)
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I gave the PropertyConfig.defaultConfig zero thought.

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We'll need to audit the use of this function, if it's being used as prop |> toGen |> doGenThing |> ofGen, then we'll lose the original configuration.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Indeed. All such calls should be replaced with Property.Generator.map (as I structured/named it in another comment).

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How about, it a PR before this one, we change Property from a single case discriminated union to a record with a single field as well as expressing all maps over that field as such? Then I think it would easier to verify where PropertyConfig.defaultConfig should be used.

Property (config, gen)

/// Restores the default shrinking behavior.
let withoutShrinks (Property (config, gen) : Property<'a>) : Property<'a> =
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this should be called withDefaultShrinks.

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm okay with this, the current name withoutShrinks sounds as if no shrinking will occur.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think a better name that withDefaultShrinks (which is a syntactic name) is withUnlimitedShrinking (which is a semantic name)

|> Report.render
/// Set the number of times a property is allowed to shrink before the test
/// runner gives up and displays the counterexample.
let withShrinks (shrinkLimit : int<shrinks>) (Property (config, gen) : Property<'a>) : Property<'a> =
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Might be nice to just take an int instead of an int<shrinks>

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you expand on this?

@@ -4,15 +4,15 @@ open System

[<Struct>]
type Property<'a> =
| Property of Gen<Journal * Outcome<'a>>
| Property of PropertyConfig * Gen<Journal * Outcome<'a>>
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What about making this into a record? How about this?

{ Config: PropertyConfig
  Generator: Gen<Journal * Outcome<'a>> }

Copy link

@ghost ghost Sep 11, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm okay with this, I think we should also mark the internals as private, and use lower camel case for the names:

// unsure of the validity of this syntax, just typing into the comment editor right now
type Property<'a> =
    private
    {
        config: PropertyConfig
        generator: Gen<Journal * Outcome<'a>>
    }

In any case, we should make 2 accessor functions, and only use them. They can be marked as inline if we have performance concerns.

let inline config (property : Property<'a>) : PropertyConfig =
    property.config

// This already exists as 'toGen'
let inline generator (property : Property<'a>) : PropertyConfig =
    property.generator

Thoughts?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am ok with removing the public access scope. However, I think the access scope should be internal instead of private.

I think we should follow the F# naming guidelines and use Pascal case for record field names.

The most common operation is mapping, not (just) getting. As such, I think we should create a submodule inside Property like this (@dharmaturtle knows what I mean):

module Config =
  let get p = p.Config
  let set p c = { p with Config = c }
  let map f p = { p with Config = f p.Config }

Copy link

@ghost ghost left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let me just say, I love the work done here. So many extra methods/functions are now gone! 🚀

We'll need to look at this carefully, so the review may take a while but I have no doubt that this is the right way to go. Thank you very much for doing this @dharmaturtle!

let report =
property {
let! actual = Range.linear 1 1_000_000 |> Gen.int
return actual < 500_000
} |> Property.reportWith propConfig
} |> Property.withShrinks shrinkLimit |> Property.report
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These two |> should be on separate lines for readability.

[<Extension>]
static member WithConfig (property : Property, config : PropertyConfig) : Property =
let (Property property) = property
Property (Property.withConfig config property)
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we can get rid of this, and only expose configuration with Property.with*

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is the same as Property.Config.set (as I strucutured/named it in another comment). I think it is better to keep. Might want to consider renaming to setConfig though.

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What would the value be in overriding a user's preferences?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It would be the user calling this function, it would be their preference. For example, @dharmaturtle talks about how he runs PB tests with a time limit during development and with a test limit during integration. I think the minimal amount of duplicated code to achieve this would be constructing the correct PropertyConfig instance in a global location and then referencing it from all the PB tests in order to set it, which (when using C#) would be achieved via this method.

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You could also make an extension method of Property that configures them to all be the same and call that on each property you want it to apply to.

public static Property ConfigureSpecifically(this Property property) =>
    property.WithTests(1000).WithoutShrinkLimit().Whatever();

// in a test

var someProperty = // ...
someProperty.ConfigureSpecifically().OverrideLocally().Check();

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good point.

I think I would still keep this myself, but I won't argue further in favor of keeping it.

@@ -71,91 +71,46 @@ type PropertyExtensions private () =
let (Property property) = property
Property.report property

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we should only expose a single way to configure properties; this new API should be the only API.

@ghost ghost added this to the 0.11.0 milestone Sep 11, 2021
@dharmaturtle dharmaturtle marked this pull request as draft September 18, 2021 14:21
@dharmaturtle
Copy link
Member Author

Gonna let #353 go first as it's smaller, more mature, and may possibly establish designs/patterns that this will follow.

@ghost ghost modified the milestones: 0.11.0, 0.12.0 Sep 21, 2021
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Simplifying property configuration
2 participants