Search code examples
javascriptecmascript-6lambdafunctional-programming

Does JavaScript allow replacing lambda with "method references"? If not, why not?


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?

Solution

  • 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"
    ]