Relative scoping for hierarchical ADT arguments

I also wish to add a use-case that wasn’t discussed here thus far (and didn’t exist for me when I initiated the idea several years ago).

I call it “by type” argument assignment (instead of by order or by name), and it’s using opaque types + givens + implicit conversion to do something which I think is cool.

What I do is that I have a configuration context object where each value gets its own unique type

import wvlet.log.LogLevel //using an external logger library
import CompilerOptions.* //importing the new configuration types defined in the companion

final case class CompilerOptions(
    parserLogLevel: ParserLogLevel,
    linterLogLevel: LinterLogLevel,
    backendLogLevel: BackendLogLevel
)
object CompilerOptions:
  given default(using
      parserLogLevel: ParserLogLevel = LogLevel.WARN,
      linterLogLevel: LinterLogLevel = LogLevel.WARN,
      backendLogLevel: BackendLogLevel = LogLevel.WARN
  ): CompilerOptions =
    CompilerOptions(
      parserLogLevel = parserLogLevel, linterLogLevel = linterLogLevel, backendLogLevel = backendLogLevel,
    )

  ///////////////////////////
  // New Types
  ///////////////////////////
  opaque type ParserLogLevel <: LogLevel = LogLevel
  given Conversion[LogLevel, ParserLogLevel] = identity
  object ParserLogLevel:
    export  LogLevel.*

  opaque type LinterLogLevel <: LogLevel = LogLevel
  given Conversion[LogLevel, LinterLogLevel] = identity
  object LinterLogLevel:
    export  LogLevel.*

  opaque type BackendLogLevel <: LogLevel = LogLevel
  given Conversion[LogLevel, BackendLogLevel] = identity
  object BackendLogLevel:
    export  LogLevel.*

So when users run the compile command def compile()(using CompilerOptions)..., they can easily define non-default configuration options like so:

import lib.*

given options.CompilerOptions.ParserLogLevel  = options.CompilerOptions.ParserLogLevel.INFO
given options.CompilerOptions.BackendLogLevel = options.CompilerOptions.BackendLogLevel.DEBUG

compile()

I wish to enable the user just do:

given options.CompilerOptions.ParserLogLevel  = .INFO
given options.CompilerOptions.BackendLogLevel = .DEBUG

So in this “by-type” argument passing use-case we have:

  • An enumeration from an external library
  • Assigned values that are not essentially the same type as the destination type.

Without a leading dot, or some alternative explicit relational scoping syntax, it can easily bring unexpected naming collisions. And I think this is a very worthy use-case. I started implementing it in my library and it’s extremely convenient, especially since more than one command requires these options as a context. I can have dozens of different options and sub-option objects and the user just doesn’t need to care what goes where. The type-system takes care of everything.

1 Like