JavaScript:

Higher-order Functions

Leturer: Иo1lz

OUTLINE

  • Preface
  • Abstraction
  • Higher-order Functions
  • Summarizing with Reduce

Preface

let total = 0, count = 1;
while(count <= 10){
  total += count;
  count += 1;
}
console.log(total);
console.log(sum(range(1, 10)));

Abstraction

  • hide details
  • give us the ability to talk about problems at a higher (or more abstract) level
console.log(sum(range(1, 10)));

the functions sumrange

As an analogy, compare two reipes for pea soup.

Put 1  cup of dried peas per person into a container. Add water until the peas are well covered. Leave the peas in water for at least 12 hours. Take the peas out of the water and put them in a cooking pan. Add 4 cups of water per person. Cover the pan and keep the peas simmering for two hours. Take half an onion per person. Cut it into pieces with a knife. Add it to the peas. Take a stalk of celery per person. Cut it into pieces with a knife. Add it to the peas. Take a carrot per person. Cut it into pieces. With a knife! Add it to the peas. Cook for 10 more minutes.
Per person: 1 cup dried split peas, half a chopped onion, stalk of celery, and a carrot.

Soak peas for 12 hours. Simmer for 2 hours in 4 cups of water (per person). Chop and add vegetables. Cook for 10 more minutes.

The second is shorter and easier to interpret.

But ... you do need to understand a few more cooking related words.

In programming, to notice when you are working at too low a level of abstraction.

Abstracting Repetition

for(let i = 0;i < 10;i++){
  console.log(i);
}

Can we abstract "doing something N times" as a function?

function repeatLog(n){
  for(let i = 0;i < n;i++){
    console.log(i);
  }
}

But what if we want to do something other than logging the number?

function repeat(n, action){
  for(let i = 0;i < n;i++){
    action(i);
  }
}

repeat(3, console.log);
// -> 0
// -> 1
// -> 2

We don't have to pass a predefined function to repeat

let labels = [];

// define repeat() here

repeat(5, i => {
    labels.push(`Unit ${i + 1}`);
});

console.log(labels);
// -> ["Unit 1", "Unit 2", "Unit 3", "Unit 4", "Unit 5"]

Higher-order Functions

Function that operate on other functions, either by taking them or by return them.

function greaterThan(n){
  return m => m > n;
}

let greaterThan10 = greaterThan(10);

console.log(greaterThan10(11));
// -> true

1. functions that create new functions

function noisy(f){
  return (...args) => {
    console.log("calling with", args);
    let result = f(...args);
    console.log("called with", args, ", returned", result);
    return result;
  };
}

noisy(Math.min)(3, 2, 1);
// -> calling with [3, 2, 1]
// -> called with [3, 2, 1], returned 1

2. functions that change other functions

function unless(test, then){
  if(!test) then();
}

repeat(3, n => {
  unless(n % 2 == 1, () => {
    console.log(n, "is even");
  });
});
// -> 0 is even
// -> 2 is even

3. functions that provide new types of control flow

There is a built-in array method => forEach

["A", "B"].forEach(l => console.log(l));
// -> A
// -> B

Script Data Set

One area where higher-order functions shine is data process

To process data, we'll need some actual data.

The example data set contains some pieces of information about the 140 scripts defined in Unicode

{
    name: "Coptic",
    ranges: [[994, 1008], [11392, 11508], [11513, 11520]],
    direction: "ltr",
    year: -200, 
    living: false,
    link: "https://en.wikipedia.org/wiki/Coptic_alphabet"
}

Download the file scripts.js

require('./<path>/scripts.js');

Filtering Arrays

function filter(array, test){
  let passed = [];
  
  for(let element of array){
    if(test(element)){
      passed.push(element);
    }
  }
  
  return passed;
}

console.log(filter(SCRIPTS, script => script.living));
// -> [{name: "Adlam", ...}, ...]
console.log(SCRIPTS.filter(s => s.direction == "ttb"));
// -> [name: "Mongolian", ...}, ...]

Like forEach, filter is a standard array method

Transforming with Map

function map(array, transform){
  let mapped = [];
  
  for(let element of array){
    mapped.push(transform(element));
  }
  
  return mapped;
}

let rtlScripts = SCRIPTS.filter(s => s.direction == "rtl");

console.log(map(rtlScripts, s => s.name));
// -> ["Adlam", "arabic", "Imperial Aramaic", ...]

Like forEach and filter, map is a standard array method.

Summarizing with Reduce

Another common thing to do with arrays is to compute a single value from them.

The higher-order operation that represents this pattern is called reduce

function reduce(array, combine, start){
  let current = start;
  
  for(let element of array){
    current = combine(current, element);
  }
  
  return current;
}

console.log(reduce([1, 2, 3, 4], (a, b) => a + b, 0));
// -> 10

The standard array method reduce has an added convenience

console.log([1, 2, 3, 4].reduce((a, b) => a + b));
// -> 10

Use reduce to find the script with the most characters

function characterCount(script){
  return script.ranges.reduce((count, [from, to]) => {
    return count + (to - from);
  }, 0);
}

console.log(SCRIPTS.reduce((a, b) => {
  return characterCount(a) < characterCount(b) ? b : a;
}));
// -> {name: "Han", ...}

Composability

Consider how we would have written the previous example without higher-order functions

let biggest = null;

for(let script of SCRIPTS){
  if(biggest == null || characterCount(biggest) < characterCount(script)){
    biggest = script;
  }
}

console.log(biggest);
// -> {name: "Han", ...}

Higher-order functions start to shine when you need to  compose  operations

function average(array){
  return array.reduce((a, b) => a + b) / array.length;
}

console.log(Math.round(average(
	SCRIPTS.filter(s => s.living).map(s => s.year))));
// -> 1165

console.log(Math.round(average(
  	SCRIPTS.filter(s => !s.living).map(s => s.year))));
// -> 204

You could definitely also write this computation as one big loop

let total = 0, count = 0;

for(let script of SCRIPTS){
  if(script.living){
    total += script.year;
    count += 1;
  }
}

console.log(Math.round(total / count));
// -> 1165

Recognizing Text

We have a characterScript function and a way to correctly loop over characters.

function countBy(items, groupName){
  let counts = [];
  
  for(let item of items){
    let name = groupName(item);
    let known = counts.findIndex(c => c.name == name);
    
    if(known == -1){
      counts.push({name, count: 1});
    }else{
      counts[known].count++;
    }
  }
  
  return counts;
}

console.log(countBy([1, 2, 3, 4, 5], n => n > 2));
// -> [{name: false, count: 2}, ...]
function textScripts(text){
    let script = countBy(text, char => {
        let script = characterScript(char.codePointAt(0));
        
        return script ? script.name : "none";
    }).filter(({name}) => name != "none");
    let total = scripts.reduce((n, {count}) => n + count, 0);
    
    if (total == 0) return "No scripts found";

    return srcipts.map(({name, count}) => {
        return `${Math.round(count * 100 / total)} % ${name}`;
    }).join(", ");
}

console.log(textScripts('英國的狗說"woof", 俄羅斯的狗說"тяв"'))
// -> 61% Han, 22% Latin, 17% Cyrillic

Thanks for listening.

JavaScript: Higher-order Functions

By Иo1lz

JavaScript: Higher-order Functions

This is the slide that refer from "Eloquent JavaScript" chapter 5.

  • 146