npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2024 – Pkg Stats / Ryan Hefner

malc

v1.0.7

Published

Make a lambda calculus

Downloads

6

Readme

malcjs - Make a Lambda Calculus

An npm package version of the malc project for JavaScript, including a custom Node.js REPL.

Getting started

  1. Clone this repo.
  2. npm i to install dependencies.
  3. npm test to run the tests.
  4. npm start or ./malc to start the REPL.

Usage

The best way to learn your lambdas is to play with them in the REPL. This package provides a lightweight Node.js REPL implementation that supplies all of the included lambda functions and conversion utils as globals. You can therefore do sessions like this:

λ> lst = LIST_ELEMENT(ONE)(LIST_ELEMENT(TWO)(LIST_ELEMENT(THREE)(LIST_ELEMENT(FOUR)(EMPTY_LIST))))
[Function (anonymous)]
λ> toArrayInt(MAP(PLUS(ONE))(lst))
[ 2, 3, 4 ]

The built-in underscore _ variable is useful for working with lambdas and translating them to and from JavaScript without having to resort to excessive wrapping of expressions. For example:

λ> LIST_ELEMENT(ONE)(LIST_ELEMENT(TWO)(LIST_ELEMENT(THREE)(LIST_ELEMENT(FOUR)(LIST_ELEMENT(FIVE)(EMPTY_LIST)))))
[Function (anonymous)]
λ> LIST_ELEMENT(_)(LIST_ELEMENT(_)(LIST_ELEMENT(_)(EMPTY_LIST)))
[Function (anonymous)]
λ> MAP(REVERSE)(_)
[Function (anonymous)]
λ> toArray(_).map(toArrayInt)
[ [ 5, 4, 3, 2, 1 ], [ 5, 4, 3, 2, 1 ], [ 5, 4, 3, 2, 1 ] ]

REPL help commands

  • .malc - see a list of the predefined lambda functions available in the global scope.
  • .malc <function> - see the definition of the specified function, if it exists.
  • .utils - see a list of the utility functions for converting lambdas to and from JavaScript.
  • .utils <function> - see the definition of the specified utility function, if it exists.

Learn You a Lambda Calculus

Malc is a learning tool. If you get too ambitious with these functions, they will blow up in your face. Try a complex operation on strings, and see what happens (strings are encoded as numbers and working with large lambda numbers is super inefficient). But you can nevertheless get a real feeling for how any computation, in principle, can be modeled in lambda calculus by composing functions together into more complex expressions. Just stick to basic examples.

Starting from nothing—using no built-in JavaScript features other than function application—we can encode numbers, branching, recursion, and even data structures in this interpretation of the first, smallest, and simplest programming language.

The basic building block of this implementation of malc is the JavaScript arrow function. Since JavaScript has first class functions, and these are easily written using arrow syntax, this popular language provides a simple and familiar environment in which to experiment with lambda calculus—if you can stand all the parentheses.

Try completing the included worksheet, which also offers a basic walkthrough of lambda calculus and functional programming concepts.

If that's easy enough, you can work on 9 Malc Problems. This project steals liberally from the Haskell Prelude, and you are encouraged to do the same in solving these.

Consider using a TDD approach to develop new lambdas. Write tests first, following the pattern in this library, and then try to write the functions themselves. For example:

Write a function that returns the prefix of a list of a given length and returns an empty list if the given length is 0 or when passed an empty list.

The output in the REPL should look like this:

λ> LIST_ELEMENT(ONE)(LIST_ELEMENT(TWO)(LIST_ELEMENT(THREE)(EMPTY_LIST)))
[Function (anonymous)]
λ> TAKE(TWO)(_)
[Function (anonymous)]
λ> toArrayInt(_)
[ 1, 2 ]
λ> TAKE(TWO)(EMPTY_LIST)
[Function (anonymous)]
λ> toArray(_)
[]

Here is the test we can write:

