Scala idiom for optional keyword arguments

What is the scala idiom for writing a function which takes a huge number of key/value pair arguments which the caller can usually omit any/most/some of?

In lisp languages this is done with keyword arguments (common lisp and also clojure). For example I can define the function to take any of the keys :a, :b, :c, :d,:e, :f, :g, :h and the types associated with any of the arguments can be specified if desired. Then when the function is called the caller can specify any or all of the keys and their associated values.

I know that scala does not support this exact thing, but there must be some way to achieve the same effect?

The case I have is that I have a gnuPlot function which takes a descriptor for a set of curves to plot. But a gnu-plot plot has lots and lots and lots of options for log axes, soft/hard grid, boundaries, line or scatter plot, axis labels, etc etc etc. I don’t want the caller of gnuPlot to have to specify all of these, only the ones he cares about.

The lisp code call site might look like the following:

(gnu-plot data :x-log t :y-log nil :label-font "Helvetica" :x-min 100.0 :line-width 3)
(gnu-plot data :y-max 1000.0 :line-color "green")

I considered just having a second argument which is a Map[String,Any], but I’ve never worked with Any type in Scala. I would have to do manual type checking on the expected types. I don’t even know whether it is possible.

I also considered creating a case class called gnuPlotOptions whose member variables are the supported options and are declared with the correct types, but I don’t know how to instantiate a case class only specifying some of the arguments in any order.

Perhaps there is a much better way?

Have you considered default arguments or default case class instance?

Example with default arguments:

def default1 = random.nextInt
def method(arg1: Int = default1, arg2: String = "hey")(arg3: Char = arg2(0)) = ???

Example with default case class instance:

case class Options(arg1: Int, arg2: String, arg3: Char)
val defaultOpts = Options(8, "hey", 'x')
// changing just one argument from the defaults
val customOpts = defaultOpts.copy(arg2 = "nope")
// use custom options
method(customOpts)

If defaults are not appropriate then you can use Options to designate a lack of value, e.g.

def method(arg1Opt: Option[Int] = None, arg2Opt: Option[String] = None, arg3Opt: Option[Char] = None) = ???
// changing just one argument from the defaults
method(arg2Opt = Some("nope"))

Similar change can work with case classes.

There are also heterogenous lists (they work if each argument has unique type). Google e.g. “shapeless hlist”: https://github.com/milessabin/shapeless/blob/master/core/src/main/scala/shapeless/hlists.scala
Also a potential alternative in Dotty are records, but that’s probably a stretch (i.e. not really a direct alternative).

I think you understand my question, but let me see if I understand your suggestion.

The call-site would like like the following?

gnuplot(data)(defaultOpts.copy(logx=true,linestyle="solid",color="red"))
gnuplot(data)(defaultOpts.copy(logy=true,yMax=1000.0,lineWidth=3))

or are you suggesting a calling syntax of:

gnuplot(data)(logX=true, linestyle="solid", color="red")
gnuplot(data)(color="green", logY=true, yMax=1000.0, lineWidth=3)

In case you’re suggesting the latter, that would suite me perfectly. I wasn’t aware defining default values for arguments, also allowed optionality in any order.

Both options work. I’ve provided both of them so you can choose the one that suits you better. Grouping arguments in a case class is beneficial if you want to pass them through many methods - passing a single case class as a parameter multiple times is more convenient than passing expanded parameter group multiple times.

If you’re naming all arguments then you can pass them in any order (except if you have varargs). More info about named and default arguments:
https://docs.scala-lang.org/tour/default-parameter-values.html
https://docs.scala-lang.org/tour/named-arguments.html

That works well. Thanks. One surprise that I noticed is that the defaults do not have letsec semantics.

I.e.,

 def gnuPlot(dataToPlot: List[(String, List[Double], List[Double])])(
              terminals: Set[String]=Set("png"), 
              title             : String = "",
              comment           : String = title) = {...}

title is marked as an unbound variable when used as the default value of comment. :frowning:

A default parameter value cannot refer to a parameter in the same or later parameter group. That’s why I’ve split parameters into two groups before:

You can split your parameters into groups so that the rule isn’t violated.

Another option is to use Options, e.g:

def gnuPlot(title: String = "", commentOpt: Option[String] = None) = {
  // 'title' is in the same parameter group as 'commentOpt'
  // but we're outside of parameters section, so it doesn't matter
  val comment = commentOpt.getOrElse(title)
  ...
}

However, then the callsite would have to have parens for all the groups. and have the correct arguments in the correct group, so we’d lose the order independence. right?

gnuplot(data)()()()(xLog=true)()()(color="red")

Yes, that’s right. If too many parameter groups are needed then solution using Options look preferable.

I don’t know for sure why the limitation about default parameter values is that strict, but it may be related to overriding. You can override default parameter values. Look here:

class Parent {
  def method(arg: Int = 5): Unit =
    println(arg)
}

class Child extends Parent {
  override def method(arg: Int = 8): Unit =
    super.method(arg)
}

val instance: Parent = new Child
instance.method() // prints: 8

It would probably be hard to resolve all potential inter-dependencies between parameters in the same parameter group if you’re allowed to override them freely and provide arguments in any order (using their names).