cons: Data Structures from Nothing
Published: 2022-06-11 03:03:03

## Preface

There is an interesting data structure in lisp, Cons, which uses function closures to encapsulate a two-tuple data structure and build any other data structure needed on top of it. Before this, although I knew about closures and other concepts and had written some higher-order functions, I had not thought that I could use functions to build data structures, and I will explain how to do that in ts below.

> [lisp cons's wiki](https://en.wikipedia.org/wiki/Cons)

The basic definition is as follows

$$
c=cons(a,b)\\\
car(c)=a\\\
cdr(c)=b
$$

## Initial Implementation

The cons initial implementation is simple and can be implemented in just a few lines of code. As you can see, it simply uses closures to bind the arguments a and b and return them when the function is executed.

```ts
function cons(a: number, b: number): (n: number) => number {
  return (n: number) => (n === 0 ? a : b)
}
function car(cons: (n: number) => number) {
  return cons(0)
}
function cdr(cons: (n: number) => number) {
  return cons(1)
}
```

It can be used in the following way

```ts
const n = cons(1, 2)
console.log(car(n)) // 1
console.log(cdr(n)) // 2
```

To make it look a little more practical, first supplement its type with a generic type

```ts
export interface Cons<A, B> {
  (s: 0): A
  (s: 1): B
  _0: A
  _1: B
}

export function cons<A, B>(a: A, b: B): Cons<A, B> {
  return ((s: 0 | 1) => (s === 0 ? a : b)) as any
}
export function car<T extends Cons<any, any>>(cons: T): T['_0'] {
  return cons(0)
}
export function cdr<T extends Cons<any, any>>(cons: T): T['_1'] {
  return cons(1)
}
```

Verify it with the following unit test

```ts
it('cons', () => {
  const c = cons('hello', 1)
  expect(car(c) as string).toBe('hello')
  expect(cdr(c) as number).toBe(1)
})
```

It doesn't look like much, but it will be used below (the simplest binary structure) to combine into more complex data structures like chained tables and trees.

## Chain Tables

In fact, once you have a binary, you can go on to combine any data structure you need, and a linked table is a simple example of this.
Here the car part of cons stores the value of the current node in the chain table, and cdr stores a pointer to the next node.

![1654666418817](https://blog.rxliuli.com/resource/85214de926184bf895c289404a0d8853.png)

```ts
type List<T> = Cons<T, List<T>> | null
function list(): null
function list<T>(.. .args: T[]): Exclude<List<T>, null>
function list<T>(... .args: T[]): List<T> {
  function iter(i: number): List<T> {
    return i === args.length ? null : cons(args[i], iter(i + 1))
  }
  return iter(0)
}
```

Here is a chain table storing 4 elements

```ts
list(1, 2, 3, 4)
```

It will be a cons nested call procedure

```ts
cons(1, cons(2, cons(3, cons(4, null))))
```

![1654666409802](https://blog.rxliuli.com/resource/fede3624da9245e581f7c06ce5ee557a.png)

You can write `map/filter/reduce` functions for it

map/filter is nothing complicated, just recursive processing of each node

```ts
function map<T, R>(list: List<T>, f: (i: T) => R): List<R> {
  return list === null ? null : (cons(f(car(list)), map(cdr(list)!, f)) as any)
}
function filter<T>(list: List<T>, f: (i: T) => boolean): List<T> {
  return list === null
    ? null
    : f(car(list))
    ? cons(car(list), filter(cdr(list), f))
    : filter(cdr(list), f)
}
```

The following diagram uses map processing

![1654667371418](https://blog.rxliuli.com/resource/84dd1713ca704ed8946a142fcb6bf4f5.png)

reduce is a little special in that it is an iterative recursion

```ts
function reduce<T>(list: List<T>, f: (res: T, v: T, i: number) => T): T
function reduce<T, R>(
  list: List<T>,
  f: (res: R, v: T, i: number) => R,
  init: R,
): R
function reduce<T, R>(
  list: List<T>,
  f: (res: R, v: T, i: number) => R,
  init?: R,
): R {
  function iter(list: List<T>, init: R, i: number): R {
    return list === null ? init : iter(cdr(list)!, f(init, car(list), i), i + 1)
  }
  return init! == undefined
    ? iter(list, init, 0)
    : iter(cdr(list!), car(list!) as unknown as R, 1)
}
```

It can be used in the following way

```ts
const res = reduce(
  map(
    filter(list(1, 2, 3, 4), (i) => i % 2 === 0),
    (i) => i * 2,
  ),
  (a, b) => a + b,
  0,
)
console.log(res) // 12
```

In theory, it should be easy to convert map/filter to a higher-order function based on reduce, for example the following code implements map/filter based on reduce in js

```ts
function map<T, R>(list: T[], f: (i: T) => R): R[] {
  return list.reduce((res, i) => {
    res.push(f(i))
    return res
  }, [] as R[])
}
function filter<T>(list: T[], f: (i: T) => boolean): T[] {
  return list.reduce((res, i) => {
    if (f(i)) {
      res.push(i)
    }
    return res
  }, [] as T[])
}
```

If one wishes to base the map/filter implementation of the linked table here on reduce, this is currently not possible. Note that the above code uses push, which is a **modify** operation and is more recommended for immutable state in functional programming. So, is there some way to do it even without changing the state? I will try to implement it.

```ts
function concat<T>(a: List<T>, b: List<T>): List<T> {
  return a === null ? b : cons(car(a), concat(cdr(a), b))
}
function mapForReduce<T, R>(arr: List<T>, f: (i: T) => R): List<R> {
  return reduce(arr, (res, i) => concat(res, list(f(i))), null as List<R>)
}
function filterForReduce<T>(arr: List<T>, f: (i: T) => boolean): List<T> {
  return reduce(
    arr,
    (res, i) => (f(i) ? concat(res, list(i)) : res),
    null as List<T>,
  )
}
```

Of course, someone might say: Isn't it inefficient to concatenate two chains every time you iterate over an element? Yes, it is indeed very inefficient, so another slightly odd approach can be used.

```ts
function mapForReduce<T, R>(list: List<T>, f: (i: T) => R): List<R> {
  return reduce(
    list,
    (res, i) => {
      return i === null ? res : (next) => res(cons(f(i), next))
    },
    (r: List<R>) => r,
  )(null)
}
function filterForReduce<T>(list: List<T>, f: (i: T) => boolean): List<T> {
  return reduce(
    list,
    (res, i) => (f(i) ? (next) => res(cons(i, next)) : res),
    (r: List<T>) => r,
  )(null)
}
```

Admittedly, this approach doesn't seem to accumulate a stack on each recursion, but it also wraps the function res over and over, and eventually passing in a single argument to call it all at once when reduce ends doesn't solve the fundamental problem, so the following implementation of `setCar/setCdr`

## Support for modifying car/cdr

To implement set, you actually have to modify the parameters of the closure binding.

```ts
export interface Cons<A, B> {
  (s: 'car'): A
  (s: 'cdr'): B
  (s: 'setCar'): (v: A) => void
  (s: 'setCdr'): (v: B) => void
  _0: A
  _1: B
}

export function cons<A, B>(a: A, b: B): Cons<A, B> {
  return ((s: string) =>
    s === 'car'
      ? a
      : s === 'cdr'
      ? b
      : s === 'setCar'
      ? (v: A) => (a = v)
      : (v: B) => (b = v)) as any
}
export function car<T extends Cons<any, any>>(cons: T): T['_0'] {
  return cons('car')
}
export function cdr<T extends Cons<any, any>>(cons: T): T['_1'] {
  return cons('cdr')
}
export function setCar<T extends Cons<any, any>>(cons: T, v: T['_0']): void {
  return cons('setCar')(v)
}
export function setCdr<T extends Cons<any, any>>(cons: T, v: T['_1']): void {
  return cons('setCdr')(v)
}
```

This can be verified using the following unit test

```ts
it('cons', () => {
  const c = cons('hello', 1)
  expect(car(c) as string).toBe('hello')
  expect(cdr(c) as number).toBe(1)
  setCar(c, 'liuli')
  setCdr(c, 2)
  expect(car(c) as string).toBe('liuli')
  expect(cdr(c) as number).toBe(2)
})
```

However, an alternative implementation is proposed in the course that uses a higher-order function implementation entirely, without using internal judgments in cons.

```ts
export interface Cons<A, B> {
  (m: (a: A, b: B, setA: (a: A) => void, setB: (b: B) => void) => any): any
  _0: A
  _1: B
}

export function cons<A, B>(a: A, b: B): Cons<A, B> {
  return ((
    m: (a: A, b: B, setA: (a: A) => void, setB: (b: B) => void) => any,
  ) =>
    m(
      a,
      b,
      (n: A) => (a = n),
      (n: B) => (b = n),
    )) as any
}
export function car<T extends Cons<any, any>>(cons: T): T['_0'] {
  return cons((a) => a)
}
export function cdr<T extends Cons<any, any>>(cons: T): T['_1'] {
  return cons((_a, b) => b)
}
export function setCar<T extends Cons<any, any>>(
  cons: T,
  value: T['_0'],
): void {
  cons((_a, _b, setA) => setA(value))
}
export function setCdr<T extends Cons<any, any>>(
  cons: T,
  value: T['_1'],
): void {
  cons((_a, _b, _setA, setB) => setB(value))
}
```

Now, re-implement mapForReduce/filterForReduce

```ts
function mapForReduce<T, R>(list: List<T>, f: (i: T) => R): List<R> {
  const init = cons(null, null) as unknown as List<R>
  reduce(
    list,
    (curr, i) => {
      const next = cons(f(i), null) as List<R>
      setCdr(curr!, next)
      return next
    },
    init,
  )
  return cdr(init!)
}
function filterForReduce<T>(list: List<T>, f: (i: T) => boolean): List<T> {
  const init = cons(null, null) as unknown as List<T>
  reduce(
    list,
    (curr, i) => {
      if (!f(i)) {
        return curr
      }
      const next = cons(i, null) as List<T>
      setCdr(curr!, next)
      return next
    },
    init,
  )
  return cdr(init!)
}
```

For the code `mapForReduce(list(1, 2, 3, 4), i => i)` the iterative process graph

| i      | curr         | init                             |
| ------ | ------------ | -------------------------------- | --- |
| 1      | (null, null) | (null, null)                     |
| 2      | (1, null)    | (null, (1, null))                |
| 3      | (2, null)    | (null, (1, (2, null)))           |
| 4      | (3, null)    | (null, (1, (2, (3, null))))      | )   |
| return |              | (null, (1, (2, (3, (4, null))))) | ))  |

As you can see, each time you modify the previous node, and use cons to construct a new node with the current value as the next iteration, you end up traversing the entire list and making a copy of the entire list.

## Trees

Since you can construct a chain table with cons, you can also construct a tree structure by defining a tree structure.

A node is composed of a value and possible children, so we define a tree node like this.

```ts
cons(value, list(sub1, sub2, ...subN))
```

![1654739911536](https://blog.rxliuli.com/resource/a1c112afef614305907391dcf9ee1434.png)

The implementation is very simple

```ts
type Tree<T> = Cons<T, List<Tree<T>>>
function tree<T>(value: T, children: List<Tree<T>>> = null) {
  return cons(value, children)
}

const t = tree(1, list(tree(2, list(tree(3)))), tree(4)))
expect(car(t)).toBe(1)
expect(car(car(cdr(t)!))) .toBe(2)
```

It is also possible to implement map/filter/reduce with a tree structure, and you can see that the line implementations are simple list-based map/filter implementations.

```ts
function treeReduce<T, R>(tree: Tree<T>, f: (r: R, v: T) => R, init: R): R {
  init = f(init, car(tree))
  map(cdr(tree), (i) => {
    init = treeReduce(i, f, init)
  })
  return init
}
function treeMap<T, R>(tree: Tree<T>, f: (v: T) => R): Tree<R> {
  return cons(
    f(car(tree)),
    map(cdr(tree)! , (i) => treeMap(i, f)),
  )
}
function treeFilter<T>(tree: Tree<T>, f: (v: T) => boolean): Tree<T> | null {
  if (!f(car(tree))) {
    return null
  }
  return cons(
    car(tree),
    filter(
      map(cdr(tree)! , (i) => treeFilter(i, f)! ,
      (i) => i ! == null,
    ),
  )
}
```

Test it

```ts
const t = tree(1, list(tree(2, list(tree(3)))), tree(4)))
it('basic', () => {
  expect(car(t)).toBe(1)
  expect(car(car(cdr(t)!))) .toBe(2)
})
it('treeReduce', () => {
  expect(treeReduce(t, (r, i) => [. .r, i], [] as number[])).toEqual([
    1, 2, 3, 4,
  ])
})
it('treeMap', () => {
  expect(
    treeReduce(
      treeMap(t, (i) => i * 2),
      (r, i) => [. .r, i],
      [] as number[],
    ),
  ).toEqual([1, 2, 3, 4].map((i) => i * 2))
})
it('treeFilter', () => {
  const res = treeFilter(t, (i) => i % 2 === 1)
  expect(treeReduce(res!, (r, i) => [. .r, i], [] as number[])).toEqual([1])
})
```

## Conclusion

lisp is a seemingly simple language, because it seems to be all about expressions, and it doesn't offer as many ways to construct complex data (like object objects, structs, classes, etc.) as modern languages. But as you can see here, it is possible to construct arbitrarily complex data structures by supporting the simplest complex data, and the building blocks of all complex data are so simple as to be unbelievable.

> In fact, it is so easy to write a lisp toy parser and runtime using ts that I will demonstrate it later.