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.
3 Answers 3
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());
}
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);
}
}
-
\$\begingroup\$ This is an alternative solution without explanation why it's better or review about the OP's code. \$\endgroup\$2016年01月25日 17:03:55 +00:00Commented Jan 25, 2016 at 17:03
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.
lazyLookup
— so why don't you fix the warning? \$\endgroup\$lazyLookup
returnsOptional<T>
andlazyAttributes.get
returnsOptional
I can't change the latter and changinglazyLookup
to justOptional
just forwards the checking togetFileCode
and other accessors. \$\endgroup\$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\$// consider this expensive
comment make the current code rather too sketchy to review. \$\endgroup\$sleep(1)
or something. \$\endgroup\$