Skip to content

fix(schema): move SchemaExpr &&/|| to companion implicit class to en…#1463

Open
googley42 wants to merge 10 commits into
zio:mainfrom
googley42:schema-expr-boolean-ops-as-extension
Open

fix(schema): move SchemaExpr &&/|| to companion implicit class to en…#1463
googley42 wants to merge 10 commits into
zio:mainfrom
googley42:schema-expr-boolean-ops-as-extension

Conversation

@googley42

@googley42 googley42 commented Jun 15, 2026

Copy link
Copy Markdown
Contributor

…able downstream bridges

In Scala 2, a direct method found by name on a type suppresses implicit class receiver views even when the argument type does not match (spec §6.26 — the view fires only when the method is "not found"). With && and || as direct methods on SchemaExpr, downstream libraries cannot provide an implicit class bridge such as

implicit class Bridge[S](self: SchemaExpr[S, Boolean]) {
  def &&(other: DomainExpr[S]): DomainExpr[S] = ...
}

because Scala commits to SchemaExpr.&&(SchemaExpr) by name and reports a type mismatch on the argument rather than trying the bridge.

Moving &&/|| to BooleanOps, an implicit class in the SchemaExpr companion, removes the direct-method lock-in. The companion implicit always provides &&(SchemaExpr) for the existing use case; a downstream bridge providing &&(OtherType) is unambiguous because only one candidate applies per call site based on the argument type.

Source compatibility is fully maintained: se1 && se2 continues to work identically. Binary compatibility is intentionally broken (direct virtual method replaced by implicit class dispatch) as expected for this kind of extensibility improvement.

Scala 3 is unaffected by the receiver-view limitation but benefits from the same structural change; implicit class is retained over extension syntax for cross-version consistency.

Closes #1470

…able downstream bridges

In Scala 2, a direct method found by name on a type suppresses implicit
class receiver views even when the argument type does not match (spec
§6.26 — the view fires only when the method is "not found"). With &&
and || as direct methods on SchemaExpr, downstream libraries cannot
provide an implicit class bridge such as

  implicit class Bridge[S](self: SchemaExpr[S, Boolean]) {
    def &&(other: DomainExpr[S]): DomainExpr[S] = ...
  }

because Scala commits to SchemaExpr.&&(SchemaExpr) by name and
reports a type mismatch on the argument rather than trying the bridge.

Moving &&/|| to BooleanOps, an implicit class in the SchemaExpr
companion, removes the direct-method lock-in. The companion implicit
always provides &&(SchemaExpr) for the existing use case; a downstream
bridge providing &&(OtherType) is unambiguous because only one
candidate applies per call site based on the argument type.

Source compatibility is fully maintained: se1 && se2 continues to work
identically. Binary compatibility is intentionally broken (direct
virtual method replaced by implicit class dispatch) as expected for
this kind of extensibility improvement.

Scala 3 is unaffected by the receiver-view limitation but benefits from
the same structural change; implicit class is retained over extension
syntax for cross-version consistency.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings June 15, 2026 06:31

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Note

Copilot was unable to run its full agentic suite in this review.

Moves boolean && / || combinators off SchemaExpr methods and into a companion-scope implicit class to avoid Scala 2 “member method suppresses implicit receiver views” behavior and enable downstream extensions.

Changes:

  • Removed && / || from SchemaExpr instance methods
  • Added SchemaExpr.BooleanOps implicit value class providing && / || for SchemaExpr[A, Boolean]

