code: def concatenate(*layers):  # pragma: no cover
      
      if not layers:
          return noop()
      ops = layers[0].ops
  
      def begin_update(X, *a, **k):
          forward, backward = split_backward(layers)
          values = [fwd(X, *a, **k) for fwd in forward]
  
          output = ops.xp.hstack(values)
          shapes = [val.shape for val in values]
  
          def finish_update(gradient, *args, **kwargs):
              layer_grads = []
              start = 0
              for bwd, shape in zip(backward, shapes):
                  end = start + shape[1]
                  if bwd is not None:
                      d = bwd(
                          ops.xp.ascontiguousarray(gradient[:, start:end]),
                          *args,
                          **kwargs
                      )
                      if d is not None and hasattr(X, "shape"):
                          if not layer_grads:
                              layer_grads.append(d)
                          else:
                              layer_grads[-1] += d
                  start = end
              if layer_grads:
                  return ops.asarray(layer_grads[-1])
              else:
                  return None
  
          return output, finish_update
  
      layer = FunctionLayer(begin_update)
      layer._layers = list(layers)
  
      def on_data(self, X, y=None):
          for layer in self._layers:
              for hook in layer.on_data_hooks:
                  hook(layer, X, y)
  
      layer.on_data_hooks.append(on_data)
      return layer
docs: Compose two or more models `f`, `g`, etc, such that their outputs are
      concatenated, i.e. `concatenate(f, g)(x)` computes `hstack(f(x), g(x))`