Skip to content

Instantly share code, notes, and snippets.

@tebello-thejane
Last active July 17, 2020 13:20
Show Gist options
  • Save tebello-thejane/646334aa68912e42181370fe8ab2666e to your computer and use it in GitHub Desktop.
Save tebello-thejane/646334aa68912e42181370fe8ab2666e to your computer and use it in GitHub Desktop.
Yet another Java lens design

A quick mockup of a probably marginally useful lens implementation in 80 lines of code. This is incredibly rudimentary, as the whole point is to provide a more convenient technique for in-place updating of deep fields -- writing a flat sequence of lenses instead of an unwieldy tree of getters and setters. This is not meant to implement full optics in a rigorous fashion, so we only have set, get, and (as a bonus) over. You won't find any prisms or traversals or isomorphisms here.

Lenses compose with a nice syntax, and lenses are simple to write by hand. The goal was to create a convenient syntax to create lenses and compose them, with ease of usage a simple bonus.

import lombok.*;
public class App {
public static void main(String[] args) {
// We can't go down a series of lenses if an intermediate lens focuses on an uninstantiated field --
// how are you going to modify or read a null field?
Person thePerson = new Person(
new Company(null,
new Address(null, 15)),
null, null);
System.out.println(thePerson);
// Lens composition syntax is simple and intuitive
Lens<Person, Integer> newLens =
Person.$company
.into(Company.$address)
.into(Address.$buildingNumber);
// Will modify the object tree in place and then return it for convenience -- it does not clone then modify
Person newPerson = Lenses.lens(thePerson, newLens).set(20);
// Modification is in-place and mutates the object -- such is Java. So these two are aliases for each other.
System.out.println(thePerson);
System.out.println(newPerson);
int buildingNumber = Lenses.lens(thePerson, newLens).get();
System.out.println(buildingNumber);
Person emptyPerson = new Person();
// IF you really insist on using lenses to build an object...
Lenses.lens(emptyPerson, Person.$lineManager)
.set(Lenses.lens(new Person(), Person.$company)
.set(Lenses.lens(Lenses.lens(new Company(), Company.$name)
.set("Wow (Pty) Ltd."), Company.$address)
.set(Lenses.lens(new Address(), Address.$streetName)
.set("Terrible Idea Str."))));
System.out.println(emptyPerson);
}
}
@Getter
@Setter
@ToString
@AllArgsConstructor
@NoArgsConstructor
class Person {
// A lens is literal just a simple pair -- a getter and setter. The setter may be void.
public static Lens<Person, Company> $company =
new Lens<>($this -> $this.company, ($this, $company) -> $this.company = $company);
//We can use method references:
public static Lens<Person, Person> $supervisor = new Lens<>(Person::getSupervisor, Person::setSupervisor);
public static Lens<Person, Person> $lineManager = new Lens<>(Person::getLineManager, Person::setLineManager);
private Company company;
private Person supervisor;
private Person lineManager;
}
@ToString
@AllArgsConstructor
@NoArgsConstructor
class Company {
public static Lens<Company, String> $name = new Lens<>(Company::getName, Company::setName);
// We can define a lens for a field without getter and setter, since lenses in a sense aim to replace accessors
public static Lens<Company, Address> $address =
new Lens<>($this -> $this.address, ($this, $address) -> $this.address = $address);
@Getter
@Setter
private String name;
// Field is not accessible the old-fashioned way (getter and setter)
private Address address;
}
@Getter
@Setter
@ToString
@AllArgsConstructor
@NoArgsConstructor
class Address {
public static Lens<Address, String> $streetName = new Lens<>(Address::getStreetName, Address::setStreetName);
public static Lens<Address, Integer> $buildingNumber = new Lens<>(Address::getBuildingNumber, Address::setBuildingNumber);
private String streetName;
private int buildingNumber;
}
import lombok.*;
import java.util.function.BiConsumer;
import java.util.function.Function;
@RequiredArgsConstructor
@Getter
class Lens<A, B> {
private final Function<A, B> getter;
private final BiConsumer<A, B> setter;
// Lens composition
public <C> Lens<A, C> into(Lens<B, C> next) {
final Function<A, C> newGetter = this
.getGetter()
.andThen(next.getGetter());
final BiConsumer<A, C> newSetter = (A a, C c) -> {
final B inside = this
.getGetter()
.apply(a);
next
.getSetter()
.accept(inside, c);
this
.getSetter()
.accept(a, inside);
};
return new Lens<>(newGetter, newSetter);
}
}
class Lenses {
public static <A, B> LensRunner<A, B> lens(A theObj, Lens<A, B> theLens) {
return new LensRunner<>(theObj, theLens);
}
}
@RequiredArgsConstructor
class LensRunner<A, B> {
private final A theObj;
private final Lens<A, B> theLens;
public B get() {
try {
return theLens.getGetter().apply(theObj);
} catch (NullPointerException e) {
throw new LensException("Bad getter call. Does every lens in the chain focus on a field that's non-null?", e);
}
}
public A set(B newB) {
final A ret = theObj;
try {
theLens
.getSetter()
.accept(ret, newB);
} catch (NullPointerException e) {
throw new LensException("Bad setter call. Does every lens in the chain focus on a field that's non-null?", e);
}
return ret;
}
public A over(Function<B, B> func) {
return set(func.apply(get()));
}
}
class LensException extends RuntimeException {
public LensException(String bad, Throwable e) {
super(bad, e);
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment