3

This question presents a version of Haskell's scanl in Python, but is there a Ruby version of this function?

Community
  • 1
  • 1
Alex Moore-Niemi
  • 2,913
  • 2
  • 24
  • 22

3 Answers3

1

You can use reduce() and implement it by yourself.

def scanl(op, init, range)
  op = op.to_proc unless op.is_a?(Proc)
  range.reduce([init]) { |a, e| a.push(op.call(a.last,e)) }
end

p scanl(lambda { |a, b| a + b }, 0, 1..10)
#=> [0, 1, 3, 6, 10, 15, 21, 28, 36, 45, 55]
p scanl(:+, 0, 1..10)
#=> [0, 1, 3, 6, 10, 15, 21, 28, 36, 45, 55]

Alternatively you can use map() and put the initial element in front of the array.

def scanl(op, init, range)                             
  op = op.to_proc unless op.is_a?(Proc)                
  acc = init                                           
  range.map { |e| acc = op.call(acc, e) }.unshift(init)
end                                                 
sschmeck
  • 7,233
  • 4
  • 40
  • 67
  • nice i had a feeling i could piggy-back on reduce somehow. do you have to do the `is_a?(Proc)` check? i'd figure it'd be a no-op if it was already a proc and thus safe – Alex Moore-Niemi Jun 29 '16 at 15:56
  • 1
    yeah i mean that you can just run `.to_proc` always, if it is a proc, no-op, if it isn't, then you made it a proc, no? – Alex Moore-Niemi Jun 29 '16 at 15:59
  • although it is less concise, i think i prefer my version for the reason @SergioTulentsev points out. though the `to_proc` bit is helpful – Alex Moore-Niemi Jun 29 '16 at 16:06
  • @SergioTulentsev I updated my answer and replaced `Array#+` by `Array#push`. – sschmeck Jun 29 '16 at 20:29
  • @AlexMoore-Niemi You are rigth with `to_proc` call. The condition don't changes the behavoir. The condition makes the intention clearer, maybe. ;-) – sschmeck Jun 29 '16 at 20:33
  • "nice i had a feeling i could piggy-back on reduce somehow" – That's a pretty boring feeling, since `reduce` is general ;-) IOW: it can do *everything* a loop can do, and so you can *always* piggy-back on it. There's a proof sketch in Haskell on the Wikipedia page for `Fold`, if you're interested. The gist is this: a collection can be either empty or not. The two arguments to `reduce` (the initial and the block) tell it what to do with an empty collection and what to do with a not-empty collection. Since you have all cases covered, there is nothing `reduce` can't do. I once re-wrote all of … – Jörg W Mittag Jun 29 '16 at 23:59
  • 1
    `Enumerable` using `inject` instead of `each`, just for fun. – Jörg W Mittag Jun 29 '16 at 23:59
0

In Ruby 2.0, where the Enumerator class exists, we can build a nicer implementation that works properly with infinite ranges:

def scanl(elem, &op)
  Enumerator.new do |yielder|
    acc = elem
    loop do
      yielder << acc
      acc = op.call(acc)
    end
  end.lazy
end

And use it like so:

scanl(1, &:next).take(10)                                                                                                                                                                                                                                     
#=> #<Enumerator::Lazy: ...>
scanl(1, &:next).take(10).force                                                                                                                                                                                                                                     
#=> [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
fib = scanl([0,1]) {|x, y| [y, x + y]}.map(&:first)
fib.take(10).force
#=> [0, 1, 1, 2, 3, 5, 8, 13, 21, 34]
Qqwy
  • 5,214
  • 5
  • 42
  • 83
-1

Probably a nicer version could be written, but this is what I came up with:

require 'spec_helper'

class Array
  def scanl accumulator, &block
    results = [accumulator] + self
    results.each_with_index do |e, i|
      results[i] = block.call(accumulator, e)
      accumulator = results[i]
    end
    results
  end
end

describe "#scanl" do
  it 'is similar to foldl, but returns a list of successive reduced values from the left' do
    # example from http://learnyouahaskell.com/higher-order-functions
    expect([3, 5, 2, 1].scanl(0, &:+)).to eq([0,3,8,10,11])
  end
end

I considered changing scanl to take just a method name like :+ instead of a block to be more like reduce. Thoughts?

Alex Moore-Niemi
  • 2,913
  • 2
  • 24
  • 22
  • Come chat here http://chat.stackexchange.com/rooms/8595/the-2nd-monitor . I think we can work out something – Marc-Andre Jun 29 '16 at 15:45
  • Just compare the quality of the questions. – Sergio Tulentsev Jun 29 '16 at 15:45
  • 1
    Just as a FYI, deleting questions to preserve reputation isn't the best solution. Instead, fix the question so it's a better one. Think of it this way, questions are owned by the SO community once you ask them. We'll delete them if necessary. – the Tin Man Jun 29 '16 at 16:40
  • @SergioTulentsev ah, i thought it was more correct to put my potential solution in the Answer spot than the Question. i'll do better next time. – Alex Moore-Niemi Jun 19 '19 at 18:41