LINUX.ORG.RU

ЛОР, помоги выбрать ЯП для обучения

 , , ,


0

3

Часто слышу просьбу научить программированию. Хотя пока я этим не занимаюсь, мне стало интересно, какой ЯП выбрать для введения в программирование.

Вот к каким мыслям я пришёл:

Язык должен наиболее чисто демонстрировать самые основы написания кода.

Не Си и не современные коммерческие языки (Java, C#, Go). Си, хотя примитивный в основе, усложнён из-за окружения, в котором используется. Современные коммерческие языки были созданы для решения проблем индустрии. Проблема общая: я хочу преподавать материал по мере нарастания сложности. Если в языке неизбежно приходится использовать классы или printf, то это затруднит объяснение (не хотелось бы слишком часто говорить «потом узнаешь для чего это нужно»), напугает студента (ему придётся писать код, используя возможности, которые он плохо понимает), создаст неправильное восприятие основ (как будто printf — это какая-то важная часть компьютера или ОС).

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

Языки, между которыми я выбираю: Pascal и Python.

Pascal устарел и денег не принесёт (обидно), но это и не является основной целью. Целью является программирование, а не современное окружение.

В частности, я не собираюсь задрачивать студента на Delphi или любой «продвинутый» диалект языка. Это противоречит цели. Я рассчитываю на то, что после должной тренировки “bare bones” нужно перейти на современный язык и это будет легко.

Важно упомянуть, что спека языка Oberon (Виртовский язык, тот же Паскаль, только упрощённый и доработанный) составляет 17 страниц.

Питон мне сложнее оценить, потому что я избегал работы с ним.

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

Это все мои мысли.

Что касается практики, то я имел несчастье наблюдать как человек впервые знакомился с программированием, изучая Java на javarush. На это было больно смотреть.

Edit: дальнейшие пояснения по теме:

  • Почему не Си и не ассемблер: 1 2
  • Почему Паскаль: 1 2
  • Почему не Питон: 1
  • Целевая аудитория: 1
  • Почему такая размытая аудитория: 1 2
  • Про важность иерархии: 1


Последнее исправление: kaldeon (всего исправлений: 7)
Ответ на: комментарий от MOPKOBKA

Что это за меряние писюнами? Результат можно получить умножением (x * x + x) / 2. Лямбды в С++, если не проинлайнятся, то медленнее функций. Еще когда они появились в С++ и коллега начал за них топить, я посмотрел ассемблерный листинг и ужаснулся куче обёрток что создаются над лямбдами.

jpegqs
()
Ответ на: комментарий от MOPKOBKA

Загадочно

$ g++ -Ofast -std=c++23 test3.cpp -o test-cpp2
$ time ./test-cpp2 1000000000 p
250000000500000000

real    0m1,014s
user    0m1,013s
sys     0m0,001s
$ time ./test-cpp2 1000000000 c
250000000500000000

real    0m1,143s
user    0m1,139s
sys     0m0,005s
$ g++ --version
g++ (Debian 14.2.0-8) 14.2.0

Процессор Intel(R) Celeron(R) J4005 CPU @ 2.00GHz

monk ★★★★★
()
Ответ на: комментарий от jpegqs

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

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

monk ★★★★★
()
Ответ на: комментарий от monk

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

#include <functional>
auto f = [](long x) { return x == 0; };
auto g = [](long x) { return x % 2; };

void test_true_lambda(int argc, char **argv, std::function<long(long)> f, std::function<long(long)> g) {
    long r = 0;
    long end = atoi(argv[1]) + 1;
    for (long i = 1; i < end; ++i) {
        if (f(g(i))) r += i;
    }
    std::cout << r << "\n";
}
        case 't':
            test_true_lambda(argc, argv, f, g);
            break;
jpegqs
()
Ответ на: комментарий от jpegqs

void test_true_lambda(int argc, char **argv, std::function<long(long)> f, std::function<long(long)> g) {

Если именно так, то в 8 раз медленней.

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

void test_true_lambda(int argc, char **argv, auto f, auto g) {

то время такое же, как для test_pure.

monk ★★★★★
()
Ответ на: комментарий от jpegqs

Мне кажется большинство этих тестов подтверждают надежно лишь одно, что std это большой тормоз. То вектора тормозят, то range, то ламерски лямбды ака std::function...

MOPKOBKA ★★★★★
()
Последнее исправление: MOPKOBKA (всего исправлений: 1)

Короче, понятно. Из этого треда получено подтверждение двух вещей:

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

Проталкивальщики крестов и ады в качестве языка обучения программированию (а не типошаблонодрочерству) особенно забавны в своей упоротости. Оно и понятно, им же выгодно, чтобы программирование оставалось уделом «избранных», потому как иначе любой студент-скриптушник сделает за неделю то, на что они годы тратят. А захочет скомпилированную версию — возьмёт Go и не будет париться. Или вообще старпак на Tcl соберёт.

Изначально я учился программированию вообще на МК-52 и потому знаю, как важна оптимизация. Но оптимизация на уровне алгоритмов и структурирования модулей куда важнее оптимизации на уровне рантайма. Поэтому у меня и JS не тормозит, и лопат с землекопами не складываю. Если топикстартер (хотя сомневаюсь, что он ещё следит за темой) хочет готовить людей мыслящих, а не винтиков ИНДУСтрии, то на низкоуровневых мелочах надо заострять как можно меньше внимания и даже на стек смотреть как на абстрактную структуру данных, даже если для обучения будет выбран какой-нибудь Forth.

Но тред давно уже ушёл куда-то не туда, посему благоразумно сваливаю.

rebforce
()
Ответ на: комментарий от rebforce

Поэтому у меня и JS не тормозит

Можно увидеть твои приложения, уровня Figma например?

типизация средствами ЯП нужна прежде всего

Тем кто пишет больше 10 строк. Ты в их число не входишь, пользуйся дальше tcl.

Но оптимизация на уровне алгоритмов и структурирования модулей куда важнее оптимизации на уровне рантайма.

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

даже на стек смотреть как на абстрактную структуру данных

Это способ передачи аргументов, один из самых лучших. То что в других языках делается отдельными приемами такими как композиция или оператор стрелки в Clojure, в Forth делается «нативно»

: c f g ;

MOPKOBKA ★★★★★
()
Последнее исправление: MOPKOBKA (всего исправлений: 6)
Ответ на: комментарий от monk

Преобразуйте, пожалуйста «compose f g x = f (g x)»

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

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

alysnix ★★★
()
Ответ на: комментарий от MOPKOBKA

загагулина

что первый «report» от Мура о «форте» это нечто что описанно чуть ли ни как алгорифмы маркова(ам) тока в обратную сторону

аналогия как

если ам это LR - типо свёртка от терминалов к узлам ( ну как в первых книжках Аха и Ко) то форт это LL процессирование слов вниз до терминалов - а два стека типо ваще частность реализации идеи основной цимес которой как «понимать» слова разделённые пробелами

qulinxao3 ★☆
()
Ответ на: комментарий от monk

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

#include <stdio.h>
#include <stdlib.h>

#define LAMBDA(T, BODY) ({ inline T _f BODY _f; })
#define COMPOSE(T, F1, F2) LAMBDA(T, (T x) { return F1(F2(x)); })

int main(int argc, char **argv) {
    long r = 0, end = atol(argv[1]) + 1;
    __auto_type f = LAMBDA(long, (long x) { return x == 0; });
    __auto_type g = LAMBDA(long, (long x) { return x % 2; });
    __auto_type cond = COMPOSE(long, f, g);
    for (long i = 1; i < end; ++i) if (cond(i)) r += i;
    printf("%ld\n", r);
    return 0;
}
C = 0m1,154s
Haskell = 0m0,101s 
MOPKOBKA ★★★★★
()
Последнее исправление: MOPKOBKA (всего исправлений: 2)
Ответ на: комментарий от MOPKOBKA

А в хаскеле оператор % на беззнаковом типе работает или знаковом? Потому что большая разница в производительности. Беззнаковый степени двойки делается одной инструкцией, а знаковый через несколько.

Вы используете nested functions, которые не стандарт, а GCC расширение. У Clang есть подобное, но называется blocks и имеет другой синтаксис.

jpegqs
()
Последнее исправление: jpegqs (всего исправлений: 1)
Ответ на: комментарий от jpegqs

Надо godbolt смотреть весь код. Замена типов ничего особо не дает, ну эта реализация лямбд далеко не самая эффективная вообще.

На блоках дает 0m0,791s, что в 7 раз медленнее хескела.

MOPKOBKA ★★★★★
()
Последнее исправление: MOPKOBKA (всего исправлений: 3)
Ответ на: комментарий от rebforce

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

kaldeon
() автор топика
Ответ на: комментарий от alysnix

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

Нужна функция, принимающая аргументами две функции f и g и возвращающая функцию от одного аргумента x, которая возвращает f(g(x)).

В терминах Си должно работать такое:

typedef int (*func)(int x);

void call(func f)

int f(int x) { ... }
int g(int x) { ... }
func compose(func f, func g) { ... }

int main()
{
   call(f);
   call(g);
   call(compose(f, g));
   call(compose(g,compose(f, g)));
}

это можно сделать даже на си

Вот и напишите, как это можно.

monk ★★★★★
()
Ответ на: комментарий от MOPKOBKA

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

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

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

alysnix ★★★
()
Ответ на: комментарий от alysnix

Это мне понятно, это изначально был тест оптимизатора, я вот честно удивлен что gcc не смог это все преобразовать в простой код и оставить один if.

MOPKOBKA ★★★★★
()
Ответ на: комментарий от watchcat382

Проще использовать стандартные имена латинскими буквами

Ладно, убедили. Если я захочу сохранить книгу Ф.М. Достевского: «Бесы», я не буду называть её Достоевский/Бесы.txt, я лучше выберу Dostoevsky/Besy, или Dostoevskij/Besee, или Dostoevskiy/Besi, или даже Dostoevsky/Besbi, чтоб совсем красиво было. А потом буду каждый раз вспоминать какой из костыльных вариантов был использован. Зато, если вдруг этот файл понадобится иностранцу, он сможет прочитать его название. Что он будет делать с содержимым файла, если русский текст для него недоступен, не знаю. Этот вопрос ещё требует изучения.

ugoday ★★★★★
()
Ответ на: комментарий от monk

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

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

alysnix ★★★
()
Ответ на: комментарий от watchcat382

Ну допустим «принимать функцию» еще куда ни шло. Функция сортировки вон тоже функцию принимает. Хотя на самом деле указатель на функцию. Но «возвращать функцию»? Что это и самое главное зачем?

Про парадокс Блаба слышали? Вот вы сейчас в нём находитесь.

ugoday ★★★★★
()
Ответ на: комментарий от monk

func compose(func f, func g) { … }

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

alysnix ★★★
()
Ответ на: комментарий от watchcat382

Не понимаю почему пропуск нулевых значений это какое-то «сокровенное знание» и «поперек системы».

Потому что у вас у одного объекта man1.penis_size это число типа int, и у другого объекта man2.pensi_size это число типа int. Но в одном случае число — это число и с ним можно делать всё, что делают с числами, а в другом — специальное значение, маркер особой ситуации, которую нужно обрабатывать отдельно. Хотя с точки зрения системы типов — всё это обычные числа.

Так какого именно поведения вы хотите?

  1. Сущности предметной области должны быть однозначно отображаемы системой типов.
  2. Если уж мы решили упарываться по типам, пусть компилятор за меня проверяет, что я передаю правильные значения.
ugoday ★★★★★
()
Ответ на: комментарий от MOPKOBKA

У хаскеля получается такой код:

ecx = i, eax = end, ebx = r

1:	cmp	rcx, rax
	je	end
	inc	rcx
        jmp	3f

2:	add	rbx, rcx
        inc	rcx
3:	test	cl, 1
        jne	1b
        cmp	rcx, rax
        jne	2b
end:

GCC 13 и выше на -O3 умудряется разанроллить на векторах. При -mno-sse этого делать не будет.

https://godbolt.org/z/sovhcdnPn

jpegqs
()
Последнее исправление: jpegqs (всего исправлений: 1)
Ответ на: комментарий от MOPKOBKA

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

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

alysnix ★★★
()
Ответ на: комментарий от kaldeon

Неявная статическая типизация была в Паскале для именованных констант.

vM ★★
()
Ответ на: комментарий от jpegqs

короче хачкел когда делает композицию (f g) просто инлайнит одно тело функции в другое, потом оптимизирует код и получает результат. и никакой магии.

alysnix ★★★
()
Ответ на: комментарий от MOPKOBKA

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

#include <stdio.h>
#include <stdlib.h>
int main(int argc, char **argv) {
  long r = atol(argv[1]) >> 1;
  printf("%ld\n", r < 0 ? 0 : r * r + r);
}
$ cc test.c -Os && time ./a.out 1000000000
250000000500000000

real	0m0.000s
user	0m0.000s
sys	0m0.000s
jpegqs
()
Ответ на: комментарий от Stanson

полная команда ассемблера всегда соответствует конкретной инструкции. И наоборот - инструкция имеет единственное отображение в виде мнемоники с операндами.

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

Дизассемблированный код скормленный ассемблеру всегда даёт тот же самый код. Если это не так, то либо в ассемблере, либо в дизассемблере ошибка.

Если к ассемблеру и дизассемблеру было такое требование.

Разработчики ARM озаботились однозначным дизассемблированием. Для этой платформы есть правила выбора мнемоники псевдоинструкции для дизассемблера вместо мнемоники инструкции общего вида и каноничное кодирование инструкций и псевдоинструкций.

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

vM ★★
()
Ответ на: комментарий от alysnix

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

Да. Если упаковать compose в список, а потом применять оттуда, то гораздо медленнее.

monk ★★★★★
()
Ответ на: комментарий от alysnix

В том-то и дело, что в Хаскеле это рядовая функция. Её можно хранить в переменной или элементом списка.

С другой стороны, вызов функции в Хаскеле не сводится к ассемблерному вызову CALL, там всё чуть сложнее.

monk ★★★★★
()
Ответ на: комментарий от alysnix

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

ugoday ★★★★★
()
Ответ на: комментарий от MOPKOBKA

оператор стрелки в Clojure

стрелка это банальный макрос, который разворачивает (-> 4 int dec Math/sqrt в (Math/srt (dec (int 4))). И в этом смысле он столь же «нативный» как и в форте.

ugoday ★★★★★
()
Ответ на: комментарий от alysnix

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

Вот с этим всё хорошо.

$ cat test2.hs
module Main where

import System.Environment

compose f g x = f (g x)

main = do
  [x] <- getArgs
  let n = read x :: Int
  let f = (== 0)
  let g = (`mod` 2)
  let cond = compose f g
  putStrLn . show $ (compose sum (filter cond)) [1..n]

Скорость та же самая. Хотя compose не библиотечный и все функции-аргументы в переменных.

monk ★★★★★
()
Ответ на: комментарий от alysnix

Именно так. С++ умеет инлайнить вызовы методов, Haskell и Racket умеют инлайнить функциональные цепочки.

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

monk ★★★★★
()
Ответ на: комментарий от alysnix

Если от переменных с известным типом, то замедляться не должен, типы же известны будут. Я уже над этим раздумывал.

MOPKOBKA ★★★★★
()
Последнее исправление: MOPKOBKA (всего исправлений: 1)
Ответ на: комментарий от jpegqs
#!/bin/bash
echo "250000000500000000"

Ага %) Мы тут проверяем как хорошо С++ позволяет записывать алгоритмы определенным образом. Задача ненастоящая.

MOPKOBKA ★★★★★
()
Последнее исправление: MOPKOBKA (всего исправлений: 2)
Ответ на: комментарий от monk

А сломать оптимизатор можно так:

module Main where

import System.Environment

compose f g x = f (g x)
t = [compose, compose]

main = do
  [x1, x2] <- getArgs
  let n = read x1 :: Int
  let nc = read x2 :: Int
  let comp = t !! nc
  let f = (== 0)
  let g = (`mod` 2)
  let cond = comp f g
  putStrLn . show $ (comp sum (filter cond)) [1..n]
$ time ./test2 1000000000
250000000500000000

real    0m1,145s
user    0m1,134s
sys     0m0,010s
$ time ./test3 1000000000 0
250000000500000000

real    0m51,556s
user    0m51,652s
sys     0m0,130s

monk ★★★★★
()
Ответ на: комментарий от MOPKOBKA

Все есть банальный код которые что то там делает.

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

ugoday ★★★★★
()
Ответ на: комментарий от MOPKOBKA

Вашими препроцессорами только детей пугать.

ugoday ★★★★★
()
Закрыто добавление комментариев для недавно зарегистрированных пользователей (со score < 50)