Previous card:Functional programming sparks joy
Before adding more concepts, I would like to share a reflection.
I think that object-oriented programming and functional programming are such different paradigms that they are not comparable. However, I find a similar feeling.
When I discovered object-oriented design patterns, I realized that we weren’t doing anything special. Our problems were widespread.
The only difference between our development project and others could be the business domain.
What if we step forward? What if we can model the domain with two kinds of pieces?
When I was a child, I didn’t have LEGO bricks but TENTE with smaller pieces.
I still remember my helicopter of the forest brigade:
I assembled it and disassembled it hundreds of times.
However, it had more than two kinds of pieces.
What if we only need two kinds of pieces for creating software?
What if there is a branch of mathematics that tries to generalize things with two kinds of pieces and can be moved to computer programming?
I’m talking aboutcategory theory where acategory is a collection of:
Functional programming is based on acategory of sets where:
Just two kinds ofpieces and thepower of composition with all theabstractions defined and proved matematically.
AsBartosz Milewski wrote:
One of the important advantages of having a mathematical model for programming is that it’s possible to perform formal proofs of correctness of software.
Therefore, infunctional programming:
Remember that functions are first-class citizens and can be input and output as well.
It’s awesome the amount of things that can be built when “playing” with only two kinds of pieces.
Bartosz Milewski also wrote:
Category theory is full of simple but powerful ideas
Let’s review the other previous concepts (I already mentioned functions as first-class citizens):
Finally, let’s consider the business domain we are working on, the state machines, the processes, the transformations, the validations, … how do types and functions fit those needs? It seems they absolutely fit them.
So far, I’ve used plain JavaScript and an example with Lodash library.
However, to continue explaining functional programming, I’ll include examples inPureScript, a strongly-typed functional programming language that compiles to JavaScript. It’s heavily influenced by Haskell.
I would have had a clear choice with other non-purely programming languages:
I think it’s good to have libraries which add functional capabilities as a way of extending a language more quickly and with the support of the developers community.
However, I had a lot of alternatives in #"https://github.com/fantasyland/daggy">Daggy. However, this alternative involved making more decisions later.
My only purpose is explaining functional programming. PureScript is only a choice for it.
In PureScript, as in Haskell, it’s a good practice to provide type annotations as documentation though compiler is able to infer them.
The function type annotation is known as thefunction declaration. Its notation is influenced byHindley-Milner type system and it has the following parts:
::
(= “is of type” or “has the type of”)->
Thefunction definition is where the function is actually defined.
For instance, the declaration and definition ofadd
function:
add::Int->Int->Intaddxy=x+y
add
function is really:add :: Int -> (Int -> Int)
It takes an integer (the first operand) and returns a function. That function takes the other integer (the second operand) and returns the sum.
This is transparent to us because the function definition and the use work like a function of 2 parameters.
Types can be combined to create a new type.
The new type is also known as analgebraic data type (ADT).
Whyalgebraic? Because we can “play” with them on equations and symbols (I didn’t cover it here although it’s fun!).
There are two common kinds of composite types:
Let’s see what they are and some examples and then I’ll explain a curiosity that can be useful to understand the reason of their names.
This is an example of the creation of a new type with the product of two types:
data Money = Money { amount :: Amount, currency :: Currency}
Its values will contain both a value of typeAmount
and a value of typeCurrency
.
Whyproduct? Think about the number of different values for that type: theproduct of the number of different values of the typeAmount
and the number of different values of the typeCurrency
.
This is an example of the creation of a new type with the sum of two types:
data SendingMethod = Email String | Address { street :: String, city :: String, country :: String }
The new typeSendingMethod
represents a choice betweenEmail
andAddress
.
The values of this new type will contain a value of typeEmail
or a value of typeAddress
.
Whysum? Think about the number of different values for that type: thesum of the number of different values of the typeEmail
and the number of different values of the typeAddress
.
In functional programming, pattern matching is based on constructors as patterns.
Given a value, it can be disassembled down to parts that were used to construct it.
This is a powerful tool to make decisions according to types:
showSendingMethod :: SendingMethod -> StringshowSendingMethod sendingMethod = case sendingMethod of Email email -> "Sent by mail to: " <> email Address address -> "Sent to an address"
Tuple
is an example ofproduct type that represents a pair of values.
It’s defined in the moduleData.Tuple
of PureScript:
data Tuple a b = Tuple a b
where:
a
andb
represent any typeTuple
is the type constructorTuple
is the value constructorLet’s see an example:
pair :: Tuple String Stringpair = Tuple "spam" "eggs"
where:
Tuple
is used in the declaration:pair
is of typeTuple String String
Tuple
is used in the definition: the value ofpair
isTuple "spam" "eggs"
It’s known asPair in other languages.
Maybe
is an example ofsum type that is used to define optional values.
It’s defined in the moduleData.Maybe
of PureScript:
data Maybe a = Nothing | Just a
where:
a
represents any typeMaybe
is the type constructorMaybe
of a type can represent one of these options:
Nothing
Just a
Let’s see an example of use:
showTheValue :: Maybe Number -> StringshowTheValue value = case value of Nothing -> "There is no value" Just value' -> "The value is: " <> toString value'
It’s known asOption in other languages.
Either
is another example ofsum type and it’s commonly use for error handling.
It’s defined in the moduleData.Either
of PureScript:
data Either a b = Left a | Right b
where:
a
andb
represent any typeEither
is the type constructorEither
represents the choice between 2 types of values where:
Left
is used to carry an error valueRight
is used to carry a success valueLet’s see an example of use:
showTheValue :: Either String Number -> StringshowTheValue value = case value of Left value' -> "Error: " <> value' Right value' -> "The value is: " <> toString value'
It can be said that functional programming is based on making thingsexplicit as much as possible.
For instance, let’s think aboutMaybe
.
If a function returns an integer, a missing value is not expected.
WithMaybe
, it can be made explicit that the function returns an integer or not.
This is a design guideline byYaron Minsky.
Let’s see an example. Imagine that we have a type Course with this content:
data Course = Course { title :: String, started :: Boolean, lastInteraction :: Maybe Date}
There is an illegal state that can be representable:started
is false andlastInteraction
has a date.
For instance, that illegal state could be avoided with a sum type:
data Course = StartedCourse { title :: String, lastInteraction :: Date } | UnstartedCourse { title :: String }
Make interfaces easy to use correctly and hard to use incorrectly.
Interfaces occur at the highest level of abstraction (user interfaces), at the lowest (function interfaces), and at levels in between (class interfaces, library interfaces, etc.)
Atypeclass defines a family of types that support a common interface
Typeclasses are useful to define concepts likemonoids,functors,applicative andmonads for all the types or types constructors, respectively.
And then, it’s possible to create instances of those typeclasses for concrete types or types constructors.
I include more details about it in following sections.
Amonoid is useful to explain how the values of a type are combined.
Let’s see some examples with known types.
For instance, natural numbers:
Or strings:
How to express it in an easy way?
Monoids to the rescue!
Read the following definition together with the previous examples.
Amonoid is a type with a binary operation (2 elements). That operation has the following properties:
Did you identify each part of the definition in the previous examples?
Let’s see how to abstract the concept ofmonoid for any type and then, how to define it for concrete types.
In Haskell, the typeclass for amonoid includes the neutral element (calledmempty
) and the binary operation (calledmappend
):
class Monoid m where mempty :: m mappend :: m -> m -> m
However, PureScript has a previous abstraction and includes the binary operation inSemigroup
typeclass:
class Semigroup a where append :: a -> a -> a
SoMonoid typeclass extends theSemigroup
typeclass with the neutral element:
class Semigroup m <= Monoid m where mempty :: m
That’s the abstraction. Now, for instance, how to define the way of combining strings?
Themonoid for strings in PureScript (modulesData.Semigroup
andData.Monoid
, respectively):
instance semigroupString :: Semigroup String where append = concatString
instance monoidString :: Monoid String where mempty = ""
The binary operation is concatenation and the neutral element is the empty string.
In this way, it’s possible to combine all the strings from an array into a single string when using theappend
andmempty
defined for strings:
greeting :: Stringgreeting = foldl append mempty ["Hello,", " ", "world!"]-- "Hello, world!"
foldl
folds the array from the left. In this case, the result would be the same withfoldr
.You already know afunctor!!
Afunctor is a type constructor which provides a mapping operation.
Whichfunctor do you know for sure?
Array
is afunctor which provides themap
function.
Let’s remember the included example in the first part (plain JavaScript):
constsquare=number=>Math.pow(number,2);constnumbers=[2,5,8];numbers.map(square);// [4, 25, 64]
The same example in PureScript:
square :: Int -> Intsquare number = pow number 2numbers = [2, 5, 8] :: Array IntlogShow (map square numbers)-- [4,25,64]
or using aninfix function application (as an operator between the two arguments):
logShow (square `map` numbers)-- [4,25,64]logShow (square <$> numbers)-- [4,25,64]
Graphically:
It’s said that:
map
function allows the functionsquare
to belifted over an arraymap
functionlifts thesquare
functionFollowing the definition, afunctor can be formed by:
For instance, thefunctor formed by:
Maybe
(in this case, fromString
toMaybe String
)fmap
function:(String -> String)
(Maybe String -> Maybe String)
repeat :: String -> Stringrepeat aString = aString <> aStringfmap :: (String -> String) -> Maybe String -> Maybe Stringfmap f value = case value of Nothing -> Nothing Just value' -> Just (f value')message :: Maybe Stringmessage = fmap repeat (Just " bla ")-- (Just " bla bla ")
Graphically:
In this example, thefmap
functionlifts therepeat
function.
square
andrepeat
which have the same type for input and output. However, that condition isnot necessary.Let’s see how to abstract the concept offunctor for any type constructor and then, how to define it for a concrete type constructor.
The typeclass for afunctor appears inData.Functor
module:
class Functor f where map :: forall a b. (a -> b) -> f a -> f b
Following the previous example, thefunctor forMaybe
is already defined inData.Maybe
module:
instance functorMaybe :: Functor Maybe where map fn (Just x) = Just (fn x) map _ _ = Nothing
So it can be used to get the same result than before with less code:
repeat :: String -> Stringrepeat aString = aString <> aStringlogShow (map repeat (Just " bla "))-- (Just " bla bla ")
or using aninfix function application (as an operator between the two arguments):
logShow (repeat `map` (Just " bla "))-- (Just " bla bla ")logShow (repeat <$> (Just " bla "))-- (Just " bla bla ")
So far I’ve included examples ofArray
functor andMaybe
functor separately.
What if there are more than one functor?
What if we want to apply therepeat
function into aMaybe (Array String)
?
Firstly, we use the mapping function ofArray
and then, the mapping function ofMaybe
over the result.
In other words, the mapping functions are composed (operator>>>
):
logShow ((map >>> map) repeat (Just [" bla ", " ha "]))-- (Just [" bla bla "," ha ha "])
There are a lot of things aboutapplicative. I include the basicapplicative abstraction also known as anapplicative functor.
Anapplicative functor is an step further than afunctor.
Let’s see what happens if we have a function with more than one parameter.
For instance:
fullName :: String -> String -> StringfullName name surname = name <> " " <> surname
Functions are curried by default in PureScript, so in this case: the input is aString
and the output is another functionString -> String
.
What happens if instead of two strings forname
andsurname
we haveMaybe String
for each of them?
Withmap
fromfunctorMaybe
, we get another function fromMaybe String
toMaybe (String -> String)
.
In other words, the functionString -> String
is wrapped with the type constructorMaybe
.
How can we continue?
How can we get another function fromMaybe String
toMaybe String
?
apply
to the rescue!
In PureScript, the applicative for Maybe is already defined so it’s possible to useapply
with that kind of type constructor:
logShow (apply (map fullName (Just "Rachel")) (Just "M. Carmena"))-- (Just "Rachel M. Carmena")logShow (apply (map fullName (Just "Rachel")) Nothing)-- Nothing
or using aninfix function application (in this case,<*>
is the equivalent toapply
):
logShow (fullName <$> Just "Rachel" <*> Just "M. Carmena")-- (Just "Rachel M. Carmena")logShow (fullName <$> Just "Rachel" <*> Nothing)-- Nothing
Therefore, if we have a function with more than two parameters, we’d use<$>
(map
) for the first parameter and then<*>
(apply
) for the rest of parameters.
Another powerful tool to compose functions. Think about other type constructors likeEither
, form validations, etc.
Finally, after the examples, it could be easier to understand the difference betweenmap
fromfunctors andapply
fromapplicative functors:
map :: forall a b. (a -> b) -> f a -> f b
apply :: forall a b. f (a -> b) -> f a -> f b
Both get the same result thoughmap
transforms a function andapply
transforms a function which is wrapped.
Maybe
. They can be found in the modulesControl.Apply
,Control.Applicative
andData.Maybe
. There are also instances forEither
,Array
,List
, etc.Let’s see what happens if we have agetUser
function from aString
value to aMaybe User
value:
and we don’t have aString
value available to be provided to the function but aMaybe String
value.
Then, we can think about theMaybe
functor to have aMaybe String
value as an input:
However, the result will be aMaybe (Maybe User)
value.
How can we get just aMaybe User
value?
FlatteningMaybe (Maybe User)
intoMaybe User
:
Both operations are considered together bybind
from amonad:
bind :: forall a b. m a -> (a -> m b) -> m b
Following the example of thegetUser
function, it’s possible to get aMaybe User
value from aMaybe String
value, because the instance ofMonad
forMaybe
is already available in PureScript:
user :: Maybe Useruser = bind (Just "12345") getUser
or using aninfix function application (in this case,>>=
is the equivalent tobind
):
user :: Maybe Useruser = (Just "12345") >>= getUser
It’s said thatmonads are useful to chain dependent functions in series:
pure
function from afunctor. For instance, from aString
value to aMaybe String
value.Image byAnthony Jarrin fromPixabay