О чем это

Некоторое время назад меня попросили обосновать, почему я за Pascal при обучении программирования. А я скорее не конкретно за Pascal, я за наличие у языка для обучения программированию ряда важных свойств. Получается, я скорее против Python (шутка). В результате получилась эта мини-статья.

Важные для обучения свойства языка программирования:

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

1. Должен быть явный компилятор

  • Сразу видно, когда происходит трансляция (что для интерпретатора часто скрыто), это важно для понимания работы инструментов программиста.

  • Можно показать различие ошибок на этапе компиляции и ошибок времени исполнения.

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

  • Слабое соображение, но всё-таки, скомпилированная программа, как правило, заметно быстрее выполняется. Это и само по себе интересно показать, и бывает важно на олимпиадах, экзаменах и т.п.

2. Должно быть явное объявление переменных и статическая типизация

  • Тип данных — важная концепция, и продемонстрировать её гораздо проще, когда тип явно указывается для переменной.

  • Раздел объявления переменных позволяет проиллюстрировать рассказ о сегменте данных программы и механизмах управления памятью.

  • Добавляет контроля на этапе компиляции, позволяя раньше и точнее обнаружить ошибку.

  • Повышает дисциплину ума при программировании, формирует хороший стиль.

  • Следует избегать языков с развитым автоматическим преобразованием типа данных, чем автопреобразование меньше, тем лучше. Автопреобразование усложняет понимание программы, на некоторых языках есть даже жанр основанных на этом загадок ("WAT"). Также автопреобразование смазывает само понятие типа данных.

3. В синтаксисе языка явное и заметное предпочтительнее скрытого

  • Значимость непечатных символов должна быть минимальна. Этот принцип можно сформулировать так: на месте любого количества любых невидимых символов можно поставить один пробел, и смысл не изменится. Пробельные символы – не отображаются, на глаз труднее отличить, сколько их идет подряд. Интуитивно трудно принять, что количество пробелов имеет значение. И всё это приводит к неприятным и трудно обнаружимым ошибкам.

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

  • Обязательное явное указание чего-то часто лучше, чем умолчание. Тут лучше пояснить на примерах, которые будут дальше.

4. Если в языке прямо реализованы какие-то методические идеи, это хорошо

Если язык программирования разрабатывался именно для обучения (или эта цель хотя бы принималась во внимание), автор мог сделать что-то в языке специально, чтобы научить чему-то конкретному. Далее рассмотрим конкретные примеры такого.

5. Есть ещё много соображений для выбора языка

  • Важна популярность языка, как в индустрии, так и в образовании.

  • Хорошо, когда для языка есть развитая инструментальная база.

  • Для разнообразия творческих задач может быть важно, что на этом языке есть много готовых библиотек для решения разных задач в различных областях.

  • Для обучения, особенно первого изучаемого языка, важен "порог входа" – сколько всего нужно узнать, прежде чем сможешь написать свою первую "настоящую" программу (нетривиальную, не "Hello world"). Имеется в виду "вход в программирование" – минимальная подготовка для самостоятельного движения дальше.

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

Примеры реальных языков программирования

Рассмотрим какие-то из описанных свойств на конкретных примерах языков.

Pascal vs Python

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

Трансляция

Pascal – компилируемый. Компилятор выдаёт понятные сообщения об ошибках, удобно встроен в среды разработки. Компиляция происходит в машинный код, программа выполняется максимально быстро и с минимальным расходом памяти.

Python – интерпретатор. Хотя у Python есть "фаза анализа", всё сказанное про смазанность этапа трансляции и диагностику ошибок только во время выполнения к нему вполне относится. Отчасти эти неприятности могут быть скомпенсированы хорошей средой разработки со статическим анализатором кода (типа PyCharm). Работает программа на Python обычно сравнительно медленнее и прожорливее по памяти, чем на других языках (трудно найти язык ещё медленнее). Для повышения быстродействия есть варианты (CPython, PyPy, Jython), но чаще решение находится в использовании внешних библиотек, написанных на C++.

Объявление и типы переменных

Pascal – требует объявления переменных и параметров, проверяет типы на этапе компиляции, чётко разделяет области видимости. Автоматическое преобразование типов ограничено числовыми типами, хорошо описано и мотивировано.

