...

суббота, 26 декабря 2020 г.

8 ответов на вопросы о менторах

1. Ментор – это кто?

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

Гомер писал, что в облике Ментора Телемаха вела по жизни сама богиня Афина, поэтому в античных сказаниях часто «ментор» = «маска божества». А в наше время ментором называют человека, который оказывает всяческую поддержку, наставляет, обучает и помогает достичь целей.

Сократ был учителем Платона. Тот был наставником Аристотеля. А Аристотель — ментором Александра Македонского
Сократ был учителем Платона. Тот был наставником Аристотеля. А Аристотель — ментором Александра Македонского

Сократ был учителем Платона. Тот был наставником Аристотеля. А Аристотель — ментором Александра Македонского.

2. Так «ментор» и «наставник» – одно и то же?

Действительно, они преследуют одинаковые цели — научить и поделиться опытом. Но если рассматривать рабочие методы, разница оказывается большой.

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

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

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

Так что, менторство — это не просто история про обмен опытом, а ментор — не просто наставник. Это человек, который передает вам опыт своих профессиональных побед и поражений, достижений и ошибок. А его опыт позволяет вам проходить путь к своей цели намного быстрее и эффективнее.

3. Кто НЕ ментор?

  1. Преподаватель. Потому что он, используя педагогические и методические подходы, учит вас только тому, что и как делается. Его задача — дать максимально возможные теоретические знания о тех или иных процессах.

  2. Тренер. Кратко рассказывает об определенной технологии выполнения того или иного действия и помогает закрепить ее использование на практике.

  3. Коуч. Действует по шаблонной схеме, используя ваши внутренние ресурсы и опыт для решения какой-либо задачи.

  4. Эксперт. Дает оценку ситуации в определенных узких сферах, основываясь на своих знаниях и опыте.

  5. Консультант. Дает  свои советы (как правило, на основе лишь теоретических знаний) по изменению ситуации в соответствии с текущими обстоятельствами.

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

4. Менторинг или коучинг, что лучше?

Чацкий:«…Наш ментор, помните колпак его, халат,Перст указательный, все признаки ученьяКак наши робкие тревожили умы…»© А.С. Грибоедов. «Горе от ума»

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

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

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

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

5. Насколько менторство популярно и эффективно?

На развитых рынках менторство считается одним из мощнейших рычагов роста в любой профессии и одним из самых действенных способов развития soft skills — гибких навыков вроде креативности и умения работать в команде.Согласно данным Американского общества обучения и развития, 75% руководителей компаний считают, что менторство сыграло важную роль в получении ими должности.

Аналитики Endeavor Insight в течение 10 лет  проводили мониторинг среди компаний Нью-Йорка и выяснили, что более 33% из них достигли успеха из-за менторства топовых предпринимателей.

Менторинг также делает сотрудников более лояльными к компании. Летом 2019 года сервис SurveyMonkey  провел опрос на тему счастья на работе. Один из выводов: если у сотрудников есть ментор, на рабочем месте им намного комфортнее. Сотрудники с менторами склонны считать, что их компания предоставляет хорошие возможности для карьерного роста. Среди топ-менеджеров так думает 80%.

А вот еще несколько цифр мировой статистики, подтверждающих популярность этого движения:

  • 76% организаций в Европе предлагают своим сотрудникам менторство;

  • 71% компаний из списка Fortune500 имеют корпоративные менторские программы;

  • 77% компаний в мире говорят, что менторство помогает удержать ценных сотрудников в компании;

  • 78% сотрудников, задействованных в менторских программах, не попадают под сокращение;

  • 60% соискателей в Европе и США указывают менторство обязательным критерием для нового места работы;

  • на $5000 — $22000 больше ежегодно получают сотрудники, у которых был ментор.

Крайне важно менторство для стартапов. В США 70% стартапов и компаний малого бизнеса, чьи основатели пользуются поддержкой менторов, преодолевают отметку в пять лет. А это в два раза больше, чем в случае со стартапами без менторов. Издание The Village провело интересное исследование по этому поводу: они проанализировали основные причины закрытия молодых компаний и пришли к выводу, что лишь одна из них связана с погрешностями продукта. Все остальное — отсутствие тех или иных знаний и менторской поддержки у основателей команды.

Издание Harvard Business Review в свое время провело опрос 45 CEO, чтобы выяснить, какую роль сыграли менторы в их профессиональной жизни. Как выяснилось, 84% гендиректоров уверены, что именно менторы помогли им избежать дорогостоящих ошибок и быстрее повысить свой экспертный опыт в определенной сфере.

6. Зачем мне ментор, если я не новичок?

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

1. Соучредитель сайта Flickr Катерина Фейк была ментором Чеда Дикерсона, основателя Etsy — крупнейшей онлайн-площадки для продажи хендмейда и винтажных вещей.

2. Али и Хади Партови, предприниматели из Кремниевой долины, были менторами Дрю Хьюстона и Араша Фирдоуси, основателей облачного сервиса хранения данных Dropbox.

3. У Стива Джобса наставником был Майк Марккула, один из первых инвесторов и топ-менеджеров Apple.

4. CEO Google Эрик Шмидт являлся наставником для его основателей Сергея Брина и Ларри Пейджа. Перечислять такие звездные пары можно очень долго. Шмидт в своей книге «Как работает Google» писал: «Даже если СЕО — суперзвезда, ему все равно нужен ментор. Ведь и у победителей Олимпиады есть тренеры».

Так что на разных этапах и в разных ситуациях нам нужны разные менторы. Меняться должны не только их имена, но и роли, стратегии и направления, просто потому, что меняемся мы сами и наши жизненные задачи.

7. Зачем люди этим занимаются?

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

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

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

А еще видеть, как добивается успехов твой ученик — большое удовольствие для ментора. Так что менторство – всегда двусторонний процесс, в котором вы и инвестируете, и получаете инвестиции.

8. Какие бывают менторы?

Да самые разные. Помните секретаршу Верочку из кинофильма «Служебный роман»? Как она делилась опытом со своей мымрой-начальницей Людмилой Прокофьевной, что значит «быть женщиной»? Учила ее ходить «как мы ходим», а не «сваи вколачивать»… Вот вам личностный менторинг в чистом виде.

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

Может иметь место и юридический менторинг. Например, Алексей Артюх, один из лучших налоговых юристов России, пользующийся наибольшим доверием среди коллег (рейтинг симпатий ПРАВО-300) считает: «Менторство и передача опыта в профессии – важнейшее условие для формирования лучших качеств юриста. Конечно, успеха можно добиться и без ментора, но тогда путь будет намного более тяжелым и тернистым».

Так что вывод здесь однозначен: менторство - это очень интересный, многообещающий и перспективный путь.

Каждый человек может и должен быть для кого-то ментором, чтобы реализовать свои скрытые и явные таланты, чтобы почувствовать себя счастливым и нужным, чтобы помогать людям.

И каждый человек должен иметь своего ментора, и не одного. Чтобы найти себя, найти свой путь, стать успешным специалистом, чтобы решать задачи быстрее и эффективнее. Хочешь стать ментором или найти своего ментора?

P.S. Здесь я собрал вопросы, которые мне чаще всего задают при разговоре о менторстве. Надеюсь, среди них вы найдете и тот, который волнует вас. Если нет – пишите, задавайте свой вопрос напрямую.  Поговорим!

Let's block ads! (Why?)

Курс биткоина впервые превысил $25 тысяч


26 декабря 2020 года курс биткоина впервые превысил $25 тыс. Стоимость криптовалюты за прошедшие сто дней выросла в два с половиной раза.
Отметку в 25 $тыс. биткоин преодолел сегодня с третьего раза. Сначала два раза курс достигал $24998 и даже некоторое время составлял $25013, а потом понижался на $200-$300.

По данным Coindesk, стоимость криптовалюты с начала года увеличилась на 240%. Эксперты считают, что за таким ростом стоят институциональные инвесторы, среди которых Skybridge Capital (потратил $25 млн в декабре на покупку криптовалюты), MassMutual ($100 млн в декабре) и Guggenheim Investments (фонд уже потратил на покупку биткоин до 10% из своих $5 млрд). Причем инвестиционный директор Guggenheim Investments Скотт Майнерд считает, что биткоин «чрезвычайно недооценен» при текущих уровнях цен, так как должен стоить около $400 тыс.

Текущая рыночная капитализация биткойна составляет около $350 млрд. У золота этот параметр составляет примерно $10 трлн.

Business Insider уточнил текущее настроение инвесторов биткоин. Они ежегодно говорят друг другу «Happy Bitmas» (Счастливого Биткоинжества), по аналогии с рождественскими поздравлениями. Но в конце этого года инвесторы биткоина более позитивны, чем были когда-либо ранее. Многие из них предполагают, что это только начало роста криптовалюты.

По словам руководителя отдела исследований Blockchain.com Гаррика Хилмана рыночная капитализация биткоина может достичь $1 трлн в 2021 году, если инвесторы более серьезно отнесутся к его статусу резервной валюты.

16 декабря цена биткоина преодолела $20 тыс.

См. также:

Let's block ads! (Why?)

История одного «сломанного» тестового задания или осторожнее с версиями OpenSSL…

Disclaimer. Я не «настоящий сварщик», но, в связи с поиском интересной работы в сфере информационной безопасности, в последнее время регулярно решаю разные CTF и машинки на HackTheBox. Поэтому, когда мне прислали ссылку на одно из тестовых заданий в стиле CTF, я не смог пройти мимо…

Смысл тестового задания достаточно простой. Дан дамп трафика, в котором спрятан ключ шифрования, некий мусор и зашифрованный флаг. Нужно их извлечь и расшифровать флаг. Также приведена команда OpenSSL, с помощью которой был зашифрован данный флаг. Трафик достаточно интересный, но уже через 10 строк кода на питоне передо мной лежал ключ шифрования, мусор и зашифрованный флаг. Казалось бы, что может пойти не так?

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

echo "FLAG_xxxx…xxxxxx" | openssl enc -e -base64 -aes-256-cbc -nosalt -k $password 

Я вставил полученные из трафика параметры в команду, запустил и … получил мусор! Попробовал еще раз. Снова мусор. Попробовал пересобрать трафик разными способами. Нет, судя по всему, трафик собрать можно только однозначно. Но на выходе шифрования снова мусор!!! При этом OpenSSL честно предупреждает, что получать так ключ из пароля в 1 проход плохая идея…
echo "ENCRYPTED_FLAG" | openssl enc -d -base64 -aes-256-cbc -nosalt -k $key 
*** WARNING : deprecated key derivation used.
Using -iter or -pbkdf2 would be better.

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

Как мы знаем, для работы AES нужен ключ шифрования и IV (вектор инициализации). Параметр -k дает нам возможность использовать текстовую фразу, из которой уже сам OpenSSL получает нужный ключ и IV. Увидеть их можно с помощью параметра -p.

echo "FLAG_123" | openssl enc -e -base64 -aes-256-cbc -nosalt -p -k "password"
*** WARNING : deprecated key derivation used.
Using -iter or -pbkdf2 would be better.
key=5E884898DA28047151D0E56F8DC6292773603D0D6AABBDD62A11EF721D1542D8
iv =3B02902846FFD32E92FF168B3F5D16B0
C11kA+GcqkU4ocOvZAVr3g==

Это знание тоже ничего мне не дало. Тогда я всё же решил вернуться к самой безумной идее, которая возникала у меня. А именно: проблема не во мне, а что-то поменялось в OpenSSL…
Трафик датировался 2016 годом, поэтому я взял Ubuntu 14.04 и, без особой надежды на успех, просто вставил в неё первоначальные данные. И внезапно вместо мусора получил ФЛАГ! Вечер переставал быть томным… Более того, одна и та же команда с тем же паролем и параметром -p выдавала совершенно разные ключи шифрования и IV!

НОВАЯ СИСТЕМА (openssl 1.1.1h)

echo "FLAG_123" | openssl enc -e -base64 -aes-256-cbc -nosalt -p -k "password"
*** WARNING : deprecated key derivation used.
Using -iter or -pbkdf2 would be better.
key=5E884898DA28047151D0E56F8DC6292773603D0D6AABBDD62A11EF721D1542D8
iv =3B02902846FFD32E92FF168B3F5D16B0
C11kA+GcqkU4ocOvZAVr3g==

СТАРАЯ СИСТЕМА (openssl 1.0.1f)
echo "FLAG_123" | openssl enc -e -base64 -aes-256-cbc -nosalt -p -k "password"
key=5F4DCC3B5AA765D61D8327DEB882CF992B95990A9151374ABD8FF8C5A7A0FE08
iv =B7B4372CDFBCB3D16A2631B59B509E94
R3N+5v3zOz9QcNt08cwqcA==

Стало понятно, что опасения подтвердились. Изменился алгоритм генерации Key и IV из парольной фразы, что полностью сломало возможность «в лоб» решить CTF на современных версиях OpenSSL. В процессе поиска нюансов реализации я наткнулся на очень интересную работу «Password-based OpenSSL Encryption Analysis of Key Derivation Protocol» и всё стало на свои места. Вкратце, в версии 1.1.0 был добавлен новый протокол генерации ключей из пароля PBKDF2, но, что более важно в старом алгоритме PBKDF1 изменен алгоритм хеширования «по умолчанию» с MD5 на SHA-256! Таким образом, один и тот же пароль выдает разные Key и IV. Для того, чтобы расшифровать зашифрованное ранее, в новых версиях нужно использовать параметр -md md5.

“-md messagedigest: specify the message digest used for key derivation from md2, md5, sha, or sha1

После добавления этого параметра получить флаг стало возможно и на новом OpenSSL. Уж не знаю, действительно ли такой «нюанс» был учтен разработчиками тестового задания или они просто не проверяли его на современных системах, но факт остаётся фактом, пришлось глубоко погрузиться в некоторые тонкости OpenSSL.

P.S. Через знакомых я уже сообщил разработчикам тестового о найденной проблеме, а то вдруг они очень удивляются, что это люди к ним не идут…

Let's block ads! (Why?)

Что выбрать в качестве библиотеки компонентов для React-проекта

Меня зовут Ксюша Луговая. В СберКорусе я занимаюсь поддержкой библиотеки React-компонентов Korus-UI. 

С проблемой выбора библиотеки рано или поздно сталкивается почти каждый разработчик, и порой решение может быть нетривиальным. Возникают вопросы: чем руководствоваться при выборе библиотеки, какие популярные решения предлагает рынок, какие у них плюсы и минусы? Обзоры и отзывы не всегда помогают найти решение.

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

Основные критерии выбора библиотеки

Сценарии использования библиотеки. Это звучит очевидно, но четкое понимание задач — первоочередный критерий выбора.

Типы компонентов приложения. От типа приложения зависит, какие компоненты вам потребуются. Часто достаточно набора кнопок/чекбоксов, базовых полей ввода, списков/меню с готовыми стилями. Значит, можно воспользоваться простыми компонентами с минимальным количеством настроек и готовыми стилями.

Настройки, форматирование и интерактивность в дизайне. Если вам нужно значительно отформатировать и стилизовать свои компоненты, это тоже важно решить заранее.

Когда требования четко сформулированы, ответьте на вопросы:

  • Хорошо ли составлена документация проекта, есть ли интерактивные примеры?

  • Насколько активно поддерживается проект?

  • Сколько в проекте issues и как быстро они решаются?

  • Проект бесплатный или коммерчески лицензированный?

  • Насколько легко настраиваются компоненты?

  • Покрыт ли код библиотеки тестами?

  • Какие браузеры и платформы поддерживает библиотека?

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

Я отобрала следующие библиотеки, чтобы наглядно показать процесс анализа по критериям:

  • Material-UI,

  • Semantic-UI-React,

  • yandex-ui,

  • arui-feather,

  • Korus-UI.

С одной стороны, в этом списке  представлены наиболее популярные проекты – Material-UI и Semantic-UI-React, которые были созданы одним разработчиком и со временем обросли большим сообществом.

С другой стороны – библиотеки, созданные внутри крупных компаний (Яндекс, Альфа Банк) для своих проектов, которые постепенно обрели популярность в качестве opensource решений.

Далее рассмотрим сравнение библиотек в разрезе определенных выше критериев.

Компонентный состав

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

Компоненты библиотек можно разделить на несколько групп.

Layout-компоненты 

Контейнеры, карточки, таблицы, гриды и прочие. Основные характеристики:

  • Отвечают только за отображение;

  • Часто принимают на вход только props.children;

  • Не имеют своего состояния и методов жизненного цикла;

  • Примеры:  h1, section, div, span, Icon, Avatar.

Готовые дизайн-системы библиотек сильно упрощают верстку, если проекту не нужна своя тема стилей. Например, Material-UI и  Semantic-UI могут вполне справиться с этой задачей. Однако в крупных коммерческих проектах своя дизайн-система и кастомизация стилей библиотек будет избыточной.

