Zip together two arrays

Problem

You want to "zip" two arrays together. ["A", "B", "O"] and ["Apple", "Banana", "Orange"] should become [["A", "Apple"], ["B", "Banana"], ["O", "Orange"]].

Solution

Fauna doesn’t provide a Zip function, but we can create one:

try
{
    Value result = await client.Query(
        CreateFunction(
            Obj(
                "name", "zip",
                "body", Query(
                    Lambda(
                        Arr("arr1", "arr2"),
                        If(
                            Not(
                                And(
                                    IsArray(Var("arr1")),
                                    IsArray(Var("arr2"))
                                )
                            ),
                            Abort("zip requires two array arguments"),
                            Let(
                                "count1", Count(Var("arr1")),
                                "count2", Count(Var("arr2"))
                            ).In(
                                Reduce(
                                    Lambda(
                                        Arr("acc", "val"),
                                        Let(
                                            "s", Count(Var("acc")),
                                            "a", Select(
                                                Var("s"),
                                                Var("arr1"),
                                                Null()
                                            ),
                                            "b", Select(
                                                Var("s"),
                                                Var("arr2"),
                                                Null()
                                            )
                                        ).In(
                                            Append(
                                                Arr(
                                                    Arr(
                                                        Var("a"),
                                                        Var("b")
                                                    )
                                                ),
                                                Var("acc")
                                            )
                                        )
                                    ),
                                    Arr(),
                                    If(
                                        GTE(
                                            Var("count1"),
                                            Var("count2")
                                        ),
                                        Var("arr1"),
                                        Var("arr2")
                                    )
                                )
                            )
                        )
                    )
                )
            )
        )
    );

    Console.WriteLine(result);
}
catch (Exception e)
{
    Console.WriteLine($"ERROR: {e.Message}");
}
result, err := client.Query(
	f.CreateFunction(
		f.Obj{
			"name": "zip",
			"body": f.Query(
				f.Lambda(
					f.Arr{"arr1", "arr2"},
					f.If(
						f.Not(
							f.And(
								f.IsArray(f.Var("arr1")),
								f.IsArray(f.Var("arr2")),
							),
						),
						f.Abort("zip requires two array arguments"),
						f.Let().Bind(
							"count1", f.Count(f.Var("arr1"))).Bind(
							"count2", f.Count(f.Var("arr2"))).In(
							f.Reduce(
								f.Lambda(
									f.Arr{"acc", "val"},
									f.Let().Bind(
										"cnt", f.Count(
											f.Var("acc"),
										),
									).Bind(
										"a", f.Select(
											f.Var("cnt"),
											f.Var("arr1"),
											f.Default(nil),
										),
									).Bind(
										"b", f.Select(
											f.Var("cnt"),
											f.Var("arr2"),
											f.Default(nil)),
									).In(
										f.Append(
											f.Arr{
												f.Arr{
													f.Var("a"),
													f.Var("b"),
												},
											},
											f.Var("acc"),
										),
									),
								),
								f.Arr{},
								f.If(
									f.GTE(f.Var("count1"), f.Var("count2")),
									f.Var("arr1"),
									f.Var("arr2"),
								),
							),
						),
					),
				),
			),
		},
	))

if err != nil {
	fmt.Fprintln(os.Stderr, err)
} else {
	fmt.Println(result)
}
client.query(
  q.CreateFunction({
    name: 'zip',
    body: q.Query(
      q.Lambda(
        ['arr1', 'arr2'],
        q.If(
          q.Not(
            q.And(
              q.IsArray(q.Var('arr1')),
              q.IsArray(q.Var('arr2')),
            )
          ),
          q.Abort('zip requires two array arguments'),
          q.Let(
            {
              count1: q.Count(q.Var('arr1')),
              count2: q.Count(q.Var('arr2')),
            },
            q.Reduce(
              q.Lambda(
                ['acc', 'val'],
                q.Let(
                  {
                    cnt: q.Count(q.Var('acc')),
                    a: q.Select(q.Var('cnt'), q.Var('arr1'), null),
                    b: q.Select(q.Var('cnt'), q.Var('arr2'), null),
                  },
                  q.Append([[q.Var('a'), q.Var('b')]], q.Var('acc'))
                )
              ),
              [],
              q.If(
                q.GTE(q.Var('count1'), q.Var('count2')),
                q.Var('arr1'),
                q.Var('arr2')
              )
            )
          )
        )
      )
    ),
  })
)
.then((ret) => console.log(ret))
.catch((err) => console.error(
  'Error: [%s] %s: %s',
  err.name,
  err.message,
  err.errors()[0].description,
))
result = client.query(
  q.create_function({
    "name": "zip",
    "body": q.query(
      q.lambda_(
        ["arr1", "arr2"],
        q.if_(
          q.not_(
            q.and_(
              q.is_array(q.var("arr1")),
              q.is_array(q.var("arr2")),
            )
          ),
          q.abort("zip requires two array arguments"),
          q.let(
            {
              "count1": q.count(q.var("arr1")),
              "count2": q.count(q.var("arr2")),
            },
            q.reduce(
              q.lambda_(
                ["acc", "val"],
                q.let(
                  {
                    "cnt": q.count(q.var("acc")),
                    "a": q.select(q.var("cnt"), q.var("arr1"), None),
                    "b": q.select(q.var("cnt"), q.var("arr2"), None),
                  },
                  q.append([[q.var("a"), q.var("b")]], q.var("acc"))
                )
              ),
              [],
              q.if_(
                q.gte(q.var("count1"), q.var("count2")),
                q.var("arr1"),
                q.var("arr2")
              )
            )
          )
        )
      )
    ),
  })
)
print(result)
CreateFunction({
  name: 'zip',
  body: Query(
    Lambda(
      ['arr1', 'arr2'],
      If(
        Not(
          And(
            IsArray(Var('arr1')),
            IsArray(Var('arr2')),
          )
        ),
        Abort('zip requires two array arguments'),
        Let(
          {
            count1: Count(Var('arr1')),
            count2: Count(Var('arr2')),
          },
          Reduce(
            Lambda(
              ['acc', 'val'],
              Let(
                {
                  cnt: Count(Var('acc')),
                  a: Select(Var('cnt'), Var('arr1'), null),
                  b: Select(Var('cnt'), Var('arr2'), null),
                },
                Append([[Var('a'), Var('b')]], Var('acc'))
              )
            ),
            [],
            If(
              GTE(Var('count1'), Var('count2')),
              Var('arr1'),
              Var('arr2')
            )
          )
        )
      )
    )
  ),
})
Query metrics:
  •    bytesIn:  736

  •   bytesOut:  818

  • computeOps:    1

  •    readOps:    0

  •   writeOps:    1

  •  readBytes:   16

  • writeBytes:  778

  •  queryTime: 10ms

  •    retries:    0

The following query calls the UDF with two arrays:

try
{
    Value result = await client.Query(
        Call(
            "zip",
            Arr("A", "B", "O"),
            Arr("Apple", "Banana", "Orange")
        )
    );

    Console.WriteLine(result);
}
catch (Exception e)
{
    Console.WriteLine($"ERROR: {e.Message}");
}
Arr(Arr(StringV(A), StringV(Apple)), Arr(StringV(B), StringV(Banana)), Arr(StringV(O), StringV(Orange)))
result, err := client.Query(
	f.Call(
		"zip",
		f.Arr{"A", "B", "O"},
		f.Arr{"Apple", "Banana", "Orange"},
	))

if err != nil {
	fmt.Fprintln(os.Stderr, err)
} else {
	fmt.Println(result)
}
[[A Apple] [B Banana] [O Orange]]
client.query(
  q.Call('zip', ['A', 'B', 'O'], ['Apple', 'Banana', 'Orange'])
)
.then((ret) => console.log(ret))
.catch((err) => console.error(
  'Error: [%s] %s: %s',
  err.name,
  err.message,
  err.errors()[0].description,
))
[ [ 'A', 'Apple' ], [ 'B', 'Banana' ], [ 'O', 'Orange' ] ]
result = client.query(
  q.call('zip', ['A', 'B', 'C'], ['Apple', 'Banana', 'Orange'])
)
print(result)
[['A', 'Apple'], ['B', 'Banana'], ['C', 'Orange']]
Call('zip', ['A', 'B', 'O'], ['Apple', 'Banana', 'Orange'])
[ [ 'A', 'Apple' ], [ 'B', 'Banana' ], [ 'O', 'Orange' ] ]
Query metrics:
  •    bytesIn:  70

  •   bytesOut:  58

  • computeOps:   1

  •    readOps:   0

  •   writeOps:   0

  •  readBytes:   0

  • writeBytes:   0

  •  queryTime: 1ms

  •    retries:   0

The UDF handles the situation where either array is longer than the other:

try
{
    Value result = await client.Query(
        Arr(
            Call(
                "zip",
                Arr("A", "B"),
                Arr("Apple", "Banana", "Orange")
            ),
            Call(
                "zip",
                Arr("A", "B", "O"),
                Arr("Apple", "Banana")
            )
        )
    );

    Console.WriteLine(result);
}
catch (Exception e)
{
    Console.WriteLine($"ERROR: {e.Message}");
}
Arr(Arr(Arr(StringV(A), StringV(Apple)), Arr(StringV(B), StringV(Banana)), Arr(NullV, StringV(Orange))), Arr(Arr(StringV(A), StringV(Apple)), Arr(StringV(B), StringV(Banana)), Arr(StringV(O), NullV)))
result, err := client.Query(
	f.Arr{
		f.Call(
			"zip",
			f.Arr{"A", "B"},
			f.Arr{"Apple", "Banana", "Orange"},
		),
		f.Call(
			"zip",
			f.Arr{"A", "B", "O"},
			f.Arr{"Apple", "Banana"},
		),
	})

if err != nil {
	fmt.Fprintln(os.Stderr, err)
} else {
	fmt.Println(result)
}
[[[A Apple] [B Banana] [{} Orange]] [[A Apple] [B Banana] [O {}]]]
client.query([
  q.Call('zip', ['A', 'B'], ['Apple', 'Banana', 'Orange']),
  q.Call('zip', ['A', 'B', 'O'], ['Apple', 'Banana']),
])
.then((ret) => console.log(ret))
.catch((err) => console.error(
  'Error: [%s] %s: %s',
  err.name,
  err.message,
  err.errors()[0].description,
))
[
  [ [ 'A', 'Apple' ], [ 'B', 'Banana' ], [ null, 'Orange' ] ],
  [ [ 'A', 'Apple' ], [ 'B', 'Banana' ], [ 'O', null ] ]
]
result = client.query([
  q.call('zip', ['A', 'B'], ['Apple', 'Banana', 'Orange']),
  q.call('zip', ['A', 'B', 'O'], ['Apple', 'Banana']),
])
print(result)
[[['A', 'Apple'], ['B', 'Banana'], [None, 'Orange']], [['A', 'Apple'], ['B', 'Banana'], ['O', None]]]
[
  Call('zip', ['A', 'B'], ['Apple', 'Banana', 'Orange']),
  Call('zip', ['A', 'B', 'O'], ['Apple', 'Banana']),
]
[
  [ [ 'A', 'Apple' ], [ 'B', 'Banana' ], [ null, 'Orange' ] ],
  [ [ 'A', 'Apple' ], [ 'B', 'Banana' ], [ 'O', null ] ]
]
Query metrics:
  •    bytesIn: 130

  •   bytesOut: 103

  • computeOps:   1

  •    readOps:   0

  •   writeOps:   0

  •  readBytes:   0

  • writeBytes:   0

  •  queryTime: 1ms

  •    retries:   0

Discussion

The main logic is within the Reduce function call:

  • An empty array is provided as the initial accumulator.

  • The longest array is used as the list to iterate.

  • On each invocation, the reducer’s Lambda uses the size of the accumulator as the index into the two provided arrays, and Select is use to access the value and return null if no value exists at that index.

Is this article helpful? 

Tell Fauna how the article can be improved:
Visit Fauna's forums or email docs@fauna.com

Thank you for your feedback!