# Random Scala Tip #697: Avoid Anonymous Functions as Dependencies

6 min read

The Problem

Imagine that you're building an app and somewhere in your code you write something like this:

class DoFinanceStuff(cc: CreditCardService):
def grabClientMoney(card: CardData) =
val amount = // ...
// ...
cc.processPayment(amount, card)

And like the good developer that you pretend to be, you want to write some unit tests to verify that you really are grabbing the client's money. Since the codebase pervasively uses dependency injection, this should be a breeze.

So you want to mock out the CreditCardService instance you have in the DoFinanceStuff. "No problem", you say to yourself, let's see what functionality we need to provide. You open up the CreditCardService trait, and lo and behold:

trait CreditCardService:
def validateCard(cardNumber: CardNumber, expiryDate: ExpiryDate, cvv: CVV): ValidationResult
def processPayment(amount: MonetaryAmount, cardData: CardDetails): TransactionId
def getCardholderDetails(cardNumber: CardNumber): CardholderInfo
def authorizeTransaction(cardData: CardDetails, merchantId: MerchantId): AuthToken
def checkTransactionStatus(transactionId: TransactionId): TransactionStatus
def calculateRewards(transactionAmount: MonetaryAmount): RewardPoints
def updateCardLimit(cardNumber: CardNumber, newLimit: CreditLimit): CreditLimit
def getBillingCycleInfo(cardNumber: CardNumber): BillingCycle
def reportLostCard(cardNumber: CardNumber): CardStatus
def initiateDispute(transactionId: TransactionId, reason: DisputeReason): DisputeStatus
def freezeCard(cardNumber: CardNumber): CardStatus
def getTransactionHistory(cardNumber: CardNumber, startDate: Date, endDate: Date): List[Transaction]
// ...

And it goes on like this for another ~15 methods.

Now it could be argued that someone messed up with this trait and it should be broken apart. But you just wanted to write a measly test1, not refactor the whole universe. Creating a stub implementation with this many ??? seems daunting, why do we need to suffer through so much boilerplate?..

A thought goes through your mind, "a mocking library will save me from this hell". But you promptly discard this thought having remembered that mocking libraries are evil and there's a whole other hell reserved for those that use them2.

Finally you remember that you're using Scala, a language that supports and encourages functional programming.

It now seems obvious, we're only using one method from CreditCardService, we can replace it with an anonymous function. Like so:

class DoFinanceStuff(processPayment: (MonetaryAmount, CardDetails) => TransactionId)
def grabClientMoney(card: CardData) =
val amount = // ...
// ...
processPayment(amount, card)

Cool. Now we can easily provide a test implementation for this one function. The change that we need to apply to the production code is minimal:

val cc = CreditCardService(...)
val financialThing = DoFinanceStuff(cc.processPayment)

Here we use the fact that Scala automatically converts methods to anonymous functions.

All is great, we have very little boilerplate and writing the test is a breeze. No?

The Tip

Nope.

So here's the actual tip:

Avoid using anonymous functions as class dependencies.

Why? Ergonomics.

Imagine a realistic full size application, with all its transitive interdependencies all over the place. Now you look at this class header:

class DoFinanceStuff(processPayment: (MonetaryAmount, CardDetails) => TransactionId)

And you ask yourself:

  1. What is the processPayment function doing?
  2. Where did it come from?
  3. Where was it initialized?
  4. What are the relevant production implementations of this function?

All important questions to ask, and all might not have immediately obvious answers in a sufficiently convoluted realistic code. The problem with using anonymous functions like this is that no mainstream tool for Scala is equipped to answer those questions. Or more concretely, you can't hit "find implementations/references" on a function type3.

What do we do instead then? We bite the bullet, settle for a bit more boilerplate, and define a custom trait:

trait PaymentProcessor:
def processPayment(amount: MonetaryAmount, cardData: CardDetails): TransactionId

Along with an implementation that simply forwards calls to a CreditCardService:

object PaymentProcessor:
class Default(cc: CreditCardService) extends PaymentProcessor:
def processPayment(amount: MonetaryAmount, cardData: CardDetails): TransactionId =
cc.processPayment(amount, cardData)

Our original class now looks like this:

class DoFinanceStuff(pp: PaymentProcessor)

And finally the initialization becomes:

val cc = CreditCardService(...)
val financialThing = DoFinanceStuff(PaymentProcessor.Default(cc))

A single method trait (a SAM) is indeed a more boilerplate-y equivalent of an anonymous function. Testability remains just as easy as before. So what we did here is to pay some boilerplate tax to get the exact same functionality as before. Did we gain anything in return?

What we gained is that now all the questions that we wanted to answer before can easily be answered with any reasonable Scala IDE4. To wit:

  1. What is the processPayment function doing?
    Hit "find implementations" on the PaymentProcessor and find the relevant implementation (there's likely just one5).
  2. Where did it come from?
    Hit "find usages" on the PaymentProcessor type and get a good overview of where it came from
  3. Where was it initialized?
    Hit "find usages" on the relevant implementation from the first question and find the place it gets initialized in
  4. What are the relevant production implementations of this function?
    Hit "find implementations" and... You get the general idea

This is just the beginning. As you now have a named entity in your code there are lots of things that become better:

  • Giving names to things forces you to think about them more, and only good can come out of that
  • There's a natural place for documentation about this entity
  • It's easier to refactor, since both automated tooling can help and you can more easily find all the relevant usages
  • Future evolution becomes easier, as you can add methods to your new trait if the need arises 6
  • You've just taken the first step to breaking down the monster CreditCardService.

And the list goes on. None of those benefits are available for an anonymous function.

Caveat: as most things in life, this is a trade off. Sometimes all the boilerplate ceremony and naming effort is just not worth it, and you might prefer to go down the anonymous road. Use your judgement7.

Addendum8: One can also improve things a bit by using a type alias for the function type. And although it somewhat improves readability, and forces you to name the thing; it's still not as ergonomically convenient as a newly defined type (be it a trait, or some other wrapper). Type aliases are only searchable when they are explicitly named, and nothing is forcing you to name them when passing an anonymous function along, you just instantiate the anonymous function as you would any other function, without using the name of the alias.

Bonus Tip

You might've noticed that I paid for a bit more boilerplate than strictly necessary by introducing the Default class. I could've just created a quick method with an anonymous implementation, and since the trait is a SAM, it could've just been implemented with an anonymous function. The result is pretty much equivalent, but again I'd avoid the anonymous route.

This too is an ergonomics/tooling issue, but it seems that most tools are better at displaying named implementations rather than anonymous ones. And although I might use an anonymous implementation for testing, the "real" implementation is better be named (ideally with some consistent naming convention).

It's a shame to avoid nice language features due to minor ergonomic nits, but you can't have it all...

Hope you found these tips useful, or at least thought provoking. Let me know in the comments.

If you enjoyed this, reach out for a workshop on Functional Programming, where I teach how to apply Functional Programming to improve code quality in any language.

Subscribe to the newsletter
Get new posts by email:

Footnotes

  1. As they say, no good deed goes unpunished.

  2. You think I'm being hyperbolic, don't you?

  3. Although it's not difficult to imagine a Hoogle style query engine in the editor that can do this, we are not there yet.

  4. Was that an oxymoron?

  5. Coupled with an enstablished convention for naming the trait implementations (e.g., Default/Impl/Live/Prod) this becomes almost automatic. Too bad we can't express the convention in the type system. If you have ideas on how to do that, please reach out.

  6. For better or for worse, we don't want to end up with another CreditCardService monster...

  7. Or don't, it's up to you.

  8. Thanks Reddit.


Article Series
1
2
3

Comments