Компоненты-контролы (controls)

Кнопки, чекбоксы, радиокнопки, поля ввода, слайдеры — небольшие базовые компоненты, которые помогают «оживить» контент и обеспечивать взаимодействие с пользователем. 

Основные характеристики:

  • Отвечают за отображение и не имеют внутреннего состояния;

  • Принимают данные и функции обратного вызова в качестве props;

  • Состояние компонентов связано в основном только с UI (disabled, required, isLoading).

Сложные модульные компоненты

Это группа компонентов с более сложной логикой, они расширяют функции базовых компонентов и могут их комбинировать. 

Основные характеристики:

  • Имеют состояние;

  • Предоставляют данные и логику layout-компонентам (валидация, форматирование вывода, автодополнение);

  • Комбинируют другие компоненты.

Layout

Controls

Modules

Количество компонентов

Material-UI

App Bar, Avatars, Badges, Bottom Navigation, Divider, Grid List, Lists, Paper, Progress, Snackbar, Tables, 

Button, Chip, Selection Controls, Text Fields, Pickers*

Dialog, Cards, Drawers, ExpansionPanel, Menu, Stepper, Tabs, Tooltip

26**

Semantic-UI-React

Container, Divider, Flag, Header, Icon, Image, Label, List, Loader, Placeholder, Rail, Reveal, Segment, Step, Breadcrumb, Form, Grid, Menu, Message, Table, Advertisement, Card, Comment, Feed, Item, Statistic

Button, Input, Checkbox, Radio, Select, Text Area

Accordion, Dimmer, Dropdown, Embed, Modal, Popup, Progress, Rating, Search, Sidebar, Sticky, Tab, Transition, Visibility, Confirm, Pagination, Portal, Ref, Transitionable Portal

52

yandex-ui

Badge, Divider, Icon, Image, Text, UserPic, ListTile, Spacer, Link, Spin

Attach, Button, Checkbox, Menu, Radiobox, RadioButton, Select, Slider, Textarea, Textinput, Tumbler

TabsMenu, Drawer, Dropdown, Messagebox, Modal, Popup, TabsPanes, Tooltip, Progress 

30

arui-feather

Amount, CardImage, FlagIcon, Form, GridRow, GridCol, Heading, Icon, InputGroup, Label, Link, List, Paragraph, Spin

Attach, Button, CardInput, CheckBoxGroup, CheckBox, FormField, IconButton, Input, RadioGroup, Radio, Select, TagButton, Textarea, Toggle

CalendatInput, Calendar, Collapse, EmailInput, InputAutocomplete, IntlPhoneInput, Menu, MoneyInput, Notification, PhoneInput, Plate, Popup, ProgressBar, Sidebar, SlideDown, Tabs

44

Korus-UI

HTML tags factory***,

Currency, Tags

Button, Checkbox, Input,  Radio, Rating, Slider, Switcher, Textarea

Autocomplete, ButtonGroup, Collapse, Collapsible, 

DatePicker, DateRange, DateTimePicker, DateTimeRange, Dropdown, DropdownLink, DropdownSelect, Dropzone, FileDrop, FileUpload, Loader, MaskedInput, Modal, MultiSelect, Notifications, NumericRange, NumericTextBox, Pagination, Password, ProgressBar, StatusBar, StickyPanel, Tabs, TimePicker, TimeRange, Tooltip, Tour,  Validation, VStepper, Wizard, form

45

+ Компоненты-обертки для всех основных HTML-тегов

*Material-UI использует нативный календарь браузера в компонентах с выбором даты

**Основные компоненты библиотеки, для которых есть примеры в документации

***Korus-UI создает обертку для всех основных HTML-тегов c единым API

Почти 50% компонентного состава Material-UI и Semantic-UI-React и около 30% в библиотеках yandex-ui и arui-feather — малофункциональные layout-компоненты. В Korus-UI более 70% — сложные модульные компоненты.

Кастомизируемость

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

Material-UI

  • С помощью MuiThemeProvider. Компонент использует контекст библиотеки React для передачи объекта с темой всем дочерним компонентам.

  • Через добавление классов. Все компоненты поддерживают атрибут className.

Для кастомизации дочерних компонентов необходимо воспользоваться атрибутом classes.

Библиотека заточена на применение CSS-in-Js, что может вызвать определенные трудности, если стили для приложения содержатся в CSS-файлах. Для внедрения кастомных стилей CSS-in-Js предоставляется HOC withStyles() либо хук makeStyles() для функциональных компонентов.

Semantic-UI-React

У Semantic-UI-React нет своей темы, можно использовать стили  Semantic-UI.

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

Это:

yandex-ui

У библиотеки Яндекса несколько пресетов с темами. Их можно подключить сразу для всего проекта или применить стили для определенных компонентов.

Возможности:

  • Создание кастомной темы с помощью инструмента themekit

  • Переопределение значения (токены) в теме;

  • Дизайн-токены в формате yaml или json. Их можно собрать в итоговый файл (css, json, js, ios, android) и подключить к проекту.

arui-feather (Альфа Банк)

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

Korus-UI (СберКорус)

  • Кастомизация темы с помощью компонента LedaProvider. Он использует контекст библиотеки React для передачи объекта с темой всем дочерним компонентам.

  • Можно написать кастомные стили под имеющуюся вёрстку. Полный список классов  в компонентах находится в разделе API-документации (см. атрибут theme). Их можно переопределять глобально для всех компонентов одного типа или для каждого индивидуально. 

Расширяемость

В работе с библиотекой может возникнуть потребность изменить внутренний элемент компонента. Например, добавить картинку или иконку в поле ввода, заменить элемент на новый (в компонент Loader передать кастомный элемент спиннера).

Рассмотрим решения.

Material-UI

Позволяет изменять корневые элементы с помощью атрибута component.

Например, компонент List по дефолту рендерит <ul> элемент. Его можно заменить другим элементом или React компонентом:

<List component="nav">
  <ListItem button>
    <ListItemText primary="Trash" />
  </ListItem>
  <ListItem button>
    <ListItemText primary="Spam" />
  </ListItem>
</List>

Semantic-UI

Semantic-UI-React компоненты поддерживают схожий по функциональности атрибут as:

<Button as='a' />

Переданный элемент или React-компонент заменяют корневой элемент. Все неподдерживаемые пропсы передаются корневому элементу в качестве атрибутов.

yandex-ui

Предлагает использовать библиотеку render-override.  В ней есть набор хуков и компонентов для реализации переопределения элементов внутри составного компонента. 

Пример:

import React from 'react'
import { useRenderOverride } from '@yandex/ui/lib/render-override'

const ElementOriginal = ({ children }) => <div>{children}</div>
const MyComponent = ({ renderElement }) => {
  const Element = useRenderOverride(ElementOriginal, renderElement)
  return (
    <>
      <Element />
    </>
  )
}

В библиотеке yandex-ui расширяемость для существующих компонентов не реализована.

arui-feather (Альфа Банк)

Расширяемость компонентов не реализована.

Korus-UI

Расширяемость внутренних элементов библиотеки с помощью специального API. На вход компоненту можно передать атрибут из названия элемента и суффикса Render. Атрибуту присваивается функция, возвращающая элемент, которым заменяют существующий.

Простейший пример:

labelRender={() => <MyCustomLabel />}

Структура метода позволяет вносить изменения максимально гибко:

({ Element, elementProps, componentProps, componentState }) => React.Node
  • Element - сам элемент

  • elementProps - props элемента

  • componentState, componentProps - для удобства дополнительно приходят объекты с props и state всего компонента

Если мы хотим, чтобы новый элемент принимал на вход те же пропсы, можно отредактировать пример:

<L.CheckBox
  labelRender={({ elementProps }) => <MyCustomLabel {…elementProps} />}
>
  Label
</L.CheckBox>

Типизация

Для типизации React-проектов применяются 2 основных инструмента:

  • Typescript

  • PropTypes

В документации React для большой кодовой базы отдается предпочтение Typescript. Это инструмент статической типизации, который позволяет отлавливать большинство ошибок еще до исполнения кода. У PropTypes проверка типизации осуществляется только после запуска кода — это существенный недостаток по сравнению с другими инструментами.

В основном рассматриваемые библиотеки для типизации используют Typescript. У Semantic-UI основная библиотека написана на ванильном JS, а Typescript используется только в Semantic-UI-React, созданной для интеграции с библиотекой React.

Покрытие тестами

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

Ради справедливости стоит отметить, что автоматические тесты — не панацея. Высокий процент покрытия не гарантирует качество теста и охват основных пользовательских сценариев.

