Post

Harnessing power of multi-paradigm

Harnessing power of multi-paradigm

Embracing Multiple Programming Paradigms in Modern Languages

Modern programming languages like F#, Swift, Rust, and Kotlin empower developers by supporting multiple programming paradigms. This flexibility allows us to harness the strengths of different approaches, leading to more expressive and maintainable code.

  • Rust combines imperative, functional, and object-oriented paradigms.
  • Swift supports imperative and object-oriented paradigms and incorporates functional programming features.
  • Kotlin blends object-oriented and functional programming paradigms.

It’s important to note that each language differs in how deeply it supports each paradigm and often introduces its own unique nuances.


Understanding Programming Paradigms

Programming paradigms generally fall into two broad categories:

  1. Declarative
    • Functional
    • Logic
    • Reactive
  2. Imperative
    • Procedural
    • Object-Oriented

The declarative paradigm emphasizes what needs to be done, rather than how to do it. This shift in focus brings significant benefits, especially when combined with principles like:

In contrast, the imperative style is about how to achieve a result. It offers:

  • Greater control over each step
  • Less indirection

An Example in Kotlin

Let’s look at a simple example in Kotlin:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
class BirthdayNotifier(
  private val contactService: ContactService,
  private val notificationService: NotificationService,
  private val metricService: MetricService
) {

  fun notify(user: UUID) {
    val birthdayBuddies = contactService.findFriendsOf(user)
      .filter { contact -> contact.birthDate?.isToday() ?: false }
      .also { logger.info { "${it.count()} people from your contact have Birthday today" } }
      .toList()

    var failedToNotify = 0

    birthdayBuddies.forEach { contact ->
      try {
        val isAlreadyNotified = notificationService
          .findNotification(contact)
          .filterNot { notification ->
            notification.time.toLocalDate().isToday()
              && (notification.notification is BirthdayNotification)
          }
          .any()

        if (isAlreadyNotified) logger.info { "Already notified" }
        else {
          val notification = BirthdayNotification.from(contact)
          notificationService.notify(notification)
          logger.info { "Notified about birthday" }
        }

      } catch (ex: Throwable) {
        logger.error(ex) { "Failed to notify contact $contact" }
        failedToNotify += 1
      }
    }
    if (failedToNotify > 0) metricService.measureCount("birthday.notification.failed.count", failedToNotify)
  }
}

When writing code, authors naturally focus on how things are done, while readers are usually more interested in what the code is doing so they can build a mental model of the system. Recognizing these differing perspectives helps us write code that serves both needs. Use a declarative style when the what is more important than the how. When the focus is on a single operation and efficiency, imperative style is appropriate.


Zooming Out for Clarity

Let’s step back and look at what the above code is accomplishing:

  • Finding friends who have birthdays today and haven’t been notified yet
  • Notifying those friends
  • Reporting the number of notification failures

However, the original code doesn’t make these intentions immediately clear. By raising the level of abstraction, we can make the code speak more directly to its purpose:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
// other code

fun notify(user: UUID) {
  birthdayBuddiesOf(user)
    .filterNot(::alreadyNotified)
    .map(::BirthdayNotification)
    .map(::sendNotification)
    .let { notificationResults -> notificationResults.count { it.isFailure } }
    .also(::recordMetric)
}

private fun birthdayBuddiesOf(userId: UUID): List<Contact> =
  contactService.findFriendsOf(userId)
    .filter { contact -> contact.birthDate?.isToday() ?: false }
    .also { logger.info { "${it.count()} people from your contact have Birthday today" } }
    .toList()

private fun alreadyNotified(contact: Contact): Boolean =
  notificationService
    .findNotification(contact)
    .filter { notification ->
      notification.time.toLocalDate().isToday() && (notification.notification is BirthdayNotification)
    }
    .any()
    .also { alreadyNotified -> if (alreadyNotified) logger.info { "Already notified" } }

private fun recordMetric(failedCount: Int) {
  if (failedCount > 0) metricService.measureCount("birthday.notification.failed.count", failedCount)
}

private fun sendNotification(notification: BirthdayNotification): Result<Unit> =
  runCatching {
    notificationService.notify(notification)
    logger.info { "Notified about birthday" }
  }

companion object {
  private fun LocalDate.isToday(): Boolean = this == LocalDate.now()
}
// other code down here.

Conclusion

Mixing different programming paradigms can lead to clearer, more maintainable code. In this example, we blend object-oriented and functional paradigms to improve readability and express intent more directly.

This post is licensed under CC BY 4.0 by the author.