groovy
  1. groovy
  2. GROOVY-4253

findResult object/collection/map enhancement patch

    Details

    • Type: Improvement Improvement
    • Status: Closed Closed
    • Priority: Major Major
    • Resolution: Fixed
    • Affects Version/s: None
    • Fix Version/s: 1.7.5, 1.8-beta-2
    • Component/s: None
    • Labels:
      None
    • Testcase included:
      yes
    • Patch Submitted:
      Yes
    • Number of attachments :
      1

      Description

      Groovy find and findAll take the result of the passed in closure and use groovy truth to either find the first element in the collection/map or filter the collection/map.

      There are many situations situations where what you actually want to find or collect is the result of the closure, rather than test original elements and then reprocessing them all again.

      The attached patch (with javadocs & unit tests) adds 2 methods to groovy objects, collections, and maps; enhanced versions of the existing groovy find and findAll methods:

      <object/collection/map>.findResult(closure)
      <object/collection/map>.findAllResults(closure)
      

      findResult and findAllresults are valuable for shortening/clarifying code in many situations, but they're the most valuable when the closure calls a relatively expensive method (or one that isn't idempotent). When calling the method again with the results of a find/findAll is impractical.

      I had a discussion today on twitter with Robert Fletcher (http://twitter.com/rfletcherEW/status/16410798707) that spurred this patch, though there have been many times in the past that I've wanted this functionality and ended up writing the 4+ lines of boilerplate code to work around it being missing.

      Details:

      The object/collection version of findAll iterates through the object and passes each value to the closure. The first closure result that is non-null (including false) will short circuit evaluation and return the value, example:

      // works on lists/collections
      assert "RAB" == ["foo", "bar", "baz"].findResult {
          def uCaseReversed = it.reverse().toUpperCase()  // proxy for expensive method that we don't want to do more often than necessary
          if (uCaseReversed ==~ /..B/) return uCaseReversed
      }
      
      // and objects with iterators like ranges
      assert "Found 4" == (1..10).findResult {
          if (it > 3) return "Found $it"
      }
      

      The map version of findResult is slightly different as what it returns is a Map.Entry with the key equal to the key of the map entry that generated a non-null value from the closure:

      // closure has 0/1 parameters, it passes the current Map.Entry in as it iterates
      [a: 1, b: 2, c: 3, d: 4].findResult { 
          if (it.value > 2) return "I found ${it.key}:${it.value}"
      }.with {
          assert "c" == it.key
          assert "I found c:3" == it.value
      }
      
      // closure has 2 parameters, it passes the key and value in as it iterates
      [a: 1, b: 2, c: 3, d: 4].findResult { key, value ->
          if (value > 2) return "I found ${key}:${value}"
      }.with {
          assert "c" == it.key
          assert "I found c:3" == it.value
      }
      

      findAllResults will spin through the whole iterator, but will collect only those results from the closure that are non-null, so it can both collect and filter in one step:

      assert ["Found 4", "Found 5"] == (1..5).findAllResults {
          if (it > 3) return "Found $it"
      }
      

      The map version returns a map that contains the keys that had a non-null result with the result of the closure:

      def origMap = [a:1, b:3, c:5]
      origMap.findAllResults { if (it.value >= 3) return "Found ${it.key}:${it.value}" }.with { mappedResults ->
          assert 2 == mappedResults.size()
          assert "Found b:3" == mappedResults.b
          assert "Found c:5" == mappedResults.c
      }
      

      Other groovy iterators (like collect and inject) are not able to easily fulfill this role. Collect will not do any filtering, but instead returns a full list with nulls that need to be removed. Inject can sort of be made to fill this role, but it's often more work than it's worth as you need to manage the list creation/appending and the additional closure parameter. There isn't an equivalent of collect/inject on Maps.

      I'd love it if this could be added to 1.7.4, but 1.8 would work too .

        Issue Links

          Activity

          Hide
          Guillaume Laforge added a comment -

          Thanks for this implementation!

          Show
          Guillaume Laforge added a comment - Thanks for this implementation!
          Hide
          Andre added a comment - - edited

          actually can we change the name of the issue (remove findAllResults), because only findResult has been added, itīs misleading and appears in the release notes that way
          my 2ct: inject is more powerful and can do everything that findAllResults can do and more, but findAllResults is so much more intuitive - imho it should be added, doesnt do harm and you would always think: now there is find, findAll, findResult ... so where is findAllResults?

          Show
          Andre added a comment - - edited actually can we change the name of the issue (remove findAllResults), because only findResult has been added, itīs misleading and appears in the release notes that way my 2ct: inject is more powerful and can do everything that findAllResults can do and more, but findAllResults is so much more intuitive - imho it should be added, doesnt do harm and you would always think: now there is find, findAll, findResult ... so where is findAllResults?
          Hide
          Uri Moszkowicz added a comment -

          I know this issue is closed but I tend to agree that findAllResults would be useful. It does look like inject() could achieve the same but I wouldn't have thought to use it. This is one case where the implementation doesn't match the mental model.

          Show
          Uri Moszkowicz added a comment - I know this issue is closed but I tend to agree that findAllResults would be useful. It does look like inject() could achieve the same but I wouldn't have thought to use it. This is one case where the implementation doesn't match the mental model.
          Hide
          Uri Moszkowicz added a comment -

          Interestingly, I also found this question on StackOverlow:

          http://stackoverflow.com/questions/5789795/collection-findallresults-in-groovy-1-7-5

          I have another example where inject is even more verbose:

          def text = "file1: could not\nline2\nfile2: could not\nline3\n"
          println text.readLines().inject([]) { l, v -> l << v.find(~/(\S+): could not/)

          { it[1] } }.grep { it != null }

          contrasted with

          println text.readLines().findResults { it.find(~/(\s+): could not/) { it[1] }

          }

          Show
          Uri Moszkowicz added a comment - Interestingly, I also found this question on StackOverlow: http://stackoverflow.com/questions/5789795/collection-findallresults-in-groovy-1-7-5 I have another example where inject is even more verbose: def text = "file1: could not\nline2\nfile2: could not\nline3\n" println text.readLines().inject([]) { l, v -> l << v.find(~/(\S+): could not/) { it[1] } }.grep { it != null } contrasted with println text.readLines().findResults { it.find(~/(\s+): could not/) { it[1] } }
          Hide
          Paul King added a comment -

          In case there is further interest, I created GROOVY-4899

          Show
          Paul King added a comment - In case there is further interest, I created GROOVY-4899

            People

            • Assignee:
              Guillaume Laforge
              Reporter:
              Ted Naleid
            • Votes:
              0 Vote for this issue
              Watchers:
              3 Start watching this issue

              Dates

              • Created:
                Updated:
                Resolved: