Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add step to range #702

Closed
bobiblazeski opened this issue Dec 27, 2014 · 9 comments
Closed

Add step to range #702

bobiblazeski opened this issue Dec 27, 2014 · 9 comments

Comments

@bobiblazeski
Copy link
Contributor

Please add step to range like lodash, very important to avoid reversal of the result and sub integer
_.range(5,2,-0.5)
[5, 4.5, 4, 3.5, 3, 2.5]

https://lodash.com/docs#range
_.range([start=0], end, [step=1])

_.range(0, -4, -1);
// → [0, -1, -2, -3]

@buzzdecafe
Copy link
Member

please see: http://fr.umio.us/ranging-near-and-far/

@bobiblazeski
Copy link
Contributor Author

Great article, in theory I agree about everything you said, in practice there's no way I'm keeping both lodash and ramda in the same project, and ramda is forcing me to maintain a lot of functionality myself.
Lack of syntactic sugar for filter was the first strike, as now my code is littered with _.where. Lack of step at range is the second. If there was other function called say enum that worked as lodash range I could just keep on trucking.

Ramda already scored big by putting functions first thus enabling partial application, but I'm starting to feel that its too purist to be practical and I've seen too many ivory tower diamonds (scheme, prolog, j...) to marvel them from far away and turn elsewhere for writing my software.
For better or worse javascript won, and even without types _ brought functional programming to the masses, and now ramda will hopefully push the edge even further. I somehow doubt that will happen with:

"Might be useful .. for something".

To illustrate what I'm thinking please see this article by Joel Spolsky
http://www.joelonsoftware.com/articles/fog0000000020.html

A lot of software developers are seduced by the old "80/20" rule. It seems to make a lot of sense: 80% of the people use 20% of the features. So you convince yourself that you only need to implement 20% of the features, and you can still sell 80% as many copies.

Unfortunately, it's never the same 20%. Everybody uses a different set of features. In the last 10 years I have probably heard of dozens of companies who, determined not to learn from each other, tried to release "lite" word processors that only implement 20% of the features. This story is as old as the PC. Most of the time, what happens is that they give their program to a journalist to review, and the journalist reviews it by writing their review using the new word processor, and then the journalist tries to find the "word count" feature which they need because most journalists have precise word count requirements, and it's not there, because it's in the "80% that nobody uses," and the journalist ends up writing a story that attempts to claim simultaneously that lite programs are good, bloat is bad, and I can't use this damn thing 'cause it won't count my words.

@CrossEye
Copy link
Member

There are two answers to Joel Spolsky's cri de coeur. One is the one made by Microsoft and (as noted in the end of his article) by early-day Netscape: trying to make sure that your big tool can be configured to do whatever the user wants. The other is to follow the Unix philosophy and give a number of tools that can be snapped together to do what you need.

Lo-Dash, following Underscore, seems more inclined to the former. Ramda has a definite preference for the latter.

@slobodanblazeski mentions filtering. Ramda has a very simple API for filter: you pass it a unary predicate function and a list of candidates and it returns a new list containing those candidates for which the predicate returns true. This is very simple behavior, in contrast with Lo-Dash's filter:

Iterates over elements of a collection, returning an array of all elements the callback returns truthy for. The callback is bound to thisArg and invoked with three arguments; (value, index|key, collection).

If a property name is provided for callback the created "_.pluck" style callback will return the property value of the given element.

If an object is provided for callback the created "_.where" style callback will return true for elements that have the properties of the given object, else false.

Lo-Dash's API is clearly more flexible than Ramda's. But it's significantly more complex. Ramda chooses to keep things simple and allow for the same behavior with smaller composed functions. Hence, Ramda has prop and where that can be combined with filter to give you the behavior you choose:

R.filter(R.prop('blocked'), characters);
R.filter(R.where({age: 36}), characters);

That is fundamental to the philosophy of Ramda.

A step parameter to the range function is more questionable. That cannot be easily built out of current pieces in the general case. But this is the first request we've had for one. The article @buzzdecafe pointed to explains my distaste for the API of the Underscore-style version. Can you propose a version that would fit with Ramda's style, even if it's for a separate steppedRange function, or some such? Ideally if we had one, range would be easily be built on top of it.

@bobiblazeski
Copy link
Contributor Author

Can you propose a version that would fit with Ramda's style, even if it's for a separate steppedRange function, or some such? Ideally if we had one, range would be easily be built on top of it.

@CrossEye I have no clue how to wed partial application with optional arguments. My experience with common lisp optional & key makes me doubt that those things are even possible, or if it is its
probably ugly as hell.
However may I propose something simple that could do the job and its very useful in javascript programming in functional style

//  end  Number required
// start Number optional default 0
// step Number optional default 0
// returns [Number] 
R.enum(end,start,step)

R.enum(5) <=> R.enum(5,0) <=> R.enum(5,0,1)
->  0,1,2,3,4

R.enum(5,2) <=> R.enum(5,2,1) 
-> 2,3,4

R.enum(2,5,-1) 
-> 5,4,3,2

The R.enum(n) is actually the most useful since it serves as an array creator and as an replacement for for. If you want to sum first n number

