قالب وردپرس درنا توس
Home / IOS Development / About flatmap, compactMap and monads

About flatmap, compactMap and monads



The previous article used a Swift feature called compactMap but when I originally wrote that article, it was named flatMap . The reasons why the name is subtle; at a literal level, compactMap is a "flat" and a "map", just like flatMap is. The arguments for the renaming ended with the fact that flatMap is often assumed to be a monadic transformation, so compactMap – which is not monadic – therefore needs a different name. [19659002] It's an esoteric argument since you have to know what a monad is and be aware of the prerequisite in some circles that flatMap is a monad (despite Swift never making such claims).

Swift does not use the word "monad" anywhere in the documentation. Monads are far from basic to Swift. Nevertheless monads are important in some programming languages, especially Haskell. Monads is a basic unit for calculation in Haskell.

In this article I look at monads. How can something be so basic in one language, but completely optional in others? Should we try to use monads more often in Swift? What are the options?

What is a monad?

Monads is a complex concept based on simpler ideas. Monads are not particularly complicated, but if you do not have a clear mental image of the ideas they are based on, they can be very abstract.

So I'll start as simple as possible and build up monads. [ ] : String ) -> Draw {
return word . first !
}

This feature assumes that word is a non-empty String (! will cancel the program if word is an empty string), but it is otherwise straightforward.

Here is a diagram of this feature:

 Figure 1: a function

The input value is a string and the output value is a ]

.

Removal

Let's add a little bit of complexity.

Think of our function firstLetter that we have an entire list of words whose first letters we want to extract. The traditional imperative approach is to extract (extract) each element in the list and perform the calculation for the element itself.

  func    processword list  (  _    wordList :    [  String ])    {
     to    word [19659043] in    word list    {
        la    letter    = [19659015] firstLetter  (  word ) 
        // do something by letter 
    ]} 
} 

Our input is an array containing String . for loop each string in glossary uses the loop body function firstLetter we wrote and then we can use results. We can diagram this as follows:

 Figure 2: Unpack a matrix and use a function

In this chart the "wrapping value" for the "container" is the glossary ] selection of strings. Each word obtained by for the loop is represented by the "input value" in the "Unwrap" box. The function firstLetter is contained in the box "Use function" and letter is "initial value" on the right.

Map

Using a for loop to extract values ​​from an array works, the unopened value is limited in the life of the scope of the loop. If we want to continue using the extracted initials, we must store them in a location outside the loop.

  func    firstLetters  (  from    wordList :   ] [  String ])    ->    [  Character ]    {
        Result    =    [[19659042] Draw ] () 
     to    word    in    word list    {
        la    letter    =    first letter  ([19659055] word ) 
        result .  add    letter ) 
   } 
     return    result 
} 

By adding the results to a number of declared outside the loop, we can proceed to work with the results outside the loop.

We can model this in a chart as follows:

 Figure 3: Mapping from one group to another

"Lay out" and "add" the steps are still necessary but I have omitted them from the chart so that we can focus on the inputs and outputs of both same container but having different built-in types .

] firstLetters (from :) the function is only 8 lines, but Swift gives a common abstraction to make it on a line: map . The function on Array takes a function – as our firstLetter function – applies to each of the items and returns the result as an Array . This is exactly the same work we performed in our firstLetters (from :) function so that we can replace all eight lines with:

  la    letters    =    wordList      firstLetter ) 

This is unlikely to be new information for most developers – it is a very common concept in modern programming – but only 6 or 7 years ago map was unusual beyond functional programming. Although I now use map and other similar transformations several times a day, I rarely used this type of Objective-C construction.

It's important to understand why map ] works well:

By packing the outputs of a function in the same abstraction used to wrap the inputs, we can continue to work with the outputs in many ways we worked with the inputs

When all your data is wrapped in the same abstractions (maybe you are working on arrays, options, results, observability, etc.), you can start building an entire library that can handle the abstraction flexible without having to worry about the details of the content. This helps make your code composite and allows you to easily build treatment pipelines or reuse codes between projects and modules.

