Skip to content

Instantly share code, notes, and snippets.

@venil7
Last active May 24, 2020 11:08
Show Gist options
  • Save venil7/eef3f989a9eea8712e76102a9357d2e0 to your computer and use it in GitHub Desktop.
Save venil7/eef3f989a9eea8712e76102a9357d2e0 to your computer and use it in GitHub Desktop.
Functional Lense implementation in TypeScript
type LGetter<LFrom, LTo> = (f: LFrom) => LTo;
type LSetter<LFrom, LTo> = (t: LTo, f: LFrom) => LFrom;
type LUpdater<T> = ((t: T) => T);
type LModifier<LFrom, LTo> = (u: LUpdater<LTo>) => (f: LFrom) => LFrom;
class Lense<LFrom, LTo> {
constructor(private get: LGetter<LFrom, LTo>, private set: LSetter<LFrom, LTo>) { }
public compose<LTo2>(inner: Lense<LTo, LTo2>): Lense<LFrom, LTo2> {
const _get: LGetter<LFrom, LTo2> = (f: LFrom) => inner.get(this.get(f));
const _set: LSetter<LFrom, LTo2> = (t2: LTo2, f: LFrom) => this.set(inner.set(t2, this.get(f)), f);
return new Lense(_get, _set);
}
public reader: () => LGetter<LFrom, LTo> = () => this.get;
public writer: () => LSetter<LFrom, LTo> = () => this.set;
public modify: LModifier<LFrom, LTo> = updater => f =>
this.set(updater(this.get(f)), f);
}
@venil7
Copy link
Author

venil7 commented Dec 6, 2018

how to use lenses

first lets define some types that hierarchically relate to one another

type Address = {
    street: string;
    postcode: string;
}
type Person = {
    name: string;
    age: number;
    address: Address;
}
type Company = {
    name: string;
    ceo: Person;
    employees: Person[]
};

then we define values with those types

const ceo = {
    name: 'John',
    age: 33,
    address: {
        street: 'high str',
        postcode: 'ab1 3be'
    }
};
const employee1 = {
    name: 'Jack',
    age: 24,
    address: {
        street: 'lower str',
        postcode: 'bc2 4de'
    }
};
const employee2 = {
    name: 'Mary',
    age: 28,
    address: {
        street: 'middle str',
        postcode: 'cd4 5ef'
    }
};

const company: Company = {
    name: 'Acme ltd',
    ceo: ceo,
    employees: [employee1, employee2]
};

and finally we define lenses that zoom in one level at a time. Skipping levels in a lense is possible, but defeats the purpose, as we will see later - main property of lenses is composability, and we will be able to skip levels by composing different lenses.

const personToAddress = new Lense<Person, Address>(
    person => person.address,
    (address, person) => ({ ...person, address })
);

const companyToCeo = new Lense<Company, Person>(
    company => company.ceo,
    (ceo, company) => ({...company, ceo})
);

const companyToEmployees = new Lense<Company, Person[]>(
    company => company.employees,
    (employees, company) => ({...company, employees})
);

const addressToPostcode = new Lense<Address, string>(
    address => address.postcode,
    (postcode, address) => ({...address, postcode})
);

zoom in read

then we compose lenses, to define derived lenses that zoom from very large to very small

const getCeoPostCode = companyToCeo
    .compose(personToAddress)
    .compose(addressToPostcode)
    .reader();

getCeoPostCode(company); // --> 'ab1 3be'

zoom in modify

or we can compose a lense and get modifier that would modify a deep nested property

const employeesToFirstEmployee = new Lense<Person[], Person>(
    employees => employees[0],
    (employee, employees) => employees.map((e, i) => i == 0 ? employee : e)
)
const companyToFirstEmployeePostcode = companyToEmployees
    .compose(employeesToFirstEmployee)
    .compose(personToAddress)
    .compose(addressToPostcode);

const uppercaseFirsEmployeePostcode =
    companyToFirstEmployeePostcode
        .modify(oldPostCode => oldPostCode.toUpperCase());

uppercaseFirsEmployeePostcode(company); // ->

// {
//   "name": "Acme ltd",
//   "ceo": {
//       "name": "John",
//       "age": 33,
//       "address": {
//           "street": "high str",
//           "postcode": "ab1 3be"
//       }
//   },
//   "employees": [{
//       "name": "Jack",
//       "age": 24,
//       "address": {
//           "street": "lower str",
//           "postcode": "BC2 4DE" //uppercased via lense
//       }
//   }, {
//       "name": "Mary",
//       "age": 28,
//       "address": {
//           "street": "middle str",
//           "postcode": "cd4 5ef"
//       }
//   }]
// }

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment