Skip to main content

2 posts tagged with "Functional-Programming"

View All Tags

· 14 min read

A quick-start guide to the Scala 3 programming language.

This guide won’t include steps to set-up a Scala build environment locally, so I encourage you to follow along using Scastie, an online Scala integrated development environment (IDE). Scastie makes it easy to add library dependencies and get code written and tested quickly. Scala can be a hassle to install successfully and I’d much prefer to get you right into coding. If you are interested in setting up a Scala environment then I’ll have some helpful resources at the bottom of the page.

Functions

Functions let us define re-usable blocks of expressions and statements that can be modified using parameters. We can define functions in two ways: using the def or val keywords. A function definition contains four components: the name, the parameters, the return type, and the body.

The following snippet contains an example function defined in either style.

def myFunction(parameter1: String, parameter2: Int): Boolean =
parameter1 == "example" || parameter2 == 100

// or
val myFunctionValue: (String, Int) => Boolean = (parameter1, parameter2) =>
parameter1 == "example" || parameter2 == 100
// myFunctionValue: Function2[String, Int, Boolean] = repl.MdocSession$MdocApp$$Lambda$8708/739628048@224d996a

All function parameters must have a provided type like String and Int for parameter1 and parameter2 above. Although not required, the return type should also be provided, like Boolean.

In general, the def syntax is preferred because it’s less verbose, but you’ll find reasons to use either one.

The // syntax from the snippet means that any comments on the same line and to the right are not compiled as Scala code.

Higher Order Functions

Functions are also values and can be provided to other functions as parameters and to types as fields. Function values can be called the same way you would a function defined with def or val.

def combineStrings(myList: List[String]): String =
myList.mkString(" ")

def splitAndApply(input: String, splitOn: String, f: List[String] => String): String =
f(input.split(splitOn).toList)

splitAndApply("hello/world", "/", combineStrings) // "hello world"
// res0: String = "hello world"

This example defines a function combineStrings that places spaces between each text in the provided list myList. Another function named splitAndApply will split the given input text any time the given splitOn text appears in input. After splitting, the new list of split phrases is provided to the given function f which determines how to re-join the split phrases. Finally, splitAndApply is called with an example text hello/world and the splitting symbol /. The function combineStrings is provided as a value to the splitAndApply method to combine the split phrases using a space. Altogether, this example yields the text hello world.

Types

Types can be defined in several ways depending on the use-case. I'll go over the basic constructs you'll want to use most-often and leave the rest for you to find as you need them.

Standard Library Types And Collections

