///|
/// Visit every operation with its zero-based operation index.
pub fn Delta::for_each(self : Delta, visit : (Op, Int32) -> Unit) -> Unit {
  for index, op in self.ops {
    visit(op, index)
  }
}

///|
/// Map operations to arbitrary values, passing the operation index.
pub fn[T] Delta::map(self : Delta, convert : (Op, Int32) -> T) -> Array[T] {
  let output : Array[T] = []
  for index, op in self.ops {
    output.push(convert(op, index))
  }
  output
}

///|
/// Return operations accepted by a predicate, preserving original order.
pub fn Delta::filter(
  self : Delta,
  predicate : (Op, Int32) -> Bool,
) -> Array[Op] {
  let output : Array[Op] = []
  for index, op in self.ops {
    if predicate(op, index) {
      output.push(op)
    }
  }
  output
}

///|
/// Split operations by a predicate. The first array contains accepted ops;
/// both arrays preserve the order of their source operations.
pub fn Delta::partition(
  self : Delta,
  predicate : (Op) -> Bool,
) -> (Array[Op], Array[Op]) {
  let accepted : Array[Op] = []
  let rejected : Array[Op] = []
  for op in self.ops {
    if predicate(op) {
      accepted.push(op)
    } else {
      rejected.push(op)
    }
  }
  (accepted, rejected)
}

///|
/// Fold a Delta's operations, passing each operation index to the reducer.
pub fn[T] Delta::reduce(
  self : Delta,
  initial : T,
  combine : (T, Op, Int32) -> T,
) -> T {
  let mut value = initial
  for index, op in self.ops {
    value = combine(value, op, index)
  }
  value
}