I have a variable and several methods I'd like to call in a sequence where return value of one method is an input of another. Basically a pipeline. Now, is there a way of chaining the calls? In a pseudo code it looks like
def a = [1, 2, 3]
def b = calculation3(calculation2(calculation1(a)))
As you can see it looks very Clojure-like and I'd like to end up with something like (using Clojure syntax)
(-> a
calculation1
calculation2
calculation3)
I was hoping to use the with keyword but it only passes the variable a around not collecting the results and passing them as the input to the next method.
The only working solution I've found so far is 'closure composition' like this. But that seems to be too heavy handed to me.
def a = [1, 2, 3]
def b = (class1.&calculation1 >> class1.&calculation2 >> class1.&calculation3)(a)
Any idea?
You could write a function to do it:
def chain(a, Closure... fns) {
fns.toList().inject(a) { v, c -> c(v) }
}
Then call this in your code:
chain(a, class1.&calculation1,class1.&calculation2,class1.&calculation3)
I gave a bit more thought and have come up with something like this:
def compose = {...c ->
{result ->
c.each {
result = it(result)
}
result
}
}
def a = [1, 2, 3]
def b = compose(class1.&calculation1, class1.&calculation2, class1.&calculation3)(a)
Still it's not as nice as Clojure's threading.
Use inject
def pipe(fn, ...fns) {
{ args ->
fns.inject(fn(args)) { acc, value ->
value(acc)
}
}
}
Not that the first function is allowed to take a variable number of arguments while the following parameters are not.
Related
I am stuck on a problem and I would be grateful for help.
Consider the following example.
# can be changed
def inner(x):
x = 4
return x
# can not be changed
def outer(a, b):
b = b(b)
return a + b
# can be changed - I want to pass two parameters to inner
res = outer(
a = 3,
b = inner
)
print(res)
Now important to note is that I can not change the function outer(), because it comes from a common code base / package.
How can I pass multiple parameters to inner()? inner() can be changed as needed, but it needs to return the function, not the int since outer() expects a function.
Thank you in advance!
I tried passing multiple parameters, but outer() expects a function. This example illustrates the function ExternalTaskSensor of the python package airflow. The parameter
execution_date_fn
expects a function to be returned.
Groovy noob here.
I have this map
signedMap = [[k:a, v:1], [k:b, v:-2], [k:c, v:3], [k:d, v:-4]]
I need to find the maximum absolute value, in this example -4
This my current code
def absMap = []
signedMap.each {
absMap.add([k:it.k, absv:it.v.abs()])
}
def sortedAbs = absMap.sort{it.absv}
def maxAbs = sortedAbs.last().absv
Is there a more elegant way to do this?
Thanks!
If you are only intersted in the value, you could directly transform and find the maxium (instead of building a map, sorting, taking last).
signedMap.collect{ it.v.abs() }.max()
edit
To get the map for k and v you can also use max with a closure.
signedMap.max{ it.v.abs() }
It will give you the original v (so the negative -4).
Use dynamic sorters like this
def signedMap = [[k:'a', v:1], [k:'b', v:-2], [k:'c', v:3], [k:'d', v:-4], [k:'e', v:-1]]
def result = signedMap.toSorted { a, b -> Math.abs(a.v) <=> Math.abs(b.v) }
println(result)
Output:
[[k:a, v:1], [k:e, v:-1], [k:b, v:-2], [k:c, v:3], [k:d, v:-4]]
I have solved a problem that ranks fruits by the number of votes. Unfortunately, I want to solve the problem in a purely functional way without mutating the rankPosition variable. Here is my solution:
def fruits=[
[name:'apple', votes:120 , ranking:null ],
[name:'banana', votes:200, ranking: null],
[name:'apricot', votes:66, ranking:null ],
[name:'pear', votes:84, ranking:null],
[name:'kiwi', votes:77, ranking:null],
[name:'plum', votes:66, ranking:null],
[name:'berry', votes:120, ranking:null],
[name:'pineapple', votes:50, ranking:null],
[name:'grapes', votes:200, ranking:null]
]
def rankPosition= 1
def groupedByVotes = fruits.groupBy {it.votes }
println "Ratings $groupedByVotes"
def finalResults=groupedByVotes.sort().each { votes, items ->
items.each { it.ranking = rankPosition }
rankPosition += items.size()
}
println "Final Results are $finalResults"
How can I solve this problem without having to declare a rankingPosition variable external to the closure and mutating its state. Please notes that this solution works but I have since learned that I shouldn't be doing it this way.
I want to be able to fill the rankings with the correct ranking. The inject function does an accumulation but I don't know how to combine it in a way to also set the ranking with the value accumulated in the inject.
I am simply stuck, just don't seem to be able to reason about this one. My attempt below to use inject, simply did not work. Maybe there isn't a way to do this in a purely functional way, better thsn my attempt.
def res= groupedByVotes.collectEntries{votes, list1->
println "list class $list1"
def r= list1.inject(0){acc,l-> acc+l.size()}
list1.each{it.ranking=r}
println "$r"
[(votes): list1]
}
println "$res"
I anyone can then I would appreciate your solution or just assume my attempt is the most realistic way of solving this one.
This is a pure functional solution. It leaves the initial map of maps unchanged and produces a new one:
def results = groupedByVotes.sort().inject(new Tuple(1, [:]), { acc, entry ->
def newRank = acc[0] + entry.value.size()
def newValue = entry.value.collect { [*:it, ranking:acc[0]] }
return new Tuple(newRank, [*:acc[1], (entry.key):newValue] )
})
finalResults = results[1]
I probably did not explain very well what I was trying to achieve. After calculating the rank, I wanted the values inserted for the respective elements in the list. Here is what I came up with:
def sortedFruits= fruits.sort{f1, f2 -> f1.votes <=> f2.votes}
(0..sortedFruits.size()-1)
.each{ i ->
if(i==0){
sortedFruits.get(i).ranking=1
}else if(sortedFruits.get(i-1).votes==sortedFruits.get(i).votes){
sortedFruits.get(i).ranking=i
}else{
sortedFruits.get(i).ranking=i+1
}
if(i<sortedFruits.size()){
def f= sortedFruits.get(i)
println "${f}"
}
}
println "Sorted Fruits are $sortedFruits"
The final result looks like
Sorted Fruits are [[name:lemons, votes:20, ranking:1], [name:guava, votes:20, ranking:1],
[name:pineapple, votes:50, ranking:3], [name:apricot, votes:66, ranking:4], [
name:plum, votes:66, ranking:4] etc.
You can try this:
def results = groupedByVotes.sort()
.inject(new Tuple(1, []), { acc, entry ->
entry.value.each { it.ranking = acc[0] }
return new Tuple(acc[0] + entry.value.size(), acc[1] << entry.value)
})
finalResults = results[1]
In each step of the folding (done by .inject(...)) you have a Tuple containing the next rank and the partial list that has been computed so far. As a final step you extract the result list from the Tuple. But this converts the map to a list.
This solution is even simpler because it is not necessary to make a new collection if you modify the old one in place, and it preserves the map:
def finalResults = groupedByVotes.sort()
finalResults.inject(1, { acc, entry ->
entry.value.each { it.ranking = acc }
return acc + entry.value.size()
})
But both solutions are not really functional. Real functional code treats all values as if they were immutable. See my other answer (coming) for a real functional solution.
Suppose there is a function def f = { x -> x + 4 }.
Is there a way to call it somehow like 7.f() and get 11?
Yes, you can add that function as a method to the Integer class, but, instead of using the x variable, you are better using the delegate of the closure:
Integer.metaClass.f = { delegate + 4 }
assert 7.f() == 11
For example, the groovy File class has a nice iterator that will filter out just directories and not files:
void eachDir(Closure closure)
When I use eachDir, I have to use the verbose method of creating the collection first and appending to it:
def collection = []
dir1.eachDir { dir ->
collection << dir
}
Any way to get it back to the nice compact collect syntax?
I don't know of any "idiomatic" way of doing this, nice riddle! =D
You can try passing the eachDir, or any similar function, to a function that will collect its iterations:
def collectIterations(fn) {
def col = []
fn {
col << it
}
col
}
And now you can use it as:
def dir = new File('/path/to/some/dir')
def subDirs = collectIterations(dir.&eachDir)
def file = new File('/path/to/some/file')
def lines = collectIterations(file.&eachLine)
(that last example is equivalent to file.readLines())
And only for bonus points, you may define this function as a method in the Closure class:
Closure.metaClass.collectIterations = {->
def col = []
delegate.call {
col << it
}
col
}
def dir = new File('/path/to/some/dir')
def subDirs = dir.&eachDir.collectIterations()
def file = new File('/path/to/some/file')
def lines = file.&eachLine.collectIterations()
Update: On the other hand, you might also do:
def col = []
someDir.eachDir col.&add
Which I think is quite less convoluted, but it's not leveraging the collect method as you requested :)
Not for the specific example that you're talking about. File.eachDir is sort of a weird implementation IMO. It would have been nice if they implemented iterator() on File so that you could use the normal iterator methods on them rather than the custom built ones that just execute a closure.
The easiest way to get a clean one liner that does what you're looking for is to use listFiles instead combined with findAll:
dir1.listFiles().findAll { it.directory }
If you look at the implementation of eachDir, you'll see that it's doing this (and a whole lot more that you don't care about for this instance) under the covers.
For many similar situations, inject is the method that you'd be looking for to have a starting value that you change as you iterate through a collection:
def sum = [1, 2, 3, 4, 5].inject(0) { total, elem -> total + elem }
assert 15 == sum