describe("TAKE", () => {
  it("returns the prefix of a list of a given length", () => {
    const lst = λ.LIST_ELEMENT(λ.ONE)(
      λ.LIST_ELEMENT(λ.TWO)(λ.LIST_ELEMENT(λ.THREE)(λ.EMPTY_LIST))
    );
    const expr = λ.TAKE(λ.TWO)(lst);
    expect(toArrayInt(expr)).toEqual([1, 2]);
  });
  it("returns an empty list if the given length is 0", () => {
    const lst = λ.LIST_ELEMENT(λ.ONE)(
      λ.LIST_ELEMENT(λ.TWO)(λ.LIST_ELEMENT(λ.THREE)(λ.EMPTY_LIST))
    );
    const expr = λ.TAKE(λ.ZERO)(lst);
    expect(toArray(expr)).toEqual([]);
  });
  it("returns an empty list when passed an empty list", () => {
    const expr = λ.TAKE(λ.TWO)(λ.EMPTY_LIST);
    expect(toArray(expr)).toEqual([]);
  });
});

And finally, the implementation itself:

const TAKE = FIX((r) => (n) => (xs) =>
  IF_THEN_ELSE(LESS_THAN_OR_EQUAL(n)(ZERO))(EMPTY_LIST)(
    IF_THEN_ELSE(IS_EMPTY(xs))
  )(EMPTY_LIST)((x) => LIST_ELEMENT(HEAD(xs))(r(MINUS(n)(ONE))(TAIL(xs)))(x))
);

To write this, we can use basic Haskell functions as inspiration:

take                   :: Int -> [a] -> [a]
take n _      | n <= 0 =  []
take _ []              =  []
take n (x:xs)          =  x : take (n-1) xs

The first line is the type signature, which says that this function takes an Int value and a list of elements of some type a and returns a new list of elements of the same type a. Since we're working in plain lambda calculus, we don't care about type checking, but the signature gives us useful information for writing our own function.

Note the arrows. In Haskell, all functions are "curried" by default, so actually this is a function of type Int -> ([a] -> [a]), that is, it takes an Int and returns another unary function of type [a] -> [a]. Currying our functions allows us to take advantage of "partial application," or applying a multi-parameter function to one argument at a time. We can reproduce this behavior in JavaScript with consecutive arrow functions, one for each parameter.

The next three lines that all start with take do pattern matching on the input to the function. The left side of each equation is the input and the right side is the output for that given input. We can't do this in JavaScript (yet), but we can fake it with ugly, nested IF_THEN_ELSE expressions.

The vertical bar in the second line is a pattern guard, which says "the expression on the right should only be evaluated if, given n _ (where _ means any value), n is less than or equal to 0.

The last line is where the real action takes place—the real action, that is, of recursion. Haskell doesn't use control structures for branching or looping. Instead, conditions are evaluated as normal parts of expressions and looping is done with implicit (e.g., with MAP or FOLD) or explicit recursion. First, with the (x:xs), the argument list is destructured into its head and tail. The : operator is the infix list constructor, equivalent to LIST_ELEMENT. On the right side of the equation, a new list is constructed from the original head, with the tail generated through the recursive call.

Likewise in malc, we only have an IF_THEN_ELSE function for branching, but for recursion we have to use a special function called FIX. FIX is a strict version of the Y combinator (sometimes called the Z combinator). This function takes a function as an argument and returns a recursive version of that same function. Technically, FIX(f) returns the fixpoint of f. If that fixpoint is also a function, then we get something like FIX(f) = g, where g is the fixpoint of f such that f(g) = g and g = f(g), that is, another call to f. Since FIX(f) = g = f(g), we can substitute f(g) anywhere we see g:

f(g) = g
f(g) = f(g)
f(g) = f(f(g))
f(g) = f(f(f(g)))
f(g) = f(f(f(f(g))))
f(g) = f(f(f(f(f(g)))))
...

Using FIX, this is equivalent to:

FIX(f) = g
= f(FIX(f))
= f(f(FIX(f)))
= f(f(f(FIX(f))))
...

