Like Underscore, but lazier

Overview

Like Underscore, but lazier

Build Status Bower version NPM version

Lazy.js is a functional utility library for JavaScript, similar to Underscore and Lodash, but with a lazy engine under the hood that strives to do as little work as possible while being as flexible as possible.

It has no external dependencies, so you can get started right away with:

npm install lazy.js

(Note the package is called "lazy.js", with a dot.)

Or, if you're using Lazy.js in the browser:

<script type="text/javascript" src="lazy.js"></script>

<!-- optional: if you want support for DOM event and AJAX-based sequences: -->
<script type="text/javascript" src="lazy.browser.js"></script>

Now let's look at what you can do with Lazy.js. (For more thorough information, take a look at the API Docs.)

Introduction

Let's start with an array of objects representing people.

var people = getBigArrayOfPeople();

Suppose we're using this array to back some sort of search-as-you-type functionality, where users can search for people by their last names. Naturally we want to put some reasonable constraints on our problem space, so we'll provide up to 5 results at a time. Supposing the user types "Smith", we could therefore fetch results using something like this (using Underscore):

var results = _.chain(people)
  .pluck('lastName')
  .filter(function(name) { return name.startsWith('Smith'); })
  .take(5)
  .value();

This query does a lot of stuff:

  • pluck('lastName'): iterates over the array and creates a new (potentially giant) array
  • filter(...): iterates over the new array, creating yet another (potentially giant) array
  • take(5): all that just for 5 elements!

So if performance and/or efficiency were a concern for you, you would probably not do things that way using Underscore. Instead, you'd likely go the procedural route:

var results = [];
for (var i = 0; i < people.length; ++i) {
  if (people[i].lastName.startsWith('Smith')) {
    results.push(people[i].lastName);
    if (results.length === 5) {
      break;
    }
  }
}

There—now we haven't created any extraneous arrays, and we did all of the work in one iteration. Any problems?

Well, yeah. The main problem is that this is one-off code, which isn't reusable or particularly readable. If only we could somehow leverage the expressive power of Underscore but still get the performance of the hand-written procedural solution...


That's where Lazy.js comes in! Here's how we'd write the above query using Lazy.js:

var result = Lazy(people)
  .pluck('lastName')
  .filter(function(name) { return name.startsWith('Smith'); })
  .take(5);

Looks almost identical, right? That's the idea: Lazy.js aims to be completely familiar to JavaScript devs experienced with Underscore or Lodash. Every method from Underscore should have the same name and (almost) identical behavior in Lazy.js, except that instead of returning a fully-populated array on every call, it creates a sequence object with an each method.

What's important here is that no iteration takes place until you call each, and no intermediate arrays are created. Essentially Lazy.js combines all query operations into a "sequence" that behaves quite a bit like the procedural code we wrote a moment ago. (If you ever do want an array, simply call toArray on the resulting sequence.)

Of course, unlike the procedural approach, Lazy.js lets you keep your code clean and functional, and focus on solving whatever problem you're actually trying to solve instead of optimizing array traversals.

Features

So, Lazy.js is basically Underscore with lazy evaluation. Is that it?

Nope!

Indefinite sequence generation

The sequence-based paradigm of Lazy.js lets you do some pretty cool things that simply aren't possible with Underscore's array-based approach. One of these is the generation of indefinite sequences, which can go on forever, yet still support all of Lazy's built-in mapping and filtering capabilities.

Here's an example. Let's say we want 300 unique random numbers between 1 and 1000.

Lazy.generate(Math.random)
  .map(function(e) { return Math.floor(e * 1000) + 1; })
  .uniq()
  .take(300)
  .each(function(e) { console.log(e); });

Here's a slightly more advanced example: let's use Lazy.js to make a Fibonacci sequence.

var fibonacci = Lazy.generate(function() {
  var x = 1,
      y = 1;
  return function() {
    var prev = x;
    x = y;
    y += prev;
    return prev;
  };
}());

