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.

200_success
145k22 gold badges190 silver badges478 bronze badges
asked Dec 23, 2015 at 17:43
\$\endgroup\$
5
  • \$\begingroup\$ I am concerned about the unchecked operations in lazyLookup — so why don't you fix the warning? \$\endgroup\$ Commented Dec 23, 2015 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\$ Commented Dec 23, 2015 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\$ Commented Dec 23, 2015 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\$ Commented Dec 24, 2015 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\$ Commented Dec 24, 2015 at 5:31

3 Answers 3

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());
}
answered Dec 24, 2015 at 16:02
\$\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);
 }
}
answered Jan 25, 2016 at 14:17
\$\endgroup\$
1
  • \$\begingroup\$ This is an alternative solution without explanation why it's better or review about the OP's code. \$\endgroup\$ Commented Jan 25, 2016 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.

answered Dec 24, 2015 at 3:54
\$\endgroup\$

Your Answer

Draft saved
Draft discarded

Sign up or log in

Sign up using Google
Sign up using Email and Password

Post as a guest

Required, but never shown

Post as a guest

Required, but never shown

By clicking "Post Your Answer", you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.