Lec 10 - Monad and Parallel Stream

Monad

At a high level, a Monad is a design pattern (from category theory) used in functional programming to wrap values and chain computations in a safe and composable way.

The follwing containers we have learned and implemented are all considered as monad.

Container
Side-Information

Maybe<T>

The value might be there (i.e., Some<T>) or might not be there (i.e., None)

Lazy<T>

The value has been evaluated or not

The log describing the operations done on the value

Side-Information is any extra data or context that accompanies the main value, but isn't the value itself.

Each of these classes has:

  • an of method to initialize the value and side information.

  • have a flatMap method to update the value and side information.

The class may also have other methods besides the two above. Additionally, the methods may have different names.


More specifically, monads are the classes we wrote that can follow certain patterns that make them well-behaved when we create them with of and chain them with flatMap.

For ths sake of this course, we are only interested in deciding whether a class/type is a monad using the monad law or applying these laws to analyze the behavior of a monad.

Monad Laws

Let Monad be a type that is a monad and monad be an instance of it, a Monad should follow the following three laws,

1

The left identity law

Monad.of(x).flatMap(x -> f(x)) must be the same as f(x)

Our of method should not do anything extra to the value and side information — it should simply wrap the value x into the Monad. Our flatMap method should not do anything extra to the value and the side information, it should simply apply the given lambda expression to the value.

According to the nature of flatMap, we assume that the output of f(x) is a monad by nature.

2

The right identity law

monad.flatMap(x -> Monad.of(x)) must be the same as monad

Since of should behave like an identity, it should not change the value or add extra side information. The flatMap above should do nothing and the expression above should be the same as monad.

3

The associative law

monad.flatMap(x -> f(x)).flatMap(x -> g(x)) must be the same as monad.flatMap(x -> f(x).flatMap(y -> g(y)))

Regardless of how we group those calls to flatMap, their behavior must be the same.

Functors

A functor is a simpler construction than a monad in that it only ensures lambdas can be applied sequentially to the value, without worrying about side information.

Recall that when we build our Loggable<T> abstraction, we add a map that only updates the value but changes nothing to the side information. One can think of a functor as an abstraction that supports map.

Functor Laws

Let Functor be a type that is a functor and functor be an instance of it, a functor should follow the following two laws,

1

Identity Law

functor.map(x -> x) is the same as functor

2

Composition Law

functor.map(x -> f(x)).map(x -> g(x)) is the same as functor.map(x -> g(f(x)).

Some Tips about Monad and Functors
  1. For a class/type to be a Monad, it must have

    1. Two methods: of and flatMap which does their corresponding jobs respectively

    2. Adhere to the three Monad Laws

  2. For a class/type to be a Functor, it must have

    1. One method: map which does its job

    2. Adfere to the two Functor Laws

  3. A class/type can be both monad and functor!

  4. Every Monad is a Functor, But not every Functor is a Monad.

Parallel Stream

Prallel and Concurrent Programming

Sequencity means that at any one time, there is only one instruction of the program running on a processor.

  1. All parallel programs are concurrent, but not all concurrent programs are parallel.

  2. Modern computers have more than one core/processor. As such, the line between parallelism and concurrency is blurred.

Parallel Stream

In Java, a parallel stream is a type of stream that allows you to process elements concurrently using multiple threads. It’s created by

1

Calling .parallel() on a standard stream

Example

.parallel() is a lazy operation — it merely marks the stream to be processed in parallel. As such, you can insert .parallel() anywhere in the pipeline after the data source and before the terminal operation.

.sequential()

There is a method sequential() which marks the stream to be process sequentially. If you call both parallel() and sequential() in a stream, the last call "wins". The example below processes the stream sequentially:

2

Calling Stream.parallelStream() on a collection

You may notice that the output is reordered.

  1. This is because Stream has broken down the original stream into subsequences (more formally speaking, it is spliting into multiple threads), and run forEach for each subsequence in parallel.

  2. Since there is no coordination among the parallel tasks on the order of the printing, whichever parallel tasks that complete first will output the result to screen first.

  3. This causes the sequence of numbers to be reordered.

What can be parallelized

To ensure that the output of the parallel execution is correct, the stream operations

1

must not interfere with the stream data

Interference means that one of the stream operations modifies the source of the stream during the execution of the terminal operation. For instance:

would cause ConcurrentModificationException to be thrown. Note that this non-interference rule applies even if we are using stream() instead of parallelStream().

2

most of the time should be stateless

Stateful and stateless refer to the stream operations!

Stateless operations are stream operations where the processing of one element does not depend on elements processed before. For example, map(), filter(), flatMap() and peek() are stateless.

Stateful operations are stream operations that require knowledge of previous or all elements to produce a result. For example, sorted(), distinct() and limit() are stateful.

Because of this property of being stateful, stateful operations may affect the parallel stream, leading to maybe incorrect output

3

Side effects should be kept minimum

What are sides effects? Go back to review Pure Functions from Lec 08.

Why? This is because parallel streams run in multiple threads, and if multiple threads modify the same variable at the same time → you get race conditions and unpredictable behavior. For example,

The forEach lambda generates a side effect — it modifies result. ArrayList is what we call a non-thread-safe data structure. If two threads manipulate it at the same time, an incorrect result may result.

To solve this issue, we can call the .toList(). It will be quite useful in PE2.

More on reduce()

The reduce() stream operation can be parallelizable! Because instead of reducing all elements one by one sequentially, we can split the stream into chunks, reduce each chunk in parallel, and then combine the results. For example, think of this,

But for this to work correctly, the way we combine things must not depend on order or timing.


To unlock the parallelizability, you need to call the three parameter version of reduce(), which is

Besides that, to unlock the full power of parallelizability, there are three rules that identity, accumulator and combiner must follow

1

Identity Rule

  • The identity must do nothing.

  • If you're combining something with the identity, it shouldn’t change the value.

2

Associativity Rule

* is a placeholder for a general binary operation

The grouping of operations doesn’t affect the result.

Why it matters:

  • When you split the stream, reduce in different threads, and combine later,

  • You’re changing the grouping — so associativity ensures the result stays the same.

What's the target of the Associativity Law?

Both the accumulator and the combiner should adhere to this law because

1. Accumulator

  • Used to combine a stream element into a result.

  • In parallel streams, elements are grouped arbitrarily and reduced in parallel.

  • If the accumulator is not associative, different groupings will give different results.

2. Combiner

  • Merges the results of substreams.

  • If combiner is not associative, merging partial results can lead to inconsistencies.

3

Compatibility Rule

This ensures that reducing a small part, then combining it with a bigger result, is the same as doing it all at once. It's like saying:

  • I reduced t with identity (got a partial result)

  • I combined that with u

  • It better be the same as just reducing u with t directly.

Otherwise, parallel combining would give wrong results.

Performance of Parallel Stream

Parallelizing a stream does not always improve the performance. This is because creating a thread to run a task incurs some overhead, and the overhead of creating too many threads might outweigh the benefits of parallelization.

Last updated