Functor (Functor)
- Why do we learn functors
- We’ve covered some of the basics of functional programming, but we haven’t shown you how to keep side effects under control, how to handle exceptions, how to do asynchronous operations, etc.
- What is a Functor
- First of all: is an object
- Container: contains values and deformation relations of values (the deformation relations are functions)
- Functor: a special container implemented by an ordinary object that has a map method that runs a function to manipulate values (deformation relationships)
Example:
// a simple functor example
// The functor is both a container and an object, so we create container objects
class Container {
// There is a value inside the functor, which is received by the constructor.
constructor(value) {
// Store the value
// This value is maintained internally by functors. Only functors know this value
// The values starting with the underscore are private values
this._value = value
}
// The functor has an external map function. The map function is a pure function that handles values
// The map method accepts a function for a value, so accepts a function (fn).
map(fn) {
return new Container(fn(this._value))
}
}
// Create functor objects
const r = new Container(5)
.map(v= > v + 1) // The map handler is set to value +1, and the map returns a new functor, so we can continue using the map handler
.map(v= > v * v)
// Look at the returned value
console.log(r) // => Container { _value: 36 }
// We can see that r is a Container object with an internal value of 36 after two maps
Copy the code
From the above example, we found that we need to call the new command every time we create a functor, which is not very functional because using the new command is the hallmark of object-oriented programming. As a general convention of functional programming, functors have a method called of to generate new containers so let’s use of of to modify the example above
Example: Use the of transformation functor for more functional programming
class Container {
// Here we use static to create a static method
static of(value) {
return new Container(value)
}
constructor(value) {
this._value = value
}
map(fn) {
// We can also use of
return Container.of(fn(this._value))
}
}
// An example
const r = Container.of(5)
.map(v= > v + 2)
.map(v= > v * v)
console.log(r) // => Container { _value: 49 }
// In this way we have implemented more functional programming functors
// We have an r functor object instead of a value, so how do we get the value?
// We never pull out the value, it is always stored in the functor object
Copy the code
Conclusion:
- The operations of functional programming do not operate directly on values, but are performed by functors
- A functor is an object that implements a map contract (method)
- We can think of a functor as a functor, a box that encapsulates a value
- To process the values in the box, we need to pass a handler (pure function) to the box’s Map method to process the values
- The final map method returns a box containing the new value (functor)
Maybe functor
The functor accepts various functions to handle the value of the inner container, so we have a problem: the inner value of the container may be a null value (undefined), and the outer function may not handle the null value, which may cause an error.
Use the Maybe functor to handle the null case
Example:
class Maybe {
static of(value) {
return new Maybe(value)
}
constructor(value) {
this._value = value
}
map(fn) {
// Use the passed function to process internal values
return this.valid() ? Maybe.of(fn(this._value)) : Maybe.of(null)}// Create an auxiliary function to determine null values
valid() {
return this._value ! =null || this._value ! =undefined}}// Test: not null
const r = Maybe.of('Hello World')
.map(x= > x.toUpperCase())
console.log(r) // => Maybe { _value: 'HELLO WORLD' }
// Test: null
const r = Maybe.of(null)
.map(x= > x.toUpperCase())
console.log(r) // => Maybe { _value: null }
Copy the code
Either functor
In the Maybe functor we learned how to handle cases where the internal value of the functor is null, at which point we can control the exception of the passed value. So what if we get an exception when we call the incoming handler fn and return a null value?
Either functor
- Either: Either of the two, similar to if… else… The processing of
- Exceptions make functions impure, and Either functors can be used to handle exceptions
Example:
// Either functor: Either functor
// Since it's one of two, let's define two functors
class Left {
static of(value) {
return new Left(value)
}
constructor(value) {
this._value = value
}
map(fn) {
// It's different here
// Return the current object directly
return this}}class Right {
static of(value) {
return new Right(value)
}
constructor(value) {
this._value = value
}
map(fn) {
return Right.of(fn(this._value))
}
}
// Create two functors to see the differences
const l = Left.of(12).map(v= > v + 2)
const r = Right.of(12).map(v= > v + 2)
console.log(l) // => Left { _value: 12 }
console.log(r) // => Right { _value: 14 }
// Analysis: The reasons for the two results are different
// Right functor map we do what normal functors do, and get the expected result
// map in the Left functor we return the current object directly without doing any processing, its internal value does not change
// What does the Left functor do?
// For pure functions, the same input should have the same output, and the functor should give the same output when an exception occurs
// So we can use the Left functor to handle exceptions
// Example: Converting a string to JSON. Exceptions may occur during conversion
function parseJSON(str) {
try {
// If there is no exception, handle it normally
return Right.of(JSON.parse(str))
} catch(e) {
// When an exception occurs, we use the Left functor to save the exception
return Left.of({ error: e.message })
}
}
/ / use
// There is an exception
const errorP = parseJSON('{ name: rh }')
console.log(errorP) // => Left { _value: { error: 'Unexpected token n in JSON at position 2' } }
const p = parseJSON('{ "name": "rh" }')
console.log(p) // => Right { _value: { name: 'rh' } }
// We can see from the output that when an exception occurs we can use the Left functor to handle and store the exception
// The Right functor executes normally when there are no exceptions
Copy the code
IO functor
- The _value in the IO functor is a function, and we’re treating the function as a value
- IO functors can store impure actions in _value, delay the impure operation (lazy execution), and wrap the current operation
- Leave impure operations to the caller
Example: IO functor
const fp = require('lodash/fp')
class IO {
// The of function still passes in a value
static of(value) {
// we use the constructor of the IO functor
return new IO(function() {
// At this point we return the value passed in by the function
return value
})
}
// The constructor is passing in a function
constructor(fn) {
this._value = fn
}
map(fn) {
// returns IO, but we are using the constructor of IO
// We use flowRight in the FP module to combine the value(function) stored in the IO functor with the FN passed in by map
return new IO(fp.flowRight(fn, this._value))
}
}
/ / use
// Currently we are using the Node environment, and we pass in objects from Node
// When the of function of IO is called, the of function stores the value we passed in to a function to retrieve process at use
// Then use map to get the attributes
const io = IO.of(process).map(v= > v.execPath)
console.log(io) // => IO { _value: [Function] }
// From log we can see that we have an IO functor. The functor holds a function
// who is function in value? So let's look at the synthesis
// The 1. Of method returns an IO object whose value stores a function that returns the currently passed process
// 2. The map method returns a new IO functor in which value is stored as a combined function
// the map method combines fn and this._value,fn is the function we passed in v => v.execpath,this._value is the function we used to get the IO object created (that returns value).
Function = function = function = function = function = function = function
// Get the function in the IO object
const ioFn = io._value
console.log(ioFn()) / / = > / usr/local/Cellar/node / 12.6.0 / bin/node (the node execution path)
Copy the code
Summary: We may pass an impure operation to map, but after processing, we ensure that IO is a pure operation. We delay the impure operation until _value is called, so that the side effects are under control.
folktale
Functors can help us control side effects (IO functors), do exception handling, and do asynchronous operations where callbacks to the gates of hell are encountered, instead using Task functors to avoid nested callbacks.
- Asynchronous Task implementation is too complex and can be demonstrated using Tasks in Folktale
- Folktale is a standard functional programming library
- Unlike Lodash and Ramda, it doesn’t offer a lot of functionality
- It provides only a few functions for handling operations, such as compose, Curry, etc. Then there are some functors, such as Task, Either, Maybe, etc
- NPM install Folktale –save
Example:
const { compose, curry } = require('folktale/core/lambda')
const { toUpper, first } = require('lodash/fp')
Folktale provides a different curry function than Lodash
// curry(arity, fn) arity: indicates that the fn function has several parameters. Fn is the parameter to pass
const f = curry(2, (x, y) => {
return x + y
})
/ / use curry
console.log(f(1.2)) / / = > 3
console.log(f(1) (2)) / / = > 3
const ffp = compose(toUpper, first)
/ / use compose
console.log(ffp(['one'.'two'])) // => ONE
Copy the code
Task functor
- Folktale tasks in 2.x are quite different from those in 1.0. Here we use the latest version demo (2.3.2).
- You can view the documentation for other versions
Example: Use the Task functor to read the version field in the current package.json
// Node FS module reads the file
const fs = require('fs')
const { task } = require('folktale/concurrency/task')
const { split, find } = require('lodash/fp')
function readFile(fileName) {
// Return task functor
return task(resolver= > {
// fs3 parameters: 1. file path 2. encoding format 3. callback function
fs.readFile(fileName, 'utf-8', (err, data) => {
// Check to see if there is an error
if (err) {
resolver.reject(data)
}
resolver.resolve(data)
})
})
}
/ / use
// Step 1:
// Get the task functor from readFile
The // task functor provides the run method to execute, which reads package.json
// In the Task functor we can get the entire JSON data, which can be retrieved directly in onResolved
// This will be an onResolved process and we know that functors have a map method
// At this point we can call the map method of the Task functor to process the retrieved JSON
// Processing: 1. Split each line in the JSON file with a newline character
2. Then look for the data with the version field
// In onResolved we will get the data after the map process
readFile('package.json')
.map(split('\n'))
.map(find(v= > v.includes('version')))
.run() // Task subinterface
.listen({ // The task functor provides a listening method to get data
onReject: err= > {
console.log(err)
},
onResolved: value= > {
console.log(value) / / = > "version" : "1.0.0",}})Copy the code
Pointed functor
The 50% functor is the first one we’ve mentioned, but we’ve been using it for functors before
- The Conservative functor is a functor that implements the of static method
- The of method is used to avoid using new to create objects. The deeper meaning is that the of method is used to put values into Context (putting values into containers (functors) and using map), as shown in the following figure:
Example: the 50% functor
class Container {
// A functor with the of method is a conservative functor
static of(value) {
return new Container(value)
}
... // The rest is omitted
}
Copy the code
Monad functor
Before we look at Monad functors let’s look at an example of an IO functor that we wrote earlier
Example: IO functor, using IO functor to read file and output
const fp = require('lodash/fp')
const fs = require('fs')
class IO {
static of(value) {
return new IO(function() {
return value
})
}
constructor(fn) {
this._value = fn
}
map(fn) {
return new IO(fp.flowRight(fn, this._value))
}
}
// Use IO functors to read files (package.json)
const readFile = function(fileName) {
return new IO(function() {
// We use synchronous read here
return fs.readFileSync(fileName, 'utf-8')})}const printV = function(v) {
return new IO(function() {
console.log(v)
return v
})
}
/ / use
const cat = fp.flowRight(printV, readFile)
const r = cat('package.json')
console.log(r) // => IO { _value: [Function] }
IO(IO(v)) _value = [Function
/ / resolution:
// 1. FlowRight combines readFile and printV
// readFile returns an IO functor object
// 3. V in printV is the IO functor returned by readFile
// 4. So printV is the IO functor returned by readFile
// 5. So cat gets IO(IO(v)) after readFile and printV
// How do we get the final value we want?
console.log(r._value()._value()) // => Output package.json
// We can get what we want by calling _value() continuously, which is inconvenient
// How to solve it? Monad functor!
Copy the code
Monad
- The Monad functor can be flattened (to solve function nesting)
- A functor is a Monad if it has both join and of methods and obeies some laws
Example: Modify the IO functor above
const fp = require('lodash/fp')
const fs = require('fs')
class IO {
static of(value) {
return new IO(function() {
return value
})
}
constructor(fn) {
this._value = fn
}
// We need to add the join method to the normal IO functor
join() {
// Return the current functor's value (_value).
return this._value()
}
map(fn) {
return new IO(fp.flowRight(fn, this._value))
}
// When using Monad, we need to combine join and map
// Add a flatMap function
// The flatMap function flattens functors by calling join and map functions
flatMap(fn) { // fn is used by map
return this.map(fn).join()
}
}
// Use IO functors to read files (package.json)
const readFile = function(fileName) {
return new IO(function() {
// We use synchronous read here
return fs.readFileSync(fileName, 'utf-8')})}const printV = function(v) {
return new IO(function() {
console.log(v)
return v
})
}
/ / use
/ / analysis:
The function of flatMap here is to call the map method inside the functor and then call the JOIN method
// What do I get?
// The IO functor is a nested functor, nested inside the functor returned by readFile
// The join in flatMap returns the value saved by the current functor (printV man) (readFile functor)
// Call the join method in the readFile functor, which returns the result of executing the value stored in the readFile functor (_value: the function that reads the file)
const r = readFile('package.json')
.flatMap(printV)
.join()
console.log(r) // => package.json contents
// If you need to convert the contents of the file to uppercase after reading the file
const upperR = readFile('package.json')
.map(fp.toUpper)
.flatMap(printV)
.join()
console.log(upperR) // => Package. json content in uppercase
Copy the code
When to use a map and when to use a flatMap?
- Use map when only one value needs to be returned
- Use flatMap when you need to flatten nested functors