5 anti-patterns when writing code in a functional language

Anti-patterns in functional programming languages ​​can seem strange because of the difference between these languages ​​and their other types, and therefore developers often write poor implementations that are prone to errors and difficult to maintain. In this article, we will analyze the five most common anti-patterns, avoiding which you can create more convenient code to work with with fewer errors.

Functional programming languages ​​have grown in popularity in recent years. Many see the advantages of writing code in which functions are the main operating components. Programmers take advantage of immutability, which allows them to perform heavy tasks without worrying about possible concurrency issues, and also like to write generic code that is as DRY (Don’t Repeat Yourself) as possible.

For me, all this was a clear sign that functional languages ​​are gaining popularity again. However, one of the challenges of writing code in such a language is design patterns and anti-patterns that differ from standard programming languages.

I often see how engineers write a huge code base, using, in my opinion, various anti-patterns. I was also involved in creating these unwanted implementations when I first started writing production-ready applications in a functional language. Since then, I have read many books on design in functional programming that have helped me write more maintainable code.

▍ Deeply nested anonymous callbacks

An anonymous function can be a good way to promote code reusability. However, if nested too deep, such a function will become difficult for developers to understand, who want to extend its functionality. Therefore, contrary to the recommendation to follow the DRY principle, sometimes repetition will be a better solution than the wrong abstraction.

I’ve come across a code base that had an extremely concise abstract method. His code looked like this:

def buildRunner
  ((req,Resp) => ctx.TransactionContext)
  ((resp, ctx.rtx.Transaction) => Final[Context])
  (resp => Unit): Runner[ctx.Transaction, rtx.TransacitonResponse] = new Runner[ctx.Transaction, rtx.TransactionResponse] { 
   override def run((ctx.Transaction, rtx.TransactionResponse) => Response): Req => Resp = ???
  }
  
  
trait Runner[T, F] {
 def run((T,F) => Response): Req => Resp
}

Can you explain me the definition buildRunner?

Further buildRunner used in all action-related operations in the payment processor, such as authorization, capture, and revocation. I’ve been looking at this method for two days now, trying to figure out what it does.

This creates an abstraction that follows the DRY principles as much as possible for all the functions you create. However, having a nested anonymous callback can be problematic for the average programmer who wants to create new functionality or perform maintenance. It will take most specialists a couple of days to understand the purpose buildRunner.

The good thing about functional programming is that it allows you to look at the signature of a function and immediately understand its purpose. But that’s just specifically this function does not really speak for itself. It will only further confuse a specialist who wishes to make changes to the code base.

So it’s a good rule of thumb to not use anonymous functions at all if possible. Instead, it is better to resort to higher-order functions.

If you still want to use an anonymous function, be sure to specify at the beginning typeto make it easier to read. Tool http4s does this internally by wrapping its type instances Kleisli. Kleisli itself is an anonymous function acting like A => F[B]. However, wrapping an anonymous function by prepending type will provide better readability.

▍ Pattern matching

The first of the advantages we are introduced to in functional programming is the ability to match patterns. She saves us from ugly instructions if-elsewhich we often use in more common programming languages.

Pattern matching is only good for a small number of patterns. Things can very quickly turn into “callback hell” when we use more than two layers of pattern matching.

def doSomething(res: Future[Either[Throwable, Option[A]]]) = res match {
   case Success(a) =>
      a match {
        case Left(ex) => 
        case Right(b) =>  b match {
                                case Some(c) => 
                                case None =>
                              }

        }
    
   case Failure(ex) =>
}

Often, developers who are just starting to write code in a functional language don’t know how many higher-order built-in functions the language provides. Therefore, they by default run the implementation of their functions through pattern matching and a recursive function.

Using a nested conditional and recursive implementation in a function makes the code harder to read and understand. More time is spent on comments on pull requests and it is more difficult to find possible bugs.

One solution in this case is to rely only on the successful case of the conditional expression, leaving the erroneous scenario out of the function implementation. Furthermore, whenever possible, a higher-order built-in function provided by the library or language should be used, map And flatMap. This will make the code base easier to use and you can quickly determine where the error occurs.

The benefit of expressing your types in a function definition is that the implementation doesn’t need to handle all the error scenarios – the type system will redirect them to the caller and the upstream function will handle them.

▍ Using monad transformers in the interface

Monad transformers can come in handy when you’re dealing with excessive nesting. In the scenario above, such a transformer is another solution to the problem of excessive nesting – it allows you to make your API composable. However, they should not be used in the interface, as this will bind our API to a specific transformer.

Let’s take an example. The interface below can be Future[Either[Throwable, String]] instead of EitherT[Future, Throwable, String].

trait SomeInterface {
  def someFunction(): EitherT[Future, Throwable, String]
}

Any function you want to use someFunction as an API, you will also need to use EitherT.

What if it’s a series of functions and some of them return OptionT?

Then we have to call value a couple of times to get back to our effect Futurecreating unnecessary wrapping.

As an alternative, you should make someFunction returned Future[Either[Throwable, String]]and let the effect define the constraints you need in your program.

trait SomeInterface {
  def someFunction(): Future[Either[Throwable, String]]
}

In conclusion, the presence of the purest form of effect would be better than a monad transformer, since it would not restrict services that use this transformer through the API.

▍ Returning a boolean value in an API

Many APIs can return a single boolean value. A classic example taken from
book “Practical Fp in Scala”, is a function filter.

trait List[A] {
  def filter(p : A => Boolean): List[A]
}

What exactly can be said about the action of this function, looking at its definition?

If the predicate evaluates to true, then it will discard the elements of the list. On the other hand, when the predicate is trueit could also mean keeping the list items.

There is ambiguity.

Scala also has filterNot, which has the same function definition but a different name. I often encountered all sorts of errors in these functions, arising from the fact that the programmer missed the difference between them.

This nuance can be corrected by wrapping the predicate in ADT (Algebraic Data Type, algebraic data type) with meaningful values.

sealed trait Predicate 
object Predicate {
  case object Keep extends Predicate
  case object Discard extends Predicate
}

This ADT will help you create a more specific function signature like this:

def filter[A](p: A => Predicate): List[A]

Whoever uses this function will understand whether it will result in keeping or excluding items from the list.

List(1,2,4).filter{p => if(p > 2) Predicate.Keep else Predicate.Disacrd}

To solve this problem for a filter class, you can always create from a trait List extension method filterBy.

implicit class ListOp[A](lst: List[A]) {
  def filterBy(p: A => Predicate): List[A] = lst.filter{ p(_) match {
      case Predicate.Keep => true
      case Predicate.Discard => false
    }
  }
}

The key to avoid confusion about boolean function values ​​is to use ADT to provide meaningful values ​​and extend those functions with ADT. And although this will lead to writing extra routine code, it will reduce confusion and the number of errors when creating an application.

However, wrapping all booleans returned by the API with ADT can be overkill. Therefore, it is desirable to wrap them in critical components while maintaining flexibility in the rest of the application. There is already a question of agreement with the members of your team.

▍ Using a generic data structure in a trait

In typical development practice, the following statement may seem contradictory. “In order to support extensibility, the interface should be as generic as possible.” Sounds cool in theory, but it’s really not.

One of the examples is Seq is a generic representation defined in the standard Scala library. The wide universality of this representation is demonstrated by the fact that List, Vector And Stream. And this is a problem because each of these data structures acts differently.

For example, we have a trait that returns Future[Seq[String]]:

trait SomeTrait {
 def fetchAll: Future[Seq[String]]
}

Some developers will call the function fetchAll and using the function toList transform Seq V List.

How do you know that the call toList will it be safe? The interpreter can determine Seq How Streamin which case it will have different semantics and will probably throw an exception on the caller’s side.

Therefore, in order to reduce the number of surprises from the caller, it is best to define a more specific type, depending on the goals and performance of the application, for example, List, Vector, Stream.

▍ Conclusion

The problem with the described anti-patterns is that in the usual programming languages ​​they are not considered as such.

For example, we were taught that writing abstractions is good, and this allows us to follow the DRY principle. However, overly nested anonymous functions are hard to read. The best way to improve readability in such cases is to repeat the code.

An API that returns a boolean may not be a problem, and there are many such examples, including in applications. However, the implementation of an API that returns a boolean does not make us clear about the meaning of this value. Moreover, a person can often overlook small details in the documentation and, as a result, make mistakes in the implementation.

Pattern matching is a powerful tool in functional programming, but it turns out to be overly general. If you can find a better higher-order function to implement the function, then it’s better to do so.

An over-generalized data structure can increase ambiguity in the use of the API. Therefore, it is better to create more specific data types and make function declarations as clear as possible for the caller.

I hope you can avoid these anti-patterns when writing code in a functional language. Do you consider them anti-patterns? What other examples of antipatterns could you give? I would be grateful if you share your thoughts on this in the comments.

Telegram channel with prize draws, IT news and posts about retro games 🕹️

Similar Posts

Leave a Reply

Your email address will not be published. Required fields are marked *