// Output: [1, 1, 2, 3, 5, 8, 13, 21, 34, 55]
fibonacci.take(10).toArray();

OK, what else?

Asynchronous iteration

You've probably seen code snippets before that show how to iterate over an array asynchronously in JavaScript. But have you seen an example with functional goodness like this?

Lazy.generate(Lazy.identity)
  .async(1000) // specifies a 1-second interval between each element
  .map(function(x) { return String.fromCharCode(x + 65); })
  .take(26)
  .each(function(char) { console.log(char); });

All right... what else?

Event sequences

With indefinite sequences, we saw that unlike Underscore and Lodash, Lazy.js doesn't actually need an in-memory collection to iterate over. And asynchronous sequences demonstrate that it also doesn't need to do all its iteration at once.

Now here's a really cool combination of these two features: with a small extension to Lazy.js (lazy.browser.js, a separate file to include in browser-based environments), you can apply all of the power of Lazy.js to handling DOM events. In other words, Lazy.js lets you think of DOM events as a sequence—just like any other—and apply the usual map, filter, etc. functions on that sequence.

Here's an example. Let's say we want to handle all mousemove events on a given DOM element, and show their coordinates in one of two other DOM elements depending on location.

// First we define our "sequence" of events.
var mouseEvents = Lazy(sourceElement).on("mousemove");

// Map the Event objects to their coordinates, relative to the element.
var coordinates = mouseEvents.map(function(e) {
  var elementRect = sourceElement.getBoundingClientRect();
  return [
    Math.floor(e.clientX - elementRect.left),
    Math.floor(e.clientY - elementRect.top)
  ];
});

// For mouse events on one side of the element, display the coordinates in one place.
coordinates
  .filter(function(pos) { return pos[0] < sourceElement.clientWidth / 2; })
  .each(function(pos) { displayCoordinates(leftElement, pos); });

// For those on the other side, display them in a different place.
coordinates
  .filter(function(pos) { return pos[0] > sourceElement.clientWidth / 2; })
  .each(function(pos) { displayCoordinates(rightElement, pos); });

Anything else? Of course!

String processing