Map Returning an Abstraction

Let's change our original function. Instead of returning only the first character, we assume that the entry is not a single word but some arbitrary text and we will get the first letters of all the words the text contains:

  func    firstLettersOfWords  (    [   ] :   )    ->    [  Character ]    {
     la    tokenizer [19659014] =    NLTokenizer  (  parts .      word ).    tokenizer    string    =    text [19659022] return    tokenizer    poletter  (  to .:    ] text    START . <  text [19659013].   endIndex ).   map    {   area    in    text  [  vary ].   first [19659026]!  } 
} 

Cocoa's natural language tokenizer performs all the dirty work of finding word lines in the text and all we need to do is get the first letter in each word field. Let's ignore the body of the function and focus on its inputs and outputs:

 Figure 4: A function that returns a wrapped result

The entry value is a String as it was in it first chart, but now the result is not a simple Characters but [Character] (number of characters).

What happens if we have passed a number of discreet text String entries and we need map above all of these? [1] 19659018] [ firstLettersOfWords 19659010])

What we get is a result which is [[Character]] (a variety of characters). All this nesting starts to be cumbersome.

Monads

Sometimes we need to maintain multiple container surfaces, but in many cases, when we work with aggregated types like Array instead of multiple levels of the same abstraction we are only interested in collecting all objects in all layers.

Compilation of an Array of Array can be done by linking all internal values ​​ for for result in ] text list . map ( firstLettersOfWords ) {
allFirstLetters . add ( result )
} [19659028] Swift provides an easier way to write this on:

  la    allFirstLetters    =    textList [19659010].   flatMap  ([19659] 055] firstLettersOfWords ) 

A chart of what's happening here can look like this:

 Figure 1: a monad

This diagram of flatMap is a diagram of key monadic transformation called "bind". A bind transformation :

  1. starts with a container that can contain zero or more values ​​
  2. uses an added function to each of its embedded values, where the function returns results that are wrapped in the same type of container
  3. merge / merge / merge all containers produced by (2) in a single container of the same type.

A rough definition of a monad is any container that offers a binding transformation. 19659002] In the theory category, monads have a few requirements for construction from unopened values, structure protection, identity transformations and operator terrace bias, but these requirements are not really relevant to the use of flatMap in Swift.

Why are monads not used more often?

If you have matrix, options or other container types, it's probably because you've done some work before returning the container, and it's likely that futur e work you want to perform also will produce instances of the same container type. If we care about the end results, not the structure in between, the flating step of a monad helps us repeatedly use container retrieval features without ever needing deeper layers of wrapping around our results.

Monads let us scale write successful operations whose outputs each use an extra layer of the same abstraction.

Monader has a purpose, but their use remains unusual in Swift. It is certainly possible to incorporate them into all programming, but you can instinctively treat things in a different way – including a range of approaches that are very monadic without being monadic:

  • I've already shown that you are doing your own for loop and pairing when you go can achieve the same result as using flatMap on Array
  • Subsequent if let statements or Swift's optional chain can also unpack a Optional similar to using flatMap on Optional .
  • Swift's error handling is a monadic concept (one can cast an error. Abstraction is added to each cast function conversation), but Swift's try syntax allows us to use a "bind" out successive throwing features) without ever exposing a monad-like type
  • You can map make the layers of nesting build up and then apply a finishing reducing steps that make their own container unpacking and pairing

In Swift, since you may have side effects (unopened values ​​that escape their packing range) monads are not mandatory.

Why do Haskell programmers trade so much?

Unlike Swift, you can not avoid monads in Haskell. Why are they optional in one language, but compulsory in another?

The answer is that there are no general monads that are useful in Haskell as much as one-way monads – IO monad in particular – and how they interact with the Haskell runtime system.

A enveiskonad is a type that you can not never unpack. You can perform maps or flatMap operations (called Haskell volumes) on a one-way monad, but you can never perform a direct unpack or other form of "get" operation to view on the content.

Why do you definitely want to hide the content?

