Функциональное программирование на TypeScript: Option и Either

Предыдущие статьи цикла:

  1. Полиморфизм родов высших порядков
  2. Паттерн «класс типов»


В предыдущей статье мы рассмотрели понятие класса типов (type class) и бегло познакомились с классами типов «функтор», «монада», «моноид». В этой статье я обещал подойти к идее алгебраических эффектов, но решил всё-таки написать про работу с nullable-типами и исключительными ситуациями, чтобы дальнейшее изложение было понятнее, когда мы перейдем к работе с задачами (tasks) и эффектами. Поэтому в этой статье, всё еще рассчитанной на начинающих ФП-разработчиков, я хочу поговорить о функциональном подходе к решению некоторых прикладных проблем, с которыми приходится иметь дело каждый день.

Как всегда, я буду иллюстрировать примеры с помощью структур данных из библиотеки fp-ts.

Стало уже некоторым моветоном цитировать Тони Хоара с его «ошибкой на миллиард» — введению в язык ALGOL W понятия нулевого указателя. Эта ошибка, как опухоль, расползлась по другим языкам — C, C++, Java, и, наконец, JS. Возможность присвоения переменной любого типа значения null приводит к нежелательным побочным эффектам при попытке доступа по этому указателю — среда исполнения выбрасывает исключение, поэтому код приходится обмазывать логикой обработки таких ситуаций. Думаю, вы все встречали (а то и писали) лапшеобразный код вида:

