1

I'd like to pass a default string to a function and have "string interpolation" done on it in the function rather than at the call site.

For example,

def isBetween(a:Int, b:Int, 
              msg: String = s"${v} is not between ${a} and ${b}."
             )(v:Int):Either[String, Boolean] = {
  if (a <= v && v <= b) Right(true) else Left(msg)
}

This doesn't compile because none of a, b, and for sure not v are in scope when the compiler wants to do the interpolation.

The goal is to provide a default error string but allow the user to change it, if necessary. For example:

val normalBetween = isBetween(0, 100)
val customBetween = isBetween(0, 100, s"Doofus! it's gotta be ${a} <= v <= ${b} but v is ${v}!")

val result1 = normalBetween(101) // Left("101 is not between 0 and 100.")
val result2 = customBetween(101) // Left("Doofus! it's gotta be 0 <= v <= 100 but v is 101!")

I tried making msg pass-by-name; no luck.

I suppose I want something like this from the Python world:

name = 'world'
program ='python'
print('Hello {name}!This is{program}.'.format(name=name, program=program))

Any suggestions?

Tomer Shetah
  • 8,413
  • 7
  • 27
  • 35
bwbecker
  • 1,031
  • 9
  • 21
  • Well you can do exactly that, format the string instead of using the interpolator. – Luis Miguel Mejía Suárez Jan 11 '21 at 00:51
  • I considered format strings (should have said) but was stumped by the possibly changing order of the placeholders. @tomer-shetah provided one approach that I was unfamiliar with. https://stackoverflow.com/questions/26824690/string-format-with-named-values provides another. – bwbecker Jan 11 '21 at 14:11

3 Answers3

3

It's not possible to refer to a variable declared in the same (or a future) parameter list, however you can refer to a variable declared in a previous parameter list, like so:

def isBetween(
  a:Int, b:Int
)(v: Int)(
  msg: String = s"${v} is not between ${a} and ${b}."
): Either[String, Boolean] = {
  if (a <= v && v <= b) Right(true) else Left(msg)
}

If you'd like to be able to offer callers the ability to provide a custom template string, you can do so as follows:

def isBetween(
  a:Int, b:Int
)(v: Int)(
  msg: (Int, Int, Int) => String = 
    (pA, pB, pV) => s"${pV} is not between ${pA} and ${pB}."
): Either[String, Boolean] = {
  if (a <= v && v <= b) Right(true) else Left(msg(a, b, v)
}

Example usage:

val customMsg = (a: Int, b: Int, v: Int) => s"Sorry but $v is not between $a and $b!"
isBetween(5, 7)(6)(customMsg)

If you'd like to offer callers a completely "custom" isBetween, then you can do so by putting the message in the first parameter group:

def isBetween(
  msg: (Int, Int, Int) => String = 
    (pA, pB, pV) => s"${pV} is not between ${pA} and ${pB}."
)(
  a:Int, b:Int
)(v: Int): Either[String, Boolean] = {
  if (a <= v && v <= b) Right(true) else Left(msg(a, b, v))
}

val customMsg = (a: Int, b: Int, v: Int) => s"Sorry but $v is not between $a and $b!"
val customMsgIsBetween = isBetween(customMsg) _

customMsgIsBetween(5, 7)(6)
3

As @LuisMiguelMejíaSuárez suggested in the comment, you can just use java's string formatting:

def isBetween(a: Int, b: Int, msg: String = "%%d is not between %d and %d.")(v: Int): Either[String, Boolean] = {
  if (a <= v && v <= b) Right(true) else Left(msg.format(a, b).format(v))
}

def normalBetween: Int => Either[String, Boolean] = isBetween(0, 100)
def customBetween: Int => Either[String, Boolean] = isBetween(0, 100, "Doofus! it's gotta be %d <= v <= %d but v is %%d!")

val result1 = normalBetween(101) // Left("101 is not between 0 and 100.")
val result2 = customBetween(101) // Left("Doofus! it's gotta be 0 <= v <= 100 but v is 101!")
println(result1)
println(result2)

The result will be as expected. Code run at Scastie. If you are taking this approach, and your scenario inn reality is more complex than the given example, you can use named parameters in this string. More can be read about it at Named placeholders in string formatting, How to format message with argument names instead of numbers?, and many more articles.

Tomer Shetah
  • 8,413
  • 7
  • 27
  • 35
  • I'm going with this basic approach but using the positional notation instead of the two-pass trick that Tomer suggests. That is, `msg = "%3$d" is not between %1$d and %2$d"` and then `msg.format(lo, hi, v)`. That allows reusing the variables, if necessary, and seems easier to document. Thanks for the pointers! See [Scastie](https://scastie.scala-lang.org/CsoN5VVcQB62MEBbKy08ow) – bwbecker Jan 11 '21 at 14:22
1

It's worth remembering that we can use sentinel values for this. While null is discouraged in Scala for passing data around, it is still allowed, and for a temporary local use, it's fairly harmless as long as we don't let it escape scope.

def isBetween(a: Int, b: Int, msgArg: String = null)(v: Int): Either[String, Boolean] = {
  val msg = if (msgArg == null) {
    s"${v} is not between ${a} and ${b}.";
  } else {
    msgArg
  }
  if (a <= v && v <= b) {
    Right(true)
  } else {
    Left(msg)
  }
}
Silvio Mayolo
  • 62,821
  • 6
  • 74
  • 116