function assign(X, indices, values) {
  return tf.tidy(() => {
    // todo: implement broadcasting logic
    if (indices.size === 0) {
      return X.clone();
    }
    let _values;
    // when `values` is a scalar
    if (indices.shape !== values.shape && values.rank === 0) {
      _values = tf.mul(tf.ones(indices.shape), values);
    } else {
      _values = values;
    }
    const correctVals = tf.scatterND(indices, _values, X.shape);
    const mask = tf
      .scatterND(indices, tf.ones(indices.shape), X.shape)
      .toBool();
    return tf.where(mask, correctVals, X);
  });
}
assign `values` to positions indicated by `indices` in tensor `X`
@param {Tensor} X - the tensor to assign values to
@param {Tensor} indices - indices in `X` where value assignment should happen
@param {Tensor|Scalar} values - values to assign
@returns {Tensor} output tensor with values at `indices` substituted by `values`