Representation of numeric type with dynamic constraint

I am looking for a nice way to represent a numeric type that is constrained via runtime configuration. Example: Pagination of search results. The maximum page size, psMax (> 0), is given in the service configuration, the actual page size ps (with psMax >= ps > 0) is specified by the client. In the spirit of “parse, don’t validate”, I’d like to have a dedicated type (be it a generic “bounded positive int” or a specific “page size”) that represents legal values of this kind.

Independent of the implementation mechanism (plain case class, Shapeless tagged type, refined, Scala 3 opaque type,…), I see these options:

  • Plain PageSize without any reference to the max constraint whatsoever. This would become brittle in a system with multiple pagination settings - e.g. page sizes for “overview” with a max of 50 results could not be distinguished from page sizes for “details” with a max of 10 results.
  • Plain PageSize without any reference to the max constraint at the type level, but with a value level reference to the max constraint. Any validation/enforcement could only happen at value level/runtime, as well.
  • PageSize[Max <: Int] with a literal type. This could nicely enforce matching a corresponding PaginationConfig[Max <: Int] It feels like this approach would ultimately require this type parameter across the whole code base, which is inconvenient in itself and certainly won’t scale to many types of this kind.
  • PageSize { type Max <: Int }. Frankly, I don’t have much of an idea how I could get the type member to actually document/enforce anything about the max constraint. For now, just listing it here for the sake of completeness.
  • Anything else…?

Any suggestions, pointers to prior art, etc. appreciated.

I’ll start with taking the low road. How about anchoring your PageSize type as a nested type within a trait Sized, whose instances represent a size choice at runtime?

You would pass an instance of Sized as context to the various bits of your codebase and hopefully the Scala compiler will let you write signatures that reference it as sized.PageType.

I’ve gone down this road in another context and been told / found it for myself that things can get messy if there are several type anchor instances in play at the same time, but if your codebase can stick with just one anchor, it might work out…

Thanks for the suggestion. Frankly, I’m not quite sure what the concrete implementation would look like in practice and how it would improve over, say, the type parameter approach.

For starters, I’d probably want a case class PagingSpec with a PageSize member, along with a PageFrom member. PagingSpec in turn might be a member of a case class QuerySpec, where PagingSpec would be reusable across services, whereas QuerySpec is application specific. With the type parameter approach, PagingSpec and QuerySpec would have to “inherit” the type parameter.

Going with your suggestion, would they carry a reference to a Sized context instance themselves instead? (How would they reference the type(s) from case class members/constructor parameters?) Or would they be packaged inside the Sized, as well? Or something completely different?

A Scastie for you, @sangamon:

Showing two approaches - one that allows mixing of page sizes via supertypes and another that doesn’t.

You have distinct types for different sizes, but the size is represented as a runtime construct, which is I think what you were after.