class LPReduction<D, R> {
  Closure vars
  Closure objFunc
  Closure constraints
  Closure result
  // default is maximisation problem
  boolean max = true
  // default is to allow negative solution values
  boolean nonNegative = false
  LPSolver solver

  // caches the variables to avoid calling 'vars' closure more than once, 
  // in case 'constraints' closure needs this info.
  def variables

  /**
   * Maps an instance x of P into an LP instance y
   * 
   * @param x the domain object
   * @return the reduced LP instance y
   */
  LPInstance mapToLP(D x) {
    variables = vars(x)
    new LPInstance(variables,
            objFunc ? objFunc(x) : new LPFunction([:], 0),
            constraints(x),
            max,
            nonNegative)
  }

  /**
   * Solves the specified LP instance. 
   * If no solver is set, it uses ApacheSimplexSolver by default.
   * 
   * @param lpi the LP instance to solve
   * @return the solution of the LP instance
   */
  private Solution solve(LPInstance lpi) {
    if (!solver) {
      solver = new ApacheSimplexSolver()
    }
    solver.solve(lpi)
  }

  /**
   * Computes P(x) by reducing P to LP
   * 
   * @param x the domain object
   * @return P(x)
   */
  R reduce(D x) {
    LPInstance y = mapToLP(x)
    Solution solution = solve(y)
    result(solution)
  }
}