Movatterモバイル変換


[0]ホーム

URL:


Rachel M. Carmena

Functional programming sparks joy 2

Published: 20 August 2019
Last updated: 20 September 2019

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:

Helicopter

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:

  • Objects
  • Relationships (also known as morphisms or arrows) between the objects

Functional programming is based on acategory of sets where:

  • The objects aretypes (sets of values)
  • The arrows arefunctions

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.

Note: Imagine more than one category. And now, imagine arrows between categories:
  • From objects of a category to objects of another one
  • From arrows of a category to arrows of another one

Therefore, infunctional programming:

  • The objects aretypes andfunctions

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):

  • If functions were impure, it would be very difficult to “play” with them and to compose with each other.
  • Mathematically, the common needs with types and functions are defined. We only have to use them, so we’ll program in a higher abstraction level.
  • Immutability is also essential to compose functions. Otherwise, the results couldn’t be anticipated.
  • Recursion will appear when defining types or functions.
  • Currying and partial application are some of the techniques to be able to compose functions (an output must match with the input of the following function).

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.

About the examples

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.

  • TypeScript uses the pipe to represent the choice between basic types or the intersection of properties between objects.
  • I found composite types in Reason, though I didn’t find other capabilities.
  • What about the rest of options? I reviewed PureScript and it provided the characteristics I was looking for.
  • My only purpose is explaining functional programming. PureScript is only a choice for it.

    Note: Sometimes we think about selecting only one programming language (as I did here). However, for a real software product, we could choose different programming languages according to the needs. There are a lot of languages which compile to JavaScript, which run under JVM or which run under the CLR, among other options.

    Declaration and definition

    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:

    • Function name
    • :: (= “is of type” or “has the type of”)
    • Function type:
      • Input type
      • ->
      • Output type

    Thefunction definition is where the function is actually defined.

    For instance, the declaration and definition ofadd function:

    add::Int->Int->Intaddxy=x+y
    Note: As in other languages such as Haskell or F#, all functions are considered curried. So theadd 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.

    Composite types

    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:

    • Product types
    • Sum 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.

    Product types

    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 typeAmountand 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.

    Sum types

    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 typeEmailor 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.

    Note: I only included examples of combining two types for simplicity though there is no limit.
    Note: Initially I usedtype composition as section title. It wasn't right because it could be confused withcomposition fromfunction composition.

    Pattern matching

    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"

    Some composite types

    Tuple

    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:

    • The lowercase lettersa andb represent any type
    • The firstTuple is the type constructor
    • The secondTuple is the value constructor

    Let’s see an example:

    pair :: Tuple String Stringpair = Tuple "spam" "eggs"

    where:

    • The type constructorTuple is used in the declaration:pair is of typeTuple String String
    • The value constructorTuple is used in the definition: the value ofpair isTuple "spam" "eggs"

    It’s known asPair in other languages.

    Maybe

    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:

    • The lowercase lettera represents any type
    • Maybe is the type constructor

    Maybe of a type can represent one of these options:

    • A missing value:Nothing
    • The presence of a value of that type: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

    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:

    • The lowercase lettersa andb represent any type
    • Either is the type constructor

    Either represents the choice between 2 types of values where:

    • Left is used to carry an error value
    • Right is used to carry a success value

    Let’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'

    Some guidelines

    Making things explicit

    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.

    Making illegal states unrepresentable

    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 }
    Note: This design guideline reminds me another one that has a wider application. It was formulated byScott Meyers:

    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.)

    Typeclasses

    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.

    Monoids

    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:

    • They could be combined with addition, for instance.
    • Zero could be the starting point and then, add each number.
    • Given 3 numbers, the result with be the same for these operations:
      • The addition of the first 2 numbers is added with the third number.
      • The first number is added with the addition of the other 2 numbers.

    Or strings:

    • They could be combined with concatenation.
    • An empty string could be the starting point and then, concatenate each string.
    • Given 3 strings, the result with be the same for these operations:
      • The concatenation of the first 2 strings is concatenated with the third string.
      • The first string is concatenated with the concatenation of the other 2 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:

    • It has a neutral element (identity)
    • It’s associative

    Did you identify each part of the definition in the previous examples?

    Note: I included examples with simple and known types. However, think aboutany kind of type and the need to define how the values of a type are combined.

    Typeclass for monoids

    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!"
    Note:foldl folds the array from the left. In this case, the result would be the same withfoldr.

    Functors

    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:

    • Themap function allows the functionsquare to belifted over an array
    • Or just, themap functionlifts thesquare function

    How to form a functor

    Following the definition, afunctor can be formed by:

    • A type constructor
    • A mapping function

    For instance, thefunctor formed by:

    • The type constructorMaybe (in this case, fromString toMaybe String)
    • The followingfmap function:
      • From a function(String -> String)
      • To a function(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.

    Note: For simplicity in the examples, I included functions likesquare andrepeat which have the same type for input and output. However, that condition isnot necessary.
    Note: What's the purpose offunctors? Let's remember the need of composing functions and the need of adapting the input and output to compose them. We've already seen some tools for it and afunctor is another one. We'll see more of them in the next sections.

    Typeclass for functors

    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 ")

    Functor composition

    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 "])

    Applicative

    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.

    Note: In this section, I didn't include the formal definition ofapplicative functors in PureScript and the corresponding instance forMaybe. They can be found in the modulesControl.Apply,Control.Applicative andData.Maybe. There are also instances forEither,Array,List, etc.

    Monad

    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 theMaybefunctor 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:

    Note: The ability to wrap a value to a context is done by thepure function from afunctor. For instance, from aString value to aMaybe String value.

    Further knowledge

    Credit

    Image byAnthony Jarrin fromPixabay

    « Not only source code smellsFunctional programming sparks joy »

    [8]ページ先頭

    ©2009-2025 Movatter.jp