function foo(arg1, arg2, arg3) {   if (!arg1) {     return null;   }    if (!arg2) {     throw new Error("arg2 is required")   }    if (arg3 && arg3.length === 0) {     return null;   }    // наконец-то начинается бизнес-логика, использующая arg1, arg2, arg3 }

TypeScript позволяет снять небольшую часть этой проблемы — с флагом strictNullChecks компилятор не позволяет присвоить не-nullable переменной значение null, выбрасывая ошибку TS2322. Но при этом из-за того, что тип never является подтипом всех других типов, компилятор никак не ограничивает программиста от выбрасывания исключения в произвольном участке кода. Получается до смешного нелепая ситуация, когда вы видите в публичном API библиотеки функцию add :: (x: number, y: number) => number, но не можете использовать её с уверенностью из-за того, что её реализация может включать выбрасывание исключения в самом неожиданном месте. Более того, если в той же Java метод класса можно пометить ключевым словом throws, что обяжет вызывающую сторону поместить вызов в try-catch или пометить свой метод аналогичной сигнатурой цепочки исключений, то в TypeScript что-то, кроме (полу)бесполезных JSDoc-аннотаций, придумать для типизации выбрасываемых исключений сложно.

Также стоит отметить, что зачастую путают понятия ошибки и исключительной ситуации. Мне импонирует разделение, принятое в JVM-мире: Error (ошибка) — это проблема, от которой нет возможности восстановиться (скажем, закончилась память); exception (исключение) — это особый случай поток исполнения программы, который необходимо обработать (скажем, произошло переполнение или выход за границы массива). В JS/TS-мире мы выбрасываем не исключения, а ошибки (throw new Error()), что немного запутывает. В последующем изложении я буду говорить именно об исключениях как о сущностях, генерируемых пользовательским кодом и несущими вполне конкретную семантику — «исключительная ситуация, которую было бы неплохо обработать».

Функциональные подходы к решению этих двух проблем — «ошибки на миллиард» и исключительных ситуаций — мы сегодня и будем рассматривать.

Option<A> — замена nullable-типам

В современном JS и TS для безопасной работы с nullable-типам есть возможность использовать optional chaining и nullish coalescing. Тем не менее, эти синтаксические возможности не покрывают всех потребностей, с которыми приходится сталкиваться программисту. Вот пример кода, который нельзя переписать с помощью optional chaining — только путём монотонной работы с if (a != null) {}, как в Go:

const getNumber = (): number | null => Math.random() > 0.5 ? 42 : null; const add5 = (n: number): number => n + 5; const format = (n: number): string => n.toFixed(2);  const app = (): string | null => {   const n = getNumber();   const nPlus5 = n != null ? add5(n) : null;   const formatted = nPlus5 != null ? format(nPlus5) : null;   return formatted; };

Тип Option<A> можно рассматривать как контейнер, который может находиться в одном из двух возможных состояний: None в случае отсутствия значения, и Some в случае наличия значения типа A:

type Option<A> = None | Some<A>;  interface None {   readonly _tag: 'None'; }  interface Some<A> {   readonly _tag: 'Some';   readonly value: A; }

Оказалось, что для такой структуры можно определить экземпляры функтора, монады и некоторых других. Для сокращения кодовых выкладок я покажу реализацию класса типов «монада», а дальше мы проведем параллели между императивным кодом с обработкой ошибок обращения к null, приведенным выше, и кодом в функциональном стиле.

import { Monad1 } from 'fp-ts/Monad';  const URI = 'Option'; type URI = typeof URI;  declare module 'fp-ts/HKT' {   interface URItoKind<A> {     readonly [URI]: Option<A>;   } }  const none: None = { _tag: 'None' }; const some = <A>(value: A) => ({ _tag: 'Some', value });  const Monad: Monad1<URI> = {   URI,   // Функтор:   map: <A, B>(optA: Option<A>, f: (a: A) => B): Option<B> => {     switch (optA._tag) {       case 'None': return none;       case 'Some': return some(f(optA.value));     }   },   // Аппликативный функтор:   of: some,   ap: <A, B>(optAB: Option<(a: A) => B>, optA: Option<A>): Option<B> => {     switch (optAB._tag) {       case 'None': return none;       case 'Some': {         switch (optA._tag) {           case 'None': return none;           case 'Some': return some(optAB.value(optA.value));         }       }     }   },   // Монада:   chain: <A, B>(optA: Option<A>, f: (a: A) => Option<B>): Option<B> => {     switch (optA._tag) {       case 'None': return none;       case 'Some': return f(optA.value);     }   } };

Как я писал в предыдущей статье, монада позволяет организовывать последовательные вычисления. Интерфейс монады один и тот же для разных типов высшего порядка — это наличие функций chain (она же bind или flatMap в других языках) и of (pure или return).

Если бы в JS/TS был синтаксический сахар для более простой работы с интерфейсом монады, как в Haskell или Scala, то мы единообразно работали бы с nullable-типам, промисами, кодом с исключениями, массивами и много чем еще — вместо того, чтобы раздувать язык большим количеством точечных (и, зачастую, частичных) решений частных случаев (Promise/A+, потом async/await, потом optional chaining). К сожалению, подведение под основу языка какой-либо математической базы не является приоритетным направлением работы комитета TC39, поэтому мы работаем с тем, что есть.

Контейнер Option доступен в модуле fp-ts/Option, поэтому я просто импортирую его оттуда, и перепишу императивный пример выше в функциональном стиле:

import { pipe, flow } from 'fp-ts/function'; import * as O from 'fp-ts/Option';  import Option = O.Option;  const getNumber = (): Option<number> => Math.random() > 0.5 ? O.some(42) : O.none; // эти функции модифицировать не нужно! const add5 = (n: number): number => n + 5; const format = (n: number): string => n.toFixed(2);  const app = (): Option<string> => pipe(   getNumber(),   O.map(n => add5(n)), // или просто O.map(add5)   O.map(format) );

Благодаря тому, что один из законов для функтора подразумевает сохранение композиции функций, мы можем переписать app еще короче:

const app = (): Option<string> => pipe(   getNumber(),   O.map(flow(add5, format)), );

N.B. В этом крохотном примере не нужно смотреть на конкретную бизнес-логику (она умышленно сделана примитивной), а важно подметить одну вещу касательно функциональной парадигмы в целом: мы не просто «использовали функцию по-другому», мы абстрагировали общее поведение для вычислительного контекста контейнера Option (изменение значения в случае его наличия) от бизнес-логики (работа с числами). При этом само вынесенное в функтор/монаду/аппликатив/etc поведение можно переиспользовать в других местах приложения, получив один и тот же предсказуемый порядок вычислений в контексте разной бизнес-логики. Как это сделать — мы рассмотрим в последующих статьях, когда будем говорить про Free-монады и паттерн Tagless Final. С моей точки зрения, это одна из сильнейших сторон функциональной парадигмы — отделение общих абстрактных вещей с последующим переиспользованием их для композиции в более сложные структуры.

Either<E, A> — вычисления, которые могут идти двумя путями

Теперь поговорим про исключения. Как я уже писал выше, исключительная ситуация — это нарушение обычного потока исполнения логики программы, на которое как-то необходимо среагировать. При этом выразительных средств в самом языке у нас нет — но мы сможем обойтись структурой данных, несколько схожей с Option, которая называется Either:

type Either<E, A> = Left<E> | Right<A>;  interface Left<E> {   readonly _tag: 'Left';   readonly left: E; }  interface Right<A> {   readonly _tag: 'Right';   readonly right: A; }

Тип Either<E, A> выражает идею вычислений, которые могут пойти по двум путям: левому, завершающемуся значением типа E, или правому, завершающемуся значением типа A. Исторически сложилось соглашение, в котором левый путь считается носителем данных об ошибке, а правый — об успешном результате. Для Either точно так же можно реализовать множество классов типов — функтор/монаду/альтернативу/бифунктор/etc, и всё это уже есть реализовано в fp-ts/Either. Я же приведу реализацию интерфейса монады для общей справки:

import { Monad2 } from 'fp-ts/Monad';  const URI = 'Either'; type URI = typeof URI;  declare module 'fp-ts/HKT' {   interface URItoKind2<E, A> {     readonly [URI]: Either<E, A>;   } }  const left = <E, A>(e: E) => ({ _tag: 'Left', left: e }); const right = <E, A>(a: A) => ({ _tag: 'Right', right: a });  const Monad: Monad2<URI> = {   URI,   // Функтор:   map: <E, A, B>(eitherEA: Either<E, A>, f: (a: A) => B): Either<E, B> => {     switch (eitherEA._tag) {       case 'Left':  return eitherEA;       case 'Right': return right(f(eitherEA.right));     }   },   // Аппликативный функтор:   of: right,   ap: <E, A, B>(eitherEAB: Either<(a: A) => B>, eitherEA: Either<A>): Either<B> => {     switch (eitherEAB._tag) {       case 'Left': return eitherEAB;       case 'Right': {         switch (eitherEA._tag) {           case 'Left':  return eitherEA;           case 'Right': return right(eitherEAB.right(eitherEA.right));         }       }     }   },   // Монада:   chain: <E, A, B>(eitherEA: Either<E, A>, f: (a: A) => Either<E, B>): Either<E, B> => {     switch (eitherEA._tag) {       case 'Left':  return eitherEA;       case 'Right': return f(eitherEA.right);     }   } };

Рассмотрим пример императивного кода, который бросает исключения, и перепишем его в функциональном стиле. Классической предметной областью, на которой демонстрируют работу с Either, является валидация. Предположим, мы пишем API регистрации нового аккаунта, принимающий email пользователя и пароль, и проверяющий следующие условия:

  1. Email содержит знак «@»;
  2. Email хотя бы символ до знака «@»;
  3. Email содержит домен после знака «@», состоящий из не менее 1 символа до точки, самой точки и не менее 2 символов после точки;
  4. Пароль имеет длину не менее 1 символа.

После завершения всех проверок возвращается некий аккаунт, либо же ошибка валидации. Типы данных, составляющий наш домен, описываются очень просто:

interface Account {   readonly email: string;   readonly password: string; }  class AtSignMissingError extends Error { } class LocalPartMissingError extends Error { } class ImproperDomainError extends Error { } class EmptyPasswordError extends Error { }  type AppError =   | AtSignMissingError   | LocalPartMissingError   | ImproperDomainError   | EmptyPasswordError;

Императивную реализацию можно представить как-нибудь так:

const validateAtSign = (email: string): string => {   if (!email.includes('@')) {     throw new AtSignMissingError('Email must contain "@" sign');   }   return email; }; const validateAddress = (email: string): string => {   if (email.split('@')[0]?.length === 0) {     throw new LocalPartMissingError('Email local-part must be present');   }   return email; }; const validateDomain = (email: string): string => {   if (!/\w+\.\w{2,}/ui.test(email.split('@')[1])) {     throw new ImproperDomainError('Email domain must be in form "example.tld"');   }   return email; }; const validatePassword = (pwd: string): string => {   if (pwd.length === 0) {     throw new EmptyPasswordError('Password must not be empty');   }   return pwd; };  const handler = (email: string, pwd: string): Account => {   const validatedEmail = validateDomain(validateAddress(validateAtSign(email)));   const validatedPwd = validatePassword(pwd);    return {     email: validatedEmail,     password: validatedPwd,   }; };

Сигнатуры всех этих функций обладают той самой чертой, о которой я писал в начале статьи — они никак не сообщают использующему этот API программисту, что они выбрасывают исключения. Давайте перепишем этот код в функциональном стиле с использованием Either:

import * as E from 'fp-ts/Either'; import { pipe } from 'fp-ts/function'; import * as A from 'fp-ts/NonEmptyArray';  import Either = E.Either;

Переписать императивный код, выбрасывающий исключения, на код с Either’ами достаточно просто — в месте, где был оператор throw, пишется возврат левого (Left) значения:

// Было: const validateAtSign = (email: string): string => {   if (!email.includes('@')) {     throw new AtSignMissingError('Email must contain "@" sign');   }   return email; };  // Стало: const validateAtSign = (email: string): Either<AtSignMissingError, string> => {   if (!email.includes('@')) {     return E.left(new AtSignMissingError('Email must contain "@" sign'));   }   return E.right(email); };  // После упрощения через тернарный оператор и инверсии условия: const validateAtSign = (email: string): Either<AtSignMissingError, string> =>   email.includes('@') ?     E.right(email) :     E.left(new AtSignMissingError('Email must contain "@" sign'));

Аналогичным образом переписываются другие функции:

const validateAddress = (email: string): Either<LocalPartMissingError, string> =>   email.split('@')[0]?.length > 0 ?     E.right(email) :     E.left(new LocalPartMissingError('Email local-part must be present'));  const validateDomain = (email: string): Either<ImproperDomainError, string> =>   /\w+\.\w{2,}/ui.test(email.split('@')[1]) ?     E.right(email) :     E.left(new ImproperDomainError('Email domain must be in form "example.tld"'));  const validatePassword = (pwd: string): Either<EmptyPasswordError, string> =>   pwd.length > 0 ?      E.right(pwd) :      E.left(new EmptyPasswordError('Password must not be empty'));

Остается теперь собрать всё воедино в функции handler. Для этого я воспользуюсь функцией chainW — это функция chain из интерфейса монады, которая умеет делать расширение типов (type widening). Вообще, есть смысл рассказать немного о конвенции именования функций, принятой в fp-ts:

  • Суффикс W означает type Widening — расширение типов. Благодаря этому можно в одну цепочку поместить функции, возвращающие разные типы в левых частях Either/TaskEither/ReaderTaskEither и прочих структурах, основанных на типах-суммах:

    // Предположим, есть некие типы A, B, C, D, типы ошибок E1, E2, E3,  // и функции foo, bar, baz, работающие с ними: declare const foo: (a: A) => Either<E1, B> declare const bar: (b: B) => Either<E2, C> declare const baz: (c: C) => Either<E3, D> declare const a: A; // Не скомпилируется, потому что chain ожидает мономорфный по типу левой части Either: const willFail = pipe(   foo(a),   E.chain(bar),   E.chain(baz) );  // Скомпилируется корректно: const willSucceed = pipe(   foo(a),   E.chainW(bar),   E.chainW(baz) );

  • Суффикс T может означать две вещи — либо Tuple (например, как в функции sequenceT), либо монадные трансформеры (как в модулях EitherT, OptionT и тому подобное).
  • Суффикс S означает structure — например, как в функциях traverseS и sequenceS, которые принимают на вход объект вида «ключ — функция преобразования».
  • Суффикс L раньше означал lazy, но в последних релизах от него отказались в пользу ленивости по умолчанию.

Эти суффиксы могут объединяться — например, как в функции apSW: это функция ap из класса типов Apply, которая умеет делать type widening и принимает на вход структуру, по ключам которой итерирует.

Возвращаемся к написанию handler. Я использую chainW, чтобы собрать тип возможных ошибок как тип-сумму AppError:

const handler = (email: string, pwd: string): Either<AppError, Account> => pipe(   validateAtSign(email),   E.chainW(validateAddress),   E.chainW(validateDomain),   E.chainW(validEmail => pipe(     validatePassword(pwd),     E.map(validPwd => ({ email: validEmail, password: validPwd })),   )), );

Что же мы получили в результате такого переписывания? Во-первых, функция handler явно сообщает о своих побочных эффектах — она может не только вернуть объект типа Account, но и вернуть ошибки типов AtSignMissingError, LocalPartMissingError, ImproperDomainError, EmptyPasswordError. Во-вторых, функция handler стала чистой — контейнер Either это просто значение, не содержащее дополнительной логики, поэтому с ним можно работать без боязни, что произойдет что-то нехорошее в месте вызова.

NB: Разумеется, эта оговорка — просто соглашение. TypeScript как язык и JavaScript как рантайм никак нас не ограничивают от того, чтобы написать код в духе:

const bad = (cond: boolean): Either<never, string> => {   if (!cond) {     throw new Error('COND MUST BE TRUE!!!');   }   return E.right('Yay, it is true!'); };

Понятное дело, что в приличном обществе за такой код бьют канделябром по лицу на код ревью, а после просят переписать с использованием безопасных методов и комбинаторов. Скажем, если вы работаете со сторонними синхронными функциями, их стоит оборачивать в Either/IOEither с помощью комбинатора tryCatch, если с промисами — через TaskEither.tryCatch и так далее.

У императивного и функционального примеров есть один общий недостаток — они оба сообщают только о первой встреченной ошибке. То самое отделение поведения структуры данных от бизнес-логики, о котором я писал в секции про Option, позволит нам написать вариант программы, собирающей все ошибки, с минимальными усилиями. Для этого понадобится познакомиться с некоторыми новыми концепциями.

Есть у Either брат-близнец — тип Validation. Это точно такой же тип-сумма, у которого правая часть означает успех, а левая — ошибку валидации. Нюанс заключается в том, что Validation требует, чтобы для левой части типа E была определена операция contact :: (a: E, b: E) => E из класса типов Semigroup. Это позволяет использовать Validation вместо Either в задачах, где необходимо собирать все возможные ошибки. Например, мы можем переписать предыдущий пример (функцию handler) так, чтобы собрать все возможные ошибки валидации входных данных, не переписывая при этом остальные функции валидации (validateAtSign, validateAddress, validateDomain, validatePassword).

Расскажу пару слов об алгебраических структурах, умеющих объединять два элемента

Они выстраиваюся в следующую иерархию:

  • Magma (Магма), или группоид — базовый класс типов, определяющий операцию contact :: (a: A, b: A) => A. На эту операцию не налагается никаких других ограничений.
  • Если к магме добавить ограничение ассоциативности для операции concat, получим полугруппу (Semigroup). На практике оказывается, что полугруппы более полезны, так как чаще всего работа ведется со структурами, в которых порядок элементов имеет значимость — вроде массивов или деревьев.
  • Если к полугруппе добавить единицу (unit) — значение, которое можно сконструировать в любой момент просто так, — получим моноид (Monoid).
  • Наконец, если к моноиду добавим операцию inverse :: (a: A) => A, которая позволяет получить для произвольного значения его инверсию, получим группу (Group).

Groupoid hierarchy
Детальнее об иерархии алгебраических структур можно почитать в вики.

Иерархию классов типов, соответствующих таким алгебраическим структурам, можно продолжать и дальше: в библиотеке fp-ts определены классы типов Semiring, Ring, HeytingAlgebra, BooleanAlgebra, разного рода решётки (lattices) и т.п.

Нам для решения задачи получения списка всех ошибок валидации понадобится две вещи: тип NonEmptyArray (непустой массив) и полугруппа, которую можно определить для этого типа. Вначале напишем вспомогательную функцию lift, которая будет переводить функцию вида A => Either<E, B> в функцию A => Either<NonEmptyArray<E>, B>:

const lift = <Err, Res>(check: (a: Res) => Either<Err, Res>) => (a: Res): Either<NonEmptyArray<Err>, Res> => pipe(   check(a),   E.mapLeft(e => [e]), );

Для того, чтобы собрать все ошибки в большой кортеж, я возпользуюсь функцией sequenceT из модуля fp-ts/Apply:

import { sequenceT } from 'fp-ts/Apply'; import NonEmptyArray = A.NonEmptyArray;  const NonEmptyArraySemigroup = A.getSemigroup<AppError>(); const ValidationApplicative = E.getApplicativeValidation(NonEmptyArraySemigroup);  const collectAllErrors = sequenceT(ValidationApplicative);  const handlerAllErrors = (email: string, password: string): Either<NonEmptyArray<AppError>, Account> => pipe(   collectAllErrors(     lift(validateAtSign)(email),     lift(validateAddress)(email),     lift(validateDomain)(email),     lift(validatePassword)(password),   ),   E.map(() => ({ email, password })), );

Если запустим эти функции с одним и тем же некорректным примером, содержащим более одной ошибки, то получим разное поведение:

> handler('user@host.tld', '123') { _tag: 'Right', right: { email: 'user@host.tld', password: '123' } }  > handler('user_host', '') { _tag: 'Left', left: AtSignMissingError: Email must contain "@" sign }  > handlerAllErrors('user_host', '') {   _tag: 'Left',   left: [     AtSignMissingError: Email must contain "@" sign,     ImproperDomainError: Email domain must be in form "example.tld",     EmptyPasswordError: Password must not be empty   ] }

В этих примерах я хочу обратить ваше внимание на то, что мы получаем различную обработку поведения функций, составляющих костяк нашей бизнес-логики, не затрагивая при этом сами функции валидации (т.е. ту самую бизнес-логику). Функциональная парадигма как раз и заключается в том, чтобы из наличествующих строительных блоков собирать то, что требуется в текущий момент без необходимости сложного рефакторинга всей системы.


На этом текущую статью я заканчиваю, а в следующей будем говорить уже про Task, TaskEither и ReaderTaskEither. Они позволят нам подойти к идее алгебраических эффектов и понять, что это даёт в плане удобства разработки.

ссылка на оригинал статьи https://habr.com/ru/post/544636/

Дайджест интересных материалов для мобильного разработчика #383 (22 — 28 февраля)

Этот дайджест вышел небольшой, но в нем все равно есть продолжение истории про автотестирование, API сна для Android, подготовка Flutter к null safety и прогноз развития рынка приложений до 2025 года.

Этот дайджест доступен в виде еженедельной рассылки. А ежедневно новости мы рассылаем в Telegram-канале.

iOS

 Погружение в автотестирование на iOS. Часть 3. Жизненый цикл iOS приложения во время прогона тестов
Apple изменила экран подписки в iOS
Расширенное руководство по заполнению меток конфиденциальности от Apple
Как воспользоваться преимуществами нескольких схем в XCode
VComponents: компоненты для SwiftUI
Raivo OTP: открытый менеджер паролей для iOS

Android

 Стоп рефакторинг. Kotlin. Android
 Помощь  многим: Android-приложение для людей с особыми потребностями
Google представил Sleep API
Вышла бета-версия Jetpack Compose
Как интегрировать Google Pay в ваше Android-приложение и зачем он вам нужен
Анимированные переходы в Android
Фоновое использование геолокации в Android 11
Великие команды быстро мерджат
Сделайте ваши представления Android доступными с помощью всего одной строчки кода
Измеряем и оптимизируем размер изображения с помощью Glide или Picasso
Ленивое свойство с учетом жизненного цикла в Kotlin для Android-разработки
Bindables: DataBinding без observable fields и LiveData

Разработка

 Как удобно вести игровой баланс и не сломаться от количества ячеек в таблицах
 Подготовка экосистем Dart и Flutter к переходу на null safety
 Как выбрать мобильную кросс-платформу в 2021 году
 Интервью с Яной Артищевой: обучение в НИУ ВШЭ ВШБИ и страсть к VR-играм
 Не мешай ему взрослеть: как оптимизировать «растущее» приложение, чтобы оно оставалось удобным для пользователя
 Видео с Kolesa QA Meetup 3.0: QAцентризм, подготовка данных к тестам и независимые моки
Flutter Dev Podcast #25: Яндекс.Драйв
Podlodka #204: разработка приложений под TV
Дизайн приложений: примеры для вдохновения #33
Хочу стать дизайнером мобильных приложений. Что делать?
Почему работа без ТЗ — это способ сделать то, что действительно нужно заказчику
Распознавание речи в офлайне во Flutter: нет Siri, нет Google и нет, это не преобразование речи в текст
Почему так сложно понять код, написанный 5 минут назад?
Первый раз в качестве Senior-инженера
8 самых популярных языков программирования 2021
Быстрое распространение приложения через Firebase App Distribution с помощью GitHub Actions + Fastlane
Мир работает на плохом UI
Тени и неоморфизм во Flutter

Аналитика, маркетинг и монетизация

 Как увеличить срок хранения мобильного приложения? 6 проверенных способов
 Самые популярные языки для локализации в 2021 году: обзор от Alconost
“Удержание мобильных игр: полное руководство” от Wappier
«Отчет о мобильном стриминге» от Adjust: понимание нового медиа-ландшафта
myTracker начал загружать данные о доходах через S2S API
Nanit: мониторинг младенцев
Прогноз развития рынка приложений Sensor Tower: расходы потребителей достигнут $270 млрд к 2025
App Radar купил TheTool
SocialPeta и Nativex выпустили отчет о медиабаинге на мировом мобильном рынке за 2020 год
Удержание: как создать эффективную стратегию

AI, Устройства, IoT

 Bluetooth Low Energy: подробный гайд для начинающих. Bluetooth 5 и безопасность
Huawei открывает свои носимые устройства для сторонних приложений

Предыдущий дайджест. Если у вас есть другие интересные материалы или вы нашли ошибку — пришлите, пожалуйста, в почту.

ссылка на оригинал статьи https://habr.com/ru/company/productivity_inside/blog/544644/

Специфические задачи Data Science в Банке

image

В течение последних пяти лет я проработал в Центральном Аппарате Сбербанка в Управлении Валидации моделей машинного обучения (machine learning, ML) и видел много «узких мест», которые возникают при разработке и валидации моделей машинного обучения.

В этой статье сначала предполагал рассмотреть основные информационные системы некоторого абстрактного Банка X, поскольку именно на базе уже сложившихся информационных систем строится работа дата-аналитиков, а также обучаются и работают ML-алгоритмы принятия решений. Но, когда начал писать, вдруг обнаружил, что на самом деле намного интереснее обсудить ряд тем и подзадач, которые всплывают при построении и валидации самых базовых моделей Банка, то есть моделей кредитного риска.

Риск-менеджмент и расчет кредитного риска можно считать прародителями data science в Банке, так как управление кредитным риском является исконно банковской прерогативой. Именно умелое управление рисками позволяет банкам предложить что-то ценное рынку кредитно-финансовых отношений. Представление о том, что банк просто кладет себе в карман процентную маржу между процентом по кредиту и процентом по вкладу в корне не верно, хотя мне иногда приходится такое слышать от людей незнакомых с внутренней кухней банковского бизнеса.

Банк с одной стороны берет на себя все риски невозврата кредита, а с другой стороны дает гарантии вкладчику о возврате вложенных средств. Альтернатива вкладу в Банке, одолжить свои деньги напрямую заемщику без гарантий возврата. Банк же в свою очередь способен давать гарантии, так как с одной стороны обладает «подушкой безопасности» в виде основного капитала и изначально закладывает потери от невозврата кредитов в свои финансовые показатели («формирует резервы»). С другой стороны Банк умеет просчитывать вероятность того, что выданный им кредит заемщик не вернет. Конечно же никто не может предсказать в точности, вернет ли долг конкретное физическое лицо или компания, но в среднем по большому числу заемщиков вероятность оценить можно.

Банк будет финансово устойчивым только в том случае, если та прибыль, которую он зарабатывает на процентной марже покроет убытки от невозврата кредитов и прочие сопутствующие расходы Банка.

Устоявшаяся банковская практика

Перед тем, как перейти к обсуждению прогнозных моделей и непосредственно задач data science, буквально на минуту остановимся на специфике того, как банк работает с клиентом. Банк, и особенно крупный банк, это хорошо организованная система, в которой прописывается буквально каждый шаг. Это касается и взаимодействия с заемщиками.

В частности в отношении заемщиков часто применяется такое понятие, как «дефолт». Дефолт — это статус, который присваивается клиенту в том случае, когда появляется почти полная уверенность, что клиент деньги банку уже не вернет, по крайней мере в полном объеме. О правилах и процедурах, по которым клиентам присваивается статус дефолта договариваются на уровне специально созданной для этого рабочей группы. А затем вышеоговоренные правила прописывают во внутренней нормативной документации.

Если клиенту присвоен статус дефолта, обычно говорят, что «клиент вышел в дефолт». С точки зрения процессов Банка это означает, что будут запущены определенные процедуры взаимодействия с клиентом. Возможно будет решаться вопрос о банкротстве заемщика, Банк попытается реализовать заложенное имущество, взыскать денежные средства с поручителей или продать долг должника коллекторам и т.д.

Так уж исторически сложилось, что ожидаемые потери от невозврата кредитов принято раскладывать на три компоненты:

EL = PD*EAD*LGD

где EL — expected loss, ожидаемые потери;
PD — probability at default, вероятность того, что заемщику будет присвоен статус дефолта в течение следующего года, начиная с даты оценки;
EAD — exposure at default, все те денежные средства, которые клиент на дату «выхода в дефолт» должен вернуть Банку, включая как выданную денежную сумму, так и проценты, штрафы и комиссии;
LGD — loss given default, доля от общей задолженности заемщика перед банком, которую Банк себе уже не вернет. То есть это чистая потеря для Банка;

Если я где-то отхожу от учебных определений и понятий, то заранее прошу прощения, поскольку основная моя цель — это не написать правильный пересказ учебников, а ухватить суть существующих проблем. Для это приходится порой рассуждать «на пальцах».

Попробуем теперь сформулировать типовую задачу для дата-сайентиста. Первое, что стоит уметь прогнозировать — это вероятность дефолта PD. Здесь все кажется просто. У нас задача бинарной классификации. Дайте же нам данные с истинной меткой класса и всеми факторами и мы быстро соберем скрипт с двойной кросс-валидацией и подбором всех гиперпараметров, выберем модель с лучшей метрикой Джини и все будет в порядке. Но почему-то в реальности так не получается.

Нет никакой истинной метки класса

На самом деле истинную метку класса (таргет) мы не знаем. По идее таргет — это бинарная переменная, равная нулю, если заемщик «здоровый», и равная единице, если заемщику присвоен статус «дефолт». Но проблема-то в том, что правила, по которым определяется дефолт, придумываем мы сами. Стоит изменить правила и модель уже не работает даже на тренировочных исторических данных.

Мы плохо знаем своего клиента

С накоплением истории выданных кредитов появляется желание построить более сложные модели, а для этого нужны дополнительные сведения о клиентах. Тут-то и выясняется, что раньше нам эти сведения были не нужны, и соответственно их никто и не собирал. Как результат в собранных выборках много пропусков, что сводит на нет саму идею построить более «информированную модель». И, если бы только это.

Наличие большого числа клиентов вызывает соблазн разбить их не сегменты, в рамках которых построить более «узкие» и в тоже время более точные модели. Но ведь разбиение на сегменты выполняется тоже по какому-то правилу, а это правило строится на все тех же данных о клиентах. И что мы имеем? А мы имеем пропуски в данных, а соотвенно не всегда можем даже понять к какому именно сегменту отнести того или иного клиента.

Регулятор требует делать модели интерпретируемыми

Говоря «регулятор», я имею в виду Центробанк, который требует делать модели понятными. Должен быть понятен не только сам прогноз, но и правила, по которым этот прогноз был сделан. Справедливости ради, скажу, что в большей мере такое правило касается только так называемых «регуляторных» моделей. Регулятор в целях обеспечения устойчивости банковской системы в целом постоянно мониторит деятельность банков по ряду ключевых показателей, среди которых, например, находится расчет достаточности капитала на покрытие непредвиденных потерь во время возможных экономических и финансовых кризисов.
Что означает требование к интерпретируемости? А означает оно, что в большинстве случаев придется довольствоваться моделями в виде логистической регрессии или дерева решений. Про нейронные сети, ансамбли, стекинги и прочие «современные» архитекторы придется забыть.

Прокрустово ложе устоявшейся банковской практики

Отраслевой стандарт де-факто требует оценивать ожидаемые потери как произведение трех величин: PD, EAD и LGD. Это справедливо только в том случае, когда события развиваются по одному и тому же сценарию. Клиент либо возвращает кредит, либо нет. В первом случае, считается что никаких потерь нет. Во втором же случае, предполагается наличие некоторой суммы под риском (EAD).

На практике, платежное поведение клиентов не сводится к двум простым вариантам, а граница между этими вариантами весьма условна. Заемщик может выйти в дефолт и через месяц, и через год, и через два, а затем после того, как ему присвоят статус «дефолт», вдруг вернуться к платежам и выплатить весь кредит. Более того, отклонения от графика платежей могут быть и по суммам и по срокам, с опережением или наоборот отставанием от графика. Финансовый результат для Банка во всех случаях будет разный.

Я не говорю, что нельзя свести все разнообразие вариантов поведения заемщика к схеме расчета трех компонент в принципе. Конечно же все зависит от задачи. Где мы потом хотим эту модель применить? Если для оценки кредитного риска по пулам (группам) заемщиков, то все возможные отклонения учитываются различными калибровками и расчетом средневзвешенных значений. Но, если наша цель заключается в персонализации подхода при выдаче кредита, в том числе в персональном подборе предложений, важным становится прогноз потока платежей со стороны клиента или прогноз чистой приведенной стоимости.

На чем спотыкаются продвинутые data-driven альтернативы

Надо понимать, что вся отраслевая банковская практика была сформирована в те годы, когда не было никакой Big Data или машинного обучения, а все вычисления сводились к построению скоринговых карт. Брали все существенные факторы, влияющие на кредитоспособность заемщика, и оценивали в виде баллов, далее эти баллы суммировали и по сумме баллов определяли выдавать или не выдавать кредит.

С накоплением истории выданных кредитов и развитием вычислительной техники процедуры принятия решений в Банке постепенно усложнялись. Скоркарты превратились в модели логистической регрессии, которые строятся скриптами на python. Клиентов и продукты Банка начали сегментировать для того, чтобы внутри каждого сегмента строить свои узкозаточенные модели. С другой стороны с ростом объемов хранилищ данных появилась возможность собирать и вместе хранить все больше и больше информации во взаимосвязанном виде.

В конечном итоге все движется к идее, когда для каждого пришедшего клиента будет почти мгновенно обнаруживаться наилучшее предложение (оптимальный банковский продукт), которое бы максимизировало CLTV (customer lifetime value) на заданном временном горизонте, либо иную метрику в зависимости от текущего состояния Банка и целей его стейкхолдеров.

Почему бы для решения вышеописанной задачи не применить мощную нейросеть (то есть пресловутый «искусственный интеллект»)? Перечислю несколько мешающих этому обстоятельств:
— Центробанк требует, чтобы модели участвующие в расчете достаточности капитала применялись в «живом» кредитном процессе. То есть именно эти модели должны применяться в принятии решений о выдаче кредитов, быть интерпретируемыми и проходить ряд обязательных валидационных тестов;
— базы клиентских данных постоянно расширяются и дополняются. Например, относительно новыми видами данных является биометрия, веб-аналитика, аналитика мобильных приложений, скоринг социальных сетей. Добавление новых атрибутов происходит в динамике, а соответственно исторических данных по ним у нас практически нет;
— продукты и процессы Банка постоянно видоизменяются и требуется перерасчет CLTV по клиентам и расчет NPV (net present value) по новым продуктам. А для того, чтобы построить модель приемлемого качества надо подождать несколько лет, накопить исторические данные и вычислить фактические значения CLTV или NPV на выборке из реальных заемщиков;

Итог:

При всем желании нельзя рассматривать построение прогнозных моделей в Банке как чисто математическую задачу. На практике решаются бизнес-задачи, которые ко всему прочему сильно переплетены с требованиями регулятора в лице Центробанка.

Порой кажется, что в банковскую область могут проникнуть компании извне с сильным data science и поменять правила игры. Но для того, чтобы выдавать кредиты, надо играть по общим правилам, а следовательно становится Банком со всеми вытекающими последствиями.

Появление нового крутого финтех-стартапа в кредитовании, по-видимому, в большей степени завязано на поиск лазеек в правовом поле, чем на внедрение инноваций в машинном обучении.

ссылка на оригинал статьи https://habr.com/ru/post/544646/

Сервер Haute Couture: обзор Apple Xserve G4

Компания Apple — одна из самых заметных на компьютерном рынке, она же — одна из самых противоречивых. Равнодушных к ее продукции и рыночной стратегии исчезающе мало — ее либо любят, либо критикуют — бывает и вовсе ненавидят. Ее продукты хорошо известны многим, интересующимся компьютерной техникой.

Но есть одно направление, присутствие в котором предложений от Apple многими будет воспринято с удивлением. Это — рынок серверов. Сейчас де-юре компания не предлагает покупателям серверные системы, де-факто — Mac Mini с установленной macOS Server вполне успешно используется как в on-premise инсталляциях, так и предлагается в аренду некоторыми облачными провайдерами, в том числе и Selectel в рамках предложения Selectel.Lab.

Но полноценными серверами назвать этих трудолюбивых малышей сложно — нет привычных возможностей расширения и обеспечения отказоустойчивости, нет уже давно ставших стандартом возможностей управления через IPMI. Предвидим вполне резонный вопрос — а кому это вообще нужно, кроме, может быть разработчиков под соответствующую экосистему, когда есть множество других вариантов построения сервера.

Ответ также прост — с давних пор у «фруктовой компании» есть свои решения для построения корпоративных сетей, а коль скоро macOS (и, соответственно OS X) вполне себе Unix-совместимая система, значит, что компания, выбравшая Mac в качестве корпоративного стандарта, может пожелать и серверные потребности решать с использованием привычной техники и операционной системы.

В течение долгого времени, с 1993 по 2003 годы, Apple выпускала серверы, основанные на современных рабочих станциях Apple Workgroup Server. Эти машины логически были близки к современной идее использования Mac Mini в качестве сервера и были рассчитаны на обслуживание нужд небольших команд — «рабочих групп». Работали они сначала под управлением A/UX — собственной реализации Unix с графическим интерфейсом, аналогичным System 7 и совместимой с программами для нее. Последняя версия ее вышла в 1995 году и лишь в 1999 ее заменила OS X Server.

Но были в ее линейке и полноценные серверные машины, созданные изначально для работы в этой роли. Первой попыткой были Apple Network Server на базе ранних PowerPC и работавшие под AIX — реализации Unix от IBM. Просуществовала эта линейка недолго — с февраля 1996 по апрель 1997 года. Вторая попытка была более успешной, и именно о ней и пойдет речь в сегодняшней статье.

Встречаем по одежке и спецификации


И так, разрешите представить — первенец «нового» семейства Apple Xserve, представленная в мае 2002 года модель Xserve G4 (внутреннее обозначение RackMac1,1) в практически максимальной комплектации:

  • два процессора PowerPC G4 1.0 ГГц с 256 Кбайт кэша второго уровня и 2 Мбайта внешнего кэша третьего уровня каждый;
  • 2 Гбайта ОЗУ DDR266(установлен максимальный поддерживаемый объем, в оригинальной комплектации было 512 Мбайт) ;
  • жесткий диск 60 Гбайт с поддержкой горячей замены;
  • два гигабитных сетевых адаптера (один установлен на системной плате, другой выполнен в виде платы расширения PCI64).

Стоимость сервера составляла $2999 за однопроцессорную версию и $3999 за двухпроцессорную. Подробнее «начинку» рассмотрим позже, а пока — обещанная «одежка»!

Большинство читателей, полагаю, привыкли видеть сервер как некий утилитарный девайс со исключительно функциональным дизайном — максимальное использование передней панели для размещения жестких дисков, россыпь кнопок и защелок, многочисленные разноцветные индикаторы и, главное — сплошные решетки и сеточки для вентиляции. Классический черный или бежевый цвет, реже матовый серый. Строго, эффективно, утитарно — да. Красиво — на любителя, особенно если ваш любимый авто — Defender или Gelandewagen, а любимый самолет — Skyvan. Стильно — смотрите предыдущий пункт, но как правило — не тот случай.

Apple удалось сделать функциональное устройство красивым и стильным по общепризнанным канонам красоты. Передняя панель — царство шлифованного алюминия. Аккуратная линия панели управления с круглыми блестящими кнопками из полированного металла, установленными с незаметным глазу зазором и породистым нажатием. Порт FireWire для быстрого подключения внешнего диска.

Шестигранная скважина ключа блокировки отсека накопителей и устройств ввода. Две полоски светодиодов, отображающих загрузку процессоров. Собственно индикаторы — красивых сочных оттенков:

  • голубые для индикатора нагрузки,
  • зеленые для главного сетевого интерфейса,
  • желтые для отображения состояния системы и блокировки,
  • белый — главный индикатор включения.

Справа — оптический привод с лицевой панелью из такого же толстого куска алюминия и аккуратной блестящей кнопкой.

Ниже — отсеки для четырех жестких дисков — со сплошной, как вы уже догадались, алюминиевой лицевой частью. Защелок или рукояток нет, только два маленьких глазка индикаторов. Чтобы извлечь диск, нужно нажать на «мордочку» — плавно выедет удобная ручка — прямо как с ультрасовременном электромобиле Jaguar I-Pace. Диски с горячей заменой, но, что немало удивляет — с интерфейсом IDE.

В корзинках расположен короткий шлейф, а сам разъем, соединяющий их с бекплейном позаимствован у SCA (SCSI Configured Automatically), но самого SCSI в сервере не предусмотрено совершенно. Контроллер дисков реализован на базе двух чипов Promise PDC20270 и поддерживает только программные RAID-массивы, созданные средствами операционной системы.

Впрочем, дисковую подсистему нельзя назвать слабой — в феврале 2003 года на рынок вышла внешняя дисковая полка Xserve RAID — на 14 IDE дисков, снабженная двумя независимыми контроллерами с аппаратной поддержкой массивов уровня 0, 1, 3(!), 5 и 10 и подключаемая к серверу с помощью FibreChannel. Чуть позже в 2003 году вышло и первое обновление — новая версия RackMac1,2 c 1.33 ГГц чипами и CD-ROM со слотовой загрузкой вместо привычного выдвижного лотка. На его базе выпускалась версия Cluster Node с одним отсеком для диска и одной сетевой картой, без CD-ROM и видеоадаптера, но всегда с двумя процессорами.

Вынем внутренности!


Да, именно так. Не снимем крышку, а вынем из нее шасси. Apple и тут нашла необычное решение: к стойке жестко фиксируется крышка корпуса, а если открутить два винта с накатанной головкой — основное шасси выедет на встроенных направляющих.

И вот, она — начинка! Плата передней панели, отсеки жестких дисков с механизмом блокировки, бекплейн, мостиком с парой IDE контроллеров соединенный с системной платой (в терминах Apple — Logic Board). Системах охлаждения представлена парой вентиляторов-турбин, вращающихся с частотой около 5000 оборотов в минуту — одна турбина через воздуховод охлаждает процессорную плату, другая — карты в PCI-слотах в левой части сервера. В правой — расположен единственный блок питания.

Сама системная плата — непривычно «пустая», на ее поверхности расположен только один крупный чип — PCI64 мост Intel 21154BE. Дополняют пейзаж 4 слота DIMM и два разъема для райзеров — PCI64 для подключения двух слотов и универсальный слот AGP/PCI64 — да, в сервер можно установить производительную видеокарту и использовать его как стоечную рабочую станцию.

В комплект входят два устройства расширения — гигабитная сетевая карта производства Apple на чипе Broadcom BCM5701 с интерфейсом PCI64 и PCI-видеокарта ATi Radeon DDR с 32 МБайт видеопамяти «на борту». Шина для видеокарты работает на 66 МГц (редко используемая в 32-битном варианте шины возможность), что обеспечивает производительность шины на уровне ранней AGP. Интерфейс подключения монитора — обычный D-SUB VGA.

На задней панели, помимо привычных USB (пока еще версии 1.1), присутствует COM-порт для управления, разъем встроенного Ethernet-адаптера (также до 1 Гбит/сек) и пара портов FireWire 400 (для подключения внешних накопителей).

Внимательный читатель спросит — а где же чипсет? Все верно, он никуда не делся — на обратной стороне платы расположен еще один мост Intel 21154BE, а также — неизвестная микросхема, скрытая под накладкой и чип Agere 1258AK5 — составляющие системный контроллер (чипсет в более привычных терминах).

А вот чего нет на плате — это сокета для установки процессора. Вместо него — разъем для установки процессорной платы (похожий, только меньших размеров использовался в мобильных процессорах Intel Pentium II в упаковке MMC-2). На процессорной плате в традициях Apple тех времен процессоры распаяны — оба друг рядом с другом. Возле каждого — микросхема внешнего кэша.

Сами процессоры — PowerPC 7455 («Apollo 6», относятся к семейству G4, выпущены Motorola с использованием 180 нм техпроцесса), выполнены в керамической упаковке типа FCBGA — кристалл на поверхности керамической подложки, шариковые выводы для пайки. Кроме процессоров и кэша, на плате расположен модуль преобразователя питания (VRM).

Через пыль, снега и почтовую службу


Это не будет история о долгом безуспешном поиске или щедром донате, все получилось намного проще. Основатель коллекции искал что-нибудь интересное. Один из его друзей посоветовал:

— “Купи себе Mac”.
— “Хочу сервер” — ответил основатель.
— “Так и купи себе Xserve” — ответ друга и определил вектор поиска.

Поиск занял всего 10 минут — среди пачки предложений относительно свежих x86 моделей попалось лишь одно, только что появившееся — «Старый Xserve G4 — включается». Цена была весьма интересна, так что машина была тут же оплачена «не глядя» и спустя несколько часов отправилась в увлекательное путешествие по России.

Путешествие заняло почти неделю, что довольно быстро, но не прошло бесследно. К сожалению, сложились три фактора — невнимательность продавца к упаковке, непредусмотрительность покупателя (обычно тип упаковки оговаривается и контролируется ее качество) и не доведенные до ума процессы у перевозчика (хотя будет честным отметить, что за последний год виден значительный прогресс!). Сервер прибыл обернутым в картонку и зашитым в мешковину. В итоге, сильно досталось крепежным «ушам» — они просто сложились вдоль корпуса. Что сказать — урок коллекционеру!

Серверу, впрочем, повезло — после пары часов кропотливой работы, «уши» были выпрямлены: на шасси — полностью в прежнее положение, на крышке — остались небольшие, но заметные следы повреждений. Крышка была и до отправки немного помята, это и сказалось. Хотя сервер закрывается и открывается нормально, для идеального вида стоит посетить кузовной сервис, специализирующийся на ремонте Cybertruck или на худой конец DeLorean.

Судя по всему, большую часть жизни наш экземпляр провел не в солнечной Калифорнии, а крупном индустриальном городе близ Урала. И жизнь провел насыщенную — не в крупном, спокойном и чистом дата центре, а в обычной серверной. И относились к нему не как к породистой редкой технике, а как к рядовому серверу локальной сборки — за время жизни изрядно досталось фиксаторам плат расширения, один вентиляторов был безвозвратно утерян, а чистки машина не видела полжизни.

В плюсах — вместо штатных 512 Мбайт памяти, установлен 1 Гбайт. При пристальном изучении, были обнаружены повреждения механизма запирания отсеков накопителей. К счастью, были повреждены лишь лепестки, фиксирующие салазки — пять минут работы и механизм приведен в первозданное состояние. Все это звучит страшно, но, на самом деле, это очень хорошее состояние — передняя панель в отличном косметическом состоянии, даже без царапин, а сама машина практически комплектна.

Следующий шаг — тестовое включение. И тут возникли проблемы — машина включается, по индикаторам видно, что идет инициализация, но вывода ни на экран, ни на последовательную консоль не было. После передергивания всех разъемов на плате, картинка появилась. Казалось бы, успех — теперь можно разобрать и почистить. Но не тут то было — после извлечения кучи пыли, машина вернулась к прежним симптомам.

При этом, машина точно пыталась стартовать — помимо работы индикаторов, была реакция на клавиатуру, которая пропадала при удалении оперативной памяти. Странный способ диагностики? Но имеющаяся в наличии пост-карта не подошла — оказалась несовместима по напряжению питания, поэтому оставался только «метод тыка» и советы опытных маководов.

Первым делом была заменена батарея CMOS, здесь используется литиевый «бочонок» на 3.6 В, а установленная показывала еле-еле 3 В. Не помогло и это, равно как многократный сброс настроек. Помогла неожиданная и, на самом деле, абсолютно очевидная идея — очистить контакты спиртом. Обычно проблемы создает память, да и то — в очень старых машинах. Здесь же «слабым звеном» оказалась видеокарта — два прохода спиртовой салфеткой и сервер снова готов общаться с «администратором».

При покупке, конфигурацию сервера выяснить возможности не было. То, что наш экземпляр относится к старшей модели с двумя процессорами оказалось крайне приятным сюрпризом. По традиции, был увеличен до максимального объем памяти — установлены 4 модуля по 512 Мбайт, что интересно — использование ECC памяти не предполагалось. Также пришлось заменить диск — комплектный был в удручающем состоянии. Удалось найти диск «родной» емкости — 60 Гбайт, свободными остались две дисковых корзины, в четвертом отсеке даже сохранилась оригинальная заглушка.

Осталось только установить недостающий кулер. На удивление, идеально подошел вентилятор от серверной платформы Intel SR1530, пришлось только перепиновать разъем — Apple не была бы собой, если бы не поменяла местами «плюс» и «минус» в разъеме. Так как родной кулер уже имел заметный люфт подшипника, заменили оба. На этом реставрация сервера была успешно окончена.

На лицо «гуёвая», консольная внутри

История серверных ОС Apple восходит к проекту A/UX — UNIX-подобной ОС с графическим интерфейсом в стиле современной ей System 7. Точкой отсчета же для OS X Server является 1999 год, когда была представлена версия 1.0, основанная на идеях проекта Rhapsody. Как и у A/UX «под капотом» был UNIX, точнее BSD-совместимое окружение поверх микроядра Darwin. Интерфейс же был позаимствован у NextSTEP — Workspace Manager, дополненный некоторыми нюансами из Mac OS 8.

Два года спустя, на этой же основе, но с новым интерфейсом «Aqua» вышла первая Mac OS X — версия 10.0, поставлявшаяся в клиентской и серверной версиях. По сей день Mac OS основывается на тех же принципах и считается одной из самых удобных и надежных операционных систем для персональных компьютеров и рабочих станций. Серверная версия выпускается по сей день, но так и осталась в тени более популярного клиентского варианта.

На Xserve было решено установить Mac OS X Server 10.2, версию соответствую времени выпуска данного экземпляра (сервер выпущен в октябре 2002 года). Установка принципиально не отличается от прочих (в том числе десктопных) версий Mac OS X, за исключением выбора набора сервисов, среди которых есть как фирменные яблочные, так и стандартные опенсорсные, и ввода серийного номера. После установки, система встречает окном входа, где нужно ввести логин и пароль, а не выбрать пользователя из списка.

В комплект поставки входят утилиты настройки и аппаратного мониторинга сервера — как локального, так и удаленных. Для фирменных сервисов Apple имеются удобные графические средства администрирования, при этом большинство возможностей, реализованных в графических утилитах не дублируются консольными командами или возможностью правки конфигов.

И наоборот — для настраиваемых из консоли опенсорс-сервисов, например Apache или MySQL, входящих в поставку, графических утилит не предоставляется. В результате, администратору требуется привыкнуть к обоим подходам к управлению. И все же, в целом, управление системой можно назвать удобным. А интерфейс — безусловно красивым, одним из лучших созданных для операционных систем по сей день.

Продолжение следует?


Обязательно. Тема винтажного яблочного железа будет раскрыта более подробно — в ближайшее время, пожалуй, это будет одним из важных направлений развития коллекции «Digital Vintage». Обязательно будет и продолжение темы обзоров линейки Xserve, но, к сожалению, оно не будет обширным — линейка просуществовала сравнительно недолго.

После модели Xserve G5 (RackMac3,1) серверная линейка, вслед за остальными компьютерами Apple перешла на процессоры Intel, сразу начав с 64-битных Xeon серии 5100. На Xeon вышло всего три серии Xserve — две на базе процессоров с микроархитектурой Core — Xeon 5100 (Xserve1,1) и Xeon 5400 (Xserve2,1) и одно на базе процессоров Nehalem — Xeon 5500 (Xserve3,1).

Последняя модель не получила даже обновления для поддержки линейки Xeon 5600, проект Xserve был свернут. К тому времени, Apple уже вовсю строила свое облако, iCloud. Серверы собственного производства в нем применения не нашли…

Да, Xserve не были лучшими серверами на рынке — они поздно получили поддержку IPMI, слишком рано перешли на IDE/SATA диски и никогда не использовали SCSI/SAS. Их модельный ряд был весьма ограниченным, а цена достаточно высокой. И все же — этот тот редкий случай, когда сервер был не только инструментом, но и действительно красивой вещью.

Решение Apple свернуть проект, скорее всего, было стратегически верным, но мы будем скучать по этим необычным машинам. До новых встреч!

ссылка на оригинал статьи https://habr.com/ru/company/selectel/blog/544400/

Реализация мультиарендности с использованием Spring Boot, MongoDB и Redis

В этом руководстве мы рассмотрим, как реализовать мультиарендность в Spring Boot приложении с использованием MongoDB и Redis.

Используются:

  • Spring Boot 2.4

  • Maven 3.6. +

  • JAVA 8+

  • Монго 4.4

  • Redis 5

Что такое мультиарендность?

Мультиарендность (англ. multitenancy — «множественная аренда») — это программная архитектура, в которой один экземпляр программного приложения обслуживает нескольких клиентов. Все должно быть общим, за исключением данных разных клиентов, которые должны быть должным образом разделены. Несмотря на то, что они совместно используют ресурсы, арендаторы не знают друг друга, и их данные хранятся совершенно отдельно. Каждый покупатель называется арендатором.

Предложение «программное обеспечение как услуга» (SaaS) является примером мультиарендной архитектуры. Более подробно.

Модели с несколькими арендаторами

Можно выделить три основных архитектурных шаблона для мультиарендной архитектуры (Multitenancy), которые различаются степенью физического разделения данных клиента.

  1. База данных для каждого арендатора : каждый арендатор имеет свою собственную базу данных и изолирован от других арендаторов.

  2. Общая база данных, общая схема: все арендаторы совместно используют базу данных и таблицы. В каждой таблице есть столбец с идентификатором клиента, который показывает владельца строки.

  3. Общая база данных, отдельная схема : все арендаторы совместно используют базу данных, но имеют свои собственные схемы и таблицы базы данных.

Начнем

В этом руководстве мы реализуем мультиарендность на основе базы данных для каждого клиента.

Мы начнем с создания простого проекта Spring Boot на start.spring.io со следующими зависимостями:

<dependencies>     <dependency>         <groupId>org.springframework.boot</groupId>         <artifactId>spring-boot-starter-data-mongodb</artifactId>     </dependency>     <dependency>         <groupId>org.springframework.boot</groupId>         <artifactId>spring-boot-starter-web</artifactId>     </dependency>     <dependency>         <groupId>org.springframework.boot</groupId>         <artifactId>spring-boot-starter-data-redis</artifactId>     </dependency>     <dependency>         <groupId>redis.clients</groupId>         <artifactId>jedis</artifactId>     </dependency>     <dependency>         <groupId>org.projectlombok</groupId>         <artifactId>lombok</artifactId>         <optional>true</optional>     </dependency> </dependencies>

Определение текущего идентификатора клиента

Идентификатор клиента необходимо определить для каждого клиентского запроса. Для этого мы включим поле идентификатора клиента в заголовок HTTP-запроса. 

Давайте добавим перехватчик, который получает идентификатор клиента из http заголовка X-Tenant.

@Slf4j @Component public class TenantInterceptor implements WebRequestInterceptor {     private static final String TENANT_HEADER = "X-Tenant";     @Override     public void preHandle(WebRequest request) {         String tenantId = request.getHeader(TENANT_HEADER);         if (tenantId != null && !tenantId.isEmpty()) {             TenantContext.setTenantId(tenantId);             log.info("Tenant header get: {}", tenantId);         } else {             log.error("Tenant header not found.");             throw new TenantAliasNotFoundException("Tenant header not found.");         }     }     @Override     public void postHandle(WebRequest webRequest, ModelMap modelMap) {         TenantContext.clear();     }     @Override     public void afterCompletion(WebRequest webRequest, Exception e) {     } }

TenantContext — это хранилище, содержащее переменную ThreadLocal. ThreadLocal можно рассматривать как область доступа (scope of access), такую ​​как область запроса (request scope) или область сеанса (session scope).

Сохраняя tenantId в ThreadLocal, мы можем быть уверены, что каждый поток имеет свою собственную копию этой переменной и что текущий поток не имеет доступа к другому tenantId:

@Slf4j public class TenantContext {     private static final ThreadLocal<String> CONTEXT = new ThreadLocal<>();     public static void setTenantId(String tenantId) {         log.debug("Setting tenantId to " + tenantId);         CONTEXT.set(tenantId);     }     public static String getTenantId() {         return CONTEXT.get();     }     public static void clear() {         CONTEXT.remove();     } }

Настройка источников данных клиента (Tenant Datasources)

В нашей архитектуре у нас есть экземпляр Redis, представляющий базу метаданных (master database), в которой централизована вся информация о базе данных клиента. Таким образом, из каждого предоставленного идентификатора клиента информация о подключении к базе данных извлекается из базы метаданных .

RedisDatasourceService.java — это класс, отвечающий за управление всеми взаимодействиями с базой метаданных .

@Service public class RedisDatasourceService {      private final RedisTemplate redisTemplate;     private final ApplicationProperties applicationProperties;     private final DataSourceProperties dataSourceProperties;  		public RedisDatasourceService(RedisTemplate redisTemplate, ApplicationProperties applicationProperties, DataSourceProperties dataSourceProperties) {         this.redisTemplate = redisTemplate;         this.applicationProperties = applicationProperties;         this.dataSourceProperties = dataSourceProperties;     }          /**      * Save tenant datasource infos      *      * @param tenantDatasource data of datasource      * @return status if true save successfully , false error      */          public boolean save(TenantDatasource tenantDatasource) {         try {             Map ruleHash = new ObjectMapper().convertValue(tenantDatasource, Map.class);             redisTemplate.opsForHash().put(applicationProperties.getServiceKey(), String.format("%s_%s", applicationProperties.getTenantKey(), tenantDatasource.getAlias()), ruleHash);             return true;         } catch (Exception e) {             return false;         }     }          /**      * Get all of keys      *      * @return list of datasource      */           public List findAll() {         return redisTemplate.opsForHash().values(applicationProperties.getServiceKey());     }          /**      * Get datasource      *      * @return map key and datasource infos      */           public Map<String, TenantDatasource> loadServiceDatasources() {         List<Map<String, Object>> datasourceConfigList = findAll();         // Save datasource credentials first time         // In production mode, this part can be skip         if (datasourceConfigList.isEmpty()) {             List<DataSourceProperties.Tenant> tenants = dataSourceProperties.getDatasources();             tenants.forEach(d -> {                 TenantDatasource tenant = TenantDatasource.builder()                         .alias(d.getAlias())                         .database(d.getDatabase())                         .host(d.getHost())                         .port(d.getPort())                         .username(d.getUsername())                         .password(d.getPassword())                         .build();                 save(tenant);             });         }         return getDataSourceHashMap();     }          /**      * Get all tenant alias      *      * @return list of alias      */           public List<String> getTenantsAlias() {         // get list all datasource for this microservice         List<Map<String, Object>> datasourceConfigList = findAll();         return datasourceConfigList.stream().map(data -> (String) data.get("alias")).collect(Collectors.toList());     }          /**      * Fill the data sources list.      *      * @return Map<String, TenantDatasource>      */           private Map<String, TenantDatasource> getDataSourceHashMap() {         Map<String, TenantDatasource> datasourceMap = new HashMap<>();         // get list all datasource for this microservice         List<Map<String, Object>> datasourceConfigList = findAll();         datasourceConfigList.forEach(data -> datasourceMap.put(String.format("%s_%s", applicationProperties.getTenantKey(), (String) data.get("alias")), new TenantDatasource((String) data.get("alias"), (String) data.get("host"), (int) data.get("port"), (String) data.get("database"), (String) data.get("username"), (String) data.get("password"))));         return datasourceMap;     } }

В этом руководстве мы заполнили информацию о клиенте из yml-файла (tenants.yml). В производственном режиме можно создать конечные точки для сохранения информации о клиенте в базе метаданных.

Чтобы иметь возможность динамически переключаться на подключение к базе данных mongo, мы создаем класс MultiTenantMongoDBFactory, расширяющий класс SimpleMongoClientDatabaseFactory из org.springframework.data.mongodb.core. Он вернет экземплярMongoDatabase, связанный с текущим арендатором.

@Configuration public class MultiTenantMongoDBFactory extends SimpleMongoClientDatabaseFactory {  		@Autowired     MongoDataSources mongoDataSources;  		public MultiTenantMongoDBFactory(@Qualifier("getMongoClient") MongoClient mongoClient, String databaseName) {         super(mongoClient, databaseName);     }      @Override     protected MongoDatabase doGetMongoDatabase(String dbName) {         return mongoDataSources.mongoDatabaseCurrentTenantResolver();     } }

Нам нужно инициализировать конструктор MongoDBFactoryMultiTenant с параметрами по умолчанию ( MongoClient и databaseName).

Это реализует прозрачный механизм для получения текущего клиента. 

@Component @Slf4j public class MongoDataSources {      /**      * Key: String tenant alias      * Value: TenantDatasource      */     private Map<String, TenantDatasource> tenantClients;      private final ApplicationProperties applicationProperties;     private final RedisDatasourceService redisDatasourceService;      public MongoDataSources(ApplicationProperties applicationProperties, RedisDatasourceService redisDatasourceService) {         this.applicationProperties = applicationProperties;         this.redisDatasourceService = redisDatasourceService;     }      /**      * Initialize all mongo datasource      */     @PostConstruct     @Lazy     public void initTenant() {         tenantClients = new HashMap<>();         tenantClients = redisDatasourceService.loadServiceDatasources();     }      /**      * Default Database name for spring initialization. It is used to be injected into the constructor of MultiTenantMongoDBFactory.      *      * @return String of default database.      */     @Bean     public String databaseName() {         return applicationProperties.getDatasourceDefault().getDatabase();     }      /**      * Default Mongo Connection for spring initialization.      * It is used to be injected into the constructor of MultiTenantMongoDBFactory.      */     @Bean     public MongoClient getMongoClient() {         MongoCredential credential = MongoCredential.createCredential(applicationProperties.getDatasourceDefault().getUsername(), applicationProperties.getDatasourceDefault().getDatabase(), applicationProperties.getDatasourceDefault().getPassword().toCharArray());         return MongoClients.create(MongoClientSettings.builder()                 .applyToClusterSettings(builder ->                         builder.hosts(Collections.singletonList(new ServerAddress(applicationProperties.getDatasourceDefault().getHost(), Integer.parseInt(applicationProperties.getDatasourceDefault().getPort())))))                 .credential(credential)                 .build());     }      /**      * This will get called for each DB operations      *      * @return MongoDatabase      */     public MongoDatabase mongoDatabaseCurrentTenantResolver() {         try {             final String tenantId = TenantContext.getTenantId();              // Compose tenant alias. (tenantAlias = key + tenantId)             String tenantAlias = String.format("%s_%s", applicationProperties.getTenantKey(), tenantId);              return tenantClients.get(tenantAlias).getClient().                     getDatabase(tenantClients.get(tenantAlias).getDatabase());         } catch (NullPointerException exception) {             throw new TenantAliasNotFoundException("Tenant Datasource alias not found.");         }     } 73 }

Тест

Давайте создадим CRUD пример с документом Employee.

@Builder @Data @AllArgsConstructor @NoArgsConstructor @Accessors(chain = true) @Document(collection = "employee") public class Employee  {      @Id     private String id;      private String firstName;      private String lastName;      private String email; }

Также нам нужно создать классы EmployeeRepository, EmployeeService  и EmployeeController. Для тестирования при запуске приложения мы загружаем фиктивные данные в каждую базу данных клиента.

@Override public void run(String... args) throws Exception {     List<String> aliasList = redisDatasourceService.getTenantsAlias();     if (!aliasList.isEmpty()) {         //perform actions for each tenant         aliasList.forEach(alias -> {             TenantContext.setTenantId(alias);             employeeRepository.deleteAll();              Employee employee = Employee.builder()                     .firstName(alias)                     .lastName(alias)                     .email(String.format("%s%s", alias, "@localhost.com" ))                     .build();             employeeRepository.save(employee);              TenantContext.clear();         });     } }

Теперь мы можем запустить наше приложение и протестировать его. 

Итак, мы все сделали. Надеюсь, это руководство поможет вам понять, что такое мультиарендность и как она может быть реализована в Spring Boot проекте с использованием MongoDB и Redis.

Полный исходный код примера можно найти на GitHub.

ссылка на оригинал статьи https://habr.com/ru/post/544642/