Now here's something you may not have even thought of: String.match and String.split. In JavaScript, each of these methods returns an array of substrings. If you think about it, this often means doing more work than necessary; but it's the quickest way (from a developer's standpoint) to get the job done.

For example, suppose you wanted the first five lines of a block of text. You could always do this:

var firstFiveLines = text.split("\n").slice(0, 5);

But of course, this actually splits the entire string into every single line. If the string is very large, this is quite wasteful.

With Lazy.js, we don't need to split up an entire string just to treat it as a sequence of lines. We can get the same effect by wrapping the string with Lazy and calling split:

var firstFiveLines = Lazy(text).split("\n").take(5);

This way we can read the first five lines of an arbitrarily large string (without pre-populating a huge array) and map/reduce on it just as with any other sequence.

Similarly with String.match: let's say we wanted to find the first 5 alphanumeric matches in a string. With Lazy.js, it's easy!

var firstFiveWords = Lazy(text).match(/[a-z0-9]+/i).take(5);

Piece of cake.

Stream processing

Lazy.js can wrap streams in Node.js as well.

Given any Readable Stream, you can wrap it with Lazy just as with arrays:

Lazy(stream)
  .take(5) // Read just the first 5 chunks of data read into the buffer.
  .each(processData);

For convenience, specialized helper methods for dealing with either file streams or HTTP streams are also offered. (Note: this API will probably change.)

// Read the first 5 lines from a file:
Lazy.readFile("path/to/file")
  .lines()
  .take(5)
  .each(doSomething);

// Read lines 5-10 from an HTTP response.
Lazy.makeHttpRequest("http://example.com")
  .lines()
  .drop(5)
  .take(5)
  .each(doSomething);

In each case, the elements in the sequence will be "chunks" of data most likely comprising multiple lines. The lines() method splits each chunk into lines (lazily, of course).


This library is experimental and still a work in progress.

Comments
  • Create one page API docs

    Create one page API docs

    I really like the font and styles of the API docs, but what I've learned from reviewing a lot library's is that its a lot easier to find features and get an overview of a library when the API docs are on one page instead of multiple pages.

    Good examples are: http://sugarjs.com/api http://expressjs.com/api.html http://jade-lang.com/reference/

    opened by lanwin 17
  • zip behavior

    zip behavior

    Is the following behavior intended in zip? I googled for a tiny bit and there didn't seem to be any official implementation.

    Lazy([1,2,3]).zip([1,2]) // [[1, 1], [2, 2], [3, undefined]
    Lazy([1,2]).zip([1,2,3]) // [[1, 1], [2, 2]
    
    opened by olsonpm 12
  • Allow 2-arity Array.sort-style comparators to be passed into sortBy functions

    Allow 2-arity Array.sort-style comparators to be passed into sortBy functions

    Hi, firstly thanks for such a great library. I really love it.

    I would like to provide Array.sort-style 2-arity comparator functions to sortBy functions as this method of comparing elements allows the greatest amount of flexibility (for example, reverse sorting of string elements, sorting of different types elements in the same array, or multi-level sorting)

    By 2-arity comparator functions, I mean ones which take 2 arguments and allow the implementation to return 0, -1 or +1 depending on the order, for example:

    function compare(a, b) {
      if (a is less than b by some ordering criterion)
         return -1;
      if (a is greater than b by the ordering criterion)
         return 1;
      // a must be equal to b
      return 0;
    }
    

    Backbone and some other libraries check the length of the comparator function in order to decide whether to use simple 1-arity function or comparator-style 2-arity function.

    Let me know what you think: I'm happy to have a crack at doing the work via a pull-request, but would like your feedback on the implementation first.

    opened by suprememoocow 12
  • Is it possilble to have Lazy.events (Event sequences) for value change?

    Is it possilble to have Lazy.events (Event sequences) for value change?

    lazy.js is one of the best project I've ever found. This library integrates undersocre/linq.js and RxJS with lazy evaluation advantage where I feel that a clean and complete implementation of Functional/Declarative programming paradigm is deployed; All the JS programmers should learn from this. Thank you, Dan Tao.

    Having said that, I have a question: Basically, I want to bind javascript value with DOM element behavior.

    To monitor myval change event, I could code something like:

    var monitor = function()
    {
      if(myval != myval0)
      {
          triggerEach(); // function to manipulate DOM 
      }
      myval0 = myval;
    }
    var tid = setInterval(function()
    {
        monitor();
    },100);
    

    Is it possilble to have Lazy.events like this??

    opened by stken2050 12
  • Something to consider

    Something to consider

    opened by jdalton 12
  • TypeScript

    TypeScript

    Hi, I really like this library but why aren't you adding no typescript definitions? Typescript is very popular now and I personally am using only libs with definitions provided.

    opened by daniell0gda 10
  • Feature request: non-recursive flatten

    Feature request: non-recursive flatten

    I want to be able to join a sequence of sequences into a single sequence.

    flatten() does this, but it does so recursively. I feel like most of the time this is the wrong design.

    I would prefer a function that behaves like this:

    Lazy([Lazy([1, 2]), Lazy([3])]).concatAll()
    // -> sequence: [1, 2, 3]
    
    Lazy([Lazy([Lazy([1, 2]), Lazy([3, 4])]), Lazy([Lazy([5, 6])])]).concatAll()
    // -> sequence: [[1, 2], [3, 4], [5, 6]]
    
    Lazy([Lazy([1, 2]), 3]).concatAll()
    // error: '3' is not a sequence.
    

    Unfortunately there doesn’t seem to be a good way of doing this with lazy.js at the moment.

    I’m hoping that the reason that I want to be able to do this is self-evident, but if you want I can explain myself further. If I do be forewarned that there is a significant danger that I will start rambling about type theory :).

    opened by djcsdy 10
  • Support object streams

    Support object streams

    You appear to be assuming that the readable stream you get is going to give you strings (and has a setEncoding method) but it should just take whatever the body is.

    var lazy = require('lazy.js')
      , request = require('request')
      , jsonstream = require('JSONstream')
      ;
    
    var json = request('http://isaacs.iriscouch.com/registry/_all_docs?include_docs=true')
      .pipe(jsonstream.parse(['rows', true]))
    
    lazy(json).each(function (x) {console.log(x)})
    

    results in

    /Users/mikeal/Documents/git/npmetrics/node_modules/lazy.js/lazy.node.js:42
        stream.setEncoding(encoding);
               ^
    TypeError: Object #<Stream> has no method 'setEncoding'
        at /Users/mikeal/Documents/git/npmetrics/node_modules/lazy.js/lazy.node.js:42:12
        at Sequence.StreamedSequence.openStream (/Users/mikeal/Documents/git/npmetrics/node_modules/lazy.js/lazy.node.js:22:3)
        at Sequence.StreamedSequence.each (/Users/mikeal/Documents/git/npmetrics/node_modules/lazy.js/lazy.node.js:35:8)
        at Object.<anonymous> (/Users/mikeal/Documents/git/npmetrics/index.js:9:12)
        at Module._compile (module.js:456:26)
        at Object.Module._extensions..js (module.js:474:10)
        at Module.load (module.js:356:32)
        at Function.Module._load (module.js:312:12)
        at Function.Module.runMain (module.js:497:10)
        at startup (node.js:119:16)
    
    opened by mikeal 10
  • Breaking changes in 0.x release:

    Breaking changes in 0.x release: "You cannot wrap null, undefined, or primitive values using Lazy"

    Thanks a ton for keeping up the good work! We've just pulled 0.2 release and found that I breaks many cases that we use.

    The culprit is https://github.com/dtao/lazy.js/blob/master/lazy.js#L94 and the You cannot wrap null, undefined, or primitive values using Lazy error. This check makes API not compatible with 0.1 release.

    Our general use pattern is like so:

    results = MongoCollection.find(...)
    results = lazy(results).do().stuff().toArray()
    

    Often results would be null based on the find() query but because we used lazy 0.1 we would always have at least an empty array in the end.

    0.2 breaks this pattern.

    What are the thoughts behind it and is this a final call on the API?

    opened by alexgorbatchev 9
  • .map()'s second argument contains the element too, instead of the index

    .map()'s second argument contains the element too, instead of the index

    Given this I expecting the consecutive array index in i.

    Lazy({key1: "value1", key2: "value2"})
      .keys()
      .map((key, i) => {
        console.log(key, i);
      })
      .value();
    

    I am expecting this output:

    key1 0
    key2 1
    

    But I am getting also the element in i:

    key1 key1
    key2 key2
    
    opened by aldipower 6
  • Cannot use `get` on normal Sequence

    Cannot use `get` on normal Sequence

    I'm new to Lazy.js, so sorry if there are technical reasons for this, but I expected the following to work:

    Lazy("https://a.website.com/something/89885/").split('/').reverse().get(1);
    

    Is there an equivalent function that steps through a non-array to get the value at this index?

    opened by rhys-vdw 6
  • Bump lodash from 4.2.1 to 4.17.19

    Bump lodash from 4.2.1 to 4.17.19

    Bumps lodash from 4.2.1 to 4.17.19.

    Release notes

    Sourced from lodash's releases.

    4.17.16

    Commits
    Maintainer changes

    This version was pushed to npm by mathias, a new releaser for lodash since your current version.


    Dependabot compatibility score

    Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting @dependabot rebase.


    Dependabot commands and options

    You can trigger Dependabot actions by commenting on this PR:

    • @dependabot rebase will rebase this PR
    • @dependabot recreate will recreate this PR, overwriting any edits that have been made to it
    • @dependabot merge will merge this PR after your CI passes on it
    • @dependabot squash and merge will squash and merge this PR after your CI passes on it
    • @dependabot cancel merge will cancel a previously requested merge and block automerging
    • @dependabot reopen will reopen this PR if it is closed
    • @dependabot close will close this PR and stop Dependabot recreating it. You can achieve the same result by closing it manually
    • @dependabot ignore this major version will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself)
    • @dependabot ignore this minor version will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself)
    • @dependabot ignore this dependency will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself)
    • @dependabot use these labels will set the current labels as the default for future PRs for this repo and language
    • @dependabot use these reviewers will set the current reviewers as the default for future PRs for this repo and language
    • @dependabot use these assignees will set the current assignees as the default for future PRs for this repo and language
    • @dependabot use this milestone will set the current milestone as the default for future PRs for this repo and language

    You can disable automated security fix PRs for this repo from the Security Alerts page.

    dependencies 
    opened by dependabot[bot] 0
  • Attempt to fix #222

    Attempt to fix #222

    Refactor IndexedConcatenatedSequence to use an ArrayLikeSequence for other property. This force concat(ArrayLikeSequence) to return an ArrayLikeSequence instead of a Sequence and indirectly fix #222

    opened by b4nst 0
  • unshift return a Sequence instead of an ArrayLikeSequence

    unshift return a Sequence instead of an ArrayLikeSequence

    Code to reproduce

    const Lazy = require('lazy.js');
    const als = Lazy([1]);
    console.log(als.unshift(3) instanceof Lazy.ArrayLikeSequence);
    

    expected result: true actual result: false

    This is due to the use of concat with an ArrayLikeSequence inside unshift. One dirty way to fix it will be to use toArray() before concat. Although I think it should be wiser to refactor IndexedConcatenatedSequence in order to accept ArrayLikeSequence.

    opened by b4nst 0
  • Support for native asynchronous generators

    Support for native asynchronous generators

    async function* iterateDir(dir) {
        let list = await fs.readdir(dir); // fs-promise implementation of readdir
        for (let file of list) {
            yield file;
        }
    }
    
    Lazy.generate(iterateDir('./out'))
      .async(1000) // specifies a 1-second interval between each element
      .map(function(x) { return String.fromCharCode(x + 65); })
      .take(26)
      .each(function(char) { console.log(char); });
    
    opened by ghost 1
