简体   繁体   中英

How can i do this better with Ramda.js

So I have a list of divs: list

i want a subset of the list removing the divs with the .fade class. and also just grabbing the list from the div with .selected class.

so using R.takeWhile and R.dropWhile .

then i want to map over that new list and add a .active class on a subset of that list with R.take and R.forEach or R.map

something like :

var takeFromSelected = R.dropWhile(function(item){!$(item).hasClass('selected')};

var removeFadeItems = R.takeWhile(function(item){!$(item).hasClass('fade')});

var addActiveClass = function(x){ $(x).addClass('.active')};

var processList = R.pipe(R.map(addActiveClass), removeFadeItems, takeFromSelected);

processList(list);

Im really new to this FP stuff and trying to get the grip of it.

Any insight would be greatly apreciated!! Thanks! :)

Update

for future reference this is what i did :

@addActiveClass = (x)->  
  $(x).addClass('active') 
  return

@takeFromSelected = R.dropWhile((item)-> !$(item).hasClass('selected'))

@removeFadeItems = R.takeWhile((item)-> !$(item).hasClass('fade'))

@addWeekView = R.compose(addActiveClass, removeFadeItems, takeFromSelected)

From your description, it sounds like you want to use filter more than takeWhile or dropWhile .

takeWhile keeps values of the array until the first time the predicate fails:

> R.takeWhile(R.isEmpty, [[], [], [1, 2, 3], [], [1, 3]])
[ [], [] ]

dropWhile removes values of the array until the first time the predicate fails:

> R.dropWhile(R.isEmpty, [[], [], [1, 2, 3], [], [1, 3]])
[ [ 1, 2, 3 ], [], [ 1, 3 ] ]

filter removes all values that do not pass the predicate.

> R.filter(R.isEmpty, [[], [], [1, 2, 3], [], [1, 3]])
[ [], [], [] ]

In your case you want something like:

var removeFadeItems = R.filter(function(x) {
  return !$(x).hasClass('fade');
});
var takeFromSelected = R.filter(function(x) {
  return $(x).hasClass('selected');
});

Also, as @donnut stated, your map needs to return a value as well. However, you're kind of in a bad way with addClass . Since it mutates the value (which is a side-effect), using map is a bit of a misnomer. You're better off using forEach , as it is made for side-effecting things:

var addActiveClass = function(x) {
  $(x).addClass('active');
};

So you end up with:

var processList = R.pipe(
  R.forEach(addActiveClass),
  takeFromSelected,
  removeFadeItems
);
processList(list);

Refactor

Now, being that some of your functions are referentially transparent (they don't mutate things), you can refactor this to be a bit clearer, more composable and slightly more efficient.

The first thing to note is that you're rewrapping your divs in each function. The $ is a good function to use to wrap things just once. So let's start the pipeline with that.

var processList = R.pipe(
  R.map($),
  ...

Now, invoker allows you to call a function on an object. We want to call addClass on the jquery wrapped object with the argument of active . Let's make a function for that:

var addActive = R.invoker(1, 'addClass', 'active');

We can add this to the pipeline.

var processList = R.pipe(
  R.map($),
  R.forEach(addActive),
  ...

The filters are similar to what we did in addActive , let's refactor those too first by making the predicates separate:

var faded = R.invoker(1, 'hasClass', 'fade');
var notFaded = R.not(faded);
var selecteded = R.invoker(1, 'hasClass', 'selected');

The great thing here is the composability of ramda functions allows us to say R.not(faded) , and things just work without thinking about it.

So lets add this to the pipeline.

var processList = R.pipe(
  R.map($),
  R.forEach(addActive),
  R.filter(notFaded),
  R.filter(selecteded)
);

This doesn't seem to have changed much of what the processing looks like. This is good! The primitives have changed, they're simpler and easier to see what's going on, but the overall flow is the same.

Now it's time to get heady. Because of parametricity, you can combine the two filters together without worrying about whether or not they make sense. There's a law that states R.pipe(R.filter(p), R.filter(q)) == R.pipe(R.filter(R.and(p, q)) . What this means is that you do not have to filter the array twice, you can filter it just once and apply the predicates in turn.

var processList = R.pipe(
  R.map($),
  R.forEach(addActive),
  R.filter(R.and(notFaded, selecteded))
);

If addClass did not mutate its argument, we could also use parametricity to combine the map and forEach into one. We can solve this by making our own non-mutating addClass with clone :

var newActive = R.pipe(
  R.invoker(0, 'clone'), 
  R.invoker(1, 'addClass', 'active')
);

So we can use map again!. The pipeline can change to:

var processList = R.pipe(
  R.map($),
  R.map(newActive),
  R.filter(R.and(notFaded, selecteded))
);

And we can now use parametricity to combine the maps together. The law states R.pipe(R.map(f), R.map(g)) == R.map(R.pipe(f, g)) . Instead of mapping twice over the array, we map once, and compose the functions within the map in turn. So our pipeline now looks like this:

var processList = R.pipe(
  R.map(R.pipe($, newActive)),
  R.filter(R.and(notFaded, selecteded))
);

There are further refactorings and optimizations we can make. We could filter before mapping so we end up iterating over less elements, or abstract the invoker calls to a little jquery wrapper DSL. You're encouraged to continue with the refactoring, but this is a pretty nice change from the original. Each of functions does a very little, is much more composable, more testable, and more understandable.

The whole refactoring is as follows.

Before:

var removeFadeItems = R.filter(function(x) {
  return !$(x).hasClass('fade');
});
var takeFromSelected = R.filter(function(x) {
  return $(x).hasClass('selected');
});
var addActiveClass = function(x) {
  $(x).addClass('active');
};

var processList = R.pipe(
  R.forEach(addActiveClass),
  takeFromSelected,
  removeFadeItems
);

processList(list);

After:

var faded      = R.invoker(1, 'hasClass', 'fade');
var selecteded = R.invoker(1, 'hasClass', 'selected');
var notFaded = R.not(faded);
var newActive = R.pipe(
  R.invoker(0, 'clone'), 
  R.invoker(1, 'addClass', 'active')
);

var processList = R.pipe(
  R.map(R.pipe($, newActive)),
  R.filter(R.and(notFaded, selecteded))
);

processList(list);

I suppose you ask this question because you get an unexpected result.

A general remark is that the function addActiveClass introduces a side-effect — it modifies the DOM. This isn't wrong, but from a FP point of view you better isolate this effect, eq in a IO monad.

The function takeFromSelected and removeFadeItems use predicate functions (function(item){ !$(item) ... };); to determine what to drop and take. A predicate function needs to return true or false. In your case they do not return anything. Solution is to add return in front of !$(item)

The function addActiveClass, the one with the side-effect, does not have a return value. This breaks the pipeline as the next function removeFadeItems will not reveive anything. Just make addActiveClass return the $(item).

I haven't tested it, but if you add the three return s, it will probably work.

Good luck.

update: because addActiveClass returns a jQuery object and passes that on to removeFadeItems you do not need to wrap item in a $() again.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM