Assuming I have a bunch of functions of arity 2: f: a b -> x, g: c d -> y,
etc. up to unary function u: a -> a. What I would like to do is to chain them in such a way:
f(_, g(_, .... z(_, u(_))...)
where inside _ placeholders consecutive values from given input array will be injected. I'm trying to solve this using Ramda library.
Another, very similar problem I have is chaining the functions the same way but the _ placeholder being filled with the same value against which this composition is being executed.
To be more specific:
// 1st problem
someComposition( f(v[0], g(v[1], ..... z(v[n-1], u(v[n]))...) )(v);
// 2nd problem
someComposition2( f(v, g(v, ..... z(v, u(v))...) )(v);
Best what I could came up with for 2nd problem was, assuming all function are currable, following piece of code (hate it because of the (v) repetitions):
compose(
z(v),
...
g(v),
f(v),
u
)(v);
I tried solving it with compose, composeK, pipe, ap but none of them seem to applied to this situation or I just simply am not able to see the solution. Any help is more then welcome.
There's nothing directly built into Ramda for either of these. (Disclaimer: I'm one of the Ramda authors.) You can create composition functions like these, if you like, though:
const {tail, compose, reduce, identity, reverse} = R;
const f = (x, y) => `f(${x}, ${y})`;
const g = (x, y) => `g(${x}, ${y})`;
const h = (x, y) => `h(${x}, ${y})`;
const i = (x, y) => `i(${x}, ${y})`;
const j = (x) => `j(${x})`;
const multApply = (fns) => (...vals) => fns.length < 2
? fns[0].apply(null, vals)
: fns[0](vals[0], multApply(tail(fns))(...tail(vals)));
console.log(multApply([f, g, h, i, j])('a', 'b', 'c', 'd', 'e'));
//=> f(a, g(b, h(c, i(d, j(e)))))
const nest = compose(reduce((g, f) => (v) => f(v, g(v)), identity), reverse);
console.log(nest([f, g, h, i, j])('v')) //=> f(v, g(v, h(v, i(v, j(v)))));
<script src="//cdnjs.cloudflare.com/ajax/libs/ramda/0.25.0/ramda.js"></script>
Neither of these does any error checking for empty lists, or for an arguments list shorter than the function list (in the first case.) But other than that, they seem to fit the bill.
There's sure to be a recursive version of the second one to match the first, but this implementation is already fairly simple. I didn't readily see a version of the first as simple as the second, but it might well exist.
There's probably some handy Ramda function that I don't know of, or some super functional combinator-thingy I don't understand that makes this easy, but anyway:
You could create your own composition function to compose a list of binary functions and inject values. This function takes a list of functions and a list of arguments. It partially applies the first function it gets to the first argument and keeps on doing so until it's out of arguments, at which it returns a final composed (unary) function:
// Utils
const { add, subtract, multiply, identity, compose, isNil } = R;
const square = x => x * x;
// Our own compose method
const comp2_1 = ([f2, ...f2s ], [x, ...xs], f = identity) =>
isNil(x)
? compose(f2, f)
: comp2_1(f2s, xs, compose(f2(x), f));
// An example
const myFormula = comp2_1(
[add, subtract, multiply, square],
[10, 5, 2]);
// 3 + 10 = 13
// 13 - 5 = 8
// 8 * 2 = 16
// 16 * 16 = 256
console.log(myFormula(3));
<script src="//cdnjs.cloudflare.com/ajax/libs/ramda/0.25.0/ramda.js"></script>
This example will only work for xs.length === fs.length + 1. You might want it to be a bit more flexible by, for instance, continuing the composition even when we're out of arguments:
/* ... */
isNil(x)
? isNil(f2)
? f
: comp2_1(f2s, [], compose(f2, f))
: /* ... */
Related
I have a pure function that takes 18 arguments process them and returns an answer.
Inside this function I call many other pure functions and those functions call other pure functions within them as deep as 6 levels.
This way of composition is cumbersome to test as the top level functions,in addition to their logic,have to gather parameters for inner functions.
# Minimal conceptual example
main_function(a, b, c, d, e) = begin
x = pure_function_1(a, b, d)
y = pure_function_2(a, c, e, x)
z = pure_function_3(b, c, y, x)
answer = pure_function_4(x,y,z)
return answer
end
# real example
calculate_time_dependant_losses(
Ap,
u,
Ac,
e,
Ic,
Ep,
Ecm_t,
fck,
RH,
T,
cementClass::Char,
ρ_1000,
σ_p_start,
f_pk,
t0,
ts,
t_start,
t_end,
) = begin
μ = σ_p_start / f_pk
fcm = fck + 8
Fr = σ_p_start * Ap
_σ_pb = σ_pb(Fr, Ac, e, Ic)
_ϵ_cs_t_start_t_end = ϵ_cs_ti_tj(ts, t_start, t_end, Ac, u, fck, RH, cementClass)
_ϕ_t0_t_start_t_end = ϕ_t0_ti_tj(RH, fcm, Ac, u, T, cementClass, t0, t_start, t_end)
_Δσ_pr_t_start_t_end =
Δσ_pr(σ_p_start, ρ_1000, t_end, μ) - Δσ_pr(σ_p_start, ρ_1000, t_start, μ)
denominator =
1 +
(1 + 0.8 * _ϕ_t0_t_start_t_end) * (1 + (Ac * e^2) / Ic) * ((Ep * Ap) / (Ecm_t * Ac))
shrinkageLoss = (_ϵ_cs_t_start_t_end * Ep) / denominator
relaxationLoss = (0.8 * _Δσ_pr_t_start_t_end) / denominator
creepLoss = (Ep * _ϕ_t0_t_start_t_end * _σ_pb) / Ecm_t / denominator
return shrinkageLoss + relaxationLoss + creepLoss
end
I see examples of functional composition (dot chaining,pipe operator etc) with single argument functions.
Is it practical to compose the above function using functional programming?If yes, how?
The standard and simple way is to recast your example so that it can be written as
# Minimal conceptual example, re-cast
main_function(a, b, c, d, e) = begin
x = pure_function_1'(a, b, d)()
y = pure_function_2'(a, c, e)(x)
z = pure_function_3'(b, c)(y) // I presume you meant `y` here
answer = pure_function_4(z) // and here, z
return answer
end
Meaning, we use functions that return functions of one argument. Now these functions can be easily composed, using e.g. a forward-composition operator (f >>> g)(x) = g(f(x)) :
# Minimal conceptual example, re-cast, composed
main_function(a, b, c, d, e) = begin
composed_calculation =
pure_function_1'(a, b, d) >>>
pure_function_2'(a, c, e) >>>
pure_function_3'(b, c, y) >>>
pure_function_4
answer = composed_calculation()
return answer
end
If you really need the various x y and z at differing points in time during the composed computation, you can pass them around in a compound, record-like data structure. We can avoid the coupling of this argument handling if we have extensible records:
# Minimal conceptual example, re-cast, composed, args packaged
main_function(a, b, c, d, e) = begin
composed_calculation =
pure_function_1'(a, b, d) >>> put('x') >>>
get('x') >>> pure_function_2'(a, c, e) >>> put('y') >>>
get('x') >>> pure_function_3'(b, c, y) >>> put('z') >>>
get({'x';'y';'z'}) >>> pure_function_4
answer = composed_calculation(empty_initial_state)
return value(answer)
end
The passed around "state" would be comprised of two fields: a value and an extensible record. The functions would accept this state, use the value as their additional input, and leave the record unchanged. get would take the specified field out of the record and put it in the "value" field in the state. put would mutate the extensible record in the state:
put(field_name) = ( {value:v ; record:r} =>
{v ; put_record_field( r, field_name, v)} )
get(field_name) = ( {value:v ; record:r} =>
{get_record_field( r, field_name) ; r} )
pure_function_2'(a, c, e) = ( {value:v ; record:r} =>
{pure_function_2(a, c, e, v); r} )
value(r) = get_record_field( r, value)
empty_initial_state = { novalue ; empty_record }
All in pseudocode.
Augmented function application, and hence composition, is one way of thinking about "what monads are". Passing around the pairing of a produced/expected argument and a state is known as State Monad. The coder focuses on dealing with the values while treating the state as if "hidden" "under wraps", as we do here through the get/put etc. facilities. Under this illusion/abstraction, we do get to "simply" compose our functions.
I can make a small start at the end:
sum $ map (/ denominator)
[ _ϵ_cs_t_start_t_end * Ep
, 0.8 * _Δσ_pr_t_start_t_end
, (Ep * _ϕ_t0_t_start_t_end * _σ_pb) / Ecm_t
]
As mentioned in the comments (repeatedly), the function composition operator does indeed accept multiple argument functions. Cite: https://docs.julialang.org/en/v1/base/base/#Base.:%E2%88%98
help?> ∘
"∘" can be typed by \circ<tab>
search: ∘
f ∘ g
Compose functions: i.e. (f ∘ g)(args...; kwargs...) means f(g(args...; kwargs...)). The ∘ symbol
can be entered in the Julia REPL (and most editors, appropriately configured) by typing
\circ<tab>.
Function composition also works in prefix form: ∘(f, g) is the same as f ∘ g. The prefix form
supports composition of multiple functions: ∘(f, g, h) = f ∘ g ∘ h and splatting ∘(fs...) for
composing an iterable collection of functions.
The challenge is chaining the operations together, because any function can only pass on a tuple to the next function in the composed chain. The solution could be making sure your chained functions 'splat' the input tuples into the next function.
Example:
# splat to turn max into a tuple-accepting function
julia> f = (x->max(x...)) ∘ minmax;
julia> f(3,5)
5
Using this will in no way help make your function cleaner, though, in fact it will probably make a horrible mess.
Your problems do not at all seem to me to be related to how you call, chain or compose your functions, but are entirely due to not organizing the inputs in reasonable types with clean interfaces.
Edit: Here's a custom composition operator that splats arguments, to avoid the tuple output issue, though I don't see how it can help picking the right arguments, it just passes everything on:
⊕(f, g) = (args...) -> f(g(args...)...)
⊕(f, g, h...) = ⊕(f, ⊕(g, h...))
Example:
julia> myrev(x...) = reverse(x);
julia> (myrev ⊕ minmax)(5,7)
(7, 5)
julia> (minmax ⊕ myrev ⊕ minmax)(5,7)
(5, 7)
I wrote this sml function that allows me to display the first 5 columns of the Ascii table.
fun radix (n, base) =
let
val b = size base
val digit = fn n => str (String.sub (base, n))
val radix' =
fn (true, n) => digit n
| (false, n) => radix (n div b, base) ^ digit (n mod b)
in
radix' (n < b, n)
end;
val n = 255;
val charList = List.tabulate(n+1,
fn x => print(
"DEC"^"\t"^"OCT"^"\t"^"HEX"^"\t"^"BIN"^"\t"^"Symbol"^"\n"^
Int.toString(x)^"\t"^
radix (x, "01234567")^"\t"^
radix (x, "0123456789abcdef")^"\t"^
radix (x, "01")^"\t"^
Char.toCString(chr(x))^"\t"
)
);
But I want the header : "DEC"^"\t"^"OCT"^"\t"^"HEX"^"\t"^"BIN"^"\t"^"Symbol" to be displayed only once at the beginning, but I can't do it. Does anyone know a way to do it?
On the other hand I would like to do without the resursive call of the "radix" function. Is that possible? And is it a wise way to write this function?
I want the header : "DEC"... to be displayed only once at the beginning
Currently the header displays multiple times because it is being printed inside of List.tabulate's function, once for each number in the table. So you can move printing the header outside of this function and into a parent function.
For clarity I might also move the printing of an individual character into a separate function. (I think you have indented the code in your charList very nicely, but if a function does more than one thing, it is doing too many things.)
E.g.
fun printChar (i : int) =
print (Int.toString i ^ ...)
fun printTable () =
( print "DEC\tOCT\tHEX\tBIN\tSymbol\n"
; List.tabulate (256, printChar)
; () (* why this? *)
)
It is very cool that you found Char.toCString which is safe compared to simply printing any character. It seems to give some pretty good names for e.g. \t and \n, but hardly for every function. So if you really want to spice up your table, you could add a helper function,
fun prettyName character =
if Char.isPrint character
then ...
else case ord character of
0 => "NUL (null)"
| 1 => "SOH (start of heading)"
| 2 => "STX (start of text)"
| ...
and use that instead of Char.toCString.
Whether to print a character itself or some description of it might be up to Char.isPrint.
I would like to do without the resursive call of the "radix" function.
Is that possible?
And is it a wise way to write this function?
You would need something equivalent to your radix function either way.
Sure, it seems okay. You could shorten it a bit, but the general approach is good.
You have avoided list recursion by doing String.sub constant lookups. That's great.
How do I evaluate the function in only one of its variables, that is, I hope to obtain another function after evaluating the function. I have the following piece of code.
deff ('[F] = fun (x, y)', 'F = x ^ 2-3 * y ^ 2 + x * y ^ 3');
fun (4, y)
I hope to get 16-3y ^ 2 + 4y ^ 3
If what you want to do is to write x = f(4,y), and later just do x(2) to get -36, that is called partial application:
Intuitively, partial function application says "if you fix the first arguments of the function, you get a function of the remaining arguments".
This is a very useful feature, and very common Functional Programming Languages, such as Haskell, but even JS and Python now are able to do it. It is also possible to do this in MATLAB and GNU/Octave using anonymous functions (see this answer). In Scilab, however, this feature is not available.
Workround
Nonetheless, Scilab itself uses a workarounds to carry a function with its arguments without fully evaluating. You see this being used in ode(), fsolve(), optim(), and others:
Create a list containing the function and the arguments to partial evaluation: list(f,arg1,arg2,...,argn)
Use another function to evaluate such list and the last argument: evalPartList(list(...),last_arg)
The implementation of evalPartList() can be something like this:
function y = evalPartList(fList,last_arg)
//fList: list in which the first element is a function
//last_arg: last argument to be applied to the function
func = fList(1); //extract function from the list
y = func(fList(2:$),last_arg); //each element of the list, from second
//to last, becomes an argument
endfunction
You can test it on Scilab's console:
--> deff ('[F] = fun (x, y)', 'F = x ^ 2-3 * y ^ 2 + x * y ^ 3');
--> x = list(fun,4)
x =
x(1)
[F]= x(1)(x,y)
x(2)
4.
--> evalPartList(x,2)
ans =
36.
This is a very simple implementation for evalPartList(), and you have to be careful not to exceed or be short on the number of arguments.
In the way you're asking, you can't.
What you're looking is called symbolic (or formal) computational mathematics, because you don't pass actual numerical values to functions.
Scilab is numerical software so it can't do such thing. But there is a toolbox scimax (installation guide) that rely on a the free formal software wxmaxima.
BUT
An ugly, stupid but still sort of working solution is to takes advantages of strings :
function F = fun (x, y) // Here we define a function that may return a constant or string depending on the input
fmt = '%10.3E'
if (type(x)==type('')) & (type(y)==type(0)) // x is string is
ys = msprintf(fmt,y)
F = x+'^2 - 3*'+ys+'^2 + '+x+'*'+ys+'^3'
end
if (type(y)==type('')) & (type(x)==type(0)) // y is string so is F
xs = msprintf(fmt,x)
F = xs+'^2 - 3*'+y+'^2 + '+xs+'*'+y+'^3'
end
if (type(y)==type('')) & (type(x)==type('')) // x&y are strings so is F
F = x+'^2 - 3*'+y+'^2 + '+x+'*'+y+'^3'
end
if (type(y)==type(0)) & (type(x)==type(0)) // x&y are constant so is F
F = x^2 - 3*y^2 + x*y^3
end
endfunction
// Then we can use this 'symbolic' function
deff('F2 = fun2(y)',' F2 = '+fun(4,'y'))
F2=fun2(2) // does compute fun(4,2)
disp(F2)
I'm trying to write some code in a functional paradigm for practice. There is one case I'm having some problems wrapping my head around. I am trying to create an array of 5 unique integers from 1, 100. I have been able to solve this without using functional programming:
let uniqueArray = [];
while (uniqueArray.length< 5) {
const newNumber = getRandom1to100();
if (uniqueArray.indexOf(newNumber) < 0) {
uniqueArray.push(newNumber)
}
}
I have access to lodash so I can use that. I was thinking along the lines of:
const uniqueArray = [
getRandom1to100(),
getRandom1to100(),
getRandom1to100(),
getRandom1to100(),
getRandom1to100()
].map((currentVal, index, array) => {
return array.indexOf(currentVal) > -1 ? getRandom1to100 : currentVal;
});
But this obviously wouldn't work because it will always return true because the index is going to be in the array (with more work I could remove that defect) but more importantly it doesn't check for a second time that all values are unique. However, I'm not quite sure how to functionaly mimic a while loop.
Here's an example in OCaml, the key point is that you use accumulators and recursion.
let make () =
Random.self_init ();
let rec make_list prev current max accum =
let number = Random.int 100 in
if current = max then accum
else begin
if number <> prev
then (number + prev) :: make_list number (current + 1) max accum
else accum
end
in
make_list 0 0 5 [] |> Array.of_list
This won't guarantee that the array will be unique, since its only checking by the previous. You could fix that by hiding a hashtable in the closure between make and make_list and doing a constant time lookup.
Here is a stream-based Python approach.
Python's version of a lazy stream is a generator. They can be produced in various ways, including by something which looks like a function definition but uses the key word yield rather than return. For example:
import random
def randNums(a,b):
while True:
yield random.randint(a,b)
Normally generators are used in for-loops but this last generator has an infinite loop hence would hang if you try to iterate over it. Instead, you can use the built-in function next() to get the next item in the string. It is convenient to write a function which works something like Haskell's take:
def take(n,stream):
items = []
for i in range(n):
try:
items.append(next(stream))
except StopIteration:
return items
return items
In Python StopIteration is raised when a generator is exhausted. If this happens before n items, this code just returns however much has been generated, so perhaps I should call it takeAtMost. If you ditch the error-handling then it will crash if there are not enough items -- which maybe you want. In any event, this is used like:
>>> s = randNums(1,10)
>>> take(5,s)
[6, 6, 8, 7, 2]
of course, this allows for repeats.
To make things unique (and to do so in a functional way) we can write a function which takes a stream as input and returns a stream consisting of unique items as output:
def unique(stream):
def f(s):
items = set()
while True:
try:
x = next(s)
if not x in items:
items.add(x)
yield x
except StopIteration:
raise StopIteration
return f(stream)
this creates an stream in a closure that contains a set which can keep track of items that have been seen, only yielding items which are unique. Here I am passing on any StopIteration exception. If the underlying generator has no more elements then there are no more unique elements. I am not 100% sure if I need to explicitly pass on the exception -- (it might happen automatically) but it seems clean to do so.
Used like this:
>>> take(5,unique(randNums(1,10)))
[7, 2, 5, 1, 6]
take(10,unique(randNums(1,10))) will yield a random permutation of 1-10. take(11,unique(randNums(1,10))) will never terminate.
This is a very good question. It's actually quite common. It's even sometimes asked as an interview question.
Here's my solution to generating 5 integers from 0 to 100.
let rec take lst n =
if n = 0 then []
else
match lst with
| [] -> []
| x :: xs -> x :: take xs (n-1)
let shuffle d =
let nd = List.map (fun c -> (Random.bits (), c)) d in
let sond = List.sort compare nd in
List.map snd sond
let rec range a b =
if a >= b then []
else a :: range (a+1) b;;
let _ =
print_endline
(String.concat "\t" ("5 random integers:" :: List.map string_of_int (take (shuffle (range 0 101)) 5)))
How's this:
const addUnique = (ar) => {
const el = getRandom1to100();
return ar.includes(el) ? ar : ar.concat([el])
}
const uniqueArray = (numberOfElements, baseArray) => {
if (numberOfElements < baseArray.length) throw 'invalid input'
return baseArray.length === numberOfElements ? baseArray : uniqueArray(numberOfElements, addUnique(baseArray))
}
const myArray = uniqueArray(5, [])
It is by now a well known theorem of the lambda calculus that any function taking two or more arguments can be written through currying as a chain of functions taking one argument:
# Pseudo-code for currying
f(x,y) -> f_curried(x)(y)
This has proven to be extremely powerful not just in studying the behavior of functions but in practical use (Haskell, etc.).
Functions returning values, however, seem to not be discussed. Programmers typically deal with their inability to return more than one value from a function by returning some meta-object (lists in R, structures in C++, etc.). It has always struck me as a bit of a kludge, but a useful one.
For instance:
# R code for "faking" multiple return values
uselessFunc <- function(dat) {
model1 <- lm( y ~ x , data=dat )
return( list( coef=coef(model1), form=formula(model1) ) )
}
Questions
Does the lambda calculus have anything to say about a multiplicity of return values? If so, do any surprising conclusions result?
Similarly, do any languages allow true multiple return values?
According to the Wikipedia page on lambda calculus:
Lambda calculus, also written as λ-calculus, is a formal system for function
definition, function application and recursion
And a function, in the mathematical sense:
Associates one quantity, the argument of the function, also known as the input,
with another quantity, the value of the function, also known as the output
So answering your first question no, lambda calculus (or any other formalism based on mathematical functions) can not have multiple return values.
For your second question, as far as I know, programming languages that implement multiple return values do so by packing multiple results in some kind of data structure (be it a tuple, an array, or even the stack) and then unpacking it later - and that's where the differences lie, as some programming languages make the packing/unpacking part transparent for the programmer (for instance Python uses tuples under the hood) while other languages make the programmer do the job explicitly, for example Java programmers can simulate multiple return values to some extent by packing multiple results in a returned Object array and then extracting and casting the returned result by hand.
A function returns a single value. This is how functions are defined in mathematics. You can return multiple values by packing them into one compound value. But then it is still a single value. I'd call it a vector, because it has components. There are vector functions in mathematics there, so there are also in programming languages. The only difference is the support level from the language itself and does it facilitate it or not.
Nothing prevents you from having multiple functions, each one returning one of the multiple results that you would like to return.
For example, say, you had the following function in python returning a list.
def f(x):
L = []
for i in range(x):
L.append(x * i)
return L
It returns [0, 3, 6] for x=3 and [0, 5, 10, 15, 20] for x=5. Instead, you can totally have
def f_nth_value(x, n):
L = []
for i in range(x):
L.append(x * i)
if n < len(L):
return L[n]
return None
Then you can request any of the outputs for a given input, and get it, or get None, if there aren't enough outputs:
In [11]: f_nth_value(3, 0)
Out[11]: 0
In [12]: f_nth_value(3, 1)
Out[12]: 3
In [13]: f_nth_value(3, 2)
Out[13]: 6
In [14]: f_nth_value(3, 3)
In [15]: f_nth_value(5, 2)
Out[15]: 10
In [16]: f_nth_value(5, 5)
Computational resources may be wasted if you have to do some of the same work, as in this case. Theoretically, it can be avoided by returning another function that holds all the results inside itself.
def f_return_function(x):
L = []
for i in range(x):
L.append(x * i)
holder = lambda n: L[n] if n < len(L) else None
return holder
So now we have
In [26]: result = f_return_function(5)
In [27]: result(3)
Out[27]: 15
In [28]: result(4)
Out[28]: 20
In [29]: result(5)
Traditional untyped lambda calculus is perfectly capable of expressing this idea. (After all, it is Turing complete.) Whenever you want to return a bunch of values, just return a function that can give the n-th value for any n.
In regard to the second question, python allows for such a syntax, if you know exactly, just how many values the function is going to return.
def f(x):
L = []
for i in range(x):
L.append(x * i)
return L
In [39]: a, b, c = f(3)
In [40]: a
Out[40]: 0
In [41]: b
Out[41]: 3
In [42]: c
Out[42]: 6
In [43]: a, b, c = f(2)
---------------------------------------------------------------------------
ValueError Traceback (most recent call last)
<ipython-input-43-5480fa44be36> in <module>()
----> 1 a, b, c = f(2)
ValueError: need more than 2 values to unpack
In [44]: a, b, c = f(4)
---------------------------------------------------------------------------
ValueError Traceback (most recent call last)
<ipython-input-44-d2c7a6593838> in <module>()
----> 1 a, b, c = f(4)
ValueError: too many values to unpack
Lastly, here is an example from this Lisp tutorial:
;; in this function, the return result of (+ x x) is not assigned so it is essentially
;; lost; the function body moves on to the next form, (* x x), which is the last form
;; of this function body. So the function call only returns (* 10 10) => 100
* ((lambda (x) (+ x x) (* x x)) 10)
=> 100
;; in this function, we capture the return values of both (+ x x) and (* x x), as the
;; lexical variables SUM and PRODUCT; using VALUES, we can return multiple values from
;; a form instead of just one
* ((lambda (x) (let ((sum (+ x x)) (product (* x x))) (values sum product))) 10)
=> 20 100
I write this as a late response to the accepted answer since it is wrong!
Lambda Calculus does have multiple return values, but it takes a bit to understand what returning multiple values mean.
Lambda Calculus has no inherent definition of a collection of stuff, but it does allow you to invent it using products and church numerals.
pure functional JavaScript will be used for this example.
let's define a product as follows:
const product = a => b => callback => callback(a)(b);
then we can define church_0, and church_1 aka true, false, aka left, right, aka car, cdr, aka first, rest as follows:
const church_0 = a => b => a;
const church_1 = a => b => b;
let's start with making a function that returns two values, 20, and "Hello".
const product = a => b => callback => callback(a)(b);
const church_0 = a => b => a;
const church_1 = a => b => b;
const returns_many = () => product(20)("Hello");
const at_index_zero = returns_many()(church_0);
const at_index_one = returns_many()(church_1);
console.log(at_index_zero);
console.log(at_index_one);
As expected, we got 20 and "Hello".
To return more than 2 values, it gets a bit tricky:
const product = a => b => callback => callback(a)(b);
const church_0 = a => b => a;
const church_1 = a => b => b;
const returns_many = () => product(20)(
product("Hello")(
product("Yes")("No")
)
);
const at_index_zero = returns_many()(church_0);
const at_index_one = returns_many()(church_1)(church_0);
const at_index_two = returns_many()(church_1)(church_1)(church_0);
console.log(at_index_zero);
console.log(at_index_one);
console.log(at_index_two);
As you can see, a function can return an arbitrary number of return values, but to access these values, a you cannot simply use result()[0], result()[1], or result()[2], but you must use functions that filter out the position you want.
This is mindblowingly similar to electrical circuits, in that circuits have no "0", "1", "2", "3", but they do have means to make decisions, and by abstracting away our circuitry with byte(reverse list of 8 inputs), word(reverse list of 16 inputs), in this language, 0 as a byte would be [0, 0, 0, 0, 0, 0, 0, 0] which is equivalent to:
const Byte = a => b => c => d => e => f => g => h => callback =>
callback(a)(b)(c)(d)(e)(f)(g)(h);
const Byte_one = Byte(0)(0)(0)(0)(0)(0)(0)(1); // preserves
const Bit_zero = Byte_one(b7 => b6 => b5 => b4 => b3 => b2 => b1 => b0 => b0);
After inventing a number, we can make an algorithm to, given a byte-indexed array, and a byte representing index we want from this array, it will take care of the boilerplate.
Anyway, what we call arrays is nothing more than the following, expressed in higher level to show the point:
// represent nested list of bits(addresses)
// to nested list of bits(bytes) interpreted as strings.
const MyArray = function(index) {
return (index == 0)
? "0th"
: (index == 1)
? "first"
: "second"
;
};
except it doesnt do 2^32 - 1 if statements, it only does 8 and recursively narrows down the specific element you want. Essentially it acts exactly like a multiplexor(except the "single" signal is actually a fixed number of bits(coproducts, choices) needed to uniquely address elements).
My point is that is Arrays, Maps, Associative Arrays, Lists, Bits, Bytes, Words, are all fundamentally functions, both at circuit level(where we can represent complex universes with nothing but wires and switches), and mathematical level(where everything is ultimately products(sequences, difficult to manage without requiring nesting, eg lists), coproducts(types, sets), and exponentials(free functors(lambdas), forgetful functors)).