Comment on lines +152 to +162
/**
* Logical combinators for boolean SchemaExprs.
*
* Provided as an implicit class rather than direct methods on SchemaExpr so that
* downstream libraries can supply their own implicit class with &&/|| overloads for
* a different result type (e.g. a DDB-specific or SQL-specific expression ADT).
* In Scala 2, a direct method found by name suppresses implicit receiver views even
* when the argument type does not match; an implicit class in the companion avoids
* this and allows downstream bridges to fire without ambiguity.
*/
implicit final class BooleanOps[A](private val self: SchemaExpr[A, Boolean]) extends AnyVal {

@googley42 googley42 Jun 15, 2026

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Good catch on the Scaladoc — "without ambiguity" was too broad and has been updated to be more precise (see latest commit).

On the underlying design: the ambiguity concern holds when two implicits compete at the same priority level (e.g. both companion-scope). That case is called out in the updated doc.
However, the motivating use case — a downstream library like zio-dynamodb adding &&(CE): CE overloads — avoids this entirely: those ops are introduced via an explicit import
DdbBlocksApi._, which in Scala 2 places them in import scope, which has higher priority than companion scope (BooleanOps). So for se && ce the downstream implicit wins cleanly, and for se &&
se2 there is no applicable downstream overload (wrong arg type), so BooleanOps is the only candidate.

The opt-in object logical approach would eliminate the companion-scope collision risk entirely, but at the cost of requiring an extra import for users of SchemaExpr directly. The
non-symbolic and/or approach is the cleanest separation but trades ergonomics for purity. For a general-purpose expression algebra where &&/|| are the natural operators, companion scope
with the priority caveat documented seems the right balance — but happy to discuss if you feel differently.

Comment thread schema/shared/src/main/scala/zio/blocks/schema/SchemaExpr.scala Outdated
Comment thread schema/shared/src/main/scala/zio/blocks/schema/SchemaExpr.scala Outdated
@googley42 googley42 marked this pull request as draft June 15, 2026 06:35
googley42 and others added 4 commits June 15, 2026 07:48
The B2 type parameter with =:= evidence was equivalent to writing Boolean
directly; remove the indirection for cleaner API surface and better
type-error messages at call sites. Also clarify Scaladoc on implicit
priority guarantees for downstream ops classes.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Per symbolic-ops policy: named method is the primary API, symbolic
operator is sugar that delegates to it.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@github-actions

github-actions Bot commented Jun 15, 2026

Copy link
Copy Markdown

🚀 Preview deployed to Netlify: https://zio-blocks-pr-1463--zio-dev.netlify.app

@googley42 googley42 marked this pull request as ready for review June 15, 2026 07:42
Copilot AI review requested due to automatic review settings June 15, 2026 07:42

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 1 out of 1 changed files in this pull request and generated 4 comments.

Comment thread schema/shared/src/main/scala/zio/blocks/schema/SchemaExpr.scala Outdated
Comment thread schema/shared/src/main/scala/zio/blocks/schema/SchemaExpr.scala Outdated
Comment thread schema/shared/src/main/scala/zio/blocks/schema/SchemaExpr.scala Outdated
Comment on lines +175 to +187
def and(that: SchemaExpr[A, Boolean]): SchemaExpr[A, Boolean] =
SchemaExpr(
DynamicSchemaExpr.Logical(self.dynamic, that.dynamic, DynamicSchemaExpr.LogicalOperator.And),
self.inputSchema,
Schema[Boolean]
)

def or(that: SchemaExpr[A, Boolean]): SchemaExpr[A, Boolean] =
SchemaExpr(
DynamicSchemaExpr.Logical(self.dynamic, that.dynamic, DynamicSchemaExpr.LogicalOperator.Or),
self.inputSchema,
Schema[Boolean]
)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

The concern is noted but I don't think self.outputSchema is the right choice here. and/or combine two operands — self and that — each with their own outputSchema. Picking the LHS schema
is arbitrary and asymmetric. More fundamentally, the output schema for a boolean logical combinator carries no meaningful content to preserve: Schema[Boolean] is a fixed primitive schema,
and any schema annotations in a SchemaExpr attach to the input type (inputSchema), not to the boolean result type. Using the summoned canonical Schema[Boolean] is correct here. If the
caller needs a specific annotated boolean schema for the result, the right API would be an explicit Schema[Boolean] parameter on and/or — not silent inheritance from one side.

@googley42 googley42 marked this pull request as draft June 15, 2026 08:18
googley42 and others added 3 commits June 15, 2026 16:55
…ility

Boolean is final so this makes no practical difference, but using a type
bound rather than fixing B = Boolean is accurate to the original intent
and avoids a source-incompatible narrowing for abstract-bounded types.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@googley42 googley42 marked this pull request as ready for review June 18, 2026 10:23
Copilot AI review requested due to automatic review settings June 18, 2026 10:23

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 1 out of 1 changed files in this pull request and generated 2 comments.

Comment thread schema/shared/src/main/scala/zio/blocks/schema/SchemaExpr.scala Outdated
Comment thread schema/shared/src/main/scala/zio/blocks/schema/SchemaExpr.scala
@googley42

googley42 commented Jun 21, 2026

Copy link
Copy Markdown
Contributor Author

@jdegoes / @987Nabil if you could pls review when you have some time that would be great

@googley42 googley42 changed the title feat(schema): move SchemaExpr &&/|| to companion implicit class to en… fix(schema): move SchemaExpr &&/|| to companion implicit class to en… Jun 25, 2026
@googley42

googley42 commented Jul 9, 2026

Copy link
Copy Markdown
Contributor Author

@plokhotnyuk if you have some time to review this PR please that would great

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.

move SchemaExpr &&/|| to companion implicit class to enable Scala2 downstream bridges

2 participants