Owner
Dan Tao
Head of Engineering for Bitbucket at @atlassian
Dan Tao
A work-in-progress HTML sanitizer that strives for: performance like window.Sanitizer, readiness like DOMPurify, and ability to run in a WebWorker like neither of those.

Amuchina A work-in-progress HTML sanitizer that strives for: performance like window.Sanitizer, readiness like DOMPurify, and ability to run in a WebW

Fabio Spampinato 9 Sep 17, 2022
He is like Batman, but for Node.js stack traces

Stackman Give Stackman an error and he will give an array of stack frames with extremely detailed information for each frame in the stack trace. With

Thomas Watson 242 Jan 1, 2023
Like codepen and jsbin but works offline.

Like codepen and jsbin but works offline.

EGOIST 1.1k Jan 2, 2023
Like JSX, but native and fast

esx High throughput React Server Side Rendering For a simplified example of esx in action, check out esx-demo. esx is designed to be a high speed SSR

ESX 645 Jan 2, 2023
⚡ Something like react server components, but web workers instead of a server

react-worker-components-plugin ⚡ something like react server components, but web workers instead of a server react-worker-components-plugin is a plugi

M. Bagher Abiat 101 Nov 14, 2022
Like Obsidian Publish but for self-hosting. Plugin integrations for dataview, admonition, and more.