The following types are automatically provided by the standard library and can be used without additional imports. There’s many more than this list available to you, but these are a good place to start.

  • Int — a number negative or positive
  • Booleantrue or false
  • String — a list of characters representing text, like “carrots”
  • Double — a double-precision floating point number, something like 101.111
  • Option — an optional value with two cases Some(value) and None
  • Either — a value with two cases Left and Right with types for each case
  • List — a collection of items with a provided type, instantiated with the syntax List(1, 2, 3, 4)
  • Map — a collection of one-to-one key and value pairs, instantiated using a special function/syntax Map("a" -> 1, "b" -> 2")

Case Classes

In general, the case class should be your go-to type for constructing data. A case class can be used to define an immutable type with automatic pattern matching built-in and several other goodies.

case class Videogame(title: String, year: Int, platforms: List[String])

This snippet creates a new type called Videogame with the fields title, year, and platforms. Each field must have a type assigned to it. A new instance of Videogame can be created with the format of Videogame(”Mario Kart 8 Deluxe”, 2014, List(”Switch”)).

Although case classes can contain related functionality in the form of functions and other values, it is best to avoid adding logic beyond it’s basic fields if possible. In functional programming, types should be independent of the logic that can be used to operate on them. However, there is sometimes logic that is vital to the domain of the type regardless of context. When you have a function that will almost always be called on a type when the type is used, place it in the body of the case class.

For example, the following code contains another case class with a method.

case class Car(make: String, model: String) {
def show: String = s"Car { make: $make, model: $model } "
}

The method can then conveniently be called using the code Car("Toyota”, “GR86”).show yielding a nice visual representation “Car { make: Toyota, model: GR86 }”. The s operator used in the snippet is a little bit of magic called string interpolation that is commonly used for formatting text.

Opaque Types

Often it’s convenient to use types from the standard library and dependency libraries directly in your code. For example, String, List, and Map. However, there is a downside. Your library or app code now has a direct dependency on that type and can’t be changed. I encourage you instead to try and use the opaque type. Opaque types let you mask an existing type with your own symbol, thus allowing you to modify the underlying type implementation without affecting your provided interface.

For example, the following snippet contains an opaque type representing a list of names. The only functions I’ve provided for this new type are getLastNames, getFirstNames, and show. If in the future I decide that I don’t want to use a list of tuples to represent my names, then clients of my library won’t be affected (including my own downstream code).

opaque type Names = List[(String, String)]

def firstNames(names: Names): List[String] =
names.map(_._1)

def lastNames(names: Names): List[String] =
names.map(_._2)

def show(names: Names): String =
names
.map(name => s"${name._2}, ${name._1}")
.mkString("\n")

It may be that in the future I’ve decided to change the type of Names from List[(String, String)] to List[Name] where Name is defined as case class Name(first: String, second: String). Downstream code will not see any impact from the underlying type change.

Sealed Traits

Complex union abstract data types can be created using the sealed trait syntax. A sealed trait defines the root of a type tree with a finite number of cases. An example of a union type common to every programming language is the Boolean type. The following code defines a custom Boolean2 tree. I've added a 2 post-fix to the type definitions to avoid colliding with the standard library Boolean type.

sealed trait Boolean2
case class True() extends Boolean2
case class False() extends Boolean2

val trueValue: Boolean2 = True()
// trueValue: Boolean2 = True()
val falseValue: Boolean2 = False()
// falseValue: Boolean2 = False()

Many types you encounter will be defined using the sealed trait with case class structure. Here’s another example, a functional linked list. As in the union example, I've added a 2 post-fix to all the types to avoid type collisions with the List type from the standard library.

sealed trait List2[A]
case class Empty2[A]() extends List2[A]
case class NonEmpty2[A](head: A, tail: List2[A]) extends List2[A]

val emptyList: List2[Int] = Empty2()
// emptyList: List2[Int] = Empty2()
val nonEmptyList: List2[Int] = NonEmpty2(1, NonEmpty2(2, Empty2()))
// nonEmptyList: List2[Int] = NonEmpty2(
// head = 1,
// tail = NonEmpty2(head = 2, tail = Empty2())
// )

This List2 snippet defines a list that can have two cases:

  • Empty2 — the list is empty and contains no values
  • NonEmpty2 — the list has at least one item (the head) and may contain more items (the tail)
    • The tail of the NonEmpty list can then either be Empty or NonEmpty. And the pattern repeats itself.

Enums

Another way to define a union type is as an enum. Enums are especially useful when you have a simple union type with no recursion. The Boolean2 example from earlier can be re-written in a simpler form using enum.

enum BooleanAsEnum {
case True()
case False()
}

The only difference between the enum and sealed trait syntax is that enum types can’t be used to define higher-order types like List and are intended only for simple type enumerations. In general, it’s best practice to use enums unless you need your root type to contain type parameters like List.

Companion Objects

A companion object is a singleton type containing helpful operations to perform on the accompanying type. The best functions for a companion object are constructors that create new instances of the accompanying type, often with some domain validation. These constructors can have any name you’d like, but common names are make or create. There is also a special companion function apply that can be defined and called as if instantiating the type directly.

object Videogame {
def make(title: String, year: Int, platforms: List[String]): Option[Videogame] =
if title.isEmpty then None
else Some(Videogame(title, year, platforms))

def apply(title: String, year: Int): Videogame =
Videogame(title, year, List.empty[String])
}

// `make` function now available on `Videogame` type
val granTurismo: Option[Videogame] = Videogame.make("Gran Turismo 7", 2022, List("PS5"))
// granTurismo: Option[Videogame] = Some(
// value = Videogame(
// title = "Gran Turismo 7",
// year = 2022,
// platforms = List("PS5")
// )
// )

val unreleased: Option[Videogame] = Videogame.make("", 2025, List.empty[String])
// unreleased: Option[Videogame] = None

// equivalent to Videogame.apply
val diablo: Videogame = Videogame("Diablo", 2023)
// diablo: Videogame = Videogame(
// title = "Diablo",
// year = 2023,
// platforms = List()
// )

The above snippet defines two additional constructors to the Videogame type, make and apply. The make constructor performs some additional validation on the provided parameters and returns a None value if the input is invalid. The apply method lets callers provide just the title and year of the videogame and provides a default of List.empty[String] for the platforms field. Providing additional apply functions in a companion object can be a convenient way to provide common defaults for users. In general, the apply method should always return just the companion type and not any validation-related type like Option or Either.

Pattern Matching

Values can be deconstructed and matched using the match and case syntax. Matching can be especially helpful for determining the particular union or enumeration sub-case that a value represents. For example, we can match on our List2 snippet to perform a different step depending on if the list is empty or contains items.

def isEmpty[A](list: List2[A]): Boolean =
list match {
case Empty2() => true
case NonEmpty2(_, _) => false
}

In the above snippet, we use the _ syntax to ignore particular values of the match. We can also provide local names for fields of the matched case.

def sum(list: List2[Int]): Int =
list match {
case Empty2() => 0
case NonEmpty2(first, remaining) => first + sum(remaining)
}

This snippet uses pattern matching to define a recursive function which calculates the sum of the list. If the list contains items, we add the value of the first item to the sum of the remaining items. If the list is empty, then we default to the value 0.

Typeclasses

Typeclasses define a set of standard functionality for a category of types. For example, you may want a function that operates on any collection of items without needing duplicated logic for each collection that users need. This example is common in functional programming, but is more in-depth than what we’ve seen so far.

trait Collection[F[_]] {
extension [A](collection: F[A])
def modifyItems[B](f: A => B): F[B]
}

given Collection[List] with
extension [A](list: List[A])
def modifyItems[B](f: A => B): List[B] = list.map(f)

given Collection[Option] with
extension [A](option: Option[A])
def modifyItems[B](f: A => B): Option[B] =
option match {
case Some(item) => Some(f(item))
case None => None
}

extension [F[_]](collection: F[Int])(using Collection[F])
def addOneToAllItems: F[Int] =
collection.modifyItems((item: Int) => item + 1)

List(1, 2, 3).addOneToAllItems
// res1: List[Int] = List(2, 3, 4)

extension (option: Option[Int])
def addOne: Option[Int] = option.addOneToAllItems

Some(1).addOne
// res2: Option[Int] = Some(value = 2)
None.addOne
// res3: Option[Int] = None

The Collection typeclass above requires that implementations provide the logic for the modifyItems function. The F[_] syntax specifies that overriding types must have a single type parameter, where the _ is a placeholder for the collection’s item type. The placeholder F type can then be used when defining the required functions for Collection instances. The extension syntax is used to define functions on types that can be used as if they were defined on the types themselves. Any type with a Collection instance in scope automatically has the modifyItems function available. In the case of the snippet above, we could write List(1, 2, 3, 4).modifyItems(num ⇒ num + 2) to update the list to contain all elements with 2 added.

In order to use the Collection syntax on various types, typeclass instances need to be defined using given … with syntax. Instances have been defined in the snippet for List and Option types. Although they are very different types with different shapes, they both contain collections of items. The case of a List might be more clear than with Option, but an Option is a collection of zero or one item. If the Option contains no item (None) then the item doesn’t need to be transformed by the modifyItems function. If it contains a single item (Some) then it will be modified by the function value provided to modifyItems.

Finally, the addOneToAllItems function extends any type with a Collection instance to include the function as a new method on any values of the collection type. With Collection instances available for both List and Option, we can call addOneToAllItems on both types without needing to explicitly define the extensions for both List and Option. I've defined a special extension method addOne for the Option[Int] type because typeclasses are only available for the specific types that have instances. The Collection typeclass must be invoked with the Option trait instead of using the Some and None sub-types.

Typeclasses are powerful tools for abstracting common and repetitious operations to many different kinds of types. Even a small typeclass with one function, like Collection, can be used as a base to build more complex functions across many types.

Additional Resources

This post has only begun to scratch the surface of the syntax and structures available in Scala programs, though the ones I’ve gone through provide a strong platform that should allow you to write many types of programs and applications. I recommend tinkering with the structures discussed here first and then expanding your knowledge using the following resources as you wish to do more in your programs.

Scala-Lang Docs

The official Scala-Lang documentation site contains everything from this post and more.

SDK-Man

Manages JDK instances on your computer and allows changing JDKs on the fly. There are many versions and distributions available, but any of the v17 JDKs are good choices for basic programs.

Rock The JVM

Rock The JVM is an amazing set of tutorials and guides to learning pieces of the Scala language. I learned most of my fundamental knowledge on Scala from Daniel Ciocirlan's courses.

This Week In Scala

A weekly newsletter rounding up all the new library improvements, releases, and news from the week. The newsletter is a good way to keep up to date with what’s happening in the community.

· 12 min read

Typeclasses are a construct for declaring categorical behavior on types in the Haskell programming language, however, the concept is not restricted to just Haskell. The typeclass is an implementation of ad-hoc polymorphism, which unlike with interface or class inheritance, allows us to define polymorphic behavior on the fly.

Typeclasses provide an abstraction by defining interfaces and the values that implement them. In most object-oriented languages, interfaces are defined using direct inheritance from a child type to a parent type. Instead of operating on the class level, typeclasses define an interface, then instantiate the implementation in the form of a value. This allows flexibility with interface implementation, since the typeclass instances can be interchanged through function parameters and package imports. With typeclasses, library consumers have the ability to extend the functionality of types without modifying the source.

A typeclass consists of some generic interface and an implementation for a particular type. The type can be any representation of a value (ex. int, String, Person, Vehicle...) and the value itself can be anything. The most important part of a typeclass is that the implementation exists seperately from the implementation of the value itself and can be used like a value.

Typeclass Implementations Across Languages

For the following implementation examples, we will be implementing the Semigroup typeclass on int, string, and list types.

Semigroup defines a single combine function that takes two instances of type T as inputs and outputs their combined value.

For example, calling combine with the integers 4 and 6 should return 10 if we are using addition as the combine implementation (we could also implement this for multiplication).

As an example of how typeclasses can be used within a library, I'll also be creating a function applyTo that will combine each value in a list with a given value. For the input List(1, 2, 3, 4, 5), applyTo with a value of 8 would return List(9, 10, 11, 12, 13).

It is an implicit requirement that a Semigroup's combine operation also be associative: combining a group of values can occur in any order as in (1 + 2) + 3 = 1 + (2 + 3). However, this is not a possible restriction in most programming languages, so it will not be factored into these examples.

Scala

Although there is not first-class support for typeclasses in Scala, there are language constructs to help create them.

We first create a trait representing our Semigroup typeclass, then create a typeclass instance representing integer addition, and finally create a function which implicitly takes a typeclass instance as a parameter. The implicit keyword in Scala 2 lets us create a value in the implicit scope and summon it when the typeclass is requested.

trait Semigroup[T] {
def combine(a: T, b: T): T
}

implicit val intAdditionSemigroup: Semigroup[Int] = (a: Int, b: Int) => a + b
// intAdditionSemigroup: Semigroup[Int] = repl.MdocSession$MdocApp$$Lambda$8409/1609029207@183380af

def applyTo[A](values: List[A], value: A)(implicit semigroup: Semigroup[A]): List[A] =
values.map(v => semigroup.combine(v, value))

applyTo(List(1, 2, 3, 4, 5), 8)
// res0: List[Int] = List(9, 10, 11, 12, 13)

C#

Typeclasses in C# require some creativity because anonymous objects aren't a thing. To access typeclass instances, we will provide an instance static method which returns a singleton typeclass instance. Although this will make it difficult to use the Semigroup<T> typeclass in a generic context, it makes the code a little bit neater.

public interface Semigroup<T>
{
T Combine(T a, T b);
}

public class IntAdditionSemigroup : Semigroup<int>
{
public static IntSemigroup instance = new IntSemigroup();

public int Combine(int a, int b) = a + b;
}

public List<T> ApplyTo<T>(Semigroup<T> instance, List<T> values, T value)
{
return values.Select(v => instance.combine(v, value));
}

ApplyTo(IntAdditionSemigroup.instance, new List<int> { 1, 2, 3, 4, 5 }, 8);

Note that because C# has no equivalent of the implicit scope found in Scala, the semigroup instance must be provided directly to the ApplyTo function.

Typescript

Typescript's implementation is a little neater, but also requires passing the semigroup instance directly because an implicit scope doesn't exist. Typescript does support instantiating anonymous objects, which makes creating the typeclass instances simple.

interface Semigroup<T> {
combine(a: T, b: T): T
}

const numberAdditionSemigroup = {
combine(a: number, b: number): number {
return a + b
}
} as Semigroup<number>

function combineAll<T>(instance: Semigroup<T>, values: T[], value: T): T[] {
return values.map(v => instance.combine(v, value))
}

combineAll(numberAdditionSemigroup, [1, 2, 3, 4, 5], 8)

Rust

Rust supports Ad-Hoc polymorphism out of the box since the implementation of interfaces for types must be defined separately from the types themselves. The trait implementations in Rust act just like typeclass definitions with an accompying impl block for instance definitions.

I've made the Semigroup typeclass receive the values a and b by reference so that we don't have to take ownership of the values, which makes it easier to work with.

trait Semigroup {
fn combine(a: &Self, b: &Self) -> Self;
}

impl Semigroup for i32 {
fn combine(a: &i32, b: &i32) -> i32 {
a + b
}
}

fn apply_to<T>(values: Vec<T>, value: T) -> Vec<T> where T: Semigroup {
values.iter().map(|v| { T::combine(&v, &value) } ).collect()
}

apply_to(vec![1, 2, 3, 4, 5], 8)

We do not have the option to provide multiple implementations of Semigroup for i32 within the same scope, so any additional implementations of combine (multiplication) would have to be placed in a separate scope and imported.

Helpful Typeclasses

There are a number of common typeclasses that can be combined to implement similar behavior across all implementing types. Semigroup, Eq, and Show are simple typeclasses, but more complex ones like Monoid, Monad, and Functor can provide a lot of additional functionality.

I will be implmenting the following examples with Scala, but symmetric implementations can be made for C#, Typescript, and Rust using the methods outlined in our Semigroup example.

Eq

Eq provides a typesafe equals method eqv. Eq should be used when we want to check that the value of two values with the same type is the same. Calling eqv with two values of different types should fail to compile.

trait Eq[T] {
def eqv(a: T, b: T): Boolean
}

Because we provide a function that determines if two values are equal, we also get a function determining if two values are not equal for free.

object Eq {
def neqv[T](a: T, b: T)(implicit eq: Eq[T]): Boolean = !eq.eqv(a, b)
}

For implementing neqv I've created a companion class which takes an implicit Eq instance. The neqv method can also be defined in the Eq trait itself.

Then in our library, we can use our type-safe Eq implementation instead of the == which can vary in accuracy depending on type.

def combineIfNotEqual[T](a: T, b: T, otherwise: T)(implicit eq: Eq[T], semigroup: Semigroup[T]): T =
if (Eq.neqv(a, b)) semigroup.combine(a, b)
else otherwise

Show

Show provides a method to get an explicit function for turning a value into a String type. This is very helpful when we want to print the state of a complex object to the console without having to override any existing toString method directly on the type implementation. Also, when a function wants to print the value of a generic type to the console, it can use its Show implementation instead of relying on the built-in toString method to be correct. Often, the default toString method will print out garbage, expecially for complex class instances in the JVM.

trait Show[T] {
def show(value: T): String
}

Then, when we want to do some debugging from a function we write, we can require an explicit Show implementation which the function caller provides.

implicit val showInt: Show[Int] = (value: Int) => s"Integer($value)" // ex. Integer(5)
// showInt: Show[Int] = repl.MdocSession$MdocApp$$Lambda$8411/1082088733@55e053c0 // ex. Integer(5)

def print[T](value: T)(implicit show: Show[T]): Unit = println(show.show(value))

print(500)
// Integer(500)

For a more complex object, this can save us a lot of headache.

class PersonWithRandomAge(val first: String, val last: String) {
private val random = scala.util.Random

val age = random.nextInt(100)
}

implicit val showPerson: Show[PersonWithRandomAge] = (person: PersonWithRandomAge) =>
s"Person(name: ${person.last}, ${person.first}, age: ${person.age})"
// showPerson: Show[PersonWithRandomAge] = repl.MdocSession$MdocApp$$Lambda$8412/2120903508@249f69dc

Even better, with some help from the Scala Cats library, we can use this implementation when we have a collection of people without any extra work. We just have to be sure to implement the cats.Show trait instead of our custom Show trait.

import cats.implicits._

implicit val catsShowPerson: cats.Show[PersonWithRandomAge] = (person: PersonWithRandomAge) =>
s"Person(name: ${person.last}, ${person.first}, age: ${person.age})"
// catsShowPerson: Show[PersonWithRandomAge] = repl.MdocSession$MdocApp$$Lambda$8413/1421579510@73b650dc

val people: List[PersonWithRandomAge] = List(new PersonWithRandomAge("Kup", " Quickdew"), new PersonWithRandomAge("Hellweed", "Underhill"))
// people: List[PersonWithRandomAge] = List(
// repl.MdocSession$MdocApp$PersonWithRandomAge@6b55ffa6,
// repl.MdocSession$MdocApp$PersonWithRandomAge@8695850
// )

people.show
// res2: String = "List(Person(name: Quickdew, Kup, age: 10), Person(name: Underhill, Hellweed, age: 41))"

Monoid

The Monoid typeclass is an extension of Semigroup with an additional method empty that returns a value representing the default state of non-existence. For integers, this value would be 0 or for strings "".

It is often helpful to extend the functionality of one typeclass with that of another. We can expand on our previous Semigroup trait to implement Monoid.

trait Semigroup[T] {
def combine(a: T, b: T): T
}

trait Monoid[T] extends Semigroup[T] {
def empty: T
}

Any implementation of Monoid can be used where a Semigroup is required. This comes in handy when we want to fold over a collection of values.

implicit val intAdditionMonoid: Monoid[Int] = new Monoid[Int] {
override def combine(a: Int, b: Int): Int = a + b
override def empty: Int = 0
}
// intAdditionMonoid: Monoid[Int] = repl.MdocSession$MdocApp3$$anon$9@20789fc0

def combineAll[T](values: List[T])(implicit monoid: Monoid[T]) =
values.foldLeft(monoid.empty)(monoid.combine)

combineAll(List(1, 2, 3, 4, 5)) // 15
// res4: Int = 15

Functor

The Functor typeclass defines a map method from type A to type B so that a Functor[A] can be mapped to a Functor[B] type. This is loosely related to the mathematical definition of a Functor F which defines a mapping from a set A to a set B such that F[idA] -> F[idB] and F[g * f] -> F[g] * F[f]. In this definition, the identity element idA depends on the identity of the the category (0 for integer addtion and 1 for integer multiplication) and g and f are functions applied to the element contained in F. For example, if g(x) = x + 1 and f(x) = x + 5, then F[g(f(3))] must be equal to F[g(3)] + F[f(3)].

For the typeclass definition of Functor, we are going to take advantage of Scala's ability to define generic type arguments with a number of "holes". For example, Functor[F[_]] defines a Functor type for a generic argument named F that itself has a single type argument. In most languages, this restriction will not be possible, which can make defining this Functor type tricky.

In our example, I will use Option as the Functor argument. Option in Scala contains a previously defined map method, but I will instead show how the implementation works using just Some and None case classes.

I'm also adding an additional Functor object with a summoner function which pulls the given implicit functor out of the implicit scope. This will let us call the Functor map function directly.

trait Functor[F[_]] {
def map[A, B](functorA: F[A], mapping: A => B): F[B]
}

object Functor {
// summoner function
def apply[F[_]](implicit functor: Functor[F]): Functor[F] = functor
}

implicit val optionFunctor: Functor[Option] = new Functor[Option] {
override def map[A, B](functorA: Option[A], mapping: A => B): Option[B] =
functorA match {
case Some(a) => Some(mapping(a))
case None => None
}
}
// optionFunctor: Functor[[A >: Nothing <: Any] => Option[A]] = repl.MdocSession$MdocApp3$$anon$12@34e5b981

Functor[Option].map[Int, Int](Some(50), x => x * 10)
// res5: Option[Int] = Some(value = 500)
Functor[Option].map[Int, Int](None, x => x * 10)
// res6: Option[Int] = None

Monad

The Monad typeclass is an extension on the Functor typeclass which provides a flatten function. The flatten function squashes a value of type F[F[_]] into F[_]. Once defined, we can combine flatten and map to create flatMap, which works like map except it takes in a function of type A => F[B] instead of A => B and returns the type F[B].

trait Monad[F[_]] extends Functor[F] {
def flatten[A](nestedFunctorA: F[F[A]]): F[A]

def flatMap[A, B](functorA: F[A], mapping: A => F[B]): F[B] = flatten(map(functorA, mapping))
}

implicit val listMonad: Monad[List] = new Monad[List] {
override def map[A, B](functorA: List[A], mapping: A => B): List[B] = functorA match {
case head :: tail => mapping(head) :: map(tail, mapping)
case _ => List()
}

override def flatten[A](nestedFunctorA: List[List[A]]): List[A] = nestedFunctorA match {
case head :: tail => head ++ flatten(tail)
case _ => List()
}
}
// listMonad: Monad[List] = repl.MdocSession$MdocApp3$$anon$16@20c7f12b

object Monad {
// summoner function
def apply[F[_]](implicit monad: Monad[F]): Monad[F] = monad
}

Monad[List].map(List(100, 5, 22), (x: Int) => x * 5)
// res7: List[Int] = List(500, 25, 110)

Monad[List].flatten(List(List(1, 3, 4), List(9, 18, 0)))
// res8: List[Int] = List(1, 3, 4, 9, 18, 0)

Monad[List].flatMap(List(1, 2, 3, 4), (x: Int) => List(x % 2, x % 3))
// res9: List[Int] = List(1, 1, 0, 2, 1, 0, 0, 1)

Conclusion

Typeclasses are a flexible and functional approach to abstraction and generic programming. They are type-safe, modular, and simple to test and relieve many headaches software developers encounter with common behaviors on types.