Strict functional programming languages ​​like Haskell are not allowed to mutate the state or have side effects. This complicates any interaction with the user, file system, network, operating system or other services since all of these interactions are state and have side effects.

The way Haskell addresses these issues is that you can freely interact with these services, but you never get access to the result. Instead, you get a container (an IO monad) that you can never unpack. If you never unpack a container containing side effects, you will remain free from the effects of these side effects. Your actions remain the same regardless of whether the container contains a fully analyzed data structure or an error that is not found.

If you ever send the IO monad back to the Haskell runtime, it will be retrieved correctly – so effects like reading or writing files will be properly resolved during runtime – but the Haskell code will remain isolated from this packet and remain side effect free.

In short video on Microsoft's Channel 9 entitled Towards a Programming Language Nirvana (from 2007), Simon Peyton Jones, Erik Meijer and Butler Lampson talk about how programming is fundamental about having effects, but – as the video explains – pure functional programming languages as Haskell is about not has effects, so functional programming is completely useless.

Peyton Jones is obviously facial since Has Kell has what Peyton Jones calls "controlled" effects, achieved largely through the IO monad. Each IO monad "bind" operation in Haskell can be considered as a single effect. Building a series of effects (ie, a program) requires multiple IO monadic binders, and in this way, the IO monad is literally the building block used to build Haskell programs.

flatMap versus compactMap

Let's take the discussion back to Swift and review the feature firstLetter again. Since an empty String does not necessarily have a first letter, we used a force-unwrap (operator ! ). This will fail if the word is an empty string and requires that the caller is aware of this restriction (a prerequisite).

Prerequisites can easily become a source of unexpected fatal errors since they are not enforced by the compiler. Imagine we wanted to avoid this condition and return an optional Characters :

  func    firstLetterIfAvailable  (  _    word :   String [19659010] ]    ->       [
     
     return    words .   first 
} 

Now if we treated a number of words that use this feature:

  la    optionalLetters    =    wordList .   map  (  firstLetterIfAvailable ) 

the result will be [Character?] ] [en rekke valgfrie tegn] instead of [Character] (a number of characters).

flatMap

In many ways this [Character?] (number of optional characters) resembles the previous [Character]] of characters) by having two container wraps around the underlying grade (instead of two array wraps, we now have a selection o g an optional). It's also a fairly intuitive joining rule if you want to eliminate the optional layer (link all non-null options in sequence and exclude all null options.)

But we can not replace the word map in the previous the example of flatMap to merge this structure, as we did before. At least we can not do this, anymore . Before Swift 4.1, there was an overload of flatMap that could have helped us here, but it was renamed.

Why?

  1. The mathematical definition of monad requires that the flat operation is over two nested containers of the same type
  2. people connect the word flatMap with monads (although the verb "flat" does not literally mean such a narrow interpretation)

compactMap

It all seemed a little pedantic to me, but it's not sensible to complain: The relevant overload of flatMap was not removed. It was renamed only to compactMap .

  la    letters   ]    glossary .   compactMap  (  firstLetterIfAvailable ) 

The letters here are a single [Character] instead of [Character?] (selection of optional characters). "Compression" is achieved by rejecting zero values ​​and linking only the non-zero characters.

Outside the Swift Standard Library compactMap it is used for some flatMap -like scenario where the nested container would be Optional but the outer container would be different. For example, I have used it in my own CwlSignal library as a mapping for a map-like operator that transforms values ​​that pass through the signal path to optional values, then removes the values ​​ null and gives only the non-outputs from the transformation.

There is a valid question about what you should mention similar operations where the inner container has other types. What is the name of compression of a number of Results ? Is this also a compactMap or are we expected to leave Optional in its own isolated room? I do not know.

Conclusion

Although monads are essential in languages ​​like Haskell, they are just one of many processing tools in Swift.

I do not think there is any real need for try to be monadic in Swift. There are many cases where wrong handling or Optional or Array will encourage you to link results or filter out empty results, but if you choose to do this with flatmap or guard la or for loops or Swift's error handling is a matter of personal syntactic preference – you get the same result in each case.


Source link