Quite the series of questions you have here. Here's my take on it:
We start with a way to construct lists
nil
is a constant which represents the empty list
cons (x, list)
constructs a new list with x
added to the front of list
// nil : List a
const nil =
(c, n) => n
// cons : (a, List a) -> List a
const cons = (x, y = nil) =>
(c, n) => c (y (c, n), x)
// list : List Number
const myList =
cons (1, cons (2, cons (3, cons (4, nil))))
console.log (myList ((x, y) => x + y, 0))
// 10
And to satisfy your golfy variadic curried interface, here is autoCons
const autoCons = (init, n) =>
{ const loop = acc => (x, n) =>
isFunction (x)
? acc (x, n)
: loop (cons (x, acc))
return loop (nil) (init, n)
}
const isFunction = f =>
f != null && f.constructor === Function && f.length === 2
const nil =
(c, n) => n
const cons = (x, y = nil) =>
(c, n) => c (y (c, n), x)
console.log
( autoCons (1) ((x,y) => x + y, 0) // 1
, autoCons (1) (2) ((x,y) => x + y, 0) // 3
, autoCons (1) (2) (3) ((x,y) => x + y, 0) // 6
, autoCons (1) (2) (3) (4) ((x,y) => x + y, 0) // 10
)
Our encoding makes it possible to write other generic list functions, like isNil
// isNil : List a -> Bool
const isNil = l =>
l ((acc, _) => false, true)
console.log
( isNil (autoCons (1)) // false
, isNil (autoCons (1) (2)) // false
, isNil (nil) // true
)
Or like length
// length : List a -> Int
const length = l =>
l ((acc, _) => acc + 1, 0)
console.log
( length (nil) // 0
, length (autoCons (1)) // 1
, length (autoCons (1) (2)) // 2
, length (autoCons (1) (2) (3)) // 3
)
Or nth
which fetches the nth item in the list
// nth : Int -> List a -> a
const nth = n => l =>
l ( ([ i, res ], x) =>
i === n
? [ i + 1, x ]
: [ i + 1, res]
, [ 0, undefined ]
) [1]
console.log
( nth (0) (autoCons ("A") ("B") ("C")) // "A"
, nth (1) (autoCons ("A") ("B") ("C")) // "B"
, nth (2) (autoCons ("A") ("B") ("C")) // "C"
, nth (3) (autoCons ("A") ("B") ("C")) // undefined
)
We can implement functions like map
and filter
for our list
// map : (a -> b) -> List a -> List b
const map = f => l =>
l ( (acc, x) => cons (f (x), acc)
, nil
)
// filter : (a -> Bool) -> List a -> List a
const filter = f => l =>
l ( (acc, x) => f (x) ? cons (x, acc) : acc
, nil
)
We can even make a program using our list which takes a list as an argument
// rcomp : (a -> b) -> (b -> c) -> a -> c
const rcomp = (f, g) =>
x => g (f (x))
// main : List String -> String
const main = letters =>
autoCons (map (x => x + x))
(filter (x => x !== "dd"))
(map (x => x.toUpperCase()))
(rcomp, x => x)
(letters)
((x, y) => x + y, "")
main (autoCons ("a") ("b") ("c") ("d") ("e"))
// AABBCCEE
Run the program in your browser below
const nil =
(c, n) => n
const cons = (x, y = nil) =>
(c, n) => c (y (c, n), x)
const isFunction = f =>
f != null && f.constructor === Function && f.length === 2
const autoCons = (init, n) =>
{ const loop = acc => (x, n) =>
isFunction (x)
? acc (x, n)
: loop (cons (x, acc))
return loop (nil) (init, n)
}
const map = f => l =>
l ( (acc, x) => cons (f (x), acc)
, nil
)
const filter = f => l =>
l ( (acc, x) => f (x) ? cons (x, acc) : acc
, nil
)
const rcomp = (f, g) =>
x => g (f (x))
const main = letters =>
autoCons (map (x => x + x))
(filter (x => x !== "dd"))
(map (x => x.toUpperCase()))
(rcomp, x => x)
(letters)
((x, y) => x + y, "")
console.log (main (autoCons ("a") ("b") ("c") ("d") ("e")))
// AABBCCEE
Sorry, my bad
Let's rewind and look at our initial List
example
// list : List Number
const myList =
cons (1, cons (2, cons (3, cons (4, nil))))
console.log
( myList ((x, y) => x + y, 0) // 10
)
We conceptualize myList
as a list of numbers, but we contradict ourselves by calling myList (...)
like a function.
This is my fault. In trying to simplify the example, I crossed the barrier of abstraction. Let's look at the types of nil
and cons
–
// nil : List a
// cons : (a, List a) -> List a
Given a list of type List a
, how do we get a value of type a
out? In the example above (repeated below) we cheat by calling myList
as a function. This is internal knowledge that only the implementer of nil
and cons
should know
// myList is a list, not a function... this is confusing...
console.log
( myList ((x, y) => x + y, 0) // 10
)
If you look back at our original implementation of List
,
// nil : List a
const nil =
(c, n) => n
// cons : (a, List a) -> List a
const cons = (x, y = nil) =>
(c, n) => c (y (c, n), x)
I also cheated you giving simplified type annotations like List a
. What is List
, exactly?
We're going to address all of this and it starts with our implementation of List
List
, take 2
Below nil
and cons
have the exact same implementation. I've only fixed the type annotations. Most importantly, I added reduce
which provides a way to get values "out" of our list container.
The type annotation for List
is updated to List a r
– this can be understood as "a list containing values of type a
that when reduced, will produce a value of type r
."
// type List a r = (r, a) -> r
// nil : List a r
const nil =
(c, n) => n
// cons : (a, List a r) -> List a r
const cons = (x, y = nil) =>
(c, n) => c (y (c, n), x)
// reduce : ((r, a) -> r, r) -> List a -> r
const reduce = (f, init) => l =>
l (f, init)
Now we can maintain List
as a sane type, and push all the wonky behavior you want into the autoCons
function. Below we update autoCons
to work with our list acc
using our new reduce
function
const autoCons = (init, n) =>
{ const loop = acc => (x, n) =>
isFunction (x)
// don't break the abstraction barrier
? acc (x, n)
// extract the value using our appropriate list module function
? reduce (x, n) (acc)
: loop (cons (x, acc))
return loop (nil) (init, n)
}
So speaking of types, let's examine the type of autoCons
–
autoCons (1) // "lambda (x,n) => isFunction (x) ...
autoCons (1) (2) // "lambda (x,n) => isFunction (x) ...
autoCons (1) (2) (3) // "lambda (x,n) => isFunction (x) ...
autoCons (1) (2) (3) (add, 0) // 6
Well autoCons
always returns a lambda, but that lambda has a type that we cannot determine – sometimes it returns another lambda of its same kind, other times it returns a completely different result; in this case some number, 6
Because of this, we cannot easily mix and combine autoCons
expressions with other parts of our program. If you drop this perverse drive to create variadic curried interfaces, you can make an autoCons
that is type-able
// autoCons : (...a) -> List a r
const autoCons = (...xs) =>
{ const loop = (acc, x = nil, ...xs) =>
x === nil
? acc
: loop (cons (x, acc), ...xs)
return loop (nil, ...xs)
}
Because autoCons
now returns a known List
(instead of the mystery unknown type caused by variadic currying), we can plug an autoCons
list into the various other functions provided by our List
module.
const c =
autoCons (1, 2, 3)
const d =
autoCons (4, 5, 6)
console.log
( toArray (c) // [ 1, 2, 3 ]
, toArray (map (x => x * x) (d)) // [ 16, 25, 36 ]
, toArray (filter (x => x != 5) (d)) // [ 4, 6 ]
, toArray (append (c, d)) // [ 1, 2, 3, 4, 5, 6 ]
)
These kind of mix-and-combine expressions is not possible when autoCons
returns a type we cannot rely upon. Another important thing to notice is the List
module gives us a place to expand its functionality. We can easily add functions used above like map
, filter
, append
, and toArray
– you lose this flexibility when trying to shove everything through the variadic curried interface
Let's look at those additions to the List
module now – as you can see, each function is well-typed and has behavior we can rely upon
// type List a r = (r, a) -> r
// nil : List a r
// cons : (a, List a r) -> List a r
// reduce : ((r, a) -> r, r) -> List a r -> r
// length : List a r -> Int
const length =
reduce
( (acc, _) => acc + 1
, 0
)
// map : (a -> b) -> List a r -> List b r
const map = f =>
reduce
( (acc, x) => cons (f (x), acc)
, nil
)
// filter : (a -> Bool) -> List a r -> List a r
const filter = f =>
reduce
( (acc,x) => f (x) ? cons (x, acc) : acc
, nil
)
// append : (List a r, List a r) -> List a r
const append = (l1, l2) =>
(c, n) =>
l2 (c, l1 (c, n))
// toArray : List a r -> Array a
const toArray =
reduce
( (acc, x) => [ ...acc, x ]
, []
)
Even autoCons
makes sense as part of our module now
// autoCons : (...a) -> List a r
const autoCons = (...xs) =>
{ const loop = (acc, x = nil, ...xs) =>
x === nil
? acc
: loop (cons (x, acc), ...xs)
return loop (nil, ...xs)
}
Add any other functions to the List
module
// nth: Int -> List a r -> a
// isNil : List a r -> Bool
// first : List a r -> a
// rest : List a r -> List a r
// ...
fold
of list. Generalized. I have edit my question, too/ – ToppleA(a)(b)(f)
how would you know whetherf
is a value to be added to the list or a reducing function? – Protoactiniumf
is just distinguished bytype of
operation – Topple