11
\$\begingroup\$

I have an class that cheaply imitates a lazy evaluation scheme. The class is for representing a file and additionally providing meta data on the file. Some of the meta-data can be expensive to evaluate so I only want to evaluate it when requested, and only evaluate it once.

My implementation is such: All private members are stored in an EnumMap and each member is associated with a value in the enum Field.

private enum Field {
    FILE_CODE
    //, ...
};
private Map<Field, Optional> lazyAttributes = new EnumMap<Field, Optional>(Field.class);

The values in the EnumMap are Optionals in case evaluation failed (value not in database etc.) Accessors methods get the member by providing the Field value associated with the member and a Supplier which will evaluate member if it hasn't yet been evaluated.

private <T> Optional<T> lazyLookup(Field field, Supplier<Optional<T>> answer) {
    if (lazyAttributes.containsKey(field)) { // field was already evaluated, get answer.
        return lazyAttributes.get(field);    // unchecked Optional to Optional<T>
    } else {                                 // field is unevaluated;
        Optional result = answer.get();      // evaluate,
        lazyAttributes.put(field, result);   // then store
        return result;                       // unchecked Optional to Optional<T>
    }
}

This is an example of what an accessor looks like.

public Optional<String> getFileCode() {
    return lazyLookup(Field.FILE_CODE, 
            () -> FileCodeKeyWords.apply(getName())); // consider this expensive
}

I am concerned about the unchecked operations in lazyLookup and it seems to be an overly complicated setup. I can provide the rest of the class if requested.

\$\endgroup\$
  • \$\begingroup\$ I am concerned about the unchecked operations in lazyLookup — so why don't you fix the warning? \$\endgroup\$ – 200_success Dec 23 '15 at 20:19
  • \$\begingroup\$ How would I, @200_success? lazyLookup returns Optional<T> and lazyAttributes.get returns Optional I can't change the latter and changing lazyLookup to just Optional just forwards the checking to getFileCode and other accessors. \$\endgroup\$ – cheezsteak Dec 23 '15 at 20:25
  • \$\begingroup\$ You can change at least to Optional<?>. Gets you a little closer to the "type safe heterogeneous container pattern" but you'll have to live with some limitations because your enum isn't a type. \$\endgroup\$ – zapl Dec 23 '15 at 21:27
  • \$\begingroup\$ Could you please post the entire classes involved — enough to have a self-contained working example? The // consider this expensive comment make the current code rather too sketchy to review. \$\endgroup\$ – 200_success Dec 24 '15 at 2:59
  • \$\begingroup\$ @200_success, I can post most of the class after the holidays, but not other classes as it's a rather large project. So references will be redacted to sleep(1) or something. \$\endgroup\$ – cheezsteak Dec 24 '15 at 5:31
2
\$\begingroup\$

Your lazy method is not thread safe. If two threads check for the key at the same time they will both find it absent and both create a new object.

Wrapping your EnumMap in a ConcurrentMap will get around this.

private ConcurrentMap<Field, Optional> lazyThreadSafeAttributes = new ConcurrentHashMap(lazyAttributes);

private <T> Optional<T> lazyThreadSafeLookup(Field field, Supplier<Optional<T>> answer) {
    return lazyThreadSafeAttributes.computeIfAbsent(field, k -> answer.get());
}
\$\endgroup\$
1
\$\begingroup\$

Consider using "rich man" lazy evaluation. The project Derive4J allows you to generate a lazy constructor for any algebraic datatype at compile time (among other cool things like a pattern matching syntax).

Eg. you can easily create a lazy linked list (aka lazy cons list):

@Data
public abstract class List<A> {

  public static List<Integer> naturals() {
    return integersFrom(0);
  }

  public static List<Integer> integersFrom(final int s) {
    return iterate(s, i -> i + 1);
  }

  public static <A> List<A> iterate(A seed, UnaryOperator<A> op) {
    return lazy(() -> cons(seed, iterate(op.apply(seed), op)));
  }

  public abstract <X> X list(Supplier<X> nil,
     @FieldNames({ "head", "tail" }) BiFunction<A, List<A>, X> cons);

  public <B> List<B> map(Function<A, B> f) {
    return lazy(() -> list(
       () -> nil(),
       (h, tail) -> cons(f.apply(h), tail.map(f))
    ));
  }

  public List<A> take(int n) {
    return n <= 0
           ? nil()
           : lazy(() -> list(
              () -> nil(),
              (head, tail) -> cons(head, tail.take(n - 1))
           ));
  }

  public static void main(String[] args) {
    List<Integer> first4EvenNaturals = naturals().map(i-> 2*i).take(4);
  }
}
\$\endgroup\$
  • \$\begingroup\$ This is an alternative solution without explanation why it's better or review about the OP's code. \$\endgroup\$ – Mast Jan 25 '16 at 17:03
0
\$\begingroup\$

Consider object cache solutions for each File object's fields information instead of collections of generic Optional. Look to Guava object cache documentation. Fetching items for the cache insertion is often demonstrated using concurrency. May be desired: report progress for expensive, slow meta-data lookup processing. Failed operations could alternatively be represented by your own null-like objects, ones the cache retrieval recognizes; i.e., cache isn't empty for given String "field" key, but instead now contains one of your Null objects, perhaps ones with area for detailed failure information: 'value not in database,' 'could not connect to database,' 'invalid value received,' etc.

\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.