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.