Implicit conversion with implicit parameter

Refresh

December 2018

Views

1.6k time

1

I'm implementing a Java interface with a lot of methods with Object parameters, which in my case are really Strings containing user names:

public interface TwoFactorAuthProvider {
    boolean requiresTwoFactorAuth(Object principal);
    ... //many more methods with the same kind of parameter
}

I'm trying to use implicit conversion to convert these to User objects in my implementation:

class TwoFactorAuthProviderImpl(userRepository: UserRepository) 
    extends TwoFactorAuthProvider {

    def requiresTwoFactorAuth(user: User): Boolean = {
        ...
    }
}

When I define the conversion in the companion object of my class, it is picked up just fine and my class compiles:

object TwoFactorAuthProviderImpl {
    implicit def toUser(principal: Any): User = {
        null //TODO: do something useful
    }
}

However, to be able to do the conversion, I need access to the user repository, which the TwoFactorAuthProviderImpl instance has, but the companion object does not. I thought I could possibly use an implicit parameter to pass it:

implicit def toUser(principal: Any)(implicit repo: UserRepository): User = {
    val email = principal.asInstanceOf[String]
    repo.findByEmail(email)
}

But with the implicit parameter, the conversion is no longer picked up by the compiler (complaining that I'm not implementing the interface).

Is there a way to get the implicit conversion that I want, or is this outside the scope of what you can do with implicits?

2 answers

4

This should work just fine - can you supply the exact compilation error? Not implementing what interface? It looks like you would have to declare as follows:

class TwoFactorAuthProviderImpl(implicit userRepository: UserRepository) 

Here's an example for the REPL to show that implicits can have implicits; I'm using paste mode to ensure that module X is the companion object of the class X

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

case class X(i: Int, s: String)
object X { implicit def Int_Is_X(i: Int)(implicit s: String) = X(i, s) }

// Exiting paste mode, now interpreting.

defined class X
defined module X

scala> val i: X = 4
<console>:9: error: value X is not a member of object $iw
       val i: X = 4
                  ^

But if we add an implicit string in scope

scala> implicit val s = "Foo"
s: java.lang.String = Foo

scala> val i: X = 4
i: X = X(4,Foo)

Implicits advice

Don't go overboard with implicit conversions - I think you are going too far in this sense - the principal is implicitly a mechanism by which you can discover a user, it is not implicitly a user itself. I'd be tempted to do something like this instead:

implicit def Principal_Is_UserDiscoverable(p: String) = new {
  def findUser(implicit repo: UserRepository) = repo.findUser(p)
}

Then you can do "oxbow".findUser

1

Thanks to Oxbow's answer, I now have it working, this is only for reference.

First of all, a value that should be passed as an implicit must itself be marked implicit:

class TwoFactorAuthProviderImpl(implicit userRepository: UserRepository) ...

Second, implicit conversions are nice and all, but a method implementation signature must still match the signature of its declaration. So this does not compile, even though there is a conversion from Any to User:

def requiresTwoFactorAuth(principal: User): Boolean = { ... }

But leaving the parameter as Any, as in the declaration, and then using it as a user works just fine:

def requiresTwoFactorAuth(principal: Any): Boolean = {
    principal.getSettings().getPhoneUsedForTwoFactorAuthentication() != null
}

Also, the conversion really doesn't have to be in the companion object in this case, so in the end, I left the implicit parameters out.

The full source code:

class TwoFactorAuthProviderImpl(userRepository: UserRepository) 
    extends TwoFactorAuthProvider  {

    private implicit def toUser(principal: Any): User = {
        val email = principal.asInstanceOf[String]
        userRepository.findByEmail(email)
    }

    def requiresTwoFactorAuth(principal: Any): Boolean = {
        //using principal as a User
        principal.getSettings().getPhoneUsedForTwoFactorAuthentication() != null
    }

    ...
}