Python – отдельных объявлений переменных нет, они создаются динамически, при первом использовании (например, если ошиблись в имени переменной, будет просто создана новая). Есть глобальная область видимости. Тип переменной не фиксирован, есть по сути только тип значения. Есть объявление параметров функций, есть аннотации для указания типа, но в комплексе это мало меняет ситуацию. Преобразования типов часто явные (функции int()str()), также есть автоматическое преобразование числовых типов, но их правила сложнее и запутаннее, чем в Pascal, особенно при участии операторов сравнения. Также от числовых (и некоторых структурных) типов в Python не отделён логический.

Покажем пару примеров для иллюстрации. Сначала Python:

x: int = 1
b: bool = True
# Типы тут - только аннотации и не обязательны

print(b + x)
# Выводит "2", никаких сообщений

b = 1
# Выполняется, в PyCharm предупреждение: Expected type 'bool', got 'int' instead
print(b)
# Выводит "1"

b = b + 2
# Выполняется, никаких ошибок или предупреждений

print(False ** False == True)
# Выводит "True", WAT?

x = (1 << 53) + 1
print(x + 1.0 < x)
# Выводит "True", WAT?

И это я ещё не стал приводить примеры с использованием структурных типов данных.

Теперь посмотрим, как те же примеры (или похожие, если точной аналогии нет) выглядят в Pascal:

var
  x: Integer = 1;
  b: Boolean = True;
begin
  WriteLn(b + x);
  {Ошибка компиляции: Операция '+' не применима к типам boolean и integer}
  
  b := 1;
  {Ошибка компиляции: Нельзя преобразовать тип integer к boolean}
  
  b := b + 2;
  {Ошибка компиляции: Операция '+' не применима к типам boolean и integer}
  
  WriteLn(exp(ord(False)) = ord(True));
  {Выводит "True",
   не совсем то, что в Python (тут нет целочисленного возведения в степень),
   но в целом можно понять, что происходит}

  var x: BigInteger;
  {Ошибка компиляции: Внутриблочные переменные не могут иметь те же имена,
   что и переменные из блока верхнего уровня}

  var y: BigInteger = BigInteger(exp(ln(2)*54)) + 1;
  WriteLn(BigInteger(Real(y) + 1.0) < y);
  {Выводит "True",
   здесь мы обязаны явно использовать тип для длинной арифметики,
   и явное преобразование, чтобы сложить с вещественным числом.
   Можно выводить результаты выражений по очереди, и понять,
   что дело в ограничении точности вещественных чисел}
end.

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

Явное или скрытое

Тут в первую очередь приходят на ум значимые отступы на Python. Также можно привести в пример явное обозначение первой ветки условного оператора на Pascal. Рассмотрим код сортировки пузырьком на Python (честно украденный где-то в Интернете):

from random import randint


def bubble(array):
  n = len(a)
  i = 0
  while i < n-1:
    j = 0
    while j < n-1-i:
      if a[j] > a[j+1]:
        a[j], a[j+1] = a[j+1], a[j]
      j += 1
    i += 1


a = [randint(0, 99) for n in range(10)]
bubble(a)
print(a)

И аккуратно переведём его на Pascal (для наглядности максимально сохраняя соответствие строк):

procedure bubble(a: array of Integer);
begin
  var n := Length(a);
  var i := 0;
  while i < n-1 do
  begin
    var j := 0;
    while j < n-1-i do
    begin
      if a[j] > a[j+1] then
      begin
        var tmp := a[j];
        a[j] := a[j+1];
        a[j+1] := tmp;
      end;
      j := j + 1;
    end;
    i := i + 1;
  end;
end;

const N = 10;
var a: array of Integer;

begin
  a := new integer[N];
  randomize;
  for var i := 0 to N-1 do
  begin
    a[i] := random(100);
  end;
  
  bubble(a);
  
  for var i := 0 to N-1 do
  begin
    write (a[i]:3);
  end;
end.

В примерах специально сделаны одинаковые (и маленькие, всего два пробела) отступы, чтобы подчеркнуть эффект от явных операторных скобок. Большие отступы и подсветка в IDE конечно улучшают ситуацию. В целом на примерах видны и преимущества и недостатки обоих языков. Явность и четкость Pascal оборачивается многословностью. А краткость и «сахарность» Python (вы только посмотрите на кортежное присваивание, или инициализацию списка выражением) — незаметностью ошибок. Даже в приведённом примере на Python есть грубая ошибка (если не заметили, вместо формального параметра процедура сортировки обращается к глобальной переменной массива, и возможность такого — большая беда Python). Pascal, правда, подтягивается по части "сахара". Например, в современных версиях можно объявлять переменные внутри блока (для счётчика цикла это очень уместно), и не указывать тип в случае, когда компилятор может его однозначно определить сам (переменная в результате всё рано имеет строгий тип).

