Comments 20
Не совсем понятны критерии "простоты" языка. Вот классический Паскаль - простой?
А Фортран (ранних версий, 4-й, или 77-й)?
Ну и примеры... Человеку, незнакомому с языком далеко не сразу очевидно что делает вот это:
/// для каждого i в списке выводим на экран i+1
pub fn print_all_plus_one(l: List(Int)) {
// этот пример надуманный; как правило, приходится использовать всего один цикл
//один цикл:
let res = {
use i <- list.map(l)
int.to_string(i + 1)
}
// другой цикл:
use s <- list.each(res)
io.print(s)
}
Что такое use? Зачем оно? Как устроен "другой цикл"? Где он начинается и где заканчивается?
Понятно, что когда постоянно работаешь с языком, все это становится естественным, но для видящего все это в первый раз...
Человек ещё заодно считает, что эти 8 строк проще, чем функция высшего порядка (map +1 list).
Вдогонку. Про простоту языка.
Простой язык - это то, по коду которого сразу понятно как он будет работать? Или это язык где все сложные конструкции скрываются в одной строке кода?
Вот попробуйте на С (к примеру) без внешних библиотек расписать что делает этот самый цикл
let res = {
use i <- list.map(l)
int.to_string(i + 1)
}
Крайне поверхностная статья. Многие пункты - заслуга не языка, а экосистемы. Рассуждения про типы просто нелепые. В Си слабая типизация, Питон с аннотациями мало чем отличается. Хаскелл, конечно, же сложный язык, но вовсе не из-за "случайных последовательностей символов", и ещё там не только лямбды.
Статью нейросеть писала?
C и его последователи довольно хреновые языки, с двумя другими не сталкивался но сомневаюсь в "простоте".
В любом случае сложность библиотек куда важнее сложности языка
Уже долгое время самым простым языком среди не-эзотерических является Оберон ревизии 2013. Дедушка Вирт на склоне лет знал толк в простоте.
По мне так самый простой язык программирования C#, причем он же и самый сложный. Вы всегда можете выбрать, в каких конструкциях вам выражаться, в самых простых или посложнее.
А какими еще языками приходилось пользоваться? С чем сраниваете?
Про шарп ничего не могу сказать - не приходилось на нем писать. Просто интересно.
C, C++, Perl, Basic, Pl/1, Fortran, assembler-s-ы, Java, Rubi
Статья хороша. Жалко что это просто репост чужого (перевод).
Самый простой язык это BASIC. Чтобы никаких указателей, лябд и прочей ереси, только человекочитаемые слова. Ну, может быть Pascal ещё. А C, Go и вот этот ещё какой-то, не знаю про него ничего, точно не простые.
Ну тут надо четко обозначить что понимать под "простотой".
С точки зрения концепций, заложенных в язык - да. Бейсик, классический Паскаль достаточно просты. С уже сложнее т.к. чтобы на нем нормально писать надо четко себе представлять что такое переменная, что такое указатель, как они соотносятся, как все это в памяти будет лежать и т.п.
Ко мне вот сейчас иногда заходят с вопросами по С (С/С++ у нас используются мало, это не "основные" языки, но иногда они удобнее для решения некоторых задач).
Так вот чтобы человек, никогда с указателями не работавший, быстро в тему въехал, объясняю так:
Открываешь шкаф, ищешь коробку на которой написано "сахар" - это обращение к переменной по ее имени - var
Открываешь шкаф, там на третьей полке вторая коробка справа - это обращение по указателю - *var
Оба подхода дадут одинаковый результат - получишь коробку с сахаром. Это, пожалуй, основное что вызывает сложности в С у начинающих.
Если же говорить с точки зрения синтаксиса, то что бейсик, что паскаль (классический), что С достаточно просты - смотришь код и видишь алгоритм. И знаешь что вот эта строка кода не скрывает под собой никаких тайн. В отличии от:
let res = {
use i <- list.map(l)
int.to_string(i + 1)
}
тут возникает куча вопросов - какой тип у res? Сколько он в памяти занимает? Что скрывается за use i <- list.map(l)? Сколько времени оно будет работать? Будут ли с памятью динамически манипулировть? Там внутри цикл или что? А что такое i? Что в нем будет храниться?
Т.е. оно вроде как на экране выглядит просто, но внутри там может быть наворочено до черта всего...
то что бейсик, что паскаль (классический), что С достаточно просты - смотришь код и видишь алгоритм.
Это если там есть алгоритм. К сожалению часто встречается код, в котором автору алгоритм не получилось сформулировать, просто код в один прекрасный момент заработал, но никто не знает почему. Как раз с таким кодом в основном приходится разбираться, так как он хоть и заработал в основном (так сказать), но иногда все таки не работает, а требуется чтобы работал всегда.
От языка тут в общем то ничего не зависит, зависит от способности програмиста сформулировать и осознать алгоритм, и вычислительную задачу, которую решает этот алгоритм, и как этот алгоритм перепоручить на выполнение вычислительной машине.
С Си-подобными указателями возникает некоторая путаница в предмете указания (указатель на массив не отличается от указателя на его первый элемент). Это делает аналогию с коробкой неполной. Скорее, тут правильнее было бы говорить о позиции в шкафу. Но мне кажется, что в данном случае проще объяснить сразу через машинный адрес.
С Си-подобными указателями возникает некоторая путаница в предмете указания (указатель на массив не отличается от указателя на его первый элемент).
В целом - да. Это тоже часть идеологии С - есть адрес блока памяти, а помнить что там лежит уже на совести разработчика.
В этом плане еще проще реализовано в RPG (с которым работаю последние 6+ лет).
Там указатель - это просто адрес в памяти. Он не типизирован. Точнее, есть два типа - указатель на данные - pointer, получается вызовом %addr и указатель на процедуру - pointer(*proc) - получается вызовом %paddr().
Адресной арифметики, как в С, нет. Т.е. можно делать инкремент/декремент адреса, но всегда на заданное количество байт.
Чтобы работать с тем, что находится по указателю нужно сначала сделать маппинг. Т.е. объявить связанную с этим указателем переменную
dcl-s var1 char(100); // строка 100 символов
dcl-s var2 char(100) based(ptr); // строка 100 символов по указателю ptr
// прежде чем использовать var2 нужно инициализировать ptr
ptr = %addr(var1);
// теперь var2 и var1 суть одно и то же
// или можно аллоцировать память динамически
ptr = %alloc(100);
// но потом не забыть деаллоцировать
dealloc ptr;
Никто не запрещает связывать с одним указателем несколько переменных разных типов. Вся ответственность за последствия на совести разработчика.
А в остальном - очень простой процедурный паскалеподобный (с легким привкусом PL/I в синтаксисе) язык для эффективного решения определенного класса задач (работа с БД и коммерческие вычисления). И очень самодостаточный - в языке для решения этих задач есть все что нужно. Никаких дополнительных библиотек в 90% случаев вам не потребуется. Все типы данных что есть в БД (decimal/numeric, char/varchar, date/timw/timestamp...), работа с датам, временем, строками, арифметика с фиксированной точкой (в т.ч. и операции с округлением) - все в языке. Работа с таблицами индексами - хоть напрямую, хоть SQL непосредственно в код встраивай.
В последнее время расширили работу с массивами - цикл перебора for-each, проверка вхождения в массив in, разбиение строки на составляющие по разделителю %split, склейка в строку %concat и %concatarr. Есть динамические массивы. Есть поиск в массиве %lookup (и модификации типа %lookupge, %lookuple и т.п.), причем, если массив объявлен как сортированный, с модификаторами ascend/descend, то используется двоичный поиск, иначе - перебором по массиву.
В последних версиях добавили перезагрузку процедур
// Отправка сообщения в queLIFO/queFIFO очередь
// Возвращает количество отправленных байт
// в случае ошибки -1
dcl-pr USRQ_SendMsg int(10) overload(USRQ_Send: USRQ_SendKey);
dcl-pr USRQ_Send int(10) extproc(*CWIDEN : 'USRQ_Send') ;
hQueue int(10) value; // handle объекта (возвращается USRQ_Connect)
pBuffer char(64000) options(*varsize); // Буфер для отправки
nBuffLen int(10) value; // Количество байт для отправки
Error char(37) options(*omit); // Ошибка
end-pr;
// Отправка сообщения в queKeyd очередь
// Возвращает количество отправленных байт
// в случае ошибки -1
dcl-pr USRQ_SendKey int(10) extproc(*CWIDEN : 'USRQ_SendKey') ;
hQueue int(10) value; // handle объекта (возвращается USRQ_Connect)
pBuffer char(64000) options(*varsize); // Буфер для отправки
nBuffLen int(10) value; // Количество байт для отправки
pKey char(256) const; // Значение ключа сообщения
nKeyLen int(10) value; // Фактический размер ключа
Error char(37) options(*omit); // Ошибка
end-pr;
Не вдаваясь в подробности - USRQ_Send и USRQ_SendKey - две реальных процедуры с одинаковым типом возвращаемого значения, но работающих по разному и с разным набором параметров. А USRQ_SendMsg - универсальная объединяющая обертка. Ее используем в коде, а компилятор уже подставит вызов нужной процедуры в зависимости от того с каким набором параметров оно вызывается.
Отдельная тема - структуры. Очень простая и очень гибкая идеология. Структура суть просто байтовый буфер внутри которого размечены поля. Причем, разметку можно делать с явным указанием позиции поля внутри буфера. Причем, поля могут перекрываться (что делает ненужным С-шный union - все это описывается в рамках структуры). Причем, можно явно задать размер структуры в байтах, который будет больше чем сумма размеров его полей
Пример:
dcl-ds t_dsCPDL qualified template; // расшифровка срока хранения лога
Units char(1); // единицы (D/W/M/Y)
Count char(3); // количество
end-ds;
dcl-ds dsCPOCTL len(100) qualified;
poSts char(1); // Статус:
// • N – неактивна
// • Y – активна
// • O – вызов RRUCHK#HD
poCPDL char(4); // Срок хранения истории в формате XNNN, где:
// • X – фромат срока (D – день, M – месяц, Y – год)
// • NNN – числовое значение указанного срока
poRESA char(10); // Коды результатов на авторизацию
poRESW char(10); // Коды результатов на ожидание
poLogL char(1); // Режим логирования:
// • 0 - ошибки
// • 1 - +инфо
// • 2 - +отладка
poMsgQ char(10); // Очередь для вывода сообщений
/////////////////// переопредления ///////////////////
dsCPDL likeds(t_dsCPDL) samepos(poCPDL);
ResA char(1) dim(10) samepos(poRESA);
ResW char(1) dim(10) samepos(poRESW);
end-ds;
template тут - аналог typedef. Т.е. просто описание, без создания самой переменной. Переменная в данном случае описывается как like (для обычной переменной) или likeds для структуры
Модификаторы samepos - это как раз перекрытия. Т.е. обращаясь к dsCPOCTL.poCPDL мы работаем со строкой 4 символа (вида D002, M001 и т.п.), а обращаясь к dsCPOCTL.dsCPDL - уже с ней же, но в виде структуры из элементов dsCPOCTL.dsCPDL.Units и dsCPOCTL.dsCPDL.Count. Аналогично - dsCPOCTL.poRESA - строка из 10-ти символов, а dsCPOCTL.ResA она же, но в виде массива из 10-ти отдельных символов.
Там еще много чего можно на уровне описания данных, долго рассказывать...
Это делает аналогию с коробкой неполной.
А аналогия с коробкой просто чтобы понять суть - все равно как вы описываете способ доступа к области памяти где лежат данные - по имени переменной или по адресу этого блока в хранилище. И в реальной жизни, если вы привыкли все держать на своих местах, заглядывая в шкаф за сахаром вы сразу будете смотреть "по указателю" - на третью полку, вторая коробка справа. Потому что привыкли что там лежит сахар. И если кто-то положил туда коробку с солью, то можете схватить соль вместо сахара (хорошо если проверите прежде чем в кофе сыпать :-)
А вот в незнакомом шкафу - да, будете по имени искать, просматривая все коробки в поисках той, на которой написано "сахар".
В мире есть два языка программирования - ассемблер и все остальные.
Первый - сложный. Остальные учить можно хоть всем сразу, было бы время
Простые языки программирования