Комментарии 333
Ничего не понял, прочитав до конца. Причем тут Ада? Чем классический не раздутый хеллоу-ворлд плох для новичка? Новичек он поэтому и новичек, что потом только узнает про все остальное, а не сейчас.
Странная статья.
Строгая типизация в этом сильно помогает.
Та же Ada ведь не с потолка была придумана.
Требования к языку Ada были разработаны на основе опыта проектирования больших программных систем, так что этот язык сам по себе является стандартом, на который можно ссылаться в подобных вопросах.
Ой, всё :)
Нет, ну если серьёзно — я не знаю ни одного другого языка, дизайн которого был бы так же хорошо обоснован на основе формальных требований, составленных по результатам опыта реальной разработки сложных систем. Есть ещё пара стандартов для языка C: MISRA C и Frama-C.
дизайн, скорректированный опытом реальной разработки сложных и «сверхбольших» систем на протяжении почти 20 лет. не? :)
Java вообще задумывался как язык для программирования умных микроволновок, о сверхбольших системах авторы и не думали.
Тем не менее никто не пытается её применять в медоборудовании или авиации. А какая область применения у неё на мейнфреймах?
в нашей стране я видел следующее: нашу с вами пенсию учитывают, грузовыми жд-перевозками по стране управляют, банковские платежи в цб обрабатывают.
ps: это такой тролинг, или вы серьезно не в курсе «области применения на майнфреймах»? давайте обсудим.
Вы хотите сказать что ада применяется в медицине и авиа (в смысле «до сих пор»)?
Я не для спора, мне скорее интересна фактическая ситуация.
Насколько мне известно, для серьёзных бизнес-задач серьёзные конторы используют специализированные языки, вроде SAP ABAP. Понимаю, что в целях экономии многие готовы писать свои системы хоть на PHP, но честно говоря, специально спроектированные инструменты вызывают гораздо больше доверия.
Есть немалый список проектов, в которых используется Ada. Обычный C в mission-critical проектах не используется, используется подмножество MISRA C или Frama-C. Кстати интересно заметить, что для обеспечения надёжности эти стандарты выкинули из C много фич — к теме о бесполезных нововведениях в языках. Что касается C++, то преимущества его использования по сравнению с Ada мне не понятны, разве что программистов найти легче.
Иногда мне кажется, что если бы разработка шла с нуля, снизу-вверх (пусть бы даже и c привлечением PHP :)), получилось бы куда эффективней. По крайней мере, разработчики реально бы вжились в бизнес-процессы, а не пытались навязывать «универсальное решение».
Против Ada (как подхода) ничего не имею, идея синтезировать статический анализатор с компилятором вполне себе разумная мысль (нечто подобное пытаются осуществить и упомянутая Farma-C, и набирающий популярность Rust). Но ведь это же не единственный подход. Есть, например, TDD. Опять же, статический анализатор к C++ всегда можно подключить «сбоку». Это как выбор между кухонным комбайном (всё в одном) и набором специализированных устройств (блендер, соковыжималка, мясорубка и т.д.).
Что касается «новых фич», уверяю, ни в одном из языков (в классической вычислительной парадигме) никогда не будет «новой фичи», которой бы до этого не было в Lisp. Да и фича, фиче рознь. По крайне мере то, что я использую из C++[11-14], (на мой взгляд) существенно понижает вероятность ошибок, повышая удобство разработки.
ифкуиль на уровне грамматики «заставляет» выражать свои мысли чётче и однозначней и в этом гораздо круче любого из естественных языков.
…
Засада в том, что мало кто из реальных людей (даже теоретически) способен его выговорить
А зачем собственно что то выговаривать? Главное — суметь точно мысль выразить (как в математике).
Вполне можно представить себе спецификацию С++ написанную рабочей группой по языку на урезанном подмножестве ифкуиля (без фонетики, которая там создаёт проблемы — только смысл, воспринимаемый с экрана) с введёнными дополнениями, которые будут аналогами команд Git, нужными для работы редакторов и автоматического слияния веток с логическими (а не символьными) изменениями а также — разрешения конфликтов. Тогда ифкуиль и его подмножество будут соотноситься как SGML и XML.
без фонетики… только смысл, воспринимаемый с экрана
Для этого придуман ихьтаиль (içtaîl) — оригинальная система письма ифкуиля :). Графический вариант, действительно, считывать проще, хотя семантические корни, основанные на котрежах согласнных фонем (да и вообще — фонотактика) и разноплановые сочетания многомерных грамматических матриц никуда не деваются.
Что касается «новых фич», уверяю, ни в одном из языков (в классической вычислительной парадигме) никогда не будет «новой фичи», которой бы до этого не было в Lisp.
LISP до начала стандартизации Common LISP или вообще весь со всеми диалектами? (тогда — нечестно)
Системы типов с автовыводом, а-ля ML, Haskell?
Или это уже не "классическая вычислительная парадигма"?
«Бизнес-задачи» бывают разные. «Бизнес-задачи» — это не только то, чем занимается «коммерция» и «бизнес».
Не путайте конкретные учетные задачи, на которые заточены компоненты SAP и задачи автоматизации бизнес-процессов.
Например АСУТП писать на SAP — это надо сильно удариться головой.
Не стоит писать систему реального времени контроля за грузоперевозками в масштабе страны на SAP. Можно, но в здравом уме это никто делать не будет. Потому там майнфреймы IBM, DB2, Java.
А вот бухгалтерию и зарплату под объемы РЖД — самое дело — SAP там лег как родной) (а на самом деле просто потому, что 1С не тянул эти объемы).
ABAP хорош только в контексте учетных механизмов SAP, а как сам язык, сужу по отзывам — не очень катит супротив даже 1С.
ABAP — конкурент 1С, а не Java/С++…
С тех пор много воды утекло, очень много. Что-то конечно в Ada меняли, но что-то и осталось с тех пор. А взгляды на то, как нужно делать сложные системы, они с тех пор могли и поменяться — сложность-то поменялась.
Я к тому, что наличие формальных требований — это не догма. В этой области тоже много чего изменилось с тех пор.
Хорошо составленные требования были реализованы без какого-либо анализа. Есть требование — вводится соответствующая конструкция.
В результате, описание синтаксиса Паскаля укладывается в 30 определений, а Ада потребовала 180. Держать в голове такой компилятор не удается, а без этого производительность программиста резко падает.
Так что, «хорошо обоснован» еще не означает «хорошо сделан».
Интересно тогда, во сколько определений укладывается описание C++ или Java, или Rust. Подозреваю, что не сильно меньше. При этом, например в случае с C++, всяких undefined behaviour и подводных камней явно больше.
ЗЫ: Борланд делал компилятор С++, там в документации, что-то похожее на формальное описание было. На полсотни страниц.
Давным давно, я пытался доказывать, что язык, это не только синтаксис, но и словарь. И среди языков выиграет тот, в стандарт которого будет включена обширная библиотека.
Собственно, это и случилось. При сложном и, я бы сказал, неопрятном синтаксисе, С++ стал популярен, благодаря стандартизации библиотек. Это, действительно, требует сотен страниц описаний.
Кстати такой же стандарт ISO/IEC по Ada 2012-го года почти на 400 страниц меньше — тоже с описанием библиотеки.
Судя по тому, что для С++ придумано много сторонних библиотек типа Qt, Boost и т. п., со стандартной библиотекой там как раз не всё гладко.
Я говорил только о том, что в «языках первого поколения» описывался только синтаксис, а встроенная библиотека отдавалась на произвол конкретной реализации языка.
Как только встала задача перехода к индустриальному производству программ и обеспечении реальной переносимости ПО, стандартизация встроенной библиотеки на уровне языка, а не его реализации, стала необходимостью.
Некоторое противоречие в том, что по единству и компактности синтаксиса предпочтительнее «авторские» языки — созданные одиночками. Но создание достаточно обширной и хорошо структурированной библиотеки одиночке не под силу.
Поэтому и были надежды на проект Ада: за основу берется «ортогональный» синтаксис Паскаля, позволяющий почти все минимальным количеством конструкций, и дополняется обширными хорошо оттестированными библиотеками, создающими следующий слой языка. Но расширение синтаксиса, оказалось необходимым и сделано оно было крайне неряшливо. Не просто дополнение несколькими новыми конструкциями, а многократное увеличение их количества.
полномасштабных исследований наверное нигде нет, разве что какой нибудь институт, который поддерживает методы оценки трудоемкости разработки, в результате своих исследований, выставит завышенные коэффициенты связанные с языком напротив языков с динамической типизацией.
но вот проблема в том, что серьезные большие институты, ориентированные на исследование промышленных методов разработки ПО — не очень интересуются языками с динамической типизацией. ну… мне так показалось, когда я исследовал вопрос о методиках оценки трудоемкости разработки.
я говорю о методиках оценки типа FPA (метод баллов функциональности), COCOMO / COCOMO 2, UCP (usecase point analisys? — как FPA, но только принимает на вход прецеденты использования.).
Поясню про коэффициенты связанные с языком разработки: во всех этих методиках, как правило вычисляются некие баллы сложности алгоритма/структры которая описывается в задании на разработку, а потом эти баллы пересчитываются в человекочасы умножением на кучу разных коэффициентов. среди прочих коэффициентов есть и связанные с языками разработки.
и тут такая особенность: институты, которые разрабатывают эти методики нацелены на промышленные, «серьезные области» — собственно там, где методы оценки трудоемкости очень важны. те области, где сбой в работе по имеет серьезные экономические или даже материальные последствия. или где комплекс программ сложный а процессы — часто запутаны. где проблем много, даже если системы не обрабатывают массовые заявки (т.е. гуглемейл или вконтактик с одноклассниками и фейсбуками например — они не показательны ни разу — это не промышленная разработка. они массовы, огромны, но их «бизнеспроцессы» достаточно примитивны и потому легко масштабируются. они очень просты по сути, даже в сравнении с обычной СЭД, или системой бухгалтерского или складского учета).
так вот — в этих областях традиционно(?) языки с динамической типизацией не выживают. не востребованны они теми, кто несет серьезную ответсвенность. как то вот так случилось.
и в результате, ничего что касается языков с «динамической» типизацией, я в исследованиях больших институтов я не видел. были коэффициенты для C/C++, Java, Paskal. даже бейсик кажется был. но джаваскрипта, руби, питона там я не помню.
занимался исследованием методов оценки лет 5 назад, может сейчас что и изменилось, поправьте если что))
я к чему: ссылаться на статстику — нельзя. никто не будет проводить масштабные сраврительные исследования для кучки популярных в этом году технологий. простите, но пхп был популярен пять лет назад, питон был популярен в позапрошлом году. в этом руби на коне… никто не заплатит за такие исследования.
поэтому мы можем основываться только на собственном опыте и анализе успеха длительных промышленных проектов с которыми столкнулись и в которых участвовали.
и на модели, котрые описывают как мы ведем разработку, типовые проблемы, ситуации в работе, которые мы можем легко или сложно решить используя разные языки.
(например, рубисты вынуждены ренерить xml кож вручную, и вручную писать правила проверки, что бы xml соответвовал требуемому xsd; на джава же этой проблемы нет вообще — на основе xsd кодогенератором делаются классы с анотациями, и при парсинге/сериализации все правила xsd проверяются автомтатически == следовательно на джава разработать алгоритмы для wsdl-вебсервисов быстрее).
или другой прмер: при рефактоинге кода, изменнии структуры классов и их методов, для динамических языков нет методов которые позволили бы понять, где используется изменяемый класс. что бы выявить все эти места, нужно проводить масштабное тестирование, что трудоемко. а в случае языков со статической типизацией — все места использования класса, типа, метода проверяются сразу компилятором, и более того — IDE спомбна выполнить 90%работы по анализу кода за нас. == рефакторинг кода написанного на ryby/javascript это более трудоемкая процедура, чем скажем рефакторинг кода на java илиc++.
логические рассуждения, не больше)))
по поводу опыта могу вам сказать так: только через 8-10 лет после института, я начал понимать почему, например, на PHP очень геморройно делать большую сложную систему типа СЭД. и что проблема не в пряморукости, а в глубинных свойствах самого языка и инструментария.
и то, я начал это понимать только после того, как сам поучаствовал аналитиком 2 года на одном проекте внедрения системыдокументооборота, написанрой на пхп, как поразбирал багрепорты, как безуспешно разработчики пытались решить эти проблемы, и как система постепенро дохла, вплоть до невозможности собрать статстику по обработке поручений за неделю в течении 6 часов.
то что вы скептически относитесь к утверждениям, что статическая/жесткая типизация помогает контроллировать код — мне очень понятно. я сам был таким))
но поразмыслите детально, и педантично.
> я сам был таким))
Не очень понятен этот снисходительный тон.
Скажу сразу — я сам адепт typescript, например, и мне глубоко понятно желание видеть в своем коде сильную статическую типизацию.
Но, тем не менее, да, я скептически отношусь к утверждениям, что сильная типизация однозначно «лучше» слабой, потому что существуют разные кейсы и разные контексты применения языков. Где-то важнее надежность, которой сильная типизация безусловно способствует (хоть и не гарантирует), где-то важнее скорость разработки.
И до тех пор, пока не будет исследований, показывающих, какая типизация действительно лучше с точки зрения, подчеркиваю, бизнеса/заказчика в различных контекстах применения, я всегда на подобные споры и попытки их разведения буду смотреть осуждающе.
«Строгая типизация == раз скомпилировалось, значит работает» — а где здесь про скорость разработки? Или про «бизнеса/заказчика в различных контекстах применения»?
Похоже, Вы сами в какой-то момент подменили тезис на «лучший язык — со статической типизацией», который и опровергаете.
И я ничего не подменял. Вы цитируете мой комментарий, который является ответом уже на совершенно другое высказывание. Не надо демагогией заниматься.
Прочитайте еще раз комментарий, на который я отвечал, и вопросы у вас отпадут.
> Я вообще не спорю. Я дискутирую.
> ДИСКУ́ССИЯ, -и, жен. Спор, обсуждение какого-н. вопроса на собрании, в печати, в беседе.
> СОФИЗМ (от греч. sophisma — уловка — выдумка, головоломка), мнимое доказательство, в котором обоснованность заключения кажущаяся, порождается чисто субъективным впечатлением, вызванным недостаточностью логического или семантического анализа.
Всего доброго!
Я в очередной раз рекомендую вам ознакомиться с комментарием dplsoft. Я отвечал на этот комментарий, и ни на какой другой.
Мы вроде говорили о сильной/слабой, а не о статической/динамической типизациях.Ах… Гм… Был спросонок, перепутал. Погуглил. Уточнил. Осознал :)
Крайне редко говорят про сильную и слабую типизацию.
Вы первый или второй. Обычно говорят про динамическую-vs-статическую типизацию.
Особой корелляции между сильной-слабой типизацией и проблемами с разработкой не отмечал.
Проблемы с динамической типизацией изложил — с ростом объема кода, проблемы в разработке на динамических языках прастут «очень не линейно». Проблемы со статической типизацией — относительно линейно. Это мои оценки и наблюдения.
И до тех пор, пока не будет исследований, показывающих, какая типизация действительно лучше с точки зрения, подчеркиваю, бизнеса/заказчика в различных контекстах применения, я всегда на подобные споры и попытки их разведения буду смотреть осуждающе.Тогда у нас дед-лок))
Исследований нет, и наверное не будет, ибо некому за это заплатить. нет — исследований — будет постоянное осуждение. Будет осуждение — проблему скорее всего не решить. Но принимать-то архитектурные решения надо! И стажеров надо обучать! Как быть?
Хотя согласен, в том плане, что попытки разведения неконструктивных споров надо подавлять в зародыше.
С моей же точки зрения, если выбирать между «статикой и динамикой» в типизации при выборе языка разработки, то мой опыт говорит мне следующее: выгода зависит от объема разрабатываемого кода. ( и я сейчас буду говорить только выгодах проекта, вытекающих из минимизации трудозатрат и технических рисков. Если последствия неадекватных технических решений будут лежать на ком-то другом, или «бизнес» планирует «отскочить» до проявления проблем или «еще как-то» (вплоть до полного наплевательства на последствия, или полного одупления (непонимания лицами принимающими решения технических проблем) ) — то «бизнес», естественно, будет принимать решения совсем с других, не связаных с техническими аспектами, позиций )
и так:
* на коротком периоде, для небольших задач, одноразовых, скриптования поведения «жесткой части» — удобнее/выгоднее динамическая типизация. Низкий порог входа, быстрое прототипирование — позволят экономить бюджет. До тех пор, пока кривая проблем не начала расти (не сдвинулась с места), или не начался рефакторинг, или число модулей в вашей системе не выросло до двух.
Отсюда и область применения: скрипты инициализации, тестирование, небольшие хранимки в бд, сайты с линейной логикой работы, различные примитивные СМО и пр.
* на длинном периоде, при относительно большом коде, с рисками изменения бизнес-логики, для задач сложнее чем на одну неделю, для кода которые надо будет поддерживать — лучше сразу начать писать на языках со статической типизацией ( возможно с включением скриптов на динамических языках ). Потому что линейный рост числа проблем (а не галлопирующий экспонентой) в зависимости от объема кода — очень даже комфортен. Причем выбирать стоит те языки, у которых как можно меньше проблем с обратной совместимостью.
А корелляции между «сильной» и «слабой» типизацией я не заметил.
Я не могу запретить вам его осуждать, но рассуждения, особенно логически стройные, буду рад услышать.
Я принял вас за очередного молодого адепта «новомодных языков». Извиняюсь)я сам был таким))Не очень понятен этот снисходительный тон.
На самом деле, в современных языках статическая типизация оказывается даже быстрее при написании скриптов, потому что:
- на надо гуглить какое там апи у такого-то модуля — среда разработки сама адекватно всё подскажет.
- не надо запускать и дебажить для того, чтобы обнаружить глупую опечатку — компилятор и даже среда разработки понимают, что ты написал, и как можно раньше рассказывают тебе о проблемах.
- не надо постоянно вручную проверять и преобразовывать типы, на случай, если на вход передадут какую-то дичь. Типичный пример:
String( arg ).toLowerCase()
- типы вручную пишутся только там, где пользователю это важно — в остальных местах они выводятся автоматически
IDE с поддержкой динамически типизированных языков уже существуют и активно развиваются.
> не надо запускать и дебажить для того, чтобы обнаружить глупую опечатку — компилятор и даже среда разработки понимают, что ты написал, и как можно раньше рассказывают тебе о проблемах.
Аналогично, для динамически типизированных языков есть средства линтинга, и профессионалы не выпускают код без него. Для Python я с ходу могу назвать 4 таких средства, для Javascript — 2, для многих других есть встроенные анализаторы (например, в Perl исключение по use strict генерируется именно таким контролем, а не ошибкой собственно рантайма типов).
Но есть таки принципиальная разница. Грубо говоря, для статически типизированных языков то, что вычисляется при компиляции, будет аксиомой при выполнении (объект типа X => у его методов будет именно та сигнатура, что у класса X). Для динамически типизированных это не обязательно — Python, Javascript позволяют подменять подложку реализации на ходу. Линтинг в принципе не способен отловить такие ситуации. Их запрет для возможности статического анализа — уже вопрос административной политики при разработке, а не собственно языка.
> не надо постоянно вручную проверять и преобразовывать типы, на случай, если на вход передадут какую-то дичь. Типичный пример: String( arg ).toLowerCase()
Да, фактор существенный. Мне в этом смысле понравились правила от Hola для Javascript: например, если требуется, чтобы на входе функции было число, пишется +x, а если строка — ""+x. Рантайм умеет опознавать эти ситуации и переводить их в конверсию типа.
Но вот lowercase это уже за пределами обычной возможности системы типов, надо делать свой враппер.
> типы вручную пишутся только там, где пользователю это важно — в остальных местах они выводятся автоматически
Пока что это достаточно малая часть таких языков. И даже в них автоматический вывод работает не всегда и не везде адекватно. А главное — что для уверенности программиста, что тип получился именно нужный, приходится его указывать явно.
IDE с поддержкой динамически типизированных языков уже существуют и активно развиваются.
Выглядит это примерно так:
Аналогично, для динамически типизированных языков есть средства линтинга
Линтеры тут ничем не помогут. Вообще, существование линтеров говорит о косяках в языке, который допускает "не кошерные" выражения. Если же речь не о линтерах, а тайпчекерах, то для них опять же нужны тайпхинты, что возвращает нас к статической типизации.
И даже в них автоматический вывод работает не всегда и не везде адекватно.
О чём опять же программисту сообщается сразу, а не когда в рантайме прилетит что-то не то.
А главное — что для уверенности программиста, что тип получился именно нужный, приходится его указывать явно.
Как правило программисту всё-равно какой там тип, лишь бы крякал как утка.
Наоборот, они по определению контролируют только форму, но не содержание.
"сообщал о подозрительных или непереносимых на другие платформы выражениях" — не тянет это на семантику.
А что же это, если не семантика? В понятие синтаксиса уже не влазит.
Но и до семантики как до луны. Типа "ставьте точки с запятой в конце строк" — это синтаксис, а "не используйте arguments" — это уже семантика?
Мнэээ… перед этим шла речь о непереносимых выражениях, например. Если кто-то напишет x<<200, где x типа int, что это, как не проблема семантики (содержания, смысла… проигнорируем сверхтонкие отличия)? Ну или пора вводить новую систему терминов.
Что за arguments тут — я не понял. Имеется в виду массив всех аргументов функции в JS? Вот тут, да, это настолько специфично для языка, что ближе к синтаксису, чем к семантике. Но я таки предпочёл бы тут видеть какое-то новое слово...
Выглядит это примерно так:
Ну да. А вы считаете это недостаточным?
Линтеры тут ничем не помогут.
Я видел последующую дискуссию про то, что такое линтер. Я тут имел в виду статический анализатор любого вида, который способен опознать ситуации типа неиспользованных переменных, типичных опечаток и тому подобного. Такие средства вполне способны проанализировать код и найти, например, неверно передаваемый параметр. С Питоном у меня это реально работало.
Если же речь не о линтерах, а тайпчекерах, то для них опять же нужны тайпхинты,
Если такое средство видит, что параметр используется как индекс в списке, оно предположит, что тут должно быть целое. Если его индексируют строкой — оно должно быть совместимым со словарём. И так далее. Явные хинты тут не обязательны. Хотя, да, желательны — я бы предпочёл их видеть много где — но в последних версиях Питона, например, это уже делается.
О чём опять же программисту сообщается сразу, а не когда в рантайме прилетит что-то не то.
Это если он не сумел вывести тип. А если вывел, но не тот, что думал программист?
Как правило программисту всё-равно какой там тип, лишь бы крякал как утка.
Так в том и дело, что он хочет утку, а получается вдруг амадина. Она тоже крякает, но как-то невыразительно :)
Ну да. А вы считаете это недостаточным?
Я считаю это бесполезным, чуть менее, чем полностью.
С Питоном у меня это реально работало.
Это замечательно, но это лишь малая часть того, что может обнаружить компилятор в статически типизированном языке. Кроме того, чтобы все эти тайпчекеры работали, вам так и так придётся отказаться от динамических возможностей языка.
Это если он не сумел вывести тип. А если вывел, но не тот, что думал программист?
Он не может "не суметь". Всё, что он может — вывести тип, не совместимый с другим кодом и упасть. Если не упал, значит тип совместим и не важно, что там думал программист. Например, он мог думать, что вернётся User, а вернулся Proxy, который реализован так, что мимикрирует под User. Ну и какая программисту разница, если всё работает корректно?
Я считаю это бесполезным, чуть менее, чем полностью.
Почему бесполезно?
Оно подсказывает, какие слова тут можно подставить, и умеет их дополнять. Также — показывает имена и типы аргументов, по которым можно понять, что и как задавать.
Оно может анализировать код и показывать ошибочные (например, опечатка в имени метода) или подозрительные конструкции.
Я не знаю, может, Вы имеете в виду какую-то злобную специфику JS. Я с ним слишком мало работал, чтобы знать такие тонкости. Но для Питона и для >95% случаев меня функциональность таких средств устраивает; когда она не работает — уже описывал раньше — когда я сам как автор кода вмешиваюсь в логику и делаю её изменчивой в рантайме.
Кроме того, чтобы все эти тайпчекеры работали, вам так и так придётся отказаться от динамических возможностей языка.
В большинстве случаев — да. Я не могу, например, писать
if isinstance(max_time, str):
max_time = int(max_time)
да и безусловную конверсию лучше не применять (хотя если средство хоть чем-то умнее крышки дубового стола, оно будет работать в логике, аналогичной Static Single Assignment).
Или, я не смогу использовать присвоение некоторой переменной одного из трёх значений True, False или None (у меня это долго был любимый приём) — оно выведет тип для этого значения, только если придумает внутри себя enum, а так как все три в Питоне это синглтоны двух разных типов, ему будет сложновато это сделать.
Но таких случаев в реальной практике оказалось ой немного. А вот простые варианты типа "в этой переменной всегда целое" оно вывело бы на ура. Точно так же как упомянутая рядом схема Хиндли-Милнера работает в ML и семействе.
Он не может "не суметь".
См. выше пример с конверсией в целое на ходу. Вывод типа "местами снег, местами град, местами variant — или int, или str" и есть то "не шмогла".
Ну и какая программисту разница, если всё работает корректно?
Разница в том, что мимикрия подобного рода означает слишком динамическую типизацию. И в Питоне, и в JS это поиск по названию метода в цепочках словарей методов суперклассов (прототипов).
Если мы вводим синтаксическую поддержку статической типизации, её лучше иметь в вариантах указания "тут постарайся оптимизировать" или "тут ты обязан вывести конкретный целочисленный тип, а его размер, так уж и быть, я тебе доверю выбирать".
Почему бесполезно?
Потому, что выдаёт вообще всё, что угодно, кроме того, что там в объекте действительно есть. Потыкавшись в эту менюшку, быстро отучаешься использовать подсказки.
показывает имена и типы аргументов,
Только для стандартных API и для JSDoc аннотаций (привет, статическая типизация)
оно выведет тип для этого значения, только если придумает внутри себя enum, а так как все три в Питоне это синглтоны двух разных типов, ему будет сложновато это сделать.
Эту задачу решают "типы-суммы" и "типы-произведения". TypeScript, например, это умеет. При этом он умеет ограничивать такие типы в разных ветках кода:
let foo : number|string = Math.random() > .5 ? '0' : 0
if( typeof foo === 'number' ) {
console.log( foo.toFixed(2) )
} else {
console.log( foo.toLowerString() )
}
Вывод типа "местами снег, местами град, местами variant — или int, или str" и есть то "не шмогла".
Как программист написал — так оно и вывело. Или приведите пример, что ли.
Разница в том, что мимикрия подобного рода означает слишком динамическую типизацию.
Скорее динамическую диспетчеризацию (которая приятно дополняет статическую типизацию). Без неё многие задачи вообще не решаются.
Потому, что выдаёт вообще всё, что угодно, кроме того, что там в объекте действительно есть.
Насколько я вижу, всё, что он выдал там, действительно есть в объекте (то есть, при вызове по имени будет поднято по цепочке прототипов и найдено). Да, их много, но самое важное находится вверху списка. Если бы не было такой сортировки, разбираться было бы в разы сложнее.
Только для стандартных API и для JSDoc аннотаций (привет, статическая типизация)
Так это и хорошо, что он умеет читать эти аннотации. Проверить соответствие аннотации сути одной функции просто и это локальное действие, не требуется никуда далеко заглядывать, и потом опираться на эту аннотацию — это как раз то, как это всё может работать в рантайме с "высочайшей" динамичностью типизации.
Эту задачу решают "типы-суммы" и "типы-произведения". TypeScript, например, это умеет.
Хорошо, значит, прогресс в эту сторону идёт.
Скорее динамическую диспетчеризацию (которая приятно дополняет статическую типизацию).
Да, это правильный термин для данного случая.
Я таки слишком неясно выразился. Имелось в виду, что программист ошибётся в какой-то мелочи, и результатом станет не тот тип, который ожидался. Явное называние ожидаемого типа в этом случае является дополнительной защитой со стороны программиста против собственных ошибок. (Ну и против ошибок компилятора, которые всегда есть, но вероятность в каждом конкретном случае ниже на несколько порядков...)
Типы (топ-левел-функций) лучше указывать потому, что типы — это документация, да и явное указание типов позволяет сделать более читабельные сообщения об ошибках типизации (вы накладываете больше констрейнтов, у тайпчекера меньше вариантов).
Да, это тот же самый принцип, что я описал абзацем выше. Но и типы промежуточных переменных — это такая же документация и такая же помощь тайпчекеру.
Ещё одни странные люди, сменив версию Ангулара с 1 на 2, тоже перешли на TypeScript (жесткая типизация). Наверное, им надоело есть кактус.
Строгая типизация не означает, что в рантайме у вас внезапно что-нибудь не отвалится)
Поэтому, при рассмотрении строгости типизации лучше вводить несколько канонических уровней и сравнивать с ними:
1. Вообще никаких неявных конверсий (Go, или близко к нему).
2. Конверсии по умолчанию — между числами, или только явно разрешённые функции, или только в особых контекстах (основной компилируемый майнстрим, C++, C#, Java и т.п.)
3. Размыта граница между строкой и тем, что она представляет (JavaScript, Perl...)
4. Вообще всё есть строка, оптимизации есть, но это принципиально не отменяют (sh, Tcl...)
Плох не хеллоу-ворлд, а языки, которые как-будто делают с целью упрощения написания хеллоу-ворлд-ов и написания красивых, но далёких от жизни туториалов.
Звучало бы странно, если бы не популяроность таких языков и непопулярность "сложных" языков, которые на самом деле упрощают жизнь при реальном программировании.
Синтатиксис и семантика примера из статьи демонстрирует аналогичный код на многих современных распространённых языках программирования.
Прошу прощения, не хочу показывать пальцем и создавать кому-либо антирекламу, пост не для этого написан.
Вы сможете привести улучшенную версию программы из моего примера на каком-нибудь из современных распространённых языков программирования вроде Java или С#?
Пример языка, в которых используется обработка ошибок на основе условий и исключений, как в примере? Java, C#, C++ и т. д.
Java, C#, C++ — примеры языков, в которых используется обработка ошибок на основе условий и исключений, если с первого раза было не понятно.
Ada ещё добавляет проверки диапазонов на основе типов данных.
Запретить вводить людей младше года и старше 120 — это совершенно искусственный пример, хуже Hello World. А в реальности это зачем?
Лучше писать проверки диапазона руками в коде? Возможно, дело вкуса.
У вас в любом случае будут ограничения диапазонов. Заданные вами или компилятором или архитектурой процессора. Максимально компактные диапазоны позволяют оптимизировать вычисление и хранение данных, обнаруживать большее число ошибок. Разумеется число 120 неоправданно занижено. А вот диапазон {0… 256} выглядит вполне разумно на ближайшие 100 лет.
И что вы имеете против такого задания возраста?
Вот именно, что замеряют. И результатом такого замера является значение типа "возраст" с соответствующими ограничениями. Если замеренное значение не вписывается в эти ограничения, то это повод бить тревогу.
Почему формула сравнения именно такая — вопрос к тому, кто устанавливает правила. Возможно в какой-то специфической области сравнение должно быть именно такое. Но суть ведь не в том, как надо сравнивать возраста людей.
КМК, лучше условия проверять явно и там где это необходимо.
Многие проверки можно выкинуть, когда точно знаешь какие диапазоны где приходят. Так что единственное место, где проверки останутся — на входе в приложения, но они там и так по любому будут. Но тут их хотябы компилятор сам сгенерирует, а не программист вручную.
А зачем на Аде писать говнокод?
- persons должен уже содержать нормализованные объекты, а не сырые данные, полученные с сервера. Иначе при изменении ответа сервера вам придётся рефакторить код всего приложения. А поддержать несколько серверов вообще не представляется возможным.
- Для итерирования есть удобные циклы с полноценной поддержкой break и continue. Незачем лепить замыкания на ровном месте.
2. Я много программировал до и после удобного использования замыканий. Разница огромная. Где раньше надо было написать портянку циклов с break, continue, и пятёркой вспомогательных переменных, сейчас без напряжения мозга просто пишешь, что ты хочешь получить, сберегая мыслительные ресурсы для остальной части задачи.
Как по мне, поддержка запросов к коллекциям сейчас является необходимой чертой современного языка. Даже мастодонты java и c++ сдались и ввели эту поддержку. Это как раньше были языки без поддержки рекурсии и динамической памяти, но вымерли.
О том и речь, что программист должен изолировать от приложения то, на что он не влияет — ввод пользователя и ответ сервера.
- Циклы не требуют каких-то особых "мыслительных ресурсов". Написанный вами код ничем от циклов не отливается. Ну вот совсем. Только отлаживать его — то ещё "удовольствие". ни промежуточные результаты выполнения не посмотреть, ни по шагам пройтись, ни даже прочитать содержимое переменных из родительского скоупа, на которые нет ссылки из вложенного (хвала JIT).
А как по мне — обобщённое программирование и исполнение кода времени компиляции являются необходимыми чертами современного языка :-) А ещё, как по мне, forEach — не является "запросом к коллекции", а является он обычным циклом, но зачем-то сделанным на редкость неудобным.
Циклы не требуют каких-то особых «мыслительных ресурсов». Написанный вами код ничем от циклов не отличаетсяБанально устаёшь писать длинные тупые циклы, где нужно найти сумму элементов, максимальный элемент, минимальный среди максимальных. Внимание ослабевает, сажаешь ошибки. Проще написать запрос в декларативном стиле, что тебе нужно от данных, и идти дальше, не заостряя внимания на подобных задачах.
Только отлаживать его — то ещё «удовольствие». ни промежуточные результаты выполнения не посмотреть, ни по шагам пройтись, ни даже прочитать содержимое переменных из родительского скоупа, на которые нет ссылки из вложенного (хвала JIT).Конкретно в Visual Studio (C#/C++) в режиме DEBUG нет никаких проблем: брейкпоинты можно ставить на любой оператор внутри лямбды, переключая контекст в окне Call Stack, можно добраться до любых переменных в watches. Если где-то это не так, это проблема IDE, а не подхода в целом.
Два года разработки на golang, где все на циклах и очень мало обобщений, и сейчас отдыхаю, изучая python. Да, он медленнее, не такой клевый для параллельного кода, но насколько же быстрее попробовать какую-то идею! Одна-две строки, вместо десятка строк кода, который может быть и выкинешь после…
Проще написать запрос в декларативном стиле, что тебе нужно от данных, и идти дальше
const users_by_name = users.reduce( ( index , user )=> {
const name = `${ user.name_first() } ${ user.name_last() }`
return { ...index , [ name ] : user }
} , {} )
const users_by_age = users.reduce( ( index , user )=> {
const age = user.age()
return { ...index , [ age ] : [ ...( index[ age ] || [] ) , user ] }
} , {} )
const users_by_name = {}
const users_by_age = {}
for( let user of users ) {
const name = `${ user.name_first() } ${ user.name_last() }`
users_by_name[ name ] = user
const age = user.age()
if( users_by_age[ age ] ) users_by_age[ age ].push( user )
else users_by_age[ age ] = [ user ]
}
Конкретно в Visual Studio (C#/C++) в режиме DEBUG нет никаких проблем
Это замечательно, но человек привёл пример на JS.
Это замечательно, но человек привёл пример на JSТо есть, с лямбдами писать пока рановато, потому что ваш IDE их хуже поддерживает, чем классический императивный код. Ну, ок.
IDE тут, к сожалению, ни при чём. Это свойства языка / виртуальной машины.
Не вижу никаких проблем сделать DEBUG-режим запуска VM, в котором будут фиксироваться стек-фреймы и их контекст. Чтобы дебаггер мог получить список стекфреймов, отобразить его в отдельном окне и двойным кликом переключать контекст так, чтобы при наведении мыши на переменные (или добавление переменных в watches) их значение показывалось в соотвествии с выбранным контекстом.
Отладчик в мире JS принадлежит виртуальной машине, а не среде разработки. Среда разработки может, конечно, поключиться к виртуальной машине и управлять отладчиком удалённо, но только в рамках его функционала.
Это всё уже давно реализовано. Проблема "декларативного" fluent-кода, на который сейчас многие молятся, в том, что в нём у вас промежуточные вычисления находятся на стеке, а не помещаются в какую-то переменную.
Вот как тут узнать, что вернул filter?
Вот как тут узнать, что вернул filter?Не знаю, как в вашей среде, но обычно в отладчиках можно выделить часть выражения
и нажать хоткей 'evaluate', или добавить в watches и исследовать там структуру выражения.a.filter(i=>i>b)
.map(i=>i*i)
Если кто-то этого не умеет — это его проблемы, а не лямбд.
Особенное удобство — ставить точки остановка в каждой "лямбде", чтобы можно было пройтись по шагам. Ну или очень внимательно на каждом шаге выбирать между "step into" и "step over", а если ошибся — начинать заново.
Вы пользовались IntelliTrace? Оно решает проблему с «на каждом шаге выбирать между „step into“ и „step over“, а если ошибся — начинать заново».
У меня претензии только к тем, кто пишет код без оглядки на то, как его потом дебажить. Я верю, что для других языков есть всякие крутые инструменты, но в JS если есть возможность использовать циклы и переменные, то лучше использовать циклы и переменные, а не изобретать звездолёт, для которого нужен специальный дебаггер, который изобретут только в следующем году и то, только под одну платформу.
1) Написать понятный код, в терминах задачи, с использованием цепочки filter, reduce и прочих высокоуровневых конструкций (код, который очевиден и не требует дебага),
либо
2) Написать цикл, в котором происходит какая-то обработка. Человек, читающий код, всё равно должен будет прокрутить в уме выполнение кода и привести этот цикл в термины задачи, чтобы понять, что тут делается.
Вы правда считаете первый код "простым, понятным и очевидным"? Боюсь вы один такой, для обычных людей итеративный алгоритм проще, понятней и очевидней рекурсивного.
Вы хотите сказать, что пишите код без ошибок, раз он у вас "дебага не требует"?
const users_by_name = users.reduce( ( index , user )=> {
const name = `${ user.name_first() } ${ user.name_last() }`
return { ...index , [ name ] : user }
} , {} )
Тут же нет рекурсии (если я правильно понимаю написанное).
Ну и таки лично я подобное пишу без дебага, как
users_by_name = users.ToDictionary(user => user.name_first() + ' ' + user.name_last());
В JS и с этим проблемы?
Тут же нет рекурсии (если я правильно понимаю написанное).
Под капотом, конечно, нет, но описание рекурсивное.
Ну и таки лично я подобное пишу без дебага, как
У массивов нет такого метода. Впрочем, не важно. Требования изменились, теперь надо искать как по имя-фамилия, так и по фамилия-имя. Ваши действия?
Мои будут такими:
const users_by_name = {}
for( let user of users ) {
const names = [
`${ user.name_first() } ${ user.name_last() }` ,
`${ user.name_last() } ${ user.name_first() }` ,
]
for( let name of names ) {
users_by_name[ name ] = user
}
}
users.Single(user =>
user.name_first() + ' ' + user.name_last() == searchterm
|| user.name_last() + ' ' + user.name_first() == searchterm)
Ну, или ближе к вашему варианту
users.SelectMany(user => new []{
new KeyValuePair(user.name_first() + ' ' + user.name_last(), user),
new KeyValuePair(user.name_last() + ' ' + user.name_first(), user)
}).ToDictionary()
> но описание рекурсивное.
Не вижу
Distinct
перед ToDictionary
Незнаю-незнаю, удваивать размер словаря и одновременно оптимизировать… Если там следующим шагом отчество комбинаторно добавится — Single может выиграть )
> Если name_first == name_last
А так бывает? )
Проще найти полных тёзок, там и ваш вариант упадёт.
А так бывает? )В тестах бывает )))
найти полных тёзокПо ТЗ, должна быть возможность однозначного поиска человека по ФИО, значит, таких данных нет на входе. Хотя, защитное программирование никто не отменял… С другой стороны, код vintage с циклом будет глючить, выдавая только первого тёзку. Может, лучше бы падал, чтобы привлечь внимание к проблеме.
Заметьте, что лучше получить эксепшен "пользователь такой-то и такой-то имеют одинаковые имена, что не допустимо по такой-то причине", чем "ой, что-то пошло не так не могу добавить объект абракадабра в какой-то словарь, так как там уже есть какая-то белиберда".
Значит неправильное ТЗ )
Его не надо реализовывать, надо найти ответственного и
Очень просто: у нас онлайн-игра, где ещё на стадии регистрации отсекаются похожие имена, чтобы игроки не путались.
Но без учёта случая имя==фамилия, есть проблема, что юзер может уронить приложение, введя некоторые данные )))
Ввод данных пусть заботит того, кто его пишет
Tom Tom на кого похож?
1) Это выдуманный пример, который вырос из конкретного кода на JS.
2) Этот код на JS не имеет подобных проверок. Совсем.
3) Не имеет он их потому, что данные были проверены на стадии регистрации. На этапе регистрации ещё надо решить вопрос обновления нашего словаря-кеша, Tom Tom уронит сначала там, там же уточнятся допустимость имён и, как следствие, правила составления словаря.
4) Если вам очень хочется вписать Distinct — я не против, но на большом словаре работу это не ускорит, а мы тут вроде что-то преждевременно оптимизируем. Ради Tom Tom прогонять весь массив через Distinct едва ли правильно.
5) Ещё раз, пример выдуманный. В реале это придётся переписать как только клиенту скажут «только имя-фамилия надо писать полностью и с 1 пробелом».
6) Да и это всё не важно, тут ТЗ: мой код должен зеркалить код vintage. Вписывать туда проверки, которых нет в исходном коде — вообще не правильно.
2) Этот код на JS не имеет подобных проверок. Совсем.
3) Не имеет он их потому, что данные были проверены на стадии регистрации.
6) Да и это всё не важно, тут ТЗ: мой код должен зеркалить код vintage. Вписывать туда проверки, которых нет в исходном коде — вообще не правильно.
В том-то и дело, что исходный пример vintage корректно обработает «Tom Tom». А с парой «John Smith», «John Smith» не упадёт, но с точки зрения логики приложения отработает неверно. Значит, вход с полными тёзками не предусматривался. А «Tom Tom» — допустим.
Давайте вы с vintage составите ТЗ, vintage под него напишет корректный код на JS, а потом можно будет обсуждать насколько моя реализация ему не соответствует. Если мне кто-то объяснит зачем.
Обычно индекс вводят когда линейный поиск явно не справляется с объёмами данных. Удвоение размера индекса лишь незначительно замедлит поиск.
Первый вариант, как уже сказали, никуда не годится. Второй — тот ещё ребус. Тут дело даже не в том, как написать по короче. Всегда можно применить специфичную функцию, которая решает твою проблему одной строкой кода. Но поскольку проблем великое множество, то и функций таких в асенале нужно иметь соответствующее число. И не просто иметь, нужно помнить как каждая работает и в каких случаях какую лучше применить. У нас тут было две похожие задачи — вы решили их двумя совершенно разными способами. Можно ещё слегка изменить условия (например, мы хотим получить список всех ключей но только в формате имя-фамилия, без дубликатов пользователей) и вам придётся снова всё переписать с нуля.
И вся эта свистопляска для чего? Потому что кто-то вам сказал, что LINQ — круто, модно, молодёжно. А циклы — удел старпёров и так уже никто не пишет в 2k17?
Не вижу
Присмотритесь:
calc( result => ({ ...result() , [ x ] : 1 }) , {} )
Чтобы понять, что будет на выходе, нужно рекурсивно развернуть result.
Вам почему то кажется, что «переписать с нуля» — это проблема. Нет, это не циклы, это занимает 10 секунд.
Напомню, что выше у вас проблема была в том, что бы что то дебажить. Мне не надо дебажить ни этот код, ни код qw1, что бы знать как он будет работать.
А в чем «разные» способы? Там только SelectMany добавился для задвоения. В Linq не тысячи функций, а десяток. И при этом qw1 решил ту же задачу другим набором, ничего специфического для задачи там тоже нет.
Простите, но я не вижу проблемы в том, что проблема решается в одну строку кода.
Вам почему то кажется, что «переписать с нуля» — это проблема.
Дополнительное время как на написание, так и на проверку эквивалентности старой логики, новые баги, конфликты при мёрже… чур меня, показалось.
Нет, это не циклы, это занимает 10 секунд.
Ухты, 10 секунд. Вы под спидами программируете?
Мне не надо дебажить ни этот код, ни код qw1, что бы знать как он будет работать.
Мне этот простой цикл тоже "не надо дебажить". К сожалению, реальные задачи чуть по сложнее.
А в чем «разные» способы? Там только SelectMany добавился для задвоения.
И пачка new KeyValuePair вместо лямбд.
на проверку эквивалентности старой логики, новые баги, конфликты при мёрже…
Нет в этой строчке кода ни багов, ни эквивалентности, а конфликтов не больше, чем в циклах.
К сожалению, реальные задачи чуть по сложнее.
И там разница между парой строчек и 2 экранами циклов ещё заметнее )
И пачка new KeyValuePair вместо лямбд.
Лямбда на месте. Слово KeyValuePair можно выбросить, тогда надо взять старую перегрузку с явным указанием ключа, дело вкуса.
Мне этот простой цикл тоже "не надо дебажить".
Напомню, что начинали мы с «Вы хотите сказать, что пишите код без ошибок, раз он у вас "дебага не требует"?». Ну да, придуманные Вами задачи пишутся без ошибок просто потому, что решаются в одну строчку. Где ошибки в JS всплывают я так и не понял.
users_by_name =
users.Select(x => new {u = x, name = $"{x.name_first} {x.name_last}"})
.Union(users.Select(x => new {u = x, name = $"{x.name_last} {x.name_first}"}))
.ToDictionary(x => x.name, x => x.u);
Какая длинная… макаронина :-)
Компилятор сможет соптимизировать создание промежуточных объектов до простого засовывания записи в словарь?
Но практически, я думаю, он оптимизирует только очень небольшое количество случаев, как цепочку filter складывает в один, объединяя условия по AND, или выполняя цикл до первого найденного элемента, если после filter стоит first
Подозреваю это даже не какая-то оптимизация компилятора, а обычные ленивые вычисления. То есть filter возвращает не реальный массив, а range над предыдущим.
Например, проход по массиву с вызовом простой лямбды точно не приводит к вызову функции для каждого элемента. Соответственно, память для хранения переменных, попадающих в замыкание, тоже не выделяется. То есть, оптимизации могут быть весьма сложными.
Не знаю, как в вашей среде, но обычно в отладчиках можно выделить часть выражения
Это вас не спасёт, когда:
- используется транспайлер и вы смотрите в код через сорсмапы.
- функция окажется не "чистой" и повторный вызов приведёт к совершенно иным последствиям.
- у вас нет доступа к нужным переменным, как на втором скриншоте.
Ну и это банально не удобно.
Вот как тут узнать, что вернул filter?
В Chrome Devtools выделяете мышкой, наводите курсор — и он показывает:
А какие проблемы с отладкой лямб в js? (По крайней мере, относительно отладки любого другого js кода).
Только вот что-то Ваш второй пример попахивает. В одном цикле у Вас выполняется 2 разных по смыслу действия, это не очень хорошо. Давайте разделим его на 2 отдельных цикла.
Теперь метод получается довольно длинный, давайте вынесем эти циклы с инициализацией пустого объекта в отдельные функции и будем просто возвращать результат в переменную.
А потом, чтобы не нарушать принцип DRY, вынесем в отдельную функцию обработку массива, чтобы достаточно было прописать только действие над каждым элементом. И каждый обработанный элемент будем мержить с результатом.
Черт, кажется, мы только что изобрели reduce...
В одном цикле у Вас выполняется 2 разных по смыслу действия, это не очень хорошо.
Да нет, действие одно — взять user и распихать его по индексам. Какой смысл дважды подряд итерироваться по одной и той же коллекции? Более того, если коллекция генерируется налету, то двойной проход по ней может давать разные объекты со всеми вытекающими.
for( let user of User.genegate( 1e6 ) ) {
// process user
}
Тут вам возвращается итератор, который налету создаёт вам объекты. Вы берёте каждый объект и обрабатываете как пожелаете.
const users = User.genegate( 1e6 ).toArray()
users.reduce( ... )
users.reduce( ... )
А тут вам необходимо сначала сериализовать итератор в массив, чтобы потом дважды по нему пробежаться, после чего массив выбрасывается (привет, GC).
DRY — замечательный принцип, но начиная с определённого порога он приводит к катастрофическому возрастанию Complexity. Поэтому в борьбе с копипастой важно знать меру и не превращать простую строчку кода в ребус.
А можно пример, когда замыкания помогают сэкономить на условиях? Как я понимаю, если нужно проверить 10 условий, то так или иначе их придётся вписывать руками.
Тогда не совсем понял, зачем в циклах break
, continue
без условий?
Так-то семантически что написать map (someArray, someFunc)
, что написать for x in someArray { someFunc(x) }
— большой разницы не видно. До недавнего времени про эти map, reduce вообще никто не вспоминал, кроме функциональщиков.
Тогда не совсем понял, зачем в циклах break, continue без условий?break — очевидно, поиск первого элемента, удовлетворяющего условию (что заменяется на функциональный first), а continue — применение filter в начале цепочки, в императивном коде способ уменьшить вложенность, замена
foreach (var x in arr) {
if (x < 100) {
// обрабатываем x
}
}
наforeach (var x in arr) {
if (x >= 100) continue;
// обрабатываем x
}
- А с чего вы взяли, что
persons
приходят с сервера? Какая вообще разница, какой источник данных, если эти данные нужно просто отфильтровать?
map
, filter
и reduce
в стандартную библиотеку Ada не завезли, так что это будет цикл. Всё равно в реальном коде хорошим тоном считается прятать такое в отдельную функцию getPensioners(persons)
.
Окей, такие вещи, допустим, не везде есть. Но как в Ada с аналогами Iterable/Enumerable/Range для пользовательских типов? И что с передачей функций по ссылке в другие функции? Что с замыканиями и лямбдами?
with Ada.Text_IO;
procedure Main is
package Sout renames Ada.Text_IO;
type T_Age is new Natural range 0 .. 120;
AGE_MALE : constant T_Age := 65;
AGE_FEMALE : constant T_Age := 55;
type T_Gender is (MALE, FEMALE);
type T_Person is record
Age : T_Age;
Gender : T_Gender;
end record;
type T_Person_Arr is array (Positive range <>) of T_Person;
Persons : constant T_Person_Arr := --test array
((30, MALE),
(40, MALE),
(60, FEMALE),
(65, MALE),
(25, FEMALE),
(67, MALE));
function Is_Pens (P : T_Person) return Boolean is
begin
case P.Gender is
when MALE =>
return P.Age >= AGE_MALE;
when FEMALE =>
return P.Age >= AGE_FEMALE;
when others =>
return False;
end case;
end Is_Pens;
type Is_Pens_Access is access function(P: T_Person) return Boolean;
function Filter (P_Arr : T_Person_Arr; Callback: Is_Pens_Access) return Integer is
Return_Int : Integer := 0;
begin
for I in P_Arr'Range loop
if Callback(P_Arr(I)) then
Return_Int := Return_Int +1;
end if;
end loop;
return Return_Int;
end Filter;
Returned_Val : Integer := 0;
begin
Returned_Val := Filter(P_Arr => Persons,
Callback => Is_Pens'Access);
Sout.Put_Line("Total pens'es: "& Integer'Image(Returned_Val));
end Main;
Я не очень понял, как вы определили операции сложения и вычитания на множестве {1..120}
, ведь они не замкнуты на нём. Кольцом вычетов оно тоже не является, так как не содержит нуля. Если операции выводят из множества, то в чём тогда вообще смысл этого типа данных? Чему будет равно a - b
, если обе переменных типа Age
и равны 120?
Случай равенства a
и b
в данном примере обрабатывается отдельным условием.
Тем не менее, чему будет равно a - b
, если обе переменных типа Age
и равны 120? Я не хочу каждый раз писать тонну условий, я хочу, чтобы язык предоставлял удобные и безопасные типы, тем более когда его систему типов так расхваливают.
Если убрать условие равенства возрастов и добавить условие ageOfBob <= ageOfMary
, то программа скажет Mary is older than Bob by 0 years
. В данном случае это странно, поэтому добавлено условие.
Другое дело — если создать переменную ageDiff : Age
и добавить строку ageDiff := abs ageOfBob - ageOfMary
, а в блоке exception
дописать when others => put_line("Something went wrong!")
, то при равенстве ageOfBob
и ageOfMary
будет генерироваться исключение и результат будет Something went wrong!
.
57:22 warning: «Constraint_Error» will be raised at run time
57:22 warning: value not in range of type «T_Age» defined at line 6
и ошибку при выполнении:
raised CONSTRAINT_ERROR: main.adb:57 range check failed
А что вас смущает в том, что операция над одними типами возвращает другой? Не знаю как в Аде, но в идеальном ЯП {1..120} - {1..120}
должно давать тип {-119..119}
.
С чего бы вдруг?
Так исходные переменные никто и не меняет — они за пределы диапазона не выходят. А вот разность двух чисел, очевидно, имеет совершенно другой диапазон. Если бросать исключение при любых операциях над числами из ограниченных диапазонов, то какой смысл вообще использовать ограниченные диапазоны? Какой смысл от чисел, над которыми нельзя производить никакие операции, так как почти любая операция даёт числа из другого диапазона? А вот от автоматического выведения диапазонов — пользы предостаточно. Компилятор может обнаружить недостижимость веток кода, переполнение максимального числового диапазона, не учёт некоторых комбинаций условий.
Всё зависит от того, какое подмножество взять и как определить на нём разность. Если, например, взять множество {0..n}
, а разность определить как разность по модулю, получим кольцо вычетов — красивую и полезную на практике структуру.
Можно определить тип
type R is mod 10;
В этом случае сложение чисел этого типа будет происходить по модулю, то есть R(7) + R(8)
будет давать 5.
В общем случае, надо делать перегрузку операторов как в c++, чтобы на каждую арифметическую операцию можно было написать свою логику и свои проверки целостности данных. И операторы прозрачной конвертации, если своему новому типу присваиваем значение системного или другого пользовательского типа (и наоборот).
Или, например, взять все чётные числа {2n | n <- Z}
и определить операции сложения и вычитания естественным образом. Получим коммутативное ассоциативное кольцо, чьими свойствами сможем в полной мере пользоваться и знать, что все операции с нашим типом безопасны и не выведут из множества чётных чисел.
Можно рассмотреть бесконечное множество типов и соответствующих структур, но лишь некоторые из них будут полезны и безопасны на практике. Об этом нужно думать при проектировании системы и с этим должен помогать язык программирования.
Операции возвращающие значения из того же диапазона будут работать абсолютно одинаково, хоть компилятор будет кидать исключение, хоть выводить новый диапазон (который эквивалентен исходному). Разница будет лишь для операций, для которых выходной диапазон отличается от входного. И вот в этом случае кидать исключение — глупо.
Наоборот! Вдруг я ошибся и определённая мной операция выводит за пределы установленного множества? Тогда я хочу получить ошибку компиляции, а не исполнения. Если мои операции намеренно выводят за границы установленного множества, то какой в этом множестве тогда смысл? Я лучше буду просто использовать Integer
, или хотя-бы явное приведение типов.
Если вы явно установили выходной диапазон, как в примере выше, то компилятор ругнётся, что задекларированный диапазон не покрывает выведенный. И даже если покрывает, то компилятор всё-равно должен выводить типы, чтобы ловить подобные ошибки:
Int a = - persons.length // a has type Int{ -MAX_INT .. 0 }
Int b = random( 10 ) // b has type Int{ 0 .. 10 }
return a + b >= 10 // compile error: always false
Вот видите, вы не заметили ошибки, а компилятор заметил :-) random( 10 ) возвращает значения от 0 до 9 включительно. В комментариях, я, конечно, накосячил.
Да мне ещё и карму слили. Хабр — торт :-)
Int a = - persons.length // a has type Int{ -MAX_INT .. 0 }
Int b = random(random( 10 ) ) // компилятор правда знает , чему равен этот диапазон?
return a + b >= 10
1) Если мы вводим тип с ограниченным набором значений, мы, скорее всего, делаем это не просто так а с вполне определённой целью: мы хотим, чтобы любая переменная данного типа содержала значения только из заданного интервала. Очевидно, что и операции над такими переменными должны иметь вполне определённый смысл. Иначе зачем вообще вводить собственный тип, ведь можно использовать Integer?
2) Хорошо, когда тип данных образует некоторую математическую структуру с доказанными свойствами. Тогда производить операции над этим типом становится просто и приятно, а рассуждать о программе — легче. В любом другом случае ограничения становятся искусственными, а свойства — некрасивыми и не очевидными. Это приведёт только к большему числу ошибок.
Очевидно, что и операции над такими переменными должны иметь вполне определённый смысл. Иначе зачем вообще вводить собственный тип, ведь можно использовать Integer?
Затем, чтобы компилятор нам помогал. Например, если мы складываем два Integer, то компилятор должен ругнуться, так как возможно переполнение, которое мы не предусмотрели. Предусмотреть это можно следующими способами:
- Использовать операцию "сложение по модулю" вместо "сложения".
- Предварительно проверить, что число меньше половины от MAX_INT.
if( user_input < MAX_INT / 2 ) {
// here user_input has type Int{ 0 .. MAX_INT/2 }
log( user_input + user_input ) // all ok
} else {
// here user_input has type Int{ MAX_INT/2 .. MAX_INT }
log( user_input + user_input ) // compile error: possible integer overflow
}
В любом другом случае ограничения становятся искусственными, а свойства — некрасивыми и не очевидными. Это приведёт только к большему числу ошибок.
Приведите конкретный пример, не будьте голословными.
Подмножество Age
множества целых чисел наследует операции над этим множеством.
Так вот — если вы хотели показать, как это круто, иметь в языке вот такие типы, то у вас это прямо скажем не очень пока получилось.
type Age is range 1… 120;
А почему собственно 120? Почему это вообще константы? Может ли этот тип быть параметризован другим типом, задающим границы? Что будет, если в операции участвуют Int и Range одновременно?
Этот пример на сегодня вызывает больше вопросов, чем дает ответов. И это все далеко не тривиальные вопросы по системе типов языка.
Тут уже привели минимум один пример: если из возраста 100 вычесть возраст 120 (оба валидны), получится отрицательное число, которое возрастом не является.
А с чего вы взяли, что разность возрастов должна являться возрастом? Возрастом кого может являться "разница возрастов"? :-)
И специальный тип данных «разница возрастов» не нужен.
Открываем ISO8601 и видим следующие типы:
- момент времени (4 марта)
- временной период (37 дней)
- временной диапазон (с 5 по 27 число)
- повторяющийся временной диапазон (каждый день с 5 до 6)
К "5 марта в 18:00" можно прибавить "1 месяц" и получить "5 апреля в 18:00".
Возрастом кого может являться «разница возрастов»? :-)Очевидно, возрастом родителя на момент, когда у него появился ребёнок :)
Поэтому, например, если мы считаем A1+A2-A3, где все три типа Age, и A1+A2 вылазит за его диапазон, но A1+A2-A3 не вылазит, промежуточная ошибка не будет замечена, но если результат всего выражения будет присвоен AR типа Age и вылезет за 1..120, будет исключение.
К вопросу о корректности разности возрастов это не относится.
Чтобы сделать, что разность возрастов была отдельным типом, нужно создать «пакет» в терминах Ada (это лучше всего соответствует «классу» в C++ и аналогах) и для него уже определить function "-". Механизм, таким образом, для этого есть, хоть и громоздкий. Там уже можно определить и все необходимые прочие операции и ограничения этого типа.
Если исключить возможность явной конверсии своих типов данных в стандартные (как целые), то можно обеспечить и типобезопасность для контроля размерностей (например, не присваивать километры миллиграммам). В смысле этих возможностей Ada не уступает какому-нибудь C++, хотя и выражает свои возможности более громоздко.
Тут уже привели минимум один пример: если из возраста 100 вычесть возраст 120 (оба валидны), получится отрицательное число, которое возрастом не является.
ЕМНИП, вывалится исключение в run-time. Т.е. когда в Ada описывается тип-диапазон, то при работе с экземплярами этого типа в run-time добавляются необходимые проверки, а в compile-time, там где компилятор видит константы, он может выдать предупреждение.
А почему собственно 120?
По этому поводу — случай из трудовых будней. В медицинское учреждение, где работаю, недавно установили (за нехилые такие деньги) Лабораторную информационную систему (ЛИС). В ней для каждого анализа (ну, типа, сахар, холестерин, билирубин и т.п.) предусмотрены нормы в зависимости от возраста (чтобы, значит, человек понимал — жить ему или достаточно). Так вот, максимальный возраст, который там можно ввести = 99 (не больше двух циферок). А на днях пришёл дедушка 1916 года рождения и нормы на бланке с его анализами, естественно, не распечатались. Пришлось «скидывать» долгожителю пару годиков, чтобы выдать осмысленный результат.
Справедливости ради замечу, что ЛИС таки в трудовой процесс вписался достаточно хорошо (хоть и понаписат на 1С) — много чего автоматизирует и по-крупному не лажает. Однако и мелких забавных косячков (удивляющих до изумления программерской безалаберностю) всплывает тоже прилично. Как говорится — се ля ви.
Тут уже привели минимум один пример: если из возраста 100 вычесть возраст 120 (оба валидны), получится отрицательное число, которое возрастом не является.
Но сравнению это не мешает: если возраст это не отдельный тип со своими операциями, а уточнение Integer, то сравнение выполняется по правилам Integer, и выход разности за допустимые пределы игнорируется. Тем более что сравнение может вылиться, например, в инструкцию SLT процессора стиля MIPS/Risc-V, которая вообще формально ничего не вычитает :)
А почему собственно 120? Почему это вообще константы? Может ли этот тип быть параметризован другим типом, задающим границы?
Есть понятие настраиваемых пакетов, которое практически точно соответствует шаблонным классам C++. Собственно, range задаёт такой же настраиваемый пакет, но встроенного определения.
Что будет, если в операции участвуют Int и Range одновременно?
В общем случае операции над range-типами исполняются так же, как над их базовым типом, а проверка на диапазон производится уже при присвоении целевой переменной (или можно форсировать конверсией к её типу какой-то части выражения). То есть, пока вы результат никуда не присвоили или не проконвертировали, сумма Integer + range 1..120 будет считаться так же, как сумма двух Integer. Если это 32-битные, то переполнение будет диагностировано по выходу любого промежуточного результата за общеизвестные -2147483648...2147483647. Более узкий диапазон, как уже сказал, будет проверяться, если вы присвоите переменной типа Age или напишете что-то в стиле Age(A1+X) как одну из компонент более сложного выражения.
Режим с игнорированием всех переполнений (аналогично unsigned в современном C, всей числовой арифметике на стандартных операциях в Java...) возможен с использованием определений типа mod, например mod 2**32 значит 32-битное беззнаковое с заворотом результата (обрезанием до 32 бит). Если нужно считать таким образом, требуется явная конверсия в такой модулярный тип и обратно. Модулярные — только целые без знака.
Резюмируя, всё это в языке хорошо определено (иначе бы его не приняли для DoD:)), и достаточно оптимально, как для задачи "добиться отсутствия неожиданных эффектов чуть менее, чем везде". Так что Ваши вопросы по системе типов всего лишь требуют внимательного похода в место типа такого.
Суть моих вопросов была в том, что все не так очевидно, если вдуматься, и накоторые вопросы по системе типов, по их неявному или явному приведению в операциях (а особенно у начинающих) вполне себе напрашиваются. И ответы на них далеко не всегда очевидны.
У меня к вам вопрос с подвохом. Позволяет ли Ada объявить пользовательский тип, который можно потом "ограничить"?
В современных неафриканских популяция вида Homo Sapiens содержится в среднем (с вариациями по планете) от ~2% до ~4.5% генов «неандертальского» человека, у всех без исключения людей — в том числе и у вас. Данные примеси не содержатся лишь в ДНК популяций народов Африки, которые и являются единственными «не-неандертальскими», строго говоря — так как неандертальцы обратно в Африку не совались.
1) Не всегда код предполагает все эти проверки и исключения — задача может быть простая и одноразовая — сделали какую-то выборку на ходу, получили данные и больше нам этот код не нужен — под это все тоже тащить кучу встроенных проверок? Даже если мы 100% уверены в наших исходных данных которые УЖЕ прошли это все в другом месте?
2) Первый пример проверок выше — это стандартный подход, встречающийся во многих языках, изучив его раз, не надо ломать голову при изучении нового языка, что же тут наворотили эдакого и зачем.
PS: Ада разрабатывалась как язык промышленного применения, где цена ошибки ОЧЕНЬ высока — там подобный подход действительно оправдан. Но использовать его же, например для скриптовых языков? Зачем?
Я бы не отказался использовать такой подход хотя бы для экономии собственного времени на отладке и юнит-тестах.
И конечно, довольно частая ситуация, когда «помнишь, ты писал скриптик, вот нам такое нужно запустить в работу» — берется скрипт на питоне, обзывается «прототип», собираются требования, и новая задача решается на скале.
Мне чем-то поможет Ада, в которую map/filter не завезли? А еще был (да есть, конечно), Eiffel, тоже хороший язык для разработки программ, где цена ошибки высока.
Мне кажется, что области применения Ada и Python-скриптов как-то не особенно пересекаются. Scala — окей, дело вкуса, хотя по сравнению с Ada её область применения довольно узкая.
Ок, значит там, где питон экономит время, Аде точно нечего делать.
А там, где скала — дело вкуса, но скала не так хорошая в каких-то вещах.
В каких? Не компилирует код в бинарник, требует jvm?
Почему вообще сюда влез — про язык Ада слышу давно, и что характерно — ни разу не то, что ни возникало желания «взять и что-то сделать», а даже понимания, мне вот это действительно нужно вот тут?
Почему вспомнил про Eiffel — однажды удивительно «срослось» — требования задачи, и то, что предлагал язык. И он очень «легко зашел» в той задаче. Правда, это было уже настолько давно, то сейчас бы ни разу не зашел.
В какие задачи Ада «легко заходит», и вот реально экономит время, по сравнению с современными инструментами? Ответ на такой вопрос был бы гораздо интереснее, чем рассказ, как компилятор ловит всякие тонкие моменты в Hello World.
А есть ли вообще существенные основания говорить, что Scala годится для создания сложных систем и не создаст проблем в будущем? Чем обоснованы синтаксис и семантика этого языка, есть ли средства формальной верификации? Привязка к JVM — уже существенное ограничение, поскольку это не годится для многих встраиваемых систем, микроконтроллеров и просто когда не хочется писать bloatware.
Ada — несовременный инструмент? Несколько странная точка зрения.
И вы, вместо рассказа, чем Ада хороша, спрашиваете у меня про скалу?
Давайте исходить из того, что я знаю «скала не очень-то», и выбираю чем бы заменить. Расскажите, почему если я заменю скалу на аду, я буду экономить больше времнеи, каких проблем у меня не будет (не важно, есть они сейчас или нет).
Точка зраения основана на странице в википедии, уж простите, более простого и быстрого источника у меня нет (а вы вот как-то не спешите рассказывать). На этой странице мне больше всего понравился пункт «испытал влияние»:
ALGOL 68, Pascal, C++ (Ada 95), Smalltalk (Ada 95), Java (Ada 2005), Eiffel (Ada 2012)
И конечно, «история» — новый стандарт — Ада 2012. Предыдущий Ада 2005.
Ок, с «несовременный», я ошибался (в 2000 ых Ада совсем пропала из мира вокруг меня, и я даже не подозревал что два стандарта выкатили за это время), давайте выберем слово «не популярный в индустрии».
В качестве показателя популярности… Вот, есть статья про всякие рейтинги https://habrahabr.ru/company/kingservers/blog/307012/
там про Ада нет ни слова. Это, конечно, мало что говорит о языке — у всех рейтингов методики странные.
Так расскажите что на этом языке хорошо пишется?
Почему, если я возьму этот инструмент, смогу победить конкурентов, которые пишут на яве (эти ваще просто массой давят), скала, котлин, го, С++ в конце концов, а вон еще хаскель медленно, но растет.
Статья не является рекламой Ada, Ada не является серебрянной пулей. Хорош или плох язык для какого-то конкретного проекта определяется требованиями к проекту — может быть там вам Pascal будет лучше всего или Assembler…
Если судить о дизайне языков по этим рейтингам, то получится, что самые совершенные и продвинутые языки — JavaScript, Java, PHP, Python и C#.
Вот что значит комменты открыли для всех
Сказал человек с тремя комментариями, два из которых написаны капсом.
Особенно when others => null
Я уж молчу про обработку исключений в конце модуля, в Аде вполне есть классический try-catch и пользователю можно говорить об ошибке, не вываливаясь с нулевым кодом возврата из программы.
В этом примере появление других исключений маловероятно.
Не могли бы вы привести пример "классического" блока try-catch на Ada? Вроде бы исключения всегда обрабатываются в отдельном блоке exception
.
begin
Work;
exception
when X1 =>
Y1;
end;
как по мне, ничем, кроме выбора и группировки ключевых слов, не отличается от
try
Work;
catch X1 =>
Y1;
end;
А вот то, что исключения в Ada не параметризованы — не переменные, а только типы — сильно усложняет передачу обстоятельств исключения.
const MAX_PERSON_AGE = 120
Ну зачем же 120. А если кому-то будет 125? 130? Придется программу ведь переписывать) давайте сразу 150-200.
Звучит она, на мой взгляд как «Больше языков. Аляпистых. Новых. МОДНЫХ. И не важно что будет потом».
Лично мое впечатление такое, что подавляющее число «создателей новых языков программирования», не имеют серьезного опыта разработки, и вообще подходят к этому делу как к приключению, к блогу, созданию развлечения для фанатов.
— «Обратная совместимость выпускамых ими версий языков? Зачем, ведь у нас есть новая вкуссная фича, мы впилили ее — вот, смотрите!». А то что вы не сможете перевести свой проект на новую версию потому, что вам надо будет переписывать мегатонны кода — это их уже не волнует.
«Ну и что! Зато смотрите как у нас бложик с этой фичей красиво обрабатывает тысячи твитов в секунду!» — Ну вот разве не так? Посмотрите на новость о снятии с поддержки руби 1.8 (кажется, поправьте меня). Мол, «грустно, конечно, что снимается с поддержки, прошло 2.5 года с момента выпуска, но эта версия до сих пор используется, потому, что код написанный для руби 1.8 не работает на интерпретаторе версии 2». (Так, или почти дословно.).
Занавес. Привлекли, поигрались, и кинули.
Не предупреждают создатели «самых правильных языков» своих «последователей», и о тм, что «язык для быстрого написания прототипов», «динамической типизацией» и «идеально правильным ООП» — на самом деле крайне редко подходит для серьезных задач, больших систем со сложной логикой, и суровых будней поддержки промышленного кода. А может и не могут предупредить, потому, что сами не знают, или не думают что их последователи будут «пихать невпихуемое». А может и знают, но хотят посмотреть что из этого выйдет — ведь не своими деньгами рискуют, а «если что» скажут типа — «Ну наш язык же для быстрого написания прототипов, о чем вы, не надо было конечно так делать».
В общем статья хоть и «кривоватая», но она о правильной проблеме — будьте осторожны с очередным «самым новым языком». Скорее всего это просто очередная мода.
И особенно осторожны будьте, пока не увидите проблемы и границы применимости языка, пока язык не найдет свою нишу.
Как всякий логический и технический инструмент, язык программирования — имеет свою область применения. Применять технический инструмент, не понимая когда этого делать не надо — это чистой воды глупость. Но как раз о недостатках своего детища — в эпоху «рекламы и красивых бложиков» — создатель нового языка, "который, как оказалось, понравился не только ему" — не предупреждает.
Об этом статья. Имхо.
Какие из языков всплывут и выживут — трудно сказать. И не всегда понятно почему одни хорошие языки всплывают, а другие — нет. и почему некоторые плохие тонут, а другие — нет, тоже не всегда ясно.
Простого программиста или среднестатистическую IT-компанию в таких условиях больше всего интересует вопрос, как не впутаться в какую-то модную технологию, которая может через пару лет оказаться на свалке вместе с ними.
Конечно, если мозгов у руководства компании нет — то компания окажется на свалке, но не потому, что ошиблась в ставке на какую-то технологию, а именно потому, что мозгов нет :)
Куда вливают бабло — то и всплывает.
Кажется, даже на хабре писали, про Оберон и ветку «языков Вирта». То, что тогда происходило — хороший пример того, как бабло вливаемое Sun, IBM, Oracle подняло наверх Java и сопутствующие технологии.
О чём молчат авторы «Hello, World!»-ов