6

In some method I would like to force parameters to be named. The reason being autogenerated code for which the order of parameters is unspecified (and will remain that way).

The closest I can get is

private val _forceNamed: Object = new Object()

def doSomething(forceNamed: Object = _forceNamed, arg1: String, arg2: String, ...): Unit = {
  if (forceNamed != _forceNamed) {
    throw Exception(something)
  }

  // actually do stuff
}

However this only fails at runtime, whereas something failing at compile time would be much nicer.

Jamie
  • 3,901
  • 3
  • 23
  • 27

5 Answers5

4

If you want to close the loophole of being able to pass in null, you can use a value class as a guard.

scala> :paste
// Entering paste mode (ctrl-D to finish)

class Foo {
   import Foo._
   def foo(x: Bar = bar, a: String, b: String) = println(a + b)
}

object Foo {
  private[Foo] class Bar(val i: Int) extends AnyVal
  private val bar = new Bar(42)
}

// Exiting paste mode, now interpreting.

defined class Foo
defined object Foo

scala> val f = new Foo
f: Foo = Foo@4a4f9c58

scala> f.foo(null, "", "")
<console>:13: error: type mismatch;
 found   : Null(null)
 required: Foo.Bar
       f.foo(null, "", "")
             ^
Jasper-M
  • 14,966
  • 2
  • 26
  • 37
  • Good answer! What I don't understand is why you can make `Bar` be `private[Foo]` and that isn't considered escaping the defining scope (the error that is reported when you try to make `Bar` as `private`). – Jamie Jan 07 '17 at 20:19
  • 1
    @Jamie Good question. I don't know the answer. I suspect the JVM will reject the class file if a private class leaks out. But private[Foo] has to be compiled to public (or default or protected, I'm not sure). Then the JVM doesn't know so it doesn't care. Don't know if that's a scalac bug or feature. – Jasper-M Jan 07 '17 at 20:31
3

We have this in our code base for this purpose:

object `(Please use explicitly named arguments)`
def foo(
  `(Please use explicitly named arguments)`:
    `(Please use explicitly named arguments)`.type =
    `(Please use explicitly named arguments)`,
  namedArg1: Int,
  namedArg2: String,
  ...
) = ...
cvogt
  • 11,260
  • 30
  • 46
  • Won't compile for me unless the parameter name is different from the object name. – jwvh Jan 07 '17 at 19:16
  • I believe you can still pass null for the argument, which of course someone should not do, but someone may do anyways – Jamie Jan 07 '17 at 19:59
2

Something like this maybe:

class Foo {
   class Bar private[Foo]()
   private val bar = new Bar
   def foo(x: Bar= bar, a: String, b: String) = println(a + b)
}
Dima
  • 39,570
  • 6
  • 44
  • 70
  • 1
    Would someone still be able to pass null? So it would still need a runtime check. Though this does have the advantage that someone would be much less likely to pass something by accident causing a failure at runtime instead of compile time. – Jamie Jan 07 '17 at 01:35
1

What a great idea.

It looks like hygiene issues with default args prohibit singleton types.

$ scala
Welcome to Scala 2.12.1 (Java HotSpot(TM) 64-Bit Server VM, Java 1.8.0_111).
Type in expressions for evaluation. Or try :help.

scala> private val x = new Object ; def f(foo: x.type = x, i: Int) = i
<console>:11: error: private value x escapes its defining scope as part of type x.type
       private val x = new Object ; def f(foo: x.type = x, i: Int) = i
                                                ^

scala> val x = new Object ; def f(foo: x.type = (x: x.type), i: Int) = i
x: Object = java.lang.Object@1e54cb33
f: (foo: x.type, i: Int)Int

scala> f(i = 42)
<console>:13: error: type mismatch;
 found   : Object
 required: x.type
       f(i = 42)
       ^

or no, this looks OK:

    private[this] val x: Object = new java.lang.Object();
    <stable> <accessor> def x: Object = $iw.this.x;
    def f(foo: x.type = $iw.this.x, i: Int): Int = i;
    <synthetic> def f$default$1: x.type = $iw.this.x

or is the problem the assignment to the default value?

but you can't do this:

scala> val x: x.type = new Object
<console>:36: error: recursive value x needs type
       val x: x.type = new Object
              ^

I guess this works because you don't have to tell it that x is x.type:

scala> object x
defined object x

scala> def f(y: x.type = x, i: Int) = i
f: (y: x.type, i: Int)Int

scala> f(i = 42)
res2: Int = 42

That still allows explicitly providing x, which could be obfuscated.

I'm too scared to investigate why this fails:

scala> object x$$ ; def f(y: x$$.type = x$$, i: Int) = i
defined object x$$
f: (y: .type, i: Int)Int

scala> f(i = 42)
res0: Int = 42

scala> f(x$$, 42)  // or x$$$
<console>:13: error: not found: value x$$
       f(x$$, 42)
         ^

But that suggests that even though the object is public, access to it is somehow crippled by name mangling.

som-snytt
  • 39,429
  • 2
  • 47
  • 129
  • Looks like a good idea, though not knowing why exactly your last example fails seems a bit scary – Jamie Jan 07 '17 at 19:57
0
Welcome to Scala 2.12.2 (Java HotSpot(TM) 64-Bit Server VM, Java 1.8.0_102).
Type in expressions for evaluation. Or try :help.

scala> type `(Please use explicitly named arguments)` = Nothing 
defined type alias $u0028Please$u0020use$u0020explicitly$u0020named$u0020arguments$u0029

scala> def foo(`(Please use explicitly named arguments)`: => `(Please use explicitly named arguments)` = ???, i: Int, j: Int) = i + j 
foo: ((Please use explicitly named arguments): => (Please use explicitly named arguments), i: Int, j: Int)Int

scala> foo(null, 1, 4)
<console>:13: error: type mismatch;
 found   : Null(null)
 required: (Please use explicitly named arguments)
    (which expands to)  Nothing
       foo(null, 1, 4)
           ^

scala> foo(i = 1, j = 4)
res1: Int = 5
Yang Bo
  • 3,586
  • 3
  • 22
  • 35
  • For anyone else new to this syntax, it is referred to as a "by-name" parameter. See: https://stackoverflow.com/a/6952195/1080804 – ecoe Apr 10 '22 at 13:48