Lifting
Now, let's review map
from another perspective. map :: (T -> R) -> [T] -> [R]
accepts 2 parameters, a function f :: T -> R
and a list list :: [T]
. [T]
is a generic type paramterized by T
, it's not the same as T
, but definitely shares some properties of T
. So, an interesting interpretation of map(f) :: [T] -> [R]
is that map
turns a function of type T -> R
into a function of [T] -> [R]
, this is called lifting.
Take the square function x -> x * x
as an example, map(x -> x * x)
turns the function on Int
into a function on [Int]
. Therefore, it makes sense to name map(x -> x * x)
as squareForList
. You can even simply name it as square
, which is the overloaded version of square
for [Int]
.
The concept of lifting is the key to understand the advanced abstractions in functional programming. Lifting allows you to reuse a function of type T -> R
(or T1 -> T2 -> R
...) in the context of List, Maybe, Lazy, Promise, etc. That saves you the work to implement similar functions from scratch just for the context.
Let me explain why lifting matters by changing the string conversion problem in the previous chapter a bit. In the original problem, we got a function convert :: String -> String
, what if the input string is not directly available, but asynchronously fetched from a web service? Do you want to chain the convert
to the callback for the asynchronous HTTP response? You can use callback, but that makes you lose functional composition.
Just like map
lifts a function on T
into a function on [T]
, we just wanted to lift it to Promise
. Here Promise
stands for an asynchronously available value of type T
. So, we'll introduce a function fmap :: (T -> R) -> Promise
, meaning fmap
turns a function of type T -> R
into a function of type Promise
. See the following example:
// Java 6
F1 convert = _(split(" "), reverse, map(toUpperCase), join("_"));
// fmap turns a function of type "T -> R" into a function of type "Promise -> Promise"
F1, Promise> convertForPromise = fmap(convert);
// await() blocks until the async result available
String result = convertForPromise.apply(promise(URL)).await();
More details here.
promise(URL) :: Promise
stands for a string value which will be available in the future. Calling await
on the promise object will block until the string is available. fmap
turns convert :: String -> String
into convertForPromise :: Promise
which can work on a promise. By the way, if you like we can omit the convert
function by inlining it as:
fmap(_(split(" "), reverse, map(toUpperCase), join("_")))
Functor
As I mentioned in the previous section, Promise, Maybe, List, Lazy, and so on are all contexts. The idea behind is the functional abstraction named Functor. In Java, a functor can be defined as follows:
interface class Functor {
Functor fmap(F1 f);
}
then, Promise
will implement the fmap
:
class Promise implements Functor {
Promise fmap(F1 f) {
...
}
}
But as I have said before, we are not in favor of the OO-style API design. A better way to define functor in Java is as follows:
public class Promises {
public static F1, Promise> fmap(F1 f) {
return Promises.fmap().apply(f);
}
}
It essentially means if we can define a function fmap
to lift a function of type T -> R
into a function of type Functor
, then Functor
is a functor. In addition, there're 2 properties named Functor Laws as the semantics constraints to ensure the type makes sense:
fmap id = id
fmap (p . q) = (fmap p) . (fmap q)
Don't be scared, it's actually very simple. Just like we put the FILO constraint on the push
and pop
of the Stack
type to make sure it behaves as what we want.
If you feel too abstract, take a look at the example of List
or Promise
. More often than not, your functor class satisfies the laws automatically. However, keep in mind that you may always want to test the functor laws for your functor class, just like you want to test FILO for a Stack
implementation. See unit tests of Promise
for the functor laws here.
Monad
Lifting a function of type T -> R
into a function of type Functor
allows us to reuse the existing functions in a different context, but sometimes the basic function we have is not as plain as toUpperCase :: String -> String
. Let's look at the following problem:
Given 1) a functionPromise
which accepts an URL and returns a promise of the web page; 2)asyncGet(String url) n
hyperlinked web pages, the contents of one page is the URL of the next page, url1 -> page1 (url2) -> page2 (url3) -> page3 (url4) ... page_n (url1), please write a functionPromise
which starts from theasyncGetK(String url, int k) url
, goes forward byk
steps, returns the page.
If what we have is a sync function String get(String url)
, that would be a simple loop like:
// Java 6
String getK(String url, int k) {
String page = url;
for (int i = 0; i < k; i++) {
page = get(page);
}
return page;
}
The point here is that the result of the previous get
can be directly passed to the next get
, because the type matches. In other words, we can compose multiple get
functions together.
But since we only have asyncGet
of type String -> Promise
, the result type Promise
of a previous asyncGet
doesn't match the parameter type url :: String
of the next asyncGet
, we are unable to compose them together directly. So, we'd really like to lift asyncGet :: String -> Promise
into asyncGetPromise :: Promise
then it's composable.
The idea is great, but what would happen if we apply fmap
to asyncGet
. Since the type of fmap
is (T -> R) -> Promise
, then the type of fmap(asyncGet)
would be Promise
. Ooops, that's too much! But if we have a join :: Promise
to flatten a nested promise, then we will get _(fmap(asyncGet), join) :: Promise
. Combining fmap
and join
together, we get a function flatMap :: (T -> Promise
, which is exactly what we want.
Being able to define a function fmap
makes a type a Functor, likewise being able to define a function flatMap
makes a type a Monad. Then the code would be like:
// Java 6
String getK(String url, int k) {
F1, Promise> asyncGetPromise = flatMap(asyncGet);
Promise page = unit(url);
for (int i = 0; i < k; i++) {
page = asyncGetPromise(page);
}
return page.await();
}
It really shares the same structure as the sync code. That is isomorphic!