Для подготовки примеров кода использованы:

  1. IntelliJ IDEA 2024.1.2 (Community Edition) + Python plug-in Community Edition + Python 3.12 SDK

  2. PascalABC.NET 3.11, язык Русский.

Пара слов про методические идеи

Часто о Pascal говорят, что он специально придуман для обучения. Это действительно так, Никлаус Вирт, будучи профессором, одной из целей имел обучение студентов структурному программированию. Но хочется показать хотя бы один пример конкретной языковой конструкции, в которой воплотился хоть какой-то конкретный методический приём. И такой пример у меня есть:

type
   TFigure = (RECT, CIRCLE);
   TFigureParams = record
       case figure: TFigure of
          RECT: (a, b: Real);
          CIRCLE: (r: Real);
   end;

function area(f: TFigureParams): Real;
begin
    case f.figure of
        RECT: area := f.a * f.b;
        CIRCLE: area := Pi * f.r * f.r;
    end;
end;

var f: TFigureParams;
begin
    f.figure := CIRCLE;
    f.r := 10;
    writeLn(area(f));
    
    f.figure := RECT;
    f.a := 2;
    f.b := 3;
    writeLn(area(f));
end.

Это пример использования записей с вариантами (помните таких?). Видите, как похоже описание типа данных и код его обработки? Это и есть методическая идея Вирта: аналогия между алгоритмическими конструкциями и структурами данных. Массив — цикл со счетчиком, запись с вариантами — оператор выбора... Если честно, это всё, третьего примера у меня нет.

Насколько это полезно сегодня, можно понять по тому, что в PascalABC записи с вариантами вообще не поддерживаются, для компиляции пришлось использовать Free Pascal Compiler (version 3.2.2). В проекте PascalABC на Github можно найти ответ на вопрос "Does PascalABC.NET support Delphi variant records?": "Generally, you should use OOP things for this, like abstract classes and interfaces.", и тут трудно спорить.

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

А что же у Python? Как ни удивительно, тут тоже есть что назвать. И это странно, но назвать можно те же самые значимые отступы, которые мы ругали в предыдущем разделе. Одной из мотиваций для этого решения у Гвидо ван Россума (автора Python) было обучить программистов правильно форматировать код, соблюдая правило отступов. Правило отступов и в других языках (и в Pascal) является настоятельной рекомендацией, а Ван Россум сделал его обязательным (и для этого — единственным способом выделения блоков в программе).

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

Популярность, инструменты, библиотеки

Если в предыдущих номинациях я бы отдал победу Pascal, то по части популярности и использования в индустрии Python уверенно лидирует. Широта областей применения Python и разнообразие библиотек не то что по сравнению с Pascal, а в общем зачете языков программирования претендует на первое место. Не буду углубляться в эту тему, такие обзоры легко найти, в целом почти все сферы применения, от машинного обучения до разработки игр. Ну и как следствие — огромное количество информации, включая многочисленные и разнообразные учебные материалы, активное сообщество (или даже насколько, в разных областях), учебные курсы, стажировки, предложения работы и т.д. Инструментарий тоже крайне развит и разнообразен, под любые потребности, от онлайн блокнотов (Jupyter Notebook / Google Colab) до промышленных IDE c элементами ИИ (IntelliJ PyCharm).

У Pascal же всё это поскромнее (мягко говоря). Востребованность в индустрии катастрофически упала, и продолжает снижаться (хотя отдельные вакансии на Delphi ещё встречаются, но редко и за заметно меньшие деньги). Область применения и активность сообщества сосредоточена в основном в сфере образования. Учебной и методической информации тоже много. Интересно отметить, что учебные материалы по Pascal отличаются немного другой направленностью, больше академичностью что ли, от материалов по Python, но и тех и других так много, что найти можно под любой вкус. Инструменты есть, и вроде их достаточно, но даже отличный PascalABC всё-таки до продуктов IntelliJ не дотягивает (хотя зато шустрее и менее прожорливый).

А есть другие варианты?

На Pascal и Python предложения не заканчиваются. Для экономии времени мысленно оценим все языки программирования (какие вспомним) по двум условным шкалам: популярность/"лёгкость" и "мощность" (оба термина надо бы пояснить, но это долго, и для быстрой оценки хватит интуитивного понимания). Интересно, что они, как правило, антагонистичны (либо лёгкий, либо мощный). И с этими оценками обычно сильно коррелирует порог вхождения в язык (начать в лёгком — легко, а в мощном — трудно). Синтаксис лёгких языков часто адаптивный, "толерантный", а у мощных — более строгий (но это не точно). Также с этой дихотомией ассоциируется отчасти высокоуровневые / низкоуровневые языки. Мощные языки обычно ближе к машинной реализации вычислений (что и является источником их мощи), а лёгкие языки оперируют более близкой к человеческому мышлению абстракцией.

