Введение

Язык Scheme (произносится "ским"), которому в следующем году исполняется 50 лет, является языком программирования, занимающим необычное место среди прочих. Это язык, который гораздо больше изучают, чем потом на нём пишут. Скорее это язык для развития ума программиста, чем для написания коммерческого кода, хотя и примеры использования Scheme в коммерческой разработке тоже встречаются. На мой личный взгляд, Scheme идеален в качестве первого языка программирования в старшем школьном и институтском возрасте, а также идеально продолжает изучение Scratch в младших классах школы и Logo в средних классах.

Достоинства Scheme:

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

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

– прекрасный учебник Массачусетского технологического института по конструированию программ "Структура и интерпретация компьютерных программ" SICP, переведённый на русский язык, использующий язык Scheme;

– интерактивная среда разработки и динамическая структура языка, позволяющие не забивать голову программиста лишними проблемами и наглядно видеть результаты каждого шага программирования;

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

Недостатки Scheme:

– язык редко используется в коммерческой разработке и имеет мало прикладных библиотек и фреймворков;

– к синтаксису языка с большим количеством скобочек нужно привыкнуть;

– программы в обычном стиле Scheme имеют огромную плотность смыслового содержания по отношению к длине их текста, в связи с чем их чтение человеком затруднено (а оплата за строки кода невыгодна);

– относительно небольшое сообщество.

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

Что такое Scheme?

Scheme – это диалект языка Lisp, имеющий некоторые глубокие, но не очень многочисленные отличия и потому выделенный в отдельный язык. Если вы понимаете Lisp, то без труда поймёте и Scheme, и наоборот.

Стандарт языка Scheme регламентируется документами, носящими в лучших традициях префиксной функциональной записи названия Revised Revised ... Revised Report on the Algorithmic Language Scheme, или просто R*RS. На сегодняшний день основные реализации поддерживают стандарты от R5RS до R7RS. Документ R7RS имеет объём 88 страниц, в которых полностью описаны синтаксис и семантика языка, начиная от формального математического определения семантики в лямбда-исчислении.

Также важной частью фактически используемого языка Scheme являются Scheme Requests for Implementation, или SRFI, представляющие собой предлагаемые сообществом дополнения к стандарту. Все SRFI пронумерованы, в настоящий момент их предложено ровно 255 штук. Реализации Scheme включают тот или иной набор SRFI “из коробки”, интегрированными во входной язык. Но это не так важно, так как особенностью Scheme является расширяемость входного языка его же средствами, поэтому большинство SRFI представлены макрокомандами, которые можно использовать в своих программах независимо от их наличия в дистрибутиве транслятора.

Реализаций языка Scheme очень много, многие из них входят в стандартные репозитории Linux. Заслуживают упоминания реализации MIT Scheme (она используется в SICP) и Racket (удобная графическая пользовательская среда). Сам автор пользуется реализацией Gambit Scheme, так как её интерпретатор и компилятор имеют легко собираемый исходный код без внешних зависимостей, а также в полной мере поддерживают национальные языки в идентификаторах (в Linux для этого может потребоваться пересборка). Последнее важно для создания на базе Scheme пользовательских предметно-ориентированных языков. Gambit Scheme (пока в экспериментальном режиме) поддерживает подключение и вызов библиотек для языка Python.

Наконец, необходимо отметить, что написание программ на Scheme (и других диалектах Лиспа) рано или поздно приведёт вас к редактору emacs.

Далее приведём несколько элементарных примеров кода на Scheme, ни в коем случае не имея в виду соревноваться с SICP.

Напишем программу Hello world

"Hello world!"

"Hello world!"

Вычислим 1/3 + 1/7

(+ 1/3 1/7)

10/21

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

Вычислим 100!

(letrec 
 ((fact (lambda (n f) 
                (if (zero? n) 
                    f 
                    (fact (- n 1) (* f n))))))
 (fact 100 1))

93326215443944152681699238856266700490715968264381621468592963895217599993229915608941463976156518286253697920827223758251185210916864000000000000000000000000

Фактически единственным способом организации повторяющихся вычислений в Scheme, как и в лямбда-исчислении, является рекурсия. Стандарт Scheme гарантирует оптимизацию хвостовой рекурсии. Имеется в языке и свой цикл do, но он является макросом, реализуемым через хвостовую рекурсию.