Obsidian Export Obsidian Publish is great but lacks support for many of the plugins we Obsidian addicts have grown accustomed to — in particular Datav

null 12 Nov 28, 2022
Like JSON-RPC, but supports streaming.

Earthstar Streaming RPC Similar to JSON-RPC, but also supports streaming (soon). Written to be used in Earthstar (github, docs). Table of Contents Usa

Earthstar Project 5 Feb 10, 2022
Word guessing game like Wordle but to compete with your friends

Battle your friends in a word guessing game WarWordly is an Open Source and Free to Play multiplayer game inspired in the famous Wordle. The idea is t

Nico Andrade 28 Dec 7, 2022
Enables creating databases based on files in Obsidian - like Dataview, but with editing!

Obsidian Database Plugin Do you like Dataview plugin for Obsidian? This one is taking Dataview to next level, but not only allowing you to view the da

Łukasz Tomaszkiewicz 115 Jan 4, 2023
📦 Writing Express but feel like Spring Boot

Springpress Custom top-level framework of Express.js, especially on TypeScript. Springpress provides expressjs utilities out of the box, lets you deep

Vectier 8 Oct 14, 2022
Like useReducer, but runs in a worker.

useWorkerizedReducer useWorkerizedReducer is like useReducer, but the reducer runs in a worker. This makes it possible to place long-running computati