R.sum(R.enum(5))

Enum is also great replacement for looping when you have to do some index fiddling

for(var j = 5;j < 2; j-=1.5) {   
       // some j index dependent code         
}
// becomes 
R.map(fn, R.enum(2,5,-1.5))

The single parameter enum combined with map, reduce & co replaces all those ugly for(var i=0; i <n; +=i) and its shorter to type then R.range(0,n). The two and three parameter versions are rarer but still very useful if you want to avoid for/while iteration.

@fyyyyy
Copy link
Contributor

fyyyyy commented Dec 28, 2014

I guess using a function for the step would be more flexible, as in

R.enum(2,5, R.add(1.5))
R.enum(2,20, R.multiply(1.5))

Btw, enum is somewhat similar to R.interpolate #513

R.interpolate(start / end / size)
R.interpolate(0, 100, 5) => [20, 40, 60, 80, 100]
R.interpolate(0, 1, 3) => [0.33333, 0.666666, 1.0]
R.interpolate(['cookie', 'house', 'garden'], 5) => ['cookie', 'cookie', 'house', 'house', 'garden']

@CrossEye
Copy link
Member

@slobodanblazeski:

I have no clue how to wed partial application with optional arguments.

We've found no credible way to do so. In Ramda, we've handled this by having more than one function with similar behavior when we really want the ability to offer an optional parameter. It's far from ideal, but it seems the best we can do, since curried functions are so important to the style of coding Ramda is meant to support.

R.enum(end,start,step)

R.enum(5) <=> R.enum(5,0) <=> R.enum(5,0,1)
->  0,1,2,3,4

R.enum(5,2) <=> R.enum(5,2,1) 
-> 2,3,4

R.enum(2,5,-1) 
-> 5,4,3,2

This is almost entirely the reverse of what I would expect from a Ramda function, I'm afraid. With only a very few obvious exceptions, every function of more than one parameter in Ramda is curried; it's how the library is intended to be used. And the parameter order for our functions is designed to take advantage of this: with those parameters less likely to change coming before those more likely to change.

If we were to curry this version of enum, the user would be required to supply step on every call and start unless it's already been partially applied, and the one that's most likely to change, end, is the one that you could most easily bury in a curried version.

(There is also a strange inconsistency between R.enum(5, 2, 1), which is exclusive in its right side and R.enum(2, 5, -1) which is inclusive in it -- why is that?)

So if we were to create something like this (and I'm not convinced, by the way) my preference would be something almost the reverse of this:

R.rangeTo(5) // == R.range(0, 5) == R.stepBy(1, 0, 5) 
//=>  [0, 1, 2, 3, 4]

R.range(2, 5) <=> R.stepBy(1, 2, 5) 
//=> [2, 3, 4]

R.stepBy(-1, 5, 2) 
//=> [5, 4, 3] // note the different length from the original example.

The single parameter enum combined with map, reduce & co replaces all those ugly for(var i=0; i <n; +=i)

By switching to map, filter, find, and reduce, I almost never find a need for such a construct in my code. When I do want a range of values, for me so far Ramda's range function has sufficed. Can you explain a little more the type of code you're writing that requires the step parameter?

@bobiblazeski
Copy link
Contributor Author

With only a very few obvious exceptions, every function of more than one parameter in Ramda is curried; it's how the library is intended to be used.

@CrossEye As a purist I agree with you, as pragmatist your approach is making coding inconvenient. I will add enum in my own utilities and see how it goes. But little voice inside me is starting to tell me that I'm not ramda target customer.

@CrossEye
Copy link
Member

With only a very few obvious exceptions, every function of more than one parameter in Ramda is curried; it's how the library is intended to be used.

@CrossEye As a purist I agree with you, as pragmatist your approach is making coding inconvenient.

That's somewhat amusing, as the reason for the currying is to be as pragmatic as we can.

A recent issue for Lo-Dash discusses code like this:

var someNaN = _.partialRight(_.some, _.isNaN);
_.reject([ [1, 2, 3], [1, 2, NaN] ], someNaN); // should yield [[1, 2, 3]]

(The actual issue isn't important here. It's pretty much caused by the inverse of what was discussed at great length in #452)

In Ramda, the currying makes this simpler:

var anyNaN = R.any(isNaN);
R.reject(anyNaN, [ [1, 2, 3], [1, 2, NaN] ]); //=> [[1, 2, 3]]

And if we wanted to build a reusable function for that, we would simply do

var rejectNaNs = R.reject(anyNaN);
// ...
rejectNaNs([ [1, 2, 3], [1, 2, NaN] ]); //=> [[1, 2, 3]]

The difference between these two

var someNaN = _.partialRight(_.some, _.isNaN);
var anyNaN = R.any(isNaN);

is a significant part of what drives Ramda's design.

@alexandermckay
Copy link

I very rarely have to use a range with a step but something like this works for me:
const isEven = compose(equals(0), modulo(__, 2))
const r = range(0, 10)
const wStep = filter(isEven, r) // [0, 2, 4, 6, 8, 10]

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

5 participants