Но без покрытия тестами невозможно проверить совместную работу компонентов (интеграционное тестирование) и их работу вообще (модульное тестирование).

Сравним библиотеки на покрытие тестами.

Инструменты 

Типы тестов

% покрытия

Material-UI

Chai, Mocha, Sinon

Unit

95.28% Statements

87.22% Branches

97.51% Functions

95.26% Lines

Semantic-UI

Jasmine, Karma

Unit

Отчет о покрытии отсутствует

Semantic-UI-React

Chai, Enzyme

Unit

Отчет о покрытии отсутствует

yandex-ui

Jest, Enzyme

Unit

Запуск тестов приводит к ошибке

arui-feather

Jest, Enzyme

Unit

88.1% Statements

73.84% Branches

66.61% Functions

87.19% Lines

Korus-UI

Cypress, Jest

Unit, end-to-end

69.28% Statements

56.14% Branches

66.29% Functions

71.78% Lines

Документация

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

Документация

Наличие интерактивных примеров

Storybook

Material-UI

https://ift.tt/2Y4AxHO

-

-

Semantic-UI-React

https://ift.tt/2nZ6H6U

+

-

yandex-ui

https://ift.tt/3pn2I2I

-

+

arui-feather

https://ift.tt/3mNo0EV

+

-

Korus-UI

https://ift.tt/37Nw58f

+

+

Поддержка

Иногда возникает проблема, которая требует сторонней помощи. Нужно заранее определить, как ее получить.

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

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

В разделе Pulse на GitHub можно ознакомиться со статистикой репозитория по количеству Pull Request и коммитов за определенный период времени. Он находится на вкладке Insights каждого репозитория. Рассмотрим статистику по выбранным библиотекам.

Material-UI

Semantic-UI

yandex-ui

 arui-feather (Альфа Банк)

 Korus-UI (СберКорус)

Популярность

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

Однако стоит помнить, что популярность — это также результат хорошей маркетинговой стратегии и SEO-оптимизации, которые могут обеспечить библиотеке первые места в выдаче поисковика. Поэтому следует изучить обсуждения в блогах и на форумах, например, на Stackoverflow, Medium, DEV. Обсуждения находятся в разделе issues проекта. 

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

Звезды

Скачивания за последний год

Соотношение количества звезд и скачиваний за последний год,  % 

Material-UI

63 400

6 372 353

0,99

Semantic-UI

48 800

541 299

9

Semantic-UI-React

11 900

8 620 967

0,14

@yandex/ui

212

15 902

1,33

arui-feather (Альфа Банк)

559

26 744

2

Работа с формами и валидация данных

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

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

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

Сравним работу с формами в различных библиотеках на конкретном примере.

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

Korus-UI

Посмотреть код
const BasicForm = () => (
  <L.Div>
    <L.Input
      isRequired
      requiredMessage="Login is required"
      form="form"
      name="login"
      placeholder="Login"
    />
    <L.Input
      isRequired
      requiredMessage="Password is required"
      form="form"
      name="password"
      placeholder="Password"
    />
    <L.Button _warning form="form">
      Submit
    </L.Button>
  </L.Div>
);

Material-UI

Посмотреть код
const BasicForm = () => {
  const [login, setLogin] = React.useState("");
  const [loginError, setLoginError] = React.useState(false);
  const [password, setPassword] = React.useState("");
  const [passwordError, setPasswordError] = React.useState(false);

  return (
    <div>
      <form
        onSubmit={(e) => {
          e.preventDefault();
          setLoginError(!login);
          setPasswordError(!password);
        }}
      >
        <p>
          <TextField
            error={loginError}
            placeholder="Login"
            value={login}
            onChange={(e) => {
              setLoginError(false);
              setLogin(e.target.value);
            }}
            onBlur={(e) => {
              setLoginError(!login);
            }}
            helperText={loginError && "Login is required"}
          />
        </p>
        <p>
          <TextField
            error={passwordError}
            placeholder="Password"
            value={password}
            onChange={(e) => {
              setPasswordError(false);
              setPassword(e.target.value);
            }}
            onBlur={(e) => {
              setPasswordError(!password);
            }}
            helperText={passwordError && "Password is required"}
          />
        </p>
        <Button type="submit" color="primary" variant="contained">
          Sign Up
        </Button>
      </form>
    </div>
  );
};

Semantic-UI-React

Посмотреть код
const BasicForm = () => {
  const [login, setLogin] = React.useState("");
  const [loginError, setLoginError] = React.useState(false);
  const [password, setPassword] = React.useState("");
  const [passwordError, setPasswordError] = React.useState(false);

  return (
    <div>
      <Form
        onSubmit={(e) => {
          e.preventDefault();
          setLoginError(!login);
          setPasswordError(!password);
        }}
      >
        <Form.Group>
          <Form.Input
            error={loginError && { content: "Login is required" }}
            placeholder="Login"
            name="login"
            value={login}
            onChange={(e) => {
              setLoginError(false);
              setLogin(e.target.value);
            }}
            onBlur={(e) => {
              setLoginError(!login);
            }}
          />
          <Form.Input
            error={passwordError && { content: "Password is required" }}
            placeholder="password"
            name="password"
            value={password}
            onChange={(e) => {
              setPasswordError(false);
              setPassword(e.target.value);
            }}
            onBlur={(e) => {
              setPasswordError(!password);
            }}
          />
          <Form.Button content="Submit" />
        </Form.Group>
      </Form>
    </div>
  );
};

arui-feather

Посмотреть код
const BasicForm = () => {
  const [login, setLogin] = React.useState("");
  const [loginError, setLoginError] = React.useState(false);
  const [password, setPassword] = React.useState("");
  const [passwordError, setPasswordError] = React.useState(false);

  return (
    <Form
      onSubmit={(e) => {
        e.preventDefault();
        setLoginError(!login);
        setPasswordError(!password);
      }}
    >
      <FormField>
        <Input
          error={loginError && "Login is required"}
          placeholder="Login"
          value={login}
          onChange={(value) => {
            setLoginError(false);
            setLogin(value);
          }}
          onBlur={(e) => {
            setLoginError(!login);
          }}
        />
      </FormField>
      <FormField>
        <Input
          error={passwordError && "Password is required"}
          placeholder="Password"
          value={password}
          onChange={(value) => {
            setPasswordError(false);
            setPassword(value);
          }}
          onBlur={(e) => {
            setPasswordError(!password);
          }}
        />
      </FormField>
      <FormField>
        <Button view="extra" type="submit">
          Submit
        </Button>
      </FormField>
    </Form>
  );
};

yandex-ui

Посмотреть код
const BasicForm = () => {
  const [login, setLogin] = React.useState("");
  const [loginError, setLoginError] = React.useState(false);
  const [password, setPassword] = React.useState("");
  const [passwordError, setPasswordError] = React.useState(false);

  return (
    <form
      onSubmit={(e) => {
        e.preventDefault();
        setLoginError(!login);
        setPasswordError(!password);
      }}
      className={cnTheme(theme)}
    >
      <Textinput
        error={loginError}
        placeholder="Login"
        value={login}
        onChange={(e) => {
          setLoginError(false);
          setLogin(e.target.value);
        }}
        onBlur={(e) => {
          setLoginError(!login);
        }}
        hint={loginError && "Login is required"}
      />
      <Textinput
        error={loginError}
        placeholder="Password"
        value={password}
        onChange={(e) => {
          setPasswordError(false);
          setPassword(e.target.value);
        }}
        onBlur={(e) => {
          setPasswordError(!login);
        }}
        hint={passwordError && "Password is required"}
      />
      <Button type="submit" view="action">
        Submit
      </Button>
    </form>
  );
};

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

В Material-UI и yandex-ui нет компонента формы, в Semantic-UI-React и arui-feather компонент формы является простой оберткой тега <form> и не предоставляет никакой дополнительной функциональности.

Встроенная валидация полей отсутствует во всех рассмотренных библиотеках, кроме Korus-UI. Для реализации более сложной логики понадобится еще одна библиотека.

Сравнительный анализ библиотек React-компонентов

Подведем итог сравнительного анализа пяти библиотек React-компонентов.

Korus-UI (СберКорус)

Material-UI

Semantic-UI-React

arui-feather (Альфа Банк)

yandex-ui

Документация

Storybook

+

+

Примеры можно редактировать прямо в документации

+

+

+

Поддержка

Количество Pull Request за последний месяц

70

241

2

0

0

Лицензия

MIT license

MIT license

MIT license

Mozilla Public License 2.0

Mozilla Public License 2.0

