Check Your Understanding

Twelve questions on the Quickstart through the Capstone, each answer proved by the build.

These questions cover the pages from the Quickstart to the Capstone. They start with recall and end with writing code of your own, and they do not follow page order, on purpose. Answer each question before you open its answer, in your head or on paper. Each answer ends with a link to the section that teaches it, and Where to go next turns your score into a plan.

Checkpoint 1: a list field's path

What does OrderFocus.lines() focus, the list or each line? What does OrderFocus.lines().count(order) return for an order of a lamp and four bulbs?

Answer and why

Each line, and 2. A List field's generated method has already stepped into the elements, so the path is a TraversalPath<Order, LineItem>. There are two lines; the four bulbs are one line with a quantity of 4.

Where this lives: Find your field.

Checkpoint 2: a type you cannot annotate

Jackson's JsonNode is not your class, so you cannot put @GeneratePrisms on it. How do you get prisms for its object, array and text nodes?

Answer and why

Declare an OpticsSpec interface for JsonNode, annotated @ImportOptics. The processor reads the spec and generates JsonNodeOptics, with one prism per method, which compose with the rest of your optics:

@ImportOptics
public interface JsonNodeOpticsSpec extends OpticsSpec<JsonNode> {

  @InstanceOf(ObjectNode.class)
  Prism<JsonNode, ObjectNode> object();

  @InstanceOf(ArrayNode.class)
  Prism<JsonNode, ArrayNode> array();

  @InstanceOf(StringNode.class)
  Prism<JsonNode, StringNode> text();

  @InstanceOf(NumericNode.class)
  Prism<JsonNode, NumericNode> numeric();

  @InstanceOf(BooleanNode.class)
  Prism<JsonNode, BooleanNode> bool();
}

Where this lives: Annotating types you don't own.

Checkpoint 3: would you approve it?

A colleague tidies a line's SKU and parses a new price in one update. Would you approve this?

Update<LineItem> tidy =
    Edits.combine(
        Edit.modify(LineItemFocus.sku(), String::strip),
        Edit.parseIfPresent(
            LineItemFocus.price(), "40.00", raw -> Validated.validNel(new BigDecimal(raw))));

Answer and why

No: it does not compile, with no suitable method found for combine. parseIfPresent makes a FallibleEdit, and Edits.combine takes only edits that cannot fail, so a failure can never be dropped silently. Put both edits in Edits.accumulate instead.

Where this lives: How the split stays compile-time safe.

Checkpoint 4: one value of a map

A catalogue keeps its prices in a Map:

@GenerateLenses
@GenerateFocus
record Catalogue(String name, Map<String, BigDecimal> prices) {}

What does CatalogueFocus.prices() return, and how do you read the price of LAMP?

Answer and why

A FocusPath to the whole map; .atKey("LAMP") gives an AffinePath to one price. A Map field is not stepped into by default, so its path still focuses the map, and .atKey picks one value, which may be absent.

Where this lives: Access by index.

Checkpoint 5: a refused order

The Capstone's accept checks every price with modifyAllValidated, then discounts the bulk lines inside map. For an order with a negative price, does the discount run?

Answer and why

No. map on a Validated changes only a Valid, and an Invalid passes through untouched, so the result is Invalid with every bad price, and nothing is discounted.

Where this lives: Take an order in.

Checkpoint 6: a field that may be null

Two components may hold null: @Nullable String couponCode, with JSpecify's annotation, and String legacyNote, with none. Which needs .nullable() after its generated method?

Answer and why

Only legacyNote. A recognised @Nullable makes the generated method return an AffinePath already. An unannotated reference gives a FocusPath, and .nullable() reads a null as absent.

Where this lives: .nullable(): read a null as absent.

Checkpoint 7: two bad fields in one patch

A PATCH of an order line sends the SKU lamp 2 and the price forty, and both fail their parsers. The edits are written SKU first, then quantity, then price. With Edits.accumulate, how many errors come back, and in what order?

Answer and why