Surma 221 Dec 1, 2022
like vite but different🦕

HotBun vite like hot es module replacement but different only one request per bundle zero requests on updating module no build step dev == prod using

Erik Siefken 14 Oct 21, 2022
Javascript library for switching fixed elements on scroll through sections. Like Midnight.js, but without jQuery

Library for Switching Fixed Elements on Scroll Sometimes designers create complex logic and fix parts of the interface. Also they colour page sections

Vladimir Lysov 38 Sep 19, 2022
Simba is a city like Florence, Vienna, or San Francisco but built via the internet.

Simba City Project setup Duplicate env.example and .env.test.example and rename to .env and .env.test Firebase Authentication We are going to use fire

Simba City 48 Dec 15, 2022
Customizable masonry Flatlist. it just behave like Flatlist but using ScrollView behind the scene

Would you like to support me? react-native-masonry-grid Customizable masonry Flatlist. it just behave like Flatlist but using ScrollView behind the sc

Numan 5 Sep 7, 2022
1KB lightweight, fast & powerful JavaScript templating engine with zero dependencies. Compatible with server-side environments like node.js, module loaders like RequireJS and all web browsers.

JavaScript Templates Contents Demo Description Usage Client-side Server-side Requirements API tmpl() function Templates cache Output encoding Local he

Sebastian Tschan 1.7k Jan 3, 2023
A few simple, but solid patterns for responsive HTML email templates and newsletters. Even in Outlook and Gmail.

Cerberus Responsive Email Patterns Coding regular emails is hard enough by itself. Making them responsive shouldn’t add to the headache. A few simple,

Ted Goas 4.6k Dec 28, 2022
DOMPurify - a DOM-only, super-fast, uber-tolerant XSS sanitizer for HTML, MathML and SVG. DOMPurify works with a secure default, but offers a lot of configurability and hooks. Demo:

DOMPurify DOMPurify is a DOM-only, super-fast, uber-tolerant XSS sanitizer for HTML, MathML and SVG. It's also very simple to use and get started with

Cure53 10.2k Jan 7, 2023
💾 Offline storage, improved. Wraps IndexedDB, WebSQL, or localStorage using a simple but powerful API.

localForage localForage is a fast and simple storage library for JavaScript. localForage improves the offline experience of your web app by using asyn

localForage 21.5k Jan 4, 2023
💾 Offline storage, improved. Wraps IndexedDB, WebSQL, or localStorage using a simple but powerful API.

localForage localForage is a fast and simple storage library for JavaScript. localForage improves the offline experience of your web app by using asyn

localForage 21.5k Jan 1, 2023