Arrow functions
Problem
A whole function for one comparison
The orders need to be sorted by total.
class Order { constructor(customer, total) { this.customer = customer; this.total = total; }}const orders = [ new Order("Maria", 75.5), new Order("Georgi", 12.9), new Order("Elena", 40),];
function byTotal(a, b) { return a.total - b.total;}
orders.sort(byTotal);
for (const o of orders) { console.log(`${o.customer}: €${o.total.toFixed(2)}`);}Georgi: €12.90Elena: €40.00Maria: €75.50sort takes a compare function. It gets two orders and returns a negative number if the first should come before the second, a positive number if it comes after it, and 0 if they are equal. a.total - b.total does exactly that.
It works. But for one comparison you write a whole function with a name, far from the place where you use it. And it is called only here. You want to pass just the rule.
The idea
An arrow function: just the rule
The same sort, on one line:
orders.sort((a, b) => a.total - b.total);(a, b) => a.total - b.total is called an arrow function. In other languages the same thing is called a lambda. To the left of => are the parameters, to the right is the body. This is the function byTotal with no name, no function and no return.
The ways to write it:
- One parameter can go without parentheses:
o => o.total > 50. Usually you write the parentheses:(o) => .... With no parameters it is() => .... - A body with several lines goes in
{ }, and thenreturnis required.
const byTotal = (a, b) => a.total - b.total;const byTotalLong = (a, b) => { const diff = a.total - b.total; return diff < 0 ? -1 : diff > 0 ? 1 : 0;};const guest = () => new Order("Guest", 0);
orders.sort(byTotalLong);for (const o of orders) { console.log(`${o.customer}: €${o.total.toFixed(2)}`);}console.log(guest().customer);Georgi: €12.90Elena: €40.00Maria: €75.50GuestAn arrow function is a value: you can put it in a variable and pass it later.
More precisely, if you are curious
If the body is an object, it goes in parentheses. Without them, { is read as the start of a body, and the function returns undefined:
const guest = () => ({ customer: "Guest" });const broken = () => { customer: "Guest" };console.log(guest().customer);console.log(broken());GuestundefinedThe idea
A function is a value
How does sort know that (a, b) => ... is the compare function? It does not need to know anything more. In JavaScript a function is a value, like a number or a string: you can put it in a variable, pass it to another function and call it with parentheses.
const student = (price) => price * 0.8;const minusFive = (price) => price - 5;
console.log(student(50));console.log(minusFive(50));4045student is an object of type function: typeof student is "function". You call it like any function: student(50). Two price rules, and not a single class.
Here Java needs a functional interface: an interface with one method that says what kind of rule it is. JavaScript does not write types and has no interfaces, so it does not need anything like that. Any function can be passed and called.
const student = (price) => price * 0.8;const minusFive = (price) => price - 5;
console.log(student(50));console.log(minusFive(50));Step by step, in words
- The arrow function is an object of type function, and student points to it. Its body does not run yet.
- A second rule, a second object. Not a single class is written.
- student(50) passes 50 to the function: price is 50, and price * 0.8 gives 40.
- minusFive(50): the same number, a different rule, 45.
More precisely, if you are curious
An arrow function takes this from the place where it is written, like a lambda in Java. A function written with function has its own this, and when forEach calls it, that is undefined:
class Cart { constructor(owner) { this.owner = owner; this.items = ["coffee", "honey"]; }
print() { this.items.forEach((item) => console.log(`${this.owner}: ${item}`)); }
printOld() { this.items.forEach(function (item) { console.log(`${this.owner}: ${item}`); }); }}
const cart = new Cart("Maria");cart.print();cart.printOld();Maria: coffeeMaria: honeyTypeError: Cannot read properties of undefined (reading 'owner')So write a rule inside a class method as an arrow function.
The idea
The four kinds of rules
The type of every rule is function, but there are four kinds of rules. It helps to recognize them, because the functions that take rules want a specific kind:
- condition: takes a value, returns
trueorfalse. "Is the order big?" - transformation: takes one thing, returns another. An order into a label.
- action: takes a value and does something with it: prints it, saves it. Its result is not used.
- supplier: takes nothing, returns a new object when you ask.
const big = (o) => o.total >= 50;const label = (o) => o.customer.toUpperCase();const show = (text) => console.log("-> " + text);const guest = () => new Order("Guest", 0);
const order = new Order("Maria", 75.5);console.log(big(order));show(label(order));console.log(guest().customer);true-> MARIAGuestIn Java, rules are objects with methods that combine them: conditions are joined with and, or and negate ("not"), and functions are chained one after another with andThen. JavaScript has no such methods. The new rule is a new function that calls the old ones: (o) => big(o) && maria(o). Two functions one after another are also combined with a function:
const withVat = (price) => price * 1.2;const rounded = (price) => Math.round(price * 100) / 100;const andThen = (f, g) => (x) => g(f(x));
const finalPrice = andThen(withVat, rounded);console.log(finalPrice(10.555));12.67andThen takes two functions and returns a third one: it calls the first, and passes its result to the second.
More precisely, if you are curious
An action still returns something: undefined, because that is the result of console.log. console.log(show("coffee")) prints "-> coffee" and then undefined.
Try it
Combined conditions
What will the program print?
const big = (o) => o.total >= 50;const maria = (o) => o.customer === "Maria";const bigAndMaria = (o) => big(o) && maria(o);const bigOrMaria = (o) => big(o) || maria(o);const notBig = (o) => !big(o);
const small = new Order("Maria", 30);const large = new Order("Georgi", 80);
console.log(bigAndMaria(small));console.log(bigOrMaria(small));console.log(notBig(large));bigAndMaria gives false, and bigOrMaria gives true, because it is Maria's. Georgi's order is €80, it is big, and ! flips the answer: false.See it
Arrays take functions
let cart = ["bread", "", "milk", " ", "coffee"];cart = cart.filter((item) => item.trim() !== "");cart.forEach((item) => console.log(item));
const customers = [ { name: "Maria", city: "Plovdiv" }, { name: "Georgi", city: "Sofia" }, { name: "Elena", city: "Plovdiv" },];const byCity = Map.groupBy(customers, (c) => c.city);byCity.forEach((list, city) => console.log(`${city}: ${list.map((c) => c.name).join(", ")}`));breadmilkcoffeePlovdiv: Maria, ElenaSofia: Georgifilter(condition)makes a new array with only the elements for which the condition is true. The old one does not change: herecartisletand just points to the new one. There is no method that removes elements from the array itself by a condition.forEach(action)does the action with each element.Map.groupBy(array, function)groups: the function says the key of each element, and the result is aMapwith an array of the elements for each key.- The
forEachof aMaptakes a function with two parameters, but in reverse order: the value first, then the key.
{ name: "Maria", city: "Plovdiv" } is an object without a class: just fields. A Map keeps the keys in the order they were added, which is why Plovdiv comes before Sofia.
The idea
A function by name, without parentheses
(s) => Number(s). Then you can pass the function itself, by name and without parentheses:const names = ["maria", "georgi"];names.forEach((name) => console.log(name));
const upper = (s) => s.toUpperCase();const parse = Number;console.log(upper("elena"));console.log(parse("42") + 1);
console.log(["1", "2", "3"].map(Number));console.log(["1", "2", "3"].map(parseInt));mariageorgiELENA43[ 1, 2, 3 ][ 1, NaN, NaN ]Java has a separate notation for this, the method reference with ::. In JavaScript the name is enough, with one catch: the function gets everything the caller passes to it. map and forEach pass the element, its index and the whole array. Number looks only at the first one and works. parseInt takes a second parameter, the radix (the base of the number system), and gets the element's index: parseInt("2", 1) is NaN. The same thing happens with console.log, which prints everything it gets:
["maria", "georgi"].forEach(console.log);maria 0 [ 'maria', 'georgi' ]georgi 1 [ 'maria', 'georgi' ]A method of the parameter, like toUpperCase, has no short form. String.prototype.toUpperCase cannot be passed directly: the program stops with a TypeError. You write (s) => s.toUpperCase().
So pass by name only functions whose parameters you know. Otherwise write an arrow function.
See it
Sorting by several fields
The compare function can check several fields one after another. If the first comparison gives 0, || moves on to the next one: 0 is "falsy" in JavaScript.
The students are sorted by grade, from the highest, and by name when the grades are equal:
const students = [ new Student("Ivan", 5.5), new Student("Ana", 6), new Student("Peter", 5.5), new Student("Boris", 4.75),];students.sort((a, b) => b.grade - a.grade || a.name.localeCompare(b.name));Ana 6Ivan 5.5Peter 5.5Boris 4.75b.grade - a.grade puts the higher grade first. When the grades are equal, a.name.localeCompare(b.name) compares the names alphabetically. You read it from left to right: "by grade in reverse, and by name when they are equal".
More precisely, if you are curious
- Without a function,
sortturns the elements into strings and compares the strings. For numbers this gives the wrong order:
console.log([10, 9, 1].sort());console.log([10, 9, 1].sort((a, b) => a - b));
const prices = [40, 12, 75];const sorted = prices.toSorted((a, b) => a - b);console.log(sorted, prices);[ 1, 10, 9 ][ 1, 9, 10 ][ 12, 40, 75 ] [ 40, 12, 75 ]sortsorts the array itself.toSortedreturns a new sorted array, and the old one stays as it is.
Try it
What does reversed reverse?
reversed takes a compare function and returns a new one that swaps a and b.What will the program print?
const students = [ new Student("Ivan", 5.5), new Student("Ana", 6), new Student("Peter", 5.5), new Student("Boris", 4.75),];
const byGradeThenName = (a, b) => a.grade - b.grade || a.name.localeCompare(b.name);const reversed = (compare) => (a, b) => compare(b, a);
students.sort(reversed(byGradeThenName));students.forEach((s) => console.log(`${s.name} ${s.grade}`));reversed reverses the whole function it gets, and that function is "by grade, then by name". So both are reversed: the grades go from the highest, and Peter comes before Ivan. If you want only the grades in reverse, you swap a and b only in the comparison of the grades, as in the card before this one.Try it
Find the bug
Pick the line with the bug.
total is const, and the arrow function tries to change it:
TypeError: Assignment to constant variable.The error comes up on line 3, but the cause is on line 2: the value of a const does not change. With let total = 0; the program prints 127.
Here JavaScript is different from Java. There, the lambda gets a copy of the value and cannot change the local variable at all. In JavaScript the arrow function sees the variable itself: it can read it and, if it is let, change it. For a sum, though, there is a clearer way, reduce, in the next lesson.
More precisely, if you are curious
An arrow function sees the variable itself, not a copy. If the variable changes after the arrow function, the next call sees the new value:
let rate = 0.2;const discount = (price) => price * (1 - rate);console.log(discount(100));
rate = 0.5;console.log(discount(100));8050In Java this is forbidden precisely because of surprises like this. If the rule needs to remember a value, pass it as a parameter.
Exercise
Rules as values
1. Sorting by several fields. A class Product with a name, a category and a price. Sort the products by category, within a category from the most expensive, and by name when the price is equal, with one sort and a compare function with ||. The products are: mouse, accessories, €25; laptop, computers, €1299; keyboard, accessories, €49.90; tablet, computers, €399; headphones, accessories, €49.90.
Accessories | Headphones | €49.90Accessories | Keyboard | €49.90Accessories | Mouse | €25.00Computers | Laptop | €1299.00Computers | Tablet | €399.002. Filtering orders. Add a city to Order. Write a function select(orders, rule) that returns a new list with only the orders for which the rule is true. Make two rules, sofia and big (at least €50), and print the orders for which both are true, the ones outside Sofia and the ones "in Sofia or at least €100". Each of the three rules is an arrow function that calls sofia and big. The orders: Maria, Sofia, €75.50; Georgi, Varna, €12.90; Elena, Sofia, €40; Dimitar, Plovdiv, €120.
Sofia, over €50: Maria, Sofia: €75.50Outside Sofia: Georgi, Varna: €12.90 Dimitar, Plovdiv: €120.00Free delivery (Sofia or over €100): Maria, Sofia: €75.50 Elena, Sofia: €40.00 Dimitar, Plovdiv: €120.00andThen(f, g), which returns a function: f first, then g. Use it to chain the discounts in two orders: the student discount first and the Friday discount first. Try it with €120 and with €15. Does the order matter?€120.00 -> student, then Friday: €88.00; Friday, then student: €90.00€15.00 -> student, then Friday: €0.00; Friday, then student: €0.00More tasks
- Cleaning a list. From an array of emails, remove the empty ones and the ones without
@with onefilter, then print the rest withforEach. - Students by class. Group students by class (
"10A","10B") withMap.groupByand print each class with the names in it. - Validators. Make an object with three validators as arrow functions: non-empty text, digits only (
/^\d+$/.test(value)), length up to 10. Try each one on a few strings.
Test