1

I want to have a sort of "transaction" construct, on which I'm doing all of the changes and then decide if to commit or rollback at the end. My issue is that I don't know how to properly define / pass the implicit values without defining them manually from where the functions are called. How can this be accomplished?

class Foo {
  var m = scala.collection.mutable.HashMap.empty[String, String]

  case class Tx(mcopy: scala.collection.mutable.HashMap[String, String]) {
    def commit = (m = mcopy)
    def rollback = () // not copying mcopy will lose all changes made to it
  }

  def withTx(block: Foo => Unit): Unit = {
    implicit val tx = new Tx(m.clone)
    try {
      block(this)
      tx.commit
    } catch {
      case _: Throwable => tx.rollback
    }
  }

  implicit val emptyTx = new Tx(m) // non-tx operations will be performed directly on 'm'

  def add(k: String, v: String)(implicit t: Tx): Unit = (t.mcopy += k -> v)
}

val f = new Foo
f.add("k0", "v0") // error: no implicit t defined...
f.withTx { foo => foo.add("k1", "v1") } // errors as well on missing implicit
solyd
  • 782
  • 2
  • 8
  • 18
  • you can create one object Implicits and define all the variables there then you just import the object. ex: com.example.Implicits._ – Raman Mishra Jan 14 '20 at 12:16
  • I want to avoid import statements to make using the class easier (just instantiate with `new Foo` and go do your thing). And even so - the implicit tx object is tied to a specific `Foo` instance, I don't see how I can import the default value once... – solyd Jan 14 '20 at 12:17
  • import inside the class then you can just instantiate and use it or create a trait and have your implicit variables there which will be available to your class as the Implicit Trait will be the parent – Raman Mishra Jan 14 '20 at 12:18
  • Sorry, I don't get it, can you please show me with code – solyd Jan 14 '20 at 12:24

2 Answers2

1

Without commenting on the wisdom of this (I think it depends), nothing prevents you from supplying default arguments on your implicit params. If you do this, a resolved implicit will take precedence, but if no implicit is found, the default argument will be used.

However, your withTx function won't work no matter what, because the implicit you define is not in the scope from the function block. (You could not have referred to tx from a function you define there.)

To modify your example (giving transactions a label to make this clear):

class Foo {
  var m = scala.collection.mutable.HashMap.empty[String, String]

  case class Tx(label : String, mcopy: scala.collection.mutable.HashMap[String, String]) {
    def commit = (m = mcopy)
    def rollback = () // not copying mcopy will lose all changes made to it
  }

  def withTx(block: Foo => Unit): Unit = {
    implicit val tx = new Tx("oopsy", m.clone)
    try {
      block(this)
      tx.commit
    } catch {
      case _: Throwable => tx.rollback
    }
  }

  implicit val emptyTx = new Tx("passthrough", m) // non-tx operations will be performed directly on 'm'

  def add(k: String, v: String)(implicit t: Tx = emptyTx): Unit = {
    println( t )
    t.mcopy += k -> v
  }
}

Then...

scala> val f = new Foo
f: Foo = Foo@3e1f13d2

scala> f.add( "hi", "there" )
Tx(passthrough,Map())

scala> implicit val tx = new f.Tx( "outside", scala.collection.mutable.HashMap.empty )
tx: f.Tx = Tx(outside,Map())

scala> f.add( "bye", "now" )
Tx(outside,Map())

But your withTx(...) function doesn't do what you want, and now, unhelpfully, it doesn't call attention to the fact it doesn't to what you want with an error. It just does the wrong thing. Instead of getting the implicit value that is not in scope, the operation in block gets the default argument, which is the opposite of what you intend.

scala> f.withTx( foo => foo.add("bye", "now") )
Tx(passthrough,Map(bye -> now, hi -> there))

Update:

To get the kind of withTx method you want, you might try:

  def withTx(block: Tx => Unit): Unit = {
    val tx = new Tx("hooray", m.clone)
    try {
      block(tx)
      tx.commit
    } catch {
      case _: Throwable => tx.rollback
    }
  }

Users would need to mark the supplied transaction as implicit in their blocks. It would be something like this:

scala> val f = new Foo
f: Foo = Foo@41b76137

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

f.withTx { implicit tx =>
  f.add("boo","hoo")
  tx.commit
}

// Exiting paste mode, now interpreting.

Tx(hooray,Map()) // remember, we print the transaction before adding to the map, just to verify the label

scala> println(f.m)
Map(boo -> hoo)

So that "worked". But actually, since you put an automatic commit in after the block completes, unless it completes with an exception, my call to tx.commit was unnecessary.

I don't think that's a great choice. Watch this:

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

f.withTx { implicit tx =>
  f.add("no","no")
  tx.rollback
}

// Exiting paste mode, now interpreting.

Tx(hooray,Map(boo -> hoo)) // remember, we print the transaction before adding to the map, just to verify the label

scala> println(f.m)
Map(no -> no, boo -> hoo)

The add(...) completed despite my explicit call to rollback! That's because rollback is just a no-op and an automatic commit follows.

To actually see the rollback, you need to throw an Exception:

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

f.withTx { implicit tx =>
  f.add("really","no")
  throw new Exception
}

// Exiting paste mode, now interpreting.

Tx(hooray,Map(no -> no, boo -> hoo)) // remember, we print the transaction before adding to the map, just to verify the label

scala> println(f.m)
Map(no -> no, boo -> hoo)

Now, finally, we can see a call to add(...) that was reverted.

Steve Waldman
  • 13,689
  • 1
  • 35
  • 45
  • Thank you, very helpful. How can I achieve dynamic scoping in Scala (if possible)? – solyd Jan 15 '20 at 08:26
  • I've added an update to show how to do the kind of thing I think you're trying to do. But as I note, I think your automatic commit/rollback logic is a bit confusing, because an explicit rollback by the user is just a no-op, "rolled back" work will be committed. – Steve Waldman Jan 16 '20 at 01:36
0

You can define your implicit variables in a separate trait or object like so:

trait MyImplicits {
  implicit val myImplicitParameter: Int = 0
}

trait MyInterface {
  def implicitMethod(a:Int)(implicit b: Int) = ???
}

object MyClass extends MyInterface with MyImplicits {
  def main(args: Array[String]) = {
    implicitMethod(1)
  }
}
user3685285
  • 6,066
  • 13
  • 54
  • 95