Understanding Transducers in Ruby

Extra information that is not a direct translation

chained built-in transformations create intermediate arrays
transduced transformations process items one by one into output array
array
.map(&fn1)
.select(&fn2)
.reduce(&fn3)
transformation = compose(map(&fn1), select(&fn2), reduce(&fn3));transformation(array)

Note that I’ll be using select instead of filter throughout this article, as filter is only present in 2.6+ as an alias of select.

[0, 1, 2, 3, 4, 5, 6, 7, 8, 9].map { |x| x + 1 }
# => [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
[1, 2, 3, 4, 5, 6, 7, 8, 9, 10].select { |x| x.even? }
# => [2, 4, 6, 8, 10]
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
.map { |x| x + 1 }
.select { |x| x.even? }
# => [2, 4, 6, 8, 10]

While we could use the&:even?shorthand here instead, we’ll tend more verbose to make it easier to read for newer Rubyists.

map_inc_reducer = -> result, input {
result.push(input + 1)
}
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9].reduce([], &map_inc_reducer)
# => [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
def map_reducer(&fn)
-> result, input {
result.push(fn.call(input))
}
end
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
.reduce([], &map_reducer { |x| x + 1 })
# => [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
.reduce([], &map_reducer { |x| x - 1 })
# => [-1, 0, 1, 2, 3, 4, 5, 6, 7, 8]
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
.reduce([], &map_reducer { |x| x * x })
# => [0, 1, 4, 9, 16, 25, 36, 49, 64, 81]
select_even_reducer = -> result, input {
result.push(input) if input.even?
result
};
[1, 2, 3, 4, 5, 6, 7, 8, 9, 10].reduce([], &select_even_reducer)
# => [2, 4, 6, 8, 10]
def select_reducer(&predicate_fn)
-> result, input {
result.push(input) if predicate_fn.call(input)
result
}
end
[1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
.reduce([] &select_reducer { |x| x.even? })
# => [2, 4, 6, 8, 10]
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
.reduce([], &map_reducer { |x| x + 1 })
.reduce([], &select_reducer { |x| x.even? })
# => [2, 4, 6, 8, 10]
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
.map { |x| x + 1 }
.select { |x| x.even? }
# => [2, 4, 6, 8, 10]
def map_reducer(&fn)
-> result, input {
result.push(fn.call(input))
}
end
def select_reducer(&predicate_fn)
-> result, input {
result.push(input) if predicate_fn.call(input)
result
}
end
array = [1, 2, 3]
array.push(4)
# => [1, 2, 3, 4]
10 + 1
# => 11
def mapping(&fn)
-> &reducing_fn {
-> result, input {
reducing_fn.call(result, fn.call(input))
}
}
end
def selecting(&predicate_fn)
-> &reducing_fn {
-> result, input {
if predicate_fn.call(input)
reducing_fn.call(result, input)
else
result
end
}
}
end
pushes = -> list, item { list.push(item) }[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
.reduce([], &mapping { |x| x + 1 }.call(&pushes))
.reduce([], &selecting { |x| x.even? }.call(&pushes))
# => [2, 4, 6, 8, 10]
pushes = -> list, item { list.push(item) }mapping { |x| x + 1 }
.call(&pushes)
.call([], 1);
# => [2]
mapping { |x| x + 1 }
.call(&pushes)
.call([2], 2)
# => [2, 3]
mapping { |x| x + 1 }
.call(&pushes)
.call([2, 3], 3)
# => [2, 3, 4]
selecting { |x| x % 2 === 0 }
.call(&pushes)
.call([2, 4], 5)
# => [2, 4]
selecting { |x| x % 2 === 0 }
.call(&pushes)
.call([2, 4], 6)
# => [2, 4, 6]
plus_one_even = mapping { |x| x + 1 }
.call(&selecting { |x| x.even? })
.call(&pushes)
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
.reduce([], &plus_one_even)
# => [2, 4, 6, 8, 10]
require 'ramda'transform = Ramda.compose(
mapping { |x| x + 1 },
filtering { |x| x.even? },
pushes
)
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
.reduce([], &transform)
require 'ramda'square = -> x { x * x }transform = Ramda.compose(
filtering { |x| x.even? },
filtering { |x| x < 10 },
mapping(&square),
mapping { |x| x + 1 },
pushes
)
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
.reduce([], &transform)
# => [1, 5, 17, 37, 65]
def transduce(transformation, reducing_fn, initial, input)
input.reduce(initial, &transformation.call(&reducing_fn))
end
transformation = Ramda.compose(
mapping { |x| x + 1 },
selecting { |x| x.even? }
)
plus_one_evens = transduce(
transformation,
pushes,
[],
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
)
# => [2, 4, 6, 8, 10]
adds = -> a, b { a + b }sum_of_plus_one_evens = transduce(
transformation,
adds,
0,
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
)
# => 30

Leaving the above alone as there are not many great transducing libraries or implementations in Ruby. Feel free to comment below if you know of some I’ve missed :)

--

--

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store