Search code examples
scalatransactionsimplicit-parameters

Default value for implicit parameter of class method


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

Solution

  • 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.