Покрытие тестами

Процент покрытия > 50%

+

+

+

Не удалось выяснить

E2E тесты

+

Поддержка браузеров и платформ 

Chrome

85.0.4183.121

>= 49

Last 2 v.

Last 2 v.

Last 2 v.

Firefox

81.0.1

>= 52

Last 2 v.

Last 2 v.

>= 23

Edge

85.0.564.44

>=14

12+

Last 2 v.

IE

11

11

11+

11+

11+

Safari

14

>= 10

Last 2 v.

Last 2 v.

Opera

Last 2 v.

>= 12.1

Yandex

Last 2 v.

?

Android

4.4+

5+

>= 4

iOS Safari

7+

Last 2 v.

>= 5.1

Кастомизируемость

Возможность подключения кастомной темы

+

+

+

+

Расширяемость

+

+

+

Типизация

Typescript

Typescript

Typescript

Typescript

Typescript

Популярность

Соотношение звезд на GitHub и количества скачиваний за последний год, %

0,99

0,14

2

1,33

Компонентный состав

Есть компонент для работы с формами

+

+

+

Наличие встроенной валидации

+

В этой таблице приведены только универсальные критерии для оценки качества инструментов. Кроме них, на выбор библиотеки значительно влияют субъективные факторы и конкретные требования проекта.

Далее я расскажу, на какие факторы ориентировались мы в СберКорусе, и как было принято решение о создании своей библиотеки.

Почему Korus-UI

Может возникнуть вопрос: зачем нужна еще одна библиотека? На рынке уже существует большое количество библиотек компонентов React на любой вкус.

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

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

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

На разработку библиотеки Korus-UI ушло 1,5 года. В процессе создания мы ориентировались на лучшие практики разработки opensource-библиотек. 

Какие преимущества предоставляет Korus-UI

Формы и валидация

В Korus-UI подход к построению форм принципиально иной: поля и кнопка отправки формы связываются атрибутом form, в который передается строка с названием формы. Так элементы одной формы могут находиться в разных контейнерах, подключаться динамически, никакие общие обертки им не нужны. При этом создается объект формы, к которому можно получить доступ в обработчиках событий или с помощью метода  L.form(). Есть возможность передать кнопке массив из названий форм, чтобы валидировать и отправлять несколько форм одним кликом.

У библиотеки Korus-UI удобный обработчик onValidationFail, который позволяет получить объект формы, не прошедшей валидацию. А еще есть приятный бонус — прокрутки к невалидным полям.

Валидация в Korus-UI — это отдельный компонент. Его основные фичи:

  • Валидация поля функцией или RegExp

  • Готовые валидаторы

  • Один или несколько валидаторов для каждого поля со своими сообщениями

  • Настраиваемые сообщения (текст и внешний вид)

  • Задание валидности поля извне через атрибут isValid

  • Валидация компонентов в состоянии unmounted

  • Валидация полей по потере фокуса, сабмите формы и по событию в приложении

  • Прокрутка к невалидным полям при сабмите формы

  • Валидация нескольких форм одной кнопкой

  • Вспомогательные функции для валидации переданных значений

Единообразный API

Все компоненты поддерживают атрибуты, начинающиеся с нижнего подчеркивания _. Такие атрибуты будут преобразованы в имена css-классов:

<L.Div _flexBox> -> <div class="flex-box"></div>

Атрибут className также поддерживается.

В каждый компонент можно передать атрибут theme, который содержит набор css-классов для элементов компонента.

Все компоненты поддерживают атрибуты с суффиксом Render (см. раздел расширяемость).

Поведение и структура события расширены, соблюдается единый стандарт для всех компонентов:

{
  …Event, // оригинальное событие, сгенерированное React'ом
                
  // событие расширено объектом component, которое содержит данные из компонента
  component: {
    isValid?: boolean, // признак валидности компонента, есть только в onBlur
    name?: string, // имя формы, к которой привязан компонент
    value: any, // значение компонента
    … // другие свойства объекта (см. API компонента)
  }
}

Названия атрибутов с булевыми значениями начинаются с:

is: isOpen, isValid, isRequired, isDisabled

has: hasCloseButton

should: shouldCorrectValue

Все компоненты поддерживают атрибут ref.

Korus-UI расширяет механизм ref, принятый в React.

Недостатки Korus-UI

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

Еще можно отметить отсутствие поддержки мобильных платформ iOS и Android. Сейчас библиотека гарантирует поддержку только последних версий популярных браузеров. Подробная информация приведена в сравнительной таблице выше.

Итоги

Мы подробно рассмотрели основные критерии выбора библиотеки React-компонентов и проанализировали библиотеки Material-UI, Semantic-UI-React, arui-feather (Альфа Банк), yandex-ui, Korus-UI (СберКорус), опираясь на критерии качества. 

Вот коротко то, что нужно учесть при выборе библиотеки: 

  • Собственные нужды и требования

  • Кастомизируемость

  • Расширяемость

  • Типизацию

  • Покрытие тестами

  • Наличие техподдержки

  • Документацию

  • Отзывы и активность сообщества библиотеки

С учетом всех требований мы разработали собственную библиотеку Korus-UI, которая имеет все шансы стать достойным конкурентом существующим на рынке инструментам. Библиотека активно развивается, количество проектов, использующих Korus-UI в компании постоянно растет.

В ближайшее время мы добавим новые компоненты. Также в планах расширение тестового покрытия, перевод на английский язык документации и актуализация Storybook.

Библиотека  Korus-UI выложена в opensource и доступна на GitHub. Это первый шаг для нашей компании в opensource, и мы уверены, что не последний:)

Отдельно хочу выразить благодарность команде разработчиков СберКоруса, отцу и идейному вдохновителю библиотеки Артёму Повольских. Если вам интересно, как устроена фронтенд-разработка в СберКорусе, читайте статью Артёма.

Делитесь в комментариях своим опытом использования библиотек компонентов. Интересно обсудить, с какими преимуществами и недостатками вы сталкивались на личном опыте и какого функционала вам не хватает в процессе работы. 

Let's block ads! (Why?)

[Перевод] Устали от глупых шуток о JS? Напишите свою библиотеку

В JavaScript есть немало моментов, вызывающих вопрос «Чего???». Несмотря на то что у большинства из них есть логическое объяснение, если вы вникнете, они всё равно могут удивлять. Но JavaScript точно не заслуживает возмутительных шуток типа этой смеха. Например, иногда мы видим такие шутки:

В этом случае критика абсолютно не заслужена. Давайте разбираться почему.


JavaScript, как и какой-либо другой популярный язык программирования, представляет числа, использующие единый стандарт. Если быть точным, это стандарт IEEE 754 для чисел в 64-битном двоичном формате. Давайте попробуем проверить эту же шутку на других языках:

Как насчёт Ruby? На каком языке 0.1 + 0.2 не равно 0.3?

$ irb
irb(main):001:0> 0.1 + 0.2 == 0.3
=> false
irb(main):002:0> 0.1 + 0.2
=> 0.30000000000000004

Ruby! Какой глупый язык.

Или Clojure? На каком языке 0.1 + 0.2 не равно 0.3?

$ clj
Clojure 1.10.1
user=> (== (+ 0.1 0.2) 0.3)
false
user=> (+ 0.1 0.2)
0.30000000000000004

Clojure! Какой глупый язык.

Или как насчёт могучего Haskell? На каком языке 0.1 + 0.2 не равно 0.3?

$ ghci
GHCi, version 8.10.1: https://www.haskell.org/ghc/  :? for help
Prelude> 0.1 + 0.2 == 0.3
False
Prelude> 0.1 + 0.2
0.30000000000000004

Haskell! Хахаха. Какой глупый язык…

Вы поняли мысль. Проблема здесь не в JavaScript. Это большая проблема представления чисел с плавающей точкой в двоичном виде. Но я не хочу пока вдаваться в подробности IEEE 754. Потому что, если нам нужны произвольные точные числа, JavaScript делает их возможными. С октября 2019 года BigInt официально входит в стандарт TC39 ECMAScript.

Зачем беспокоиться об этом?


Мы продержались с IEEE 754 целую вечность. Большую часть времени это не кажется проблемой. Правда. Почти всегда это не проблема. Но иногда это всё-таки проблема. И в такие моменты хорошо иметь варианты.

Например, в начале этого года я работал над библиотекой диаграмм. Хотел нарисовать свечные графики на SVG. А в SVG есть такая аккуратная функция, называемая transform. Вы можете применить её к группе элементов, и она изменит систему координат для этих элементов. Так что с небольшой осторожностью вы можете упростить генерацию области диаграммы. Вместо того чтобы вычислять координаты графика для каждой свечи, вы указываете одно преобразование. А затем определяете каждую свечу, используя значения сырых данных. Очень аккуратно. По крайней мере в теории.