С этой точки зрения Python будет более лёгкий (ниже порог вхождения, больше подробностей скрыто за абстракцией, больше толерантности), а Pascal — более мощный (машинный код ближе, порог вхождения выше, больше строгости). Большинство других языков, на которых обучают (и самообучаются) программированию, оказываются либо ещё легче (и разболтаннее), чем Python (JavaScript, PHP, 1С), либо ещё мощнее (и труднее), чем Pascal (С++, C#, Java).

Но есть язык программирования, который на мой взгляд оказывается между нашими реперами. Он легче и популярнее, чем Pascal, и при этом строже и мощнее, чем Python. И Именно он будет неожиданной и свежей идеей для этой статьи. Это Kotlin (или Котлин, это вообще-то русское слово, это название острова, на котором находится город Кронштадт). Про Kotlin я могу рассказывать очень долго (это мой любимый язык программирования), но это сильно выйдет за рамки текущей темы. Так что сразу приведу резюме (похожее на рекламный слоган, но что поделать):

  • Если у вас сложилось обоснованное, "выстраданное" мнение, как что-то в языке должно быть сделано — скорее всего, в Kotlin это сделано именно так.

  • Если вы страдали от отсутствия какой-то возможности в языке программирования — скорее всего, в Kotlin эта возможность есть.

Ну и давайте попробуем перевести наш тестовый код, которым мы уже испытали Python и Pascal:

import kotlin.math.pow

fun Boolean.toInt(): Int = if (this) 1 else 0

fun main() {
    var x: Int = 1
    var b: Boolean = true
    // Типы обязательны и фиксированы, даже если не указаны явно

    println(b + x)
    // Ошибка компиляции: Unresolved reference.
    //   None of the following candidates is applicable because of receiver type mismatch:
    //   public inline operator fun BigDecimal.plus(other: BigDecimal): BigDecimal ...
    // Довольно многословно, но объясняет,
    // что оператора plus для сложения Boolean и Int нет

    b = 1
    // Ошибка компиляции:
    // The integer literal does not conform to the expected type Boolean

    b = b + 2
    // Ошибка компиляции: Unresolved reference...
    // Снова долго, но смысл тот же, подходящего оператора plus нет

    println(false.toInt().toFloat().pow(false.toInt()).toInt() == true.toInt())
    // Выводит "true",
    // пришлось написать функцию Boolean.toInt, готовой нет
    // (потому что она никому на самом деле не нужна).
    // Но это было просто, и с ней удалось в точности воспроизвести пример на Python.
    // Выглядит запутанно (и так и есть), но если внимательно читать, то понятно,
    // что же там происходит, и почему ответ именно такой.

    var x: BigInteger
    // Ошибка компиляции: Conflicting declarations

    var y = 2.toBigInteger().pow(54) + 1.toBigInteger()
    println((y.toFloat() + 1.0).toBigDecimal() < y.toBigDecimal())
    // Выводит "True", все преобразования выполнены явно
}

Всё получилось, как и у Pascal, правда текст сообщений об ошибках подкачал. Да и в целом наверно чувствуется, что технология скорее с расчетом на профессионалов, а не на студентов.

Что ж, давайте и сортировку попробуем перевести:

import kotlin.random.Random

fun bubble(a: Array<Int>) {
  val n = a.size
  var i = 0
  while (i < n - 1) {
    var j = 0
    while (j < n - 1 - i) {
      if (a[j] > a[j + 1]) {
        Pair(a[j + 1], a[j]).apply {
          a[j] = first
          a[j + 1] = second
        }
      }
      j += 1
    }
    i += 1
  }
}

fun main() {
  val a = Array<Int>(10) { Random.nextInt(99) }
  bubble(a)
  println(a.contentToString())
}

Как и обещано, мощность и строгость Pascal вместе с лаконичностью и богатством Python. Даже обмен значением сделал как в Python, через построение и деструкцию пары (только здесь это явно описано в коде). И массив инициализируется через выражение, только интуитивно понятно, где тут размер массива, а где код инициализации значений.

Что же до популярности, областей применения, сообщества, библиотек и инструментов, всё это примерно как у Java (ближайшего родственника кстати), только ещё и продолжает расти.

Выводы

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