Two, in edit order: the SKU's, then the price's, each located by its path. accumulate checks every edit on its own before it writes anything:

    Validated<NonEmptyList<FieldError>, LineItem> patched =
        Edits.accumulate(
                parseIfPresent(SKU, badPatch.sku(), Sku::parse),
                modifyIfPresent(QUANTITY, badPatch.qtyDelta(), (delta, qty) -> qty + delta),
                parseIfPresent(PRICE, badPatch.price(), Price::parse))
            .apply(line);
    // Invalid(NonEmptyList[sku: not a SKU, price: not a price])
    //   <- or Valid(line) with only the present fields changed

Where this lives: Validated PATCH.

Checkpoint 8: what andThen returns

What is the type of OrderTraversals.lines().andThen(LineItemLenses.price())?

Answer and why

Traversal<Order, BigDecimal>. Many followed by exactly one is still many:

    Traversal<Order, BigDecimal> prices = OrderTraversals.lines().andThen(LineItemLenses.price());

Where this lives: Composing optics with andThen.

Checkpoint 9: a constructor that tidies

This record lowercases what it is given:

// An email address whose constructor lowercases what it is given
record NormalisedEmail(String value) {
  NormalisedEmail {
    value = value.toLowerCase(Locale.ROOT);
  }
}

Is a lens to its value lawful? What does setting ADA@EXAMPLE.COM and reading it back give?

Answer and why

No: reading back gives ada@example.com, not what was set. A lens promises that you read back what you set, and the constructor changes the value on the way in, so hkj-test's LensLaws reports the broken law:

    Lens<NormalisedEmail, String> value =
        Lens.of(NormalisedEmail::value, (_, newValue) -> new NormalisedEmail(newValue));

Where this lives: What a lens is, and Use Manual Lens Creation When on checking a hand-written lens.

Checkpoint 10: write the path

Write one expression that sets the quantity of every line of an order to 1.

Answer and why

A traversal into the lines, then the quantity, then setAll:

    Order oneOfEach = OrderFocus.lines().via(LineItemFocus.quantity()).setAll(1, order);

Where this lives: TraversalPath: Zero or More Elements.

Checkpoint 11: every bad quantity

Write a method that accepts an order only if every line has at least one item, and otherwise reports every line that does not.

Answer and why

A check that returns its verdict, run through the quantities with modifyAllValidated:

  static Validated<String, Integer> checkQuantity(Integer quantity) {
    return quantity >= 1 ? Validated.valid(quantity) : Validated.invalid("No items: " + quantity);
  }

  static Validated<List<String>, Order> acceptQuantities(Order order) {
    return OpticOps.modifyAllValidated(
        order,
        OrderFocus.lines().via(LineItemFocus.quantity()).toTraversal(),
        SelfCheckBook::checkQuantity);
  }

Quantities of 0, 4 and -1 give Invalid(["No items: 0", "No items: -1"]).

Where this lives: Every element, every error.

Checkpoint 12: one variant only

Write an update that upper-cases the reason of a Returned consignment and leaves a consignment in any other state as it is.

Answer and why

A path through the state's returned() prism, then modify, which changes only the variant the prism matches:

    Consignment tidied =
        ConsignmentFocus.state()
            .via(ConsignmentStatePrisms.returned())
            .modify(
                returned ->
                    new ConsignmentState.Returned(returned.reason().toUpperCase(Locale.ROOT)),
                consignment);

Where this lives: One variant of a sealed type.

See Example Code

The code on this page is SelfCheckBook.java and SelfCheckBookTest.java: the page includes the first, and the test asserts every result an answer predicts. The answers drawn from the earlier pages are proved by those pages' own tests, and the build checks the refused snippet against the compiler's own words. Open them after the twelve, since they hold the answers.


Where to go next

A question counts when every part of your answer was right before you opened it, and code you wrote counts when it compiles to the same result as the answer's. Whatever your score, open the Where this lives link of any answer you missed.

Your scoreNext step
10 to 12, with Checkpoints 11 and 12 among themYou can ship. Read the rest of the chapter as a task calls for it: The Optic Types for each optic in depth, and Look It Up for an answer to a specific question
Any other score from 6 to 11Reread the sections you missed, try those questions again, then carry on as for 10 to 12
5 or fewerGo back to the Quickstart and read each page through to the Capstone, working the Optics Tutorial Track alongside, then take the questions again

Previous: Capstone: An Order Desk Next: The Optic Types