Но в тестах свойств у меня были проблемы. Если бы график был маленьким, а значения данных были большими, я бы получил ошибки округления. И зачастую это нормально. Но на графике некоторые пиксели должны выстраиваться в линию. Иначе рисунок выглядит неправильно. Так что я начал изучать BigInt. Итогом стала библиотека, которую я назвал Ratio. И я покажу, как она пишется.

Класс дроби


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

$3.1415926$


Часть слева от точек (… ) — это целая часть, а справа от точки — дробная часть. Но проблема в том, что некоторые числа имеют дробные части, которые нелегко разделить на две. Так что их трудно представить в двоичном виде. Но та же проблема возникает при основании 10. Например дробь 10/9. Можно попробовать написать что-нибудь вроде этого:

$1.111111111111111111111111111111111111.11111111111111111111111111111111111$


Однако это приближение. Чтобы представить 10/9 точно, единицы должны быть бесконечными. Поэтому мы должны использовать какую-то другую нотацию для представления повторяющихся. Например такую:

$1.\dot{1}$

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

Заметьте, что 10/9 имеет идеальную точность. И всё, что нужно для точности, — это два кусочка информации. Это числитель и знаменатель. С помощью одного значения BigInt мы можем представлять произвольно большие целые числа. Но если мы создадим пару из целых чисел, то сможем представлять произвольно большие или маленькие числа.

В JavaScript это может выглядеть так:

// file: ratio.js
export default class Ratio {
  // We expect n and d to be BigInt values.
  constructor(n, d) {
    this.numerator = n;
    this.denominator = d;
  }
}

Итак, мы проделали самую хитрую часть. «Изобрели» способ представления чисел с почти бесконечной точностью. (Мы все еще ограничены объемом памяти наших устройств.) Осталось только применить математику. Так что давайте добавим функциональности.

Равенство


Первое, что хочется сделать, — это сравнить две дроби. Зачем? Потому, что мне нравится сначала писать тесты. Если я могу сравнить две дроби на равенство, то писать тесты намного проще.

В простом случае написать метод равенства довольно легко:

// file: ratio.js
export default class Ratio {
  constructor(n, d) {
    this.numerator = n;
    this.denominator = d;
  }

  equals(other) {
    return (
      this.numerator === other.numerator &&
      this.denominator === other.denominator
    );
  }
}

Вот и хорошо. Но было бы неплохо, если бы наша библиотека могла сообщить, что 1/2 равна 2/4. Для этого нужно упростить дробь. То есть, прежде чем проверять равенство, мы хотим уменьшить числители и знаменатели обеих дробей до как можно более маленьких чисел. Итак, как мы сделаем это?

Наивный подход заключается в прогоне всех чисел от 1 до min(n,d) (где nn и dd — числитель и знаменатель соответственно). И это то, что я попробовал вначале. Код выглядел как-то так:

function simplify(numerator, denominator) {
    const maxfac = Math.min(numerator, denominator);
    for (let i=2; i<=maxfac; i++) {
      if ((numerator % i === 0) && (denominator % i === 0)) {
        return simplify(numerator / i, denominator / i);
      }
    }
    return Ratio(numerator, denominator);
}

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

Рекурсивная версия алгоритма Евклида красива и элегантна:

function gcd(a, b) {
    return (b === 0) ? a : gcd(b, a % b);
}

Применима мемоизация, что делает алгоритм довольно привлекательным. Но, увы, у нас еще нет хвостовой рекурсии в V8 или SpiderMonkey. (По крайней мере не на момент написания статьи.) Это означает, что если мы запустим его с достаточно большими целыми числами, то получим переполнение стека. А большие целые числа — это что-то вроде точки отсчёта.

Так что вместо этого воспользуемся итерационной версией:

// file: ratio.js
function gcd(a, b) {
    let t;
    while (b !== 0) {
        t = b;
        b = a % b;
        a = t;
    }
    return a;
}

Не так элегантно, но делает свою работу. И с этим кодом мы можем написать функцию для упрощения дробей. Пока мы это делаем, мы внесём и небольшое изменение, чтобы знаменатели всегда были положительными (т. е. для отрицательных чисел знак меняет только числитель).
// file: ratio.js

function sign(x) {
  return x === BigInt(0) ? BigInt(0)
       : x > BigInt(0)   ? BigInt(1) 
       /* otherwise */   : BigInt(-1);
}

function abs(x) {
  return x < BigInt(0) ? x * BigInt(-1) : x;
}

function simplify(numerator, denominator) {
  const sgn = sign(numerator) * sign(denominator);
  const n = abs(numerator);
  const d = abs(denominator);
  const f = gcd(n, d);
  return new Ratio((sgn * n) / f, d / f);
}

И теперь мы можем написать наш метод равенства:
// file: ratio.js -- inside the class declaration
  equals(other) {
    const a = simplify(this);
    const b = simplify(other);
    return (
      a.numerator === b.numerator &&
      a.denominator === b.denominator
    );
  }

Теперь можно сравнить две дроби на равенство. Может показаться, что это не так уж и много, но это значит, что мы можем написать юнит-тесты и убедиться, что наша библиотека работает, как ожидается.

Преобразование в другие типы


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

Метод .toString() проще всего, так что давайте начнём с него.

// file: ratio.js -- inside the class declaration
  toString() {
    return `${this.numerator}/${this.denominator}`;
  }

Достаточно просто. Но как насчёт преобразования обратно в число? Один из способов сделать это — просто разделить числитель на знаменатель:
// file: ratio.js -- inside the class declaration
  toValue() {
    return  Number(this.numerator) / Number(this.denominator);
  }

Зачастую это работает. Но, возможно, мы захотим немного подправить код. Весь смысл нашей библиотеки заключается в том, что мы используем большие целые числа для получения необходимой точности. И иногда эти целые числа будут слишком большими, чтобы их можно было преобразовать обратно к типу Number. Но мы хотим получить Number как можно ближе к истине, где это возможно. Так что мы выполняем немного арифметических действий, пока конвертируем BigInt в Number:
// file: ratio.js -- inside the class declaration
  toValue() {
    const intPart = this.numerator / this.denominator;
    return (
      Number(this.numerator - intPart * this.denominator) /
        Number(this.denominator) + Number(intPart)
    );
  }

Извлекая целую часть, мы уменьшаем размер значений BigInt, прежде чем преобразовать их в Number. Есть и другие способы сделать это, у которых меньше проблем с диапазоном. В основном они сложнее и медленнее. Если вам интересно, я рекомендую вам заглянуть в них глубже. Но в этой статье простой подход охватывает достаточно случаев, чтобы быть полезным.

Умножение и деление


Сделаем что-нибудь с числами. Как насчёт умножения и деления? Это несложно для дробей. Умножаем числители на числители, а знаменатели на знаменатели.
// file: ratio.js -- inside the class declaration
  times(x) {
    return simplify(
      x.numerator * this.numerator,
      x.denominator * this.denominator
    );
  }

Деление похоже на код выше. Переворачиваем вторую дробь, затем умножаем.
// file: ratio.js -- inside the class declaration
  divideBy(x) {
    return simplify(
      this.numerator * x.denominator,
      this.denominator * x.numerator
    );
  }

Сложение и вычитание


Теперь у нас есть умножение и деление. Логически следующая вещь, которую нужно написать, — это сложение и вычитание. Это немного сложнее, чем умножение и деление. Но не слишком.
Чтобы сложить две дроби, сначала нужно привести их к одному знаменателю, затем сложить числители. В коде это может выглядеть примерно так:
// file: ratio.js -- inside the class declaration
  add(x) {
    return simplify(
      this.numerator * x.denominator + x.numerator * this.denominator,
      this.denominator * x.denominator
    );
  }

Всё умножается на знаменатели. И мы используем simplify(), чтобы сохранялась дробь как можно меньше в смысле чисел числителя и знаменателя.
Вычитание похоже на сложение. Мы манипулируем двумя дробями так, чтобы одинаковые знаменатели выстраивались в ряд, как раньше. Затем не складываем, а вычитаем.
// file: ratio.js -- inside the class declaration
  subtract(x) {
    return simplify(
      this.numerator * x.denominator - x.numerator * this.denominator,
      this.denominator * x.denominator
    );
  }

