Одним из наиболее интересных выступлений среди тех, на которых я присутствовал в lambda.world, были « Функциональные линзы на JavaScript» от FlavioCorpa . Он говорил о функциональных линзах на практике; более того, он начал с собственной небольшой реализации (не для производства), а затем рассказал о различных библиотеках, таких как Ramda или Monocle-TS .
Беседа началась с простого для понимания определения для тех из нас, кто знаком с процедурным / императивным программированием: «Линзы — это в основном функциональные геттеры и сеттеры». По сути, то, что мы получаем с помощью линз, — это возможность многократного использования доступа к данным структуры модульным и неизменным способом либо для получения данных, либо для их изменения. Позже мы увидим примеры, чтобы лучше понять это.
Функциональные линзы в JavaScript
Давайте начнем с очень маленькой реализации объектива:
JavaScript
1
const Lens = (getter, setter) => {get: getter, set: setter};
Те, кто не видел или не знает о функциональных линзах, могут спросить: «а это может привести к разговору в течение 1 часа?» Мы должны знать, что это концепция функционального программирования, поэтому состав и неизменность являются очень важными понятиями, которые необходимо учитывать при использовании линз.
Продолжая нашу реализацию, мы добавим типы:
Машинопись
xxxxxxxxxx
1
type LensGetter<S,A> = (whole: S) => A;
2
type LensSetter<S,A> = (whole: S) => (part: A) => S;
3
interface Lens<S,A> {
5
get: LensGetter<S,A>;
6
set: LensSetter<S,A>;
7
}
8
const Lens = <S,A>(getter: LensGetter<S, A>, setter: LensSetter<S,A>) => ({get: getter, set: setter});
Теперь мы видим , как геттер является простой функцией , которая принимает объект (целое) и возвращает часть его (часть). С помощью установщика мы стремимся создать новое целое с новой частью, которую мы передали ему. Это в основном функции get / set, к которым мы привыкли, верно? Давайте продолжим создавать наши реализации Getter / Setter и посмотрим их использование:
Машинопись
xxxxxxxxxx
1
interface User {
2
name: String;
3
company: String;
4
}
5
const user: User = {name: "Oscar", company: "Apiumhub"};
6
const getName = (whole: User): String => whole.name;
8
const setName = (whole: User) => (part: String): User => ({whole, name: part});
9
const nameLens = Lens<User, String>(getName, setName);
10
expect(nameLens.get(user)).toBe("Oscar");
12
expect(nameLens.set(user)("Joaquin")).toEqual({name: "Joaquin", company: "Apiumhub"});
Как мы видим из нашего теста, при получении нашего объектива, проходящего через a, User
мы называем его и, используя набор нашего объектива, передаем ему новое имя и возвращаем полный объект с измененным именем.
Здесь можно подумать, что когда мы кодируем реализацию, метод get может указывать на одну вещь, а набор может изменять другую. Это не имеет особого смысла, поэтому давайте продолжим. Как и все в этой жизни (и больше в мире математики / программирования), существуют законы. Законы, которые необходимо соблюдать, чтобы обеспечить правильное функционирование, в данном случае, объектива.
Вам также может понравиться:
Функциональное программирование в JavaScript .
Законы объектива
Есть законы о линзах, и их легко понять. Я постараюсь объяснить их простым способом; обратите внимание, что вы можете найти полезную литературу об этом в конце статьи.
1. (устанавливается после получения) Если я обновляю то, что получаю, объект не изменяется. (Идентичность)
Машинопись
xxxxxxxxxx
1
expect(nameLens.set(user)(nameLens.get(user))).toEqual(user);
Если этот закон соблюдается, мы должны видеть, что set и get должны фокусироваться на одной и той же части объекта
2. (получить после набора) Если я обновлю, а затем получу, я должен получить то, что я обновил.
JavaScript
xxxxxxxxxx
1
expect(nameLens.get(nameLens.set(user)("newName"))).toEqual("newName");
Первое, что будет выполнено - это набор нашего объектива, который вернет нового пользователя с новым именем. Если мы получим этого нового пользователя, мы должны получить новое имя.
3. (устанавливается после набора) Если я обновляюсь дважды, я получаю обновленный объект в последний раз.
Машинопись
xxxxxxxxxx
1
expect(nameLens.set(nameLens.set(user)("newName"))("theNewName")).toEqual(nameLens.set(user)("theNewName"));
Посмотрите на заказ; сначала выполняется внутренний, пользовательский набор с «newName». С этим объектом, который возвращается ко мне, я снова меняю его, но на этот раз на «theNewName». Последнее, что мы получаем. Ожидание отражает это.
Просмотр, установка и окончание
Теперь мы собираемся реализовать три новые функции: вид , набор и более . Эти функции будут очень простыми, но они помогут нам работать с линзами:
Машинопись
xxxxxxxxxx
1
type LensView<S,A> = (lens: Lens<S, A>, whole: S) => A;
2
type LensSet<S,A> = (lens: Lens<S, A>, whole: S, part: A) => S;
3
type LensOver<S,A> = (lens: Lens<S, A>, map: Mapper<A, A>, whole: S) => S;
Как видите, три типа довольно просты и помогут нам работать с линзами гораздо проще. Вы называете функции объектива данными, к которым они относятся:
Машинопись
xxxxxxxxxx
1
type LensView<S,A> = (lens: Lens<S, A>, whole: S) => A;
2
type LensSet<S,A> = (lens: Lens<S, A>, whole: S, part: A) => S;
3
type LensOver<S,A> = (lens: Lens<S, A>, map: Mapper<A, A>, whole: S) => S;
До сих пор мы возились с очень конкретными объектами. Давайте абстрагируемся от этих конкретных типов, чтобы начать использовать общие:
Машинопись
xxxxxxxxxx
1
const view = <S, A>(lens: Lens<S, A>, obj: S) => lens.get(obj);
2
const set = <S, A>(lens: Lens<S, A>, obj: S, part: A) => lens.set(obj)(part);
3
const over = <S,A>(lens: Lens<S, A>, map: Mapper<A, A>, obj: S) => lens.set(obj)(map(lens.get(obj)));
Изменяя User
и String
для обобщений, таких как S и A , мы уже имеем три функции, которые применяются во всех контекстах. Нам нужно было только изменить название функции.
Теперь мы собираемся обобщить часть создания объектива вместе с его геттерами и сеттерами.
Машинопись
xxxxxxxxxx
1
const prop = <S, A>(key: keyof S) => (whole: S): A => whole[key];
2
const assoc = <S, A>(key: keyof S) => (whole: S) => (part: A) => ({whole, [key]: part});
3
const lensProp = <S, A>(key: keyof S) => Lens<S,A>(prop(key), assoc(key));
4
const nameLens = lensProp("name");
В нашей проп функции в качестве параметра мы передаем значение типа: keyof S . Этот тип является типом объединения всех открытых свойств объекта S. В следующем примере он не будет скомпилирован, если мы попытаемся присвоить userProps что-то отличное от name или age:
Машинопись
xxxxxxxxxx
1
1
interface User {
2
name: string;
3
age: number
4
}
5
6let userProps: keyof User; // 'name' | 'age'
Как мы видим, одного вызова функции, указывающей ту часть структуры, на которой мы хотим сосредоточиться, было бы достаточно, чтобы иметь все, что мы объяснили в этой статье на данный момент. Все идет нормально.
Сочинение
И последнее, но не менее важное, мы будем работать с составом линз. С составом линз мы сможем получить самые глубокие данные в нашей структуре более простым способом.
Первое, что мы сделаем, это создадим функцию, которая при двух линзах возвращает составную линзу.
На уровне типов можно сказать, что у меня есть объектив, который говорит с типом A как структура данных, и B как часть A, и у меня также есть другой объектив, который знает B как структуру данных и C как внутреннюю часть. этого Наша функция должна возвращать Lens, который знает A как структуру и позволяет нам работать с частью второго уровня типа C:
Машинопись
xxxxxxxxxx
1
const compose = <A, B, C>(lens1: Lens<B, C>, lens2: Lens<A, B>): Lens<A, C> => ({
2
get: (whole: A) => lens1.get(lens2.get(whole)),
3
set: (whole: A) => (part: C) => lens2.set(whole)(lens1.set(lens2.get(whole))(part))
4
});
Код прост, и только взглянув на сигнатуру метода, мы сможем точно понять, что он делает. С этого момента мы начнем использовать немного более сложную структуру данных:
Машинопись
xxxxxxxxxx
1
interface Company {
2
name: string;
3
location: string;
4
}
5
interface Contact {
7
name: string;
8
company: Company;
9
}
10
const contact: Contact = {
12
name: "Oscar",
13
company: {
14
name: "Apiumhub",
15
location: "Barcelona"
16
}
17
};
В качестве первого шага, что мы собираемся сделать, это получить доступ, составив линзы, название компании нашего контакта. Во-первых, мы должны создать два объектива - один, который фокусируется на Company
нашей части Contact
, а другой - на компании name
(строке) Company
:
Машинопись
xxxxxxxxxx
1
const companyLens = lensProp<Contact, Company>("company");
2
const companyNameLens = lensProp<Company, string>("name");
3
const contactCompanyNameLens: Lens<Contact, string> = compose(companyNameLens, companyLens);
4
const locationLens = lensProp<Company, Location>("location");
5
const cityNameLens = lensProp<Location, string>("city");
6
const companyLocationLens: Lens<Contact, Location> = compose(locationLens, companyLens);
7
const locationCityNameLens: Lens<Contact, string> = compose(cityNameLens, companyLocationLens);
8
it('focus nested data', () => {
10
expect(view(contactCompanyNameLens, contact)).toEqual("Apiumhub");
11
expect(over(contactCompanyNameLens, toUpperCase, contact).company.name).toEqual("APIUMHUB")
12
});
13
it('composing composed lens', () => {
14
expect(view(locationCityNameLens, contact)).toEqual("Barcelona");
15
expect(over(locationCityNameLens, toUpperCase, contact).company.location.city).toEqual("BARCELONA")
16
});
Прохладно! У нас уже есть возможность создавать линзы, составлять их и работать с ними. Тем не менее, весь код в этой статье, несмотря на работу, не рекомендуется использовать в производстве; наша команда разработчиков программного обеспечения в Apiumhub широко использует библиотеку Ramda, хотя есть много других хороших библиотек, таких как Monocle-ts.
Где использовать объектив
В заключение давайте поговорим о том, когда подходящее время использовать линзы, а когда НЕ использовать их.
Я прочитал много статей и презентаций, где они говорят о том, как использовать их в домене, но это то, что должно быть хорошо продумано.
Вариант использования объектива - создание геттеров и сеттеров, поэтому мы можем столкнуться с проблемами плохого дизайна нашего домена, если в итоге мы нарушим инкапсуляцию.
Я бы осмелился сказать, что использование доменных линз является анти-паттерном . В определенных сценариях, где вам говорят использовать линзы в домене, вы видите объекты Бога, которые нуждаются в помощи всех возможных технических приемов для решения неправильного проектного решения.
С другой стороны, я вижу использование линз в пограничных слоях нашего домена - весь ввод DTO по HTTP, базе данных, событиям и т. Д.
Вывод: функциональные линзы в JavaScript
Как я уже упоминал выше, я использовал много литературы, и здесь я оставляю ее для вас. Позвольте мне сказать вам, что, если вы углубитесь в предмет, вы войдете в спираль функционального программирования, математики в целом и теории категорий в конкретной (хотя и абстрактной), которая порождает зависимость.
Ссылки
- Линзы, Магазины и Йонеда .
- Теория категории: Объектив Бартоша .
- Функциональные линзы .
- Линзы с нуля .
- Линзы - это точно коалгебры для магазина Comonad .
- Линзы, складки и обходы .
- Слайды .