4

I jump between Java and Javascript a lot at work. I also write a lot of functional code, i.e. chains of lambdas.

One thing I like about Java is I can replace a lambda with a method reference. For example, I can replace this code

List<String> trimmedStrings = List.of("hi ", "bye ").stream()
  .map(original -> original.trim())
  .collect(toList());

with this:

List<String> trimmedStrings = List.of("hi ", "bye ").stream()
  .map(String::trim)
  .collect(toList());

I often make this replacement because I'm usually happier with the end state of how the code looks.

I have been wondering if I can do this with Javascript. I just tested this code in my browser console:

["hi ", "bye "].map(original => original.trim());

first I tried replacing it the simple way, which worked but doesn't accomplish my goal:

["hi ", "bye "].map(original => String.prototype.trim.apply(original))

So I figured the following would work, but it didn't:

["hi ", "bye "].map(String.prototype.trim.apply)

it gave me an error saying Uncaught TypeError: Can't call method on undefined (in Firefox).

So my questions are:

  1. Why doesn't this work?
  2. Is there another, better way to do what I want here?
2

2 Answers 2

1

String.prototype.trim is just a function. When you call "foo".trim() you call that function and set "foo" as the context. This is just how methods work in JavaScript. I might suggest a method helper to get around this -

const method = f => f.call.bind(f)

const list = [
  "   alice    ",
  "  bob  ",
  "    charlie  "
]

console.log(list.map(method("".trim)))

Another option is to define trim in advance -

const method = f => f.call.bind(f)
   
const trim = method("".trim)

const list = [
  "   alice    ",
  "  bob  ",
  "    charlie  "
]

console.log(list.map(trim))

[
  "alice",
  "bob",
  "charlie"
]

Some methods take additional arguments. method works with that too -

const method = f => f.call.bind(f)
   
const trim = method("".trim)
const replace = method("".replace)

const list = [
  "   alice    ",
  "  bob  ",
  "    charlie  "
]

console.log(list.map(trim).map(v => replace(v, "e", "E")))

[
  "alicE",
  "bob",
  "charliE"
]

You can rewrite method to enable tacit programming (aka point-free style) -

const method = f => (...args) => data =>
  f.apply(data, args)
   
const trim = method("".trim)()
const upper = method("".toUpperCase)()
const replace = method("".replace)

const list = [
  "   alice    ",
  "  bob  ",
  "    charlie  "
]

console.log(list.map(trim).map(replace(/[aeiou]/g, upper)))

[
  "AlIcE",
  "bOb",
  "chArlIE"
]

Finally we can make method smarter to analyze f.length to determine if more arguments should be supplied by the caller -

const method = f => 
  f.length == 0
    ? data => f.call(data)
    : (...args) => data => f.apply(data, args)
   
const trim = method("".trim)
const upper = method("".toUpperCase)
const replace = method("".replace)

const list = [
  "   alice    ",
  "  bob  ",
  "    charlie  "
]

console.log(list.map(trim).map(replace(/[aeiou]/g, upper)))

[
  "AlIcE",
  "bOb",
  "chArlIE"
]
Sign up to request clarification or add additional context in comments.

2 Comments

This is all covered in the duplicate except for your final curried example.
sorry pilchard, wasn't aware of the duplicate before answering
1

The issue here is that the this context needs to be set to the string. You can use Function#call to execute a function with a particular this value.

With just String.prototype.trim.call, the this context for Function#call is not set at the time is called, so you need to use bind to keep it.

let res = ["hi ", "bye "].map(Function.prototype.call.bind(String.prototype.trim));
console.log(res);
// or to make it a bit shorter:
console.log(["hi ", "bye "].map("".trim.call.bind("".trim)));

1 Comment

nice! appreciate the shorter version too!

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.