Итак, у нас есть основные операторы. Можно складывать, вычитать, умножать и делить. Но нам всё ещё нужно несколько других методов. В частности, цифры имеют важное свойство: мы можем сравнивать их друг с другом.

Сравнения


Мы уже обсуждали .equals(). Но нам нужно нечто большее, чем просто равенство. Мы также хотели бы иметь возможность определить отношения дробей «больше, меньше». Поэтому создадим метод .lte(), который расскажет нам, является ли одна дробь меньшей или равной другой дроби. Как и в случае .equals(), не очевидно, какая из двух дробей меньше. Чтобы сравнить их, нам нужно преобразовать обе к одному знаменателю, затем сравнить числители. С небольшим упрощением это может выглядеть так:
// file: ratio.js -- inside the class declaration
  lte(other) {
    const { numerator: thisN, denominator: thisD } = simplify(
      this.numerator,
      this.denominator
    );
    const { numerator: otherN, denominator: otherD } = simplify(
      other.numerator,
      other.denominator
    );
    return thisN * otherD <= otherN * thisD;
  }

Как только мы получим .lte() и .equals(), то сможем вывести остальные сравнения. Можно выбрать любой оператор сравнения. Но если у нас есть equals() и >, стандарт FantasyLand. Вот как могут выглядеть другие операторы:
// file: ratio.js -- inside the class declaration
  lt(other) {
    return this.lte(other) && !this.equals(other);
  }

  gt(other) {
    return !this.lte(other);
  }

  gte(other) {
    return this.gt(other) || this.equals(other);
  }

Округление


Теперь мы можем сравнить дроби. А ещё можем умножать и делить, складывать и вычитать. Но если мы собираемся делать больше интересного с нашей библиотекой, нам нужно больше инструментов. Удобные объекты JavaScript Math содержат методы .floor() и .ceil().
Начнём с .floor(). Floor принимает значение и округляет его вниз. При положительных числах это означает, что мы просто сохраняем целую часть и отбрасываем оставшуюся часть. Но для отрицательных чисел мы округляем вверх от нуля, так что отрицательным числам нужно уделить немного больше внимания.
// file: ratio.js -- inside the class declaration
  floor() {
    const one = new Ratio(BigInt(1), BigInt(0));
    const trunc = simplify(this.numerator / this.denominator, BigInt(1));
    if (this.gte(one) || trunc.equals(this)) {
      return trunc;
    }
    return trunc.minus(one);
  }

Теперь можно использовать код выше, чтобы рассчитать округленные вверх значения.
// file: ratio.js -- inside the class declaration
  ceil() {
    const one = new Ratio(BigInt(1), BigInt(0));
    return this.equals(this.floor()) ? this : this.floor().add(one);
  }

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

Число в дробь


Преобразование чисел в дроби сложнее, чем может показаться на первый взгляд. И есть много разных способов проделать это преобразование. Мой способ реализации не самый точный, но он достаточно хорош. Чтобы он сработал, сначала конвертируем число в строку, которая, как мы знаем, приобретёт формат последовательности. Для этого JavaScript предоставляет нам метод .toExponential(). Метод возвращает число в экспоненциальной нотации. Вот несколько примеров для понимания идеи:
let x = 12.345;
console.log(x.toExponential(5));
// ⦘ '1.23450e+1''

x = 0.000000000042;
console.log(x.toExponential(3));
// ⦘ '4.200e-11'

x = 123456789;
console.log(x.toExponential(4));
// ⦘ '1.2346e+8'

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

Преимущество этой нотации в том, что она последовательна. Всегда есть ровно одна цифра слева от десятичной точки. А .toExponential() позволяет указать, сколько значащих цифр мы хотим. Затем идет 'e' и экспонента — всегда целое число. Поскольку значение последовательно, мы можем использовать наглое регулярное выражение, чтобы разобрать его.

Процесс идёт примерно так. Как уже упоминалось, .toExponential() принимает параметр для указания количества значащих цифр. Нам нужен максимум цифр. Итак, мы установили точность на 100 (столько позволит большинство JavaScript-движков). В этом примере, однако, мы будем придерживаться точности 10. Теперь представьте, что у нас есть число 0.987654321e0. Мы хотим перенести десятичную точку на 10 цифр вправо. Это дало бы нам 9876543210. Затем делим на 10^10, и получаем 9876543210/100000000. Это, в свою очередь, упрощает до 987654321/100000000.

Но мы должны обратить внимание на эту экспоненту. Если у нас есть число вроде 0.987654321e9, то мы всё равно сдвинем десятичную точку на 10 цифр вправо. Но мы делим на десять, к степени 10-9=1.

$0.987654321×10^9 = 9876543210/ 10^1=$

$987654321/1$


Чтобы всё было именно так, мы определили пару вспомогательных функций:
// Transform a ‘+’ or ‘-‘ character to +1 or -1
function pm(c) {
  return parseFloat(c + "1");
}

// Create a new bigint of 10^n. This turns out to be a bit
// faster than multiplying.
function exp10(n) {
  return BigInt(`1${[...new Array(n)].map(() => 0).join("")}`);
}

С их помощью мы можем собрать всю функцию fromNumber() воедино.
// file: ratio.js -- inside the class declaration
  static fromNumber(x) {
    const expParse = /(-?\d)\.(\d+)e([-+])(\d+)/;
    const [, n, decimals, sgn, pow] =
      x.toExponential(PRECISION).match(expParse) || [];
    const exp = PRECISION - pm(sgn) * +pow;
    return exp < 0
      ? simplify(BigInt(`${n}${decimals}`) * exp10(-1 * exp), BigInt(1))
      : simplify(BigInt(`${n}${decimals}`), exp10(exp));
  }

Охвачено большинство основных функций. Мы можем перейти от чисел к дробям и обратно. Но для моего конкретного приложения мне нужно было большее. В частности нужно было найти возведение в степень и логарифмы.

Возведение в степень


Возведение в степень — это когда число многократно умножается само на себя. Например, 2^3=2×2×2=8. Для простых случаев, когда степень — целое число, есть встроенный оператор BigInt: **. Так что, если мы возводим в степень дробь, это хороший вариант. Вот так дробь возводится в степень:

$\left(\frac{x}{y}\right)^{n} = \frac{x^n}{y^n}$


Следовательно, первый отрезок нашего метода возведения в степень может выглядеть примерно так:
// file: ratio.js -- inside the class declaration
  pow(exponent) {
    if (exponent.denominator === BigInt(1)) {
        return simplify(
            this.numerator ** exponent.numerator,
            this.denominator ** exponent.numerator
        );
    }
  }

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

Возведение в степень легко порождает большие числа. И когда числа становятся большими, всё замедляется. Пока я писал эту статью, я также написал вычисления, которые, не завершаясь, выполнялись в течение дней. Так что нужно соблюдать осторожность. Но ничего страшного. Все поставляется для BigInt.

Но есть и другая проблема. Что делать, если знаменатель степени — не единица? Например, что, если бы мы хотели рассчитать 8^(2/3)?

К счастью, мы можем разделить эту проблему на две проблемы поменьше. Мы хотим привести одну дробь к степени другой. Например, мы можем отнести x/y к a/b. Законы возведения в степень гласят, что следующее эквивалентно:

$\left(\frac{x}{y}\right)^\frac{a}{b} = \left(\left(\frac{x}{y}\right)^\frac{1}{b}\right)^a = \left(\frac{x^\frac{1}{b}}{y^\frac{1}{b}}\right)^a$


Мы уже знаем, как привести одно число BigInt к степени другого числа BigInt. Но как насчёт дробной степени? Ну, есть ещё один эквивалент:

$x^\frac{1}{n} = \sqrt[n]{x}$


То есть приведение xx к степени 1n1n эквивалентно нахождению n-го корня из xx. Это означает, что если мы найдем способ вычислить n-й корень BigInt, то мы сможем вычислить любую степень.

При хорошо продуманном поиске в вебе, нахождение алгоритма оценки n-го корня не займёт много времени. Наиболее распространённый метод — метод Ньютона. Он работает начиная с оценки, rr. Затем делается такой расчёт, чтобы получить лучшую оценку:

$\begin{align} r &\approx x^{\frac{1}{n}} \\ r^{\prime} &= \frac{1}{n}\left((n-1)r + \left(\frac{x}{r^{n-1}}\right)\right) \end{align}$


Мы продолжаем повторять эти расчёты до тех пор, пока не достигнем желаемой точности. К сожалению, есть некоторые корни, которые не могут быть представлены в виде конечной дроби. Другими словами, чтобы получить идеальную точность, нам понадобятся бесконечно длинные значения BigInt. На практике это означает, что мы должны выбрать произвольное ограничение итераций.