Отсортируем строки в текстовом файле

(list-sort 
 string<=? 
 (call-with-input-file 
  "myfile.txt" 
  (lambda (p) (read-all p read-line))))

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

Напишем функцию, которая из списка чего попало делает строку

(define (anylist->string args)
  (string-concatenate 
   (map (lambda (x) 
                (if (string? x) 
                    x 
                    (object->string x)))
        args)
   " "))
(anylist->string '(1 2 "Hello" 3. #t))

"1 2 Hello 3. #t"

Вызовем ту же самую функцию в отдельной нитке

(thread 
  (lambda ()
    (display 
      (anylist->string '(1 2 "Hello" 3. #t)))))

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

Напечатаем значение, которое получило имя нашей функции

anylist->string

#<procedure #2 anylist->string>

(видно, что это какой-то код).

Напечатаем покрасивее

(pp anylist->string)

(lambda (args)

  (string-concatenate (map (lambda (x) (if (string? x) x (object->string x))) args) " "))

(а здесь уже видно конкретное функциональное значение).

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

(eval (list '+ 2 2))

4

(в целом, если в вашей программе осмысленно используются вызовы eval (интерпретация данных как кода), то скорее всего за Scheme или Lisp вы взялись не зря).

Реализуем в языке оператор (если ... то ... иначе ...) и значения "истина" и "ложь":

(define-syntax если
  (syntax-rules (то иначе)
    ((если p то x иначе y) (if p (begin x) (begin y)))
    ((если p то x) (when p x))))

(define истина #t)
(define ложь #f)
(если истина то "Привет")

"Привет"

Если вас заинтересует язык Scheme, то надо просто брать SICP и читать. На 6-й странице вы научитесь вычислять арифметические выражения, на 129-й – программировать символьное дифференцирование, на 329-й – писать интерпретатор, а на 524-й – писать компилятор.

Комментарии (8)


  1. nick0x01
    06.11.2024 14:00

    SICP может быть несколько сложным для начала. Можно начать с "How to Design Programs, second edition: An Introduction to Programming and Computing", в русском переводе: "Как проектировать программы. Введение в программирование и компьютерные вычисления".


  1. ednersky
    06.11.2024 14:00

    спасибо за ссылку на русскоязычную книжку

    PS: а каждый язык силён прежде всего инфраструктурой включения наработок на нём в CI/CD.

    То есть первое, куда стоит смотреть, это "как на этом языке писать интеграционные и юниттесты", ну и как со всем этим вылезать в прод.

    А как на схеме/лисп принято код тестировать?


    1. vadimr Автор
      06.11.2024 14:00

      С точки зрения CI/CD интерпретируемый язык вроде Lisp или Scheme очень удобен, потому что можно менять код программы прямо по ходу её выполнения, не перезапуская её. А функциональный стиль подразумевает, что каждая отдельная функция – сама себе юнит, который можно тестировать, причём в нормальном случае она не имеет побочных эффектов. Поэтому сама постановка вопроса об отдельных юнит-тестах скорее является артефактом императивных языков.

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

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


      1. ednersky
        06.11.2024 14:00

        я так и не понял: так таки да или так таки нет?


        1. vadimr Автор
          06.11.2024 14:00

          Что да или нет?

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

          Если программировать на Scheme символьный ИИ, что на каком-нибудь C++ вообще малореально, то тестирование затруднено, потому что сама фишка любого ИИ в том, что его невозможно покрыть тестами.

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


    1. Autochthon
      06.11.2024 14:00

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


      1. smt_one
        06.11.2024 14:00

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


      1. ednersky
        06.11.2024 14:00

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

        Вот, например я беру в руки Rust ну или JS. Задаю вопрос:

        • как мне создать проект? сразу получаю документ о cargo или npm

        • как мне тестировать проект? получаю следующий документ

        • где мне искать написанные кем-то библиотеки? получаю третий документ

        и так далее и тому подобное.

        Rust силён не столько системой управления памятью, сколько https://crates.io/.

        Как-то так.

        Герои требуются там, где всей этой инфраструктуры нет.