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?

2
  • Well you can do exactly that, format the string instead of using the interpolator. Commented Jan 11, 2021 at 0: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. stackoverflow.com/questions/26824690/… provides another. Commented Jan 11, 2021 at 14:11

3 Answers 3

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.

Sign up to request clarification or add additional context in comments.

1 Comment

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
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)

Comments

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)
  }
}

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.