Reducing Enumerable — No-Op and Boolean

Recap

So in the previous post, we took a look at a few basic Enumerable functions made in terms of reduce

For reference, those were:

Map

def map(list, &fn)
list.reduce([]) { |a, i| a.push(fn[i]) }
end
map([1,2,3]) { |i| i * 2 }
# => [2, 4, 6]

Select

def select(list, &fn)
list.reduce([]) { |a, i| fn[i] ? a.push(i) : a }
end
select([1,2,3]) { |i| i > 1 }
# => [2, 3]

Find

def find(list, &fn)
list.reduce(nil) { |_, i| break i if fn[i] }
end
find([1,2,3]) { |i| i == 2 }
# => 2
find([1,2,3]) { |i| i == 4 }
# => nil

In these cases we’ve been reducing onto arrays or just flat out ignoring the reducing value.

One should pay special attention to the fact that we’re ignoring the accumulator, because this yields a very interesting observation about the behavior of reduce.


What are we covering this time?

This round will cover no-op and boolean functions of Enumerable.

No-Op Functions

Find only happened to do something with no accumulator because of break, but reduce can be used for no-op functions as well, like each:

def each(list, &fn)
list.reduce(nil) { |_, i| fn[i] }
list
end

While not technically an Enumerable function, it’s the base of everything else under it. That said, our implementation doesn’t satisfy that constraint.

Boolean Functions

One of the fun things you can do is reduce into a boolean or even bitwise operation.

Functions that do this are ones like: any?, all?, none?, one?, include?, member?

Let’s take a look at a few of them:

any?

def any?(list, &fn)
list.reduce(false) { |_, i|
break true if fn[i]
false
}
end
any?([1,2,3]) { |i| i > 5 }
# => false
any?([1,2,3]) { |i| i > 1 }
# => true

The caveat here is that break does not play nicely with ternaries, and if x then just doesn’t feel quite right.

Like find , any? will break early if it finds something.

Granted this could be simplified with a !! to a one-liner, but I’m going to err on the side of verbosity for these posts:

def any?(list, &fn)
!!list.reduce(nil) { |_, i| break true if fn[i] }
end

all?

def all?(list, &fn)
list.reduce(true) { |a, i| a && fn[i] }
end
all?([1,2,3]) { |i| i > 0 }
# => true
all?([1,2,3]) { |i| i > 2 }
# => false

all? introduces us to folding using && and a boolean value. The idea here is to see if all elements match the predicate function. That said, this first implementation doesn’t quite get there. Can you see what’s wrong with it?

What happens if any one of the elements is false? It should probably break out early like our other functions:

def all?(list, &fn)
list.reduce(true) { |a, i|
break false unless fn[i]
true
}
end
all?([1,2,3]) { |i| i > 0 }
# => true
all?([1,2,3]) { |i| i > 2 }
# => false

none?

none? is basically the inverse of any? . If we really wanted to we could define it in terms of any? :

def none?(list, &fn)
!any?(list, &fn)
end
none?([1,2,3]) { |i| i > 2 }
# => false
none?([1,2,3]) { |i| i > 5 }
# => true

Ideally we should reuse what we already have, but the point of this series is to go through how you’d implement things in terms of reduce:

def none?(list, &fn)
list.reduce(false) { |_, i|
break false if fn[i]
true
}
end
none?([1,2,3]) { |i| i > 2 }
# => false
none?([1,2,3]) { |i| i > 5 }
# => true

Basically you just end up with the conditions switches and voilá!

one?

def one?(list, &fn)
list.reduce(false) { |a, i|
if fn[i]
break false if a
true
else
a
end
}
end
one?([1,2,3]) { |i| i == 2 }
# => true
one?([1,2,3]) { |i| i > 1 }
# => false
one?([1,2,3]) { |i| i > 5 }
# => false

one? has the added bonus of having to make sure we get, well, one match. Now we not only have to check if the functional predicate was true, but also that it wasn’t true before now.

Because of the way this works, we can’t break out in all cases, only those in which we had more than one occurrence. Now we could have reduced onto an integer and ran a count of occurrences, but we already have a boolean status we may as well use!

include? and member?

def include?(list, item)
list.reduce(false) { |a, i|
break true if i == item
false
}
end
include?([1,2,3], 1)
# => true
include?([1,2,3], 5)
# => false

include? and member? are the same, so we’ll just group them both under this branch.

Much like find, it only has to find one item or return false otherwise.

Wrapping Up

Next time we’re going to start covering functions deal with Integer state like min and max and sorting.

Like what you read? Give Brandon Weaver a round of applause.

From a quick cheer to a standing ovation, clap to show how much you enjoyed this story.