Мы вернемся к этому моменту. А пока давайте разберёмся, как вычислить достаточно точный корень n-й степени. Так как оценка rr будет дробью, мы можем записать её как:

$r = \frac{a}{b}.$


И это позволяет нам переписать расчёты так:

$\frac{a^{\prime}}{b^{\prime}} = \frac{(n - 1)a^{n} + x b^{n}}{n b a^{n - 1}}$


Теперь всё в терминах целочисленных вычислений, подходящих для использования с BigInt. Не стесняйтесь вставлять abab в уравнение для r′r′ выше и проверьте мои выводы. В JavaScript это выглядит вот так:
const estimate = [...new Array(NUM_ITERATIONS)].reduce(r => {
  return simplify(
    (n - BigInt(1)) * r.numerator ** n + x * r.denominator ** n,
    n * r.denominator * r.numerator ** (n - BigInt(1))
  );
}, INITIAL_ESTIMATE);

Мы просто повторяем это вычисление до тех пор, пока не достигнем подходящей точности для нашей оценки корня n-й степени. Проблема в том, что нам нужно придумать подходящие значения для наших констант. То есть NUM_ITERATIONS и INITIAL_ESTIMATE.
Многие алгоритмы начинаются с INITIAL_ESTIMATE в единицу. Это разумный выбор. Зачастую у нас нет хорошего способа предположить, каким может быть корень n-й степени. Но напишем «обманку». Предположим (пока), что наш числитель и знаменатель находятся в диапазоне Number. Затем мы можем использовать Math.pow() для получения начальной оценки. Это может выглядеть так:
// Get an initial estimate using floating point math
// Recall that x is a bigint value and n is the desired root.
const initialEstimate = Ratio.fromNumber(
    Math.pow(Number(x), 1 / Number(n))
);

Итак, у нас есть значение для нашей первоначальной оценки. А как же NUM_ITERATION? Ну, на практике, то, что я делал, начиналось с предположения в 10. А потом я проводил тесты свойств. Я продолжал наращивать число до тех пор, пока вычисления укладывались в разумные сроки. И цифра, которая, наконец, сработала… 1. Одна итерация. Это меня немного огорчает, но мы немного более точны, чем при вычислениях с плавающей точкой. На практике вы можете увеличивать это число, если не вычисляете много дробных степеней.

Для простоты мы извлечём вычисление n-го корня в отдельную функцию. Если сложить всё вместе, код может выглядеть так:

// file: ratio.js -- inside the class declaration
  static nthRoot(x, n) {
    // Handle special cases
    if (x === BigInt(1)) return new Ratio(BigInt(1), BigInt(1));
    if (x === BigInt(0)) return new Ratio(BigInt(0), BigInt(1));
    if (x < 0) return new Ratio(BigInt(1), BigInt(0)); // Infinity

    // Get an initial estimate using floating point math
    const initialEstimate = Ratio.fromNumber(
      Math.pow(Number(x), 1 / Number(n))
    );

    const NUM_ITERATIONS = 1;
    return [...new Array(NUM_ITERATIONS)].reduce((r) => {
      return simplify(
        n -
          BigInt(1) * (r.numerator ** n) +
          x * (r.denominator ** n),
        n * r.denominator * r.numerator ** (n - BigInt(1))
      );
    }, initialEstimate);
  }

  pow(n) {
    const { numerator: nNumerator, denominator: nDenominator } = n.simplify();
    const { numerator, denominator } = this.simplify();
    if (nNumerator < 0) return this.invert().pow(n.abs());
    if (nNumerator === BigInt(0)) return Ratio.one;
    if (nDenominator === BigInt(1)) {
      return new Ratio(numerator ** nNumerator, denominator ** nNumerator);
    }
    if (numerator < 0 && nDenominator !== BigInt(1)) {
      return Ratio.infinity;
    }

    const { numerator: newN, denominator: newD } = Ratio.nthRoot(
      numerator,
      nDenominator
    ).divideBy(Ratio.nthRoot(denominator, nDenominator));
    return new Ratio(newN ** nNumerator, newD ** nNumerator);
  }

Неидеально и медленно. Но задача стала в основном выполнимой. Остаётся вопрос, как получить оценку, если у нас целые числа больше Number.MAX_VALUE. Однако я оставлю это как упражнение для читателя; эта статья и так уже слишком длинная.

Логарифмы


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

Почему это так сложно? Моей целью было вычислить логарифмы ради точности большей, чем точность чисел с плавающей точкой. Иначе зачем всё это? Функция вычисления логарифма с типом числа с плавающей точкой, Math.log10(), быстрая и встроенная. Итак, я посмотрел на алгоритмы, которые дают способы итеративного вычисления логарифмов. И они работают. Но они медленны в получении точности выше точности числа с плавающей точкой. Не просто немного медленнее. Намного медленнее.

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

Я вспомнил, что мне нужен метод log10() для того, чтобы можно было вычислять красивые масштабированные значения для графиков. И для этих вычислений каждый раз, когда я вызывал .log10(), я сразу же вызывал .floor(). Это означает, что мне нужна только целочисленная часть логарифма. Расчёт логарифма до 100 знаков после запятой был просто пустой тратой времени и мощностей.

Более того, есть простой способ вычислить целую часть логарифма по основанию 10. Всё, что нам нужно, — это посчитать цифры. Наивная попытка может выглядеть так:

// file: ratio.js -- inside the class declaration
  floorLog10() {
    return simplify(BigInt((this.numerator / this.denominator).toString().length - 1), BigInt(1));
  }

К сожалению, это не работает для значений меньше 1. Но даже в этом случае мы можем использовать некоторые логарифмические законы для работы с таким значением.

$\begin{align} \log_{10}\left(\frac{a}{b}\right) &= \log_{10}(a) - \log_{10}(b) \\ \log_{10}\left(\frac{1}{x}\right) &= \log_{10}(1) - \log_{10}(x) \\ &= -\log_{10}(x) \end{align}$


Поэтому:

$\log_{10}\left(\frac{b}{a}\right) = -\log_{10}\left(\frac{a}{b}\right)$


Собрав всё воедино, мы получаем более надежный метод floorLog10():
// file: ratio.js -- inside the class declaration

  invert() {
    return simplify(this.denominator, this.numerator);
  }

  floorLog10() {
    if (this.equals(simplify(BigInt(0), BigInt(1)))) {
      return new Ratio(BigInt(-1), BigInt(0));
    }
    return this.numerator >= this.denominator
      ? simplify((this.numerator / this.denominator).toString().length - 1, 1)
      : simplify(BigInt(-1), BigInt(1)).subtract(this.invert().floorLog10());
  }

Опять. Зачем мучиться?


На данный момент библиотека имеет все необходимые функции для моего приложения, для работы с графиками. Но всё равно может быть интересно, зачем все эти неприятности? Уже есть несколько библиотек произвольной точности. Почему бы просто не использовать одну из них и не покончить с этим?

Честно говоря, я бы использовал существующую библиотеку в большинстве случаев. Особенно, если я тороплюсь. Нет смысла делать всё это, если кто-то уже сделал превосходящую работу.

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

Если бы я воспользовался чужой библиотекой, я бы столкнулся с одним из двух:

  1. Разработчики не реализовали log10() или любые другие логарифмические методы.

или
  1. Разработчики реализовали метод log10() (или его эквивалент).

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

Написание собственной библиотеки позволяет мне адаптировать её к приложению. Конечно, другие люди могут найти код полезным, но я не в ответе за их потребности. Чтобы моему приложению не приходилось таскать за собой сложный код, который оно никогда не использует.

Кроме всего этого, я многому научился, создавая свою собственную библиотеку. Теперь я на практике понимаю ограничения BigInt намного лучше, чем раньше. Я знаю, что могу настроить производительность метода корня n-й степени. Я могу настроить его в зависимости от того, сколько вычислений выполняю и какая точность мне нужна.

Иногда стоит написать свою собственную библиотеку общего назначения. Даже если ты не планируешь открывать код. Даже если никто больше ею не воспользуется. Ты можешь многому научиться, и, кроме того, это может принести радость.

Если вы хотите узнать больше о проблемах с числами с плавающей точкой, обратитесь к 0.30000000000000004.com. А если вы хотите посмотреть библиотеку целиком и сделать некоторые вычисления, то можете посмотреть эту песочницу с кодом.

image

Другие профессии и курсы
ПРОФЕССИИ


КУРСЫ

Let's block ads! (Why?)