And that's how you do recursion in lambda calculus. To use FIX in practice, first translate the recursive function directly into malc lambdas:

const TAKE = (n) => (xs) =>
  IF_THEN_ELSE(LESS_THAN_OR_EQUAL(n)(ZERO))(EMPTY_LIST)(
    IF_THEN_ELSE(IS_EMPTY(xs))
  )(EMPTY_LIST)(LIST_ELEMENT(HEAD(xs))(TAKE(MINUS(n)(ONE))(TAIL(xs))));

Then, wrap the whole function in FIX. Add a parameter, e.g. r (or Y), to the beginning of the definition and replace all recursive calls to the original function with that variable:

const TAKE = FIX((r) => (n) => (xs) =>
  IF_THEN_ELSE(LESS_THAN_OR_EQUAL(n)(ZERO))(EMPTY_LIST)(
    IF_THEN_ELSE(IS_EMPTY(xs))
  )(EMPTY_LIST)(LIST_ELEMENT(HEAD(xs))(r(MINUS(n)(ONE))(TAIL(xs))));

Finally, wrap the recursive branch of the function in a dummy closure by adding a parameter (e.g. x) to the front of that branch and applying the last function in the branch to the same parameter:

const TAKE = FIX((r) => (n) => (xs) =>
  IF_THEN_ELSE(LESS_THAN_OR_EQUAL(n)(ZERO))(EMPTY_LIST)(
    IF_THEN_ELSE(IS_EMPTY(xs))
  )(EMPTY_LIST)((x) => LIST_ELEMENT(HEAD(xs))(r(MINUS(n)(ONE))(TAIL(xs)))(x))
);

Since JavaScript is a strictly-evaluated language, if we don't wrap the recursive branch in a dummy closure, it would always be evaluated and the function would never terminate. With the dummy closure, it's only evaluated until we hit the base case of the recursion.

For the curious, you can also substitute the original definitions for each of the combinators in TAKE, and since all we're doing is applying functions to reduce the expression to an encoded value, it will still work:

const TAKE = ((f) =>
  ((x) => f((y) => x(x)(y)))((x) => f((y) => x(x)(y))))((r) => (n) => (xs) =>
  ((p) => (x) => (y) => p(x)(y))(
    ((n) => (m) =>
      ((n) => n((m) => (x) => (y) => y)((x) => (y) => x))(
        ((n) => (m) =>
          m((n) =>
            n((p) => (z) =>
              z(((n) => (f) => (x) => f(n(f)(x)))(p((x) => (y) => x)))(
                p((x) => (y) => x)
              )
            )((z) => z((f) => (x) => x)((f) => (x) => x))((x) => (y) => y)
          )(n))(n)(m)
      ))(n)((f) => (x) => x)
  )(((x) => (y) => (p) => p(x)(y))((x) => (y) => x)((x) => (y) => x))(
    ((p) => (x) => (y) => p(x)(y))(((p) => p((x) => (y) => x))(xs))
  )(((x) => (y) => (p) => p(x)(y))((x) => (y) => x)((x) => (y) => x))((x) =>
    ((x) => (xs) =>
      ((x) => (y) => (p) => p(x)(y))((x) => (y) => y)(
        ((x) => (y) => (p) => p(x)(y))(x)(xs)
      ))(
      ((xs) => ((p) => p((x) => (y) => x))(((p) => p((x) => (y) => y))(xs)))(xs)
    )(
      r(
        ((n) => (m) =>
          m((n) =>
            n((p) => (z) =>
              z(((n) => (f) => (x) => f(n(f)(x)))(p((x) => (y) => x)))(
                p((x) => (y) => x)
              )
            )((z) => z((f) => (x) => x)((f) => (x) => x))((x) => (y) => y)
          )(n))(n)((f) => (x) => f(x))
      )(
        ((xs) => ((p) => p((x) => (y) => y))(((p) => p((x) => (y) => y))(xs)))(
          xs
        )
      )
    )(x)
  )
);