Skip to main content
Scala: Effects and Functional Programming
  1. Posts/

Scala: Effects and Functional Programming

·910 words·5 mins·
Roman
Author
Roman
Photographer with MSci in Computer Science and a Home Lab obsession
Table of Contents

This week I’m exploring Effects and their role in functional programming as preparation for the ZIO course | Rock the JVM. Understanding effects is crucial for writing pure functional programs while still dealing with side effects in a controlled way.

Effects in Functional Programming
#

What are Effects?
#

In functional programming, pure functions are functions that always return the same output for the same input and have no side effects. However, real-world applications need to perform side effects like printing to console, making network requests, querying a database.

Effects provide a way to describe computations that may have side effects while maintaining the principles of functional programming.

Effect Properties
#

An effect must satisfy three key properties:

  1. Type signature describes the KIND of computation it will perform
  2. Type signature describes the TYPE of VALUE it will produce
  3. If side effects are required, construction must be separate from EXECUTION

Pure Functional Programming
#

// A pure functional program is a big expression computing a value

// Expressions
def combine(a: Int, b: Int): Int = a + b

// Local reasoning: type signature describes the computation
// Referential transparency: can replace 
// expression with its value without changing behaviour
val five = combine(2,3)
val five_v2 = 2 + 3  // Same as above
val five_v3 = 5      // Same result

Referential Transparency
#

Referential transparency means we can replace any expression with the value it evaluates to without changing the program’s behaviour.

However, not all expressions are referentially transparent:

  // Example 1: printing (NOT referentially transparent)
  val resultOfPrinting: Unit = println("learning ZIO")
  val resultOfPrinting_v2: Unit = ()  // NOT the same behaviour!

  // Example 2: changing a variable (NOT referentially transparent)
  val anInt = 0
  val changingInt: Unit = (anInt = 42)  // side effect
  val changingInt_v2: Unit = ()         // NOT the same program

Examples: What Makes a Proper Effect?
#

Example 1: Option - A Pure Effect
#

Option represents computations that may or may not produce a value:

  val anOption: Option[Int] = Option(42)

Option satisfies all effect properties:

  • Kind of computation: Possibly absent values
  • Type of value: A if present
  • No side effects: Pure computation

Example 2: Future - NOT a Pure Effect
#

  val aFuture: Future[Int] = Future(42)
  //  When Future is constructed, execution is immediately scheduled!

Future fails the effect test:

  • Kind of computation: Asynchronous computation
  • Type of value: A if successful
  • Construction separate from execution: NO! Execution starts immediately

Example 3: MyIO - A Proper Effect
#

Let’s design our own effect type that properly separates construction from execution:

  case class MyIO[A](unsafeRun: () => A) {
    def map[B](f: A => B): MyIO[B] =
      MyIO(() => f(unsafeRun()))

    def flatMap[B](f: A => MyIO[B]): MyIO[B] =
      MyIO(() => f(unsafeRun()).unsafeRun())
  }

Here’s how MyIO provides proper effect handling:

  // Description of an effect - does NOT execute immediately!
  val anIOWithSideEffects = MyIO(() => {
    println("producing effect")
    42
  })

  // Only executes when explicitly called
  anIOWithSideEffects.unsafeRun()

MyIO passes all effect tests:

  • Kind of computation: Might perform a side effect
  • Type of value: A if successful
  • Construction separate from execution: YES! Only runs when unsafeRun() is called

IO Monad
#

What is MyIO?

MyIO is a wrapper for side effects that delays their execution. Instead of running code immediately, it stores the computation to be run later

Why “IO”?

IO stands for Input/Output operations that interact with the outside world (printing, reading files, network calls, etc.). These are “impure” because they have side effects. MyIO makes them manageable by treating them as values.

Methods
#

map[B](f: A => B): MyIO[B]
#

Transform the result of a computation.

val io = MyIO(() => 5)
val doubled = io.map(_ * 2)  // MyIO that will produce 10...
doubled.unsafeRun() //once you run it

Use when you want to transform the output without chaining another effect.

flatMap[B](f: A => MyIO[B]): MyIO[B]
#

Chain dependent computations together.

val program = MyIO(() => readLine())
  .flatMap(name => MyIO(() => println(s"Hello, $name")))
  
program.unsafeRun() // will read console input and print it

Use when the next computation depends on the previous result and also produces an effect.

For-Comprehension
#

For-comprehensions provide cleaner syntax for chaining multiple flatMap operations. (For more see my earlier post on map, flatMap, and for-comprehensions).

val currentTime: MyIO[Long] = 
    MyIO(() => System.currentTimeMillis())

val slowComputation: MyIO[String] = MyIO(() => {
  Thread.sleep(10000) // Wait 10 seconds
  println("Computation finished!")
  "result"
})
    
// Using for-comprehension (clean syntax)
// measures time take for the computation
def measure[A](computation: MyIO[A]): MyIO[(Long, A)] = for {
  startTime <- currentTime
  result <- computation
  endTime <- currentTime
} yield (endTime - startTime, result)

measure(slowComputation).unsafeRun()

This is equivalent to nested flatMap calls:

// Same as above, but using flatMap directly
def measure[A](computation: MyIO[A]): MyIO[(Long, A)] = 
  currentTime.flatMap { startTime =>
    computation.flatMap { result =>
      currentTime.map { endTime =>
        (endTime - startTime, result)
      }
    }
  }

Pattern
#

// 1. Define effects (nothing executes yet)
val effect1 = MyIO(() => println("Step 1"))
val effect2 = MyIO(() => println("Step 2"))

// 2. Compose them
val program = effect1.flatMap(_ => effect2)

// 3. Execute only when ready
program.unsafeRun()  // NOW both printlns happen

Key Takeaways
#

  1. Effects allow us to describe computations while maintaining functional purity
  2. Referential transparency is crucial for reasoning about code
  3. Construction must be separate from execution for proper effect handling
  4. Future is not a pure effect because it starts executing immediately
  5. Proper effect types like MyIO (or ZIO) give us control over when side effects occur