Top.Mail.Ru
Быстрый старт: Введение в Racket с картинками


Nov 3, 2024

Быстрый старт: Введение в Racket с картинками

Мэтью Флатт

Этот учебник предоставляет краткое введение в язык программирования Racket, используя одну из его библиотек для рисования картинок. Даже если вы не планируете использовать Racket для своих художественных проектов, библиотека картинок поддерживает интересные и поучительные примеры. Ведь картинка стоит пятьсот "hello world".


1. Готовы…

Скачайте Racket , установите и запустите DrRacket.


2. Установка…

Ознакомьтесь с документацией DrRacket для краткого обзора IDE DrRacket.

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

#lang slideshow

Затем нажмите кнопку “Выполнить”. Вы увидите, как текстовый курсор переместится в нижнюю текстовую область, которая является областью взаимодействия.

Если вы использовали DrRacket раньше, вам может потребоваться сбросить DrRacket, чтобы использовать язык, объявленный в исходном коде, через пункт меню “Язык Выбрать язык…” перед нажатием кнопки “Выполнить”.

3. Начнем!

Когда вы вводите выражение после символа > в окне взаимодействия и нажимаете Enter, DrRacket оценивает выражение и выводит его результат. Выражение может быть просто значением, таким как число 5 или строка “галерея искусств”:

> 5
5
> "галерея искусств"
"галерея искусств"

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

> (circle 10)

Результат функции circle является значением картинки, которое выводится как результат выражения примерно так же, как числа или строки. Аргумент функции circle определяет размер круга в пикселях. Как вы можете догадаться, существует функция rectangle, которая принимает два аргумента вместо одного:

> (rectangle 10 20)

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

> (circle 10 20)
circle: arity mismatch;
ожидаемое количество аргументов не соответствует заданному
количество
ожидаемое: 1 плюс необязательные аргументы с ключевыми словами
#:border-color и #:border-width
заданное: 2
аргументы...:
10
20

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

В дополнение к базовым конструкторам картинок, таким как circle и rectangle, существует функция hc-append, которая объединяет картинки. Когда вы начинаете составлять вызовы функций в Racket, это выглядит так:

> (hc-append (circle 10) (rectangle 10 20))

Дефис в имени функции hc-append является просто частью идентификатора; это не hc минус append. Имя функции начинается с h, потому что она объединяет картинки горизонтально, и следующая буква c, потому что картинки центрированы вертикально.

Если вы задаетесь вопросом, какие еще функции существуют — возможно, способ стека картинок вертикально и выровненных по левому краю? — переместите текстовый курсор на имя функции hc-append и нажмите клавишу F1 в DrRacket. Откроется окно браузера, и оно даст вам ссылку на документацию для hc-append. Нажмите на ссылку, и вы увидите множество других функций.

Если вы читаете это в формате HTML, вы также можете просто нажать на hc-append или любой другой импортированный идентификатор, используемый в этом учебнике.


4. Определения

Чтобы использовать определенный круг и прямоугольник несколько раз, проще дать им имена. Перейдите обратно в область определений (верхняя область) и добавьте два определения, так что полное содержимое области определений будет выглядеть так:

#lang slideshow
(define c (circle 10))
(define r (rectangle 10 20))

Затем нажмите кнопку “Выполнить” еще раз. Теперь вы можете просто ввести c или r:

> r

> (hc-append c r)

> (hc-append 20 c r c)

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

Мы могли бы оценить формы define для c и r в области взаимодействия вместо области определений. На практике, однако, область определений — это место, где живет ваша программа — это файл, который вы сохраняете, в то время как область взаимодействия предназначена для временных исследований и задач отладки.

Давайте добавим определение функции в программу. Определение функции использует define, как и наши определения форм, но с открытой скобкой перед именем функции и именами аргументов функции перед соответствующей закрывающей скобкой:

(define (square n)
; Точка с запятой начинает комментарий строки.
; Выражение ниже — это тело функции.
(filled-rectangle n n))

Синтаксис определения отражает синтаксис вызова функции:

> (square 10)

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


5. Локальное связывание

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

(define (four p)
  (define two-p (hc-append p p))
  (vc-append two-p two-p))
> (four (circle 10))

image

Более типично, Racketeers используют форму let или let* для локального связывания. Одним из преимуществ let является то, что оно может быть использовано в любой позиции выражения. Кроме того, оно связывает многие идентификаторы одновременно, вместо того, чтобы требовать отдельного define для каждого идентификатора:

(define (checker p1 p2)
  (let ([p12 (hc-append p1 p2)]
        [p21 (hc-append p2 p1)])
    (vc-append p12 p21)))
> (checker (colorize (square 10) "red")
           (colorize (square 10) "black"))

Форма let связывает многие идентификаторы одновременно, поэтому связывания не могут ссылаться друг на друга. Форма let*, напротив, позволяет более поздним связываниям использовать более ранние связывания:

(define (checkerboard p)
  (let* ([rp (colorize p "red")]
         [bp (colorize p "black")]
         [c (checker rp bp)]
         [c4 (four c)])
    (four c4)))
> (checkerboard (square 10))


6. Функции — это значения

Вместо вызова circle как функции попробуйте оценить просто circle как выражение:

circle
#<procedure:circle>

То есть идентификатор circle связан с функцией (также известной как «процедура»), точно так же, как c связан с кругом. В отличие от картинки круга, нет простого способа полностью распечатать функцию, поэтому DrRacket просто распечатывает #procedure:circle.

Этот пример показывает, что функции — это значения, как и числа и картинки (даже если они не распечатываются так же красиво). Поскольку функции — это значения, вы можете определять функции, которые принимают другие функции в качестве аргументов:

(define (series mk)
  (hc-append 4 (mk 5) (mk 10) (mk 20)))
> (series circle)

> (series square)

Когда вызывается функция, которая принимает функцию в качестве аргумента, функция-аргумент часто не нужна где-либо еще. Необходимость записывать функцию с помощью define была бы хлопотной, потому что вам нужно придумать имя и найти место для определения функции. Альтернативой является использование lambda, которое создает анонимную функцию:

> (series (lambda (size) (checkerboard (square size))))

Имена в скобках после lambda — это аргументы функции, а выражение после имен аргументов — это тело функции. Использование слова «lambda» вместо «функция» или «процедура» является частью истории и культуры Racket.

Определение формы define для функции на самом деле является сокращением для простого define, использующего lambda в качестве значения. Например, определение series можно было бы записать как

(define series
  (lambda (mk)
    (hc-append 4 (mk 5) (mk 10) (mk 20))))

Большинство Racketeers предпочитают использовать сокращенную форму функции с define вместо расширения до lambda.


7. Лексическая область видимости

Racket — это язык с лексической областью видимости, что означает, что всякий раз, когда идентификатор используется как выражение, что-то в текстовом окружении выражения определяет связывание идентификатора. Это правило применяется к идентификаторам в теле lambda, а также в любом другом месте.

В следующей функции rgb-series использование mk в каждой форме lambda относится к аргументу rgb-series, поскольку это связывание, которое находится в текстовом окружении:

(define (rgb-series mk)
  (vc-append
   (series (lambda (sz) (colorize (mk sz) "red")))
   (series (lambda (sz) (colorize (mk sz) "green")))
   (series (lambda (sz) (colorize (mk sz) "blue")))))
> (rgb-series circle)

> (rgb-series square)

Вот еще один пример, где rgb-maker принимает функцию и возвращает новую функцию, которая запоминает и использует исходную функцию.

(define (rgb-maker mk)
  (lambda (sz)
    (vc-append (colorize (mk sz) "red")
               (colorize (mk sz) "green")
               (colorize (mk sz) "blue"))))
> (series (rgb-maker circle))

> (series (rgb-maker square))

Обратите внимание, как составление функций с помощью rgb-maker создает разное выравнивание объектов внутри картинки по сравнению с использованием rgb-series.


8. Списки

Racket наследует большую часть своего стиля от языка Lisp, чье название первоначально означало «LISt Processor», и списки остаются важной частью Racket.

Функция list принимает любое количество аргументов и возвращает список, содержащий заданные значения:

> (list "red" "green" "blue")
'("red" "green" "blue")

> (list (circle 10) (square 10))

Как вы видите, список распечатывается как одинарная кавычка и пара скобок, обрамляющих распечатанную форму элементов списка. Здесь есть место для путаницы, поскольку скобки используются как для выражений, таких как (circle 10), так и для распечатанных результатов, таких как ‘(“red” “green” “blue”). Кавычка — это ключевое различие, как обсуждается в другом месте. Чтобы подчеркнуть различие, в документации и в DrRacket, результаты скобок распечатываются синим цветом, в отличие от скобок выражений.

Если у вас есть список, то вы, скорее всего, захотите сделать что-то с каждым из элементов. Функция map принимает список и функцию, которую нужно применить к каждому элементу списка; она возвращает новый список, объединяющий результаты функции:

(define (rainbow p)
  (map (lambda (color)
         (colorize p color))
       (list "red" "orange" "yellow" "green" "blue" "purple")))

1> (rainbow (square 5))

(image image image image image image)

Другая функция, которая работает со списками, — это apply. Как и map, она принимает функцию и список, но функция, переданная в apply, должна принимать все аргументы одновременно, вместо того, чтобы принимать каждый из них индивидуально. Функция apply особенно полезна с функциями, которые принимают любое количество аргументов, таких как vc-append:

> (apply vc-append (rainbow (square 5)))

Обратите внимание, что (vc-append (rainbow (square 5))) не будет работать, потому что vc-append не хочет списка в качестве аргумента; он хочет картинку в качестве аргумента и готов принять любое количество из них. Функция apply соединяет разрыв между функцией, которая хочет многих аргументов, и списком этих аргументов как единого значения.


9. Модули

Поскольку ваша программа в окне определений начинается с

#lang slideshow

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

Чтобы импортировать дополнительные библиотеки, используйте форму require. Например, библиотека pict/flash предоставляет функцию filled-flash:

(require pict/flash)
> (filled-flash 40 30)

Модули именуются и распространяются различными способами:

Некоторые модули упакованы в дистрибутив Racket или установлены в иерархию коллекций. Например, имя модуля pict/flash означает «модуль, реализованный в файле “flash.rkt”, который находится в коллекции “pict”». Когда имя модуля не содержит косой черты, оно относится к файлу “main.rkt”.

Некоторые коллекции модулей распространяются как пакеты. Пакеты можно устанавливать с помощью пункта меню Install Package… в меню File DrRacket или с помощью инструмента командной строки raco pkg. Например, установка пакета “avl” делает доступным модуль avl.

Пакеты можно зарегистрировать на сайте https://pkgs.racket-lang.org/ или установить напрямую из репозитория Git, веб-сайта, файла или каталога. См. Управление пакетами в Racket для получения дополнительной информации о пакетах.

Чтобы сохранить ваши определения, используйте пункт меню Save Definitions DrRacket.

Некоторые модули живут относительно других модулей, не обязательно принадлежа к какой-либо конкретной коллекции или пакету. Например, в DrRacket, если вы сохраните свои определения до сих пор в файле “quick.rkt” и добавите строку

(provide rainbow square)

тогда вы можете открыть новую вкладку или окно в DrRacket, ввести новую программу “use.rkt” в том же каталоге, что и “quick.rkt”:

#lang racket
(require "quick.rkt")
(rainbow (square 5))

и когда вы запустите “use.rkt”, список радужных квадратов будет выводом. Обратите внимание, что “use.rkt” написан с использованием начального импорта racket, который сам по себе не предоставляет никаких функций создания картинок, но предоставляет require и синтаксис вызова функции.

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


10. Макросы

Вот еще одна библиотека, которую стоит попробовать:

(require slideshow/code)
1> (code (circle 10))

Вместо круга результатом является картинка кода, который, если бы он использовался как выражение, создал бы круг. Другими словами, code — это не функция, а новая синтаксическая форма для создания картинок; часть между открывающей скобкой с кодом не является выражением, а вместо этого манипулируется синтаксической формой кода.

Это помогает объяснить, что мы имели в виду в предыдущем разделе, когда говорили, что racket предоставляет require и синтаксис вызова функции. Библиотеки не ограничены экспортом значений, таких как функции; они также могут определять новые синтаксические формы. В этом смысле Racket не является языком вообще; это скорее идея о том, как структурировать язык, чтобы вы могли расширить его или создать совершенно новые языки.

Один из способов введения новой синтаксической формы — это использование define-syntax с syntax-rules:

(define-syntax pict+code
  (syntax-rules ()
    [(pict+code expr)
     (hc-append 10
                expr
                (code expr))]))
> (pict+code (circle 10))

Такого рода определение является макросом. Часть (pict+code expr) — это шаблон для использования макроса; экземпляры шаблона в программе заменяются экземплярами соответствующего шаблона, который является (hc-append 10 expr (code expr)). В частности, (pict+code (circle 10)) соответствует шаблону с (circle 10) в качестве expr, поэтому он заменяется на (hc-append 10 (circle 10) (code (circle 10))).

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

На самом деле, вы можете захотеть взглянуть на исходный код этого документа. Вы увидите, что он начинается с #lang, но в остальном не очень похож на Racket; тем не менее, мы создаем этот документ, запуская его исходный код как программу Racket. Нам приходится использовать гораздо больше, чем просто syntax-rules, чтобы расширить синтаксис Racket достаточно для написания документов, но синтаксическое расширение Racket может увести вас далеко.


11. Объекты

Система объектов — это еще один пример сложного расширения языка, которое стоит изучить и использовать для пользователей Racket. Объекты иногда лучше, чем функции, даже когда у вас есть lambda, и объекты особенно хорошо подходят для графических интерфейсов пользователя. API для GUI и графической системы Racket выражается в терминах объектов и классов.

Сама система классов реализована библиотекой racket/class, а библиотека racket/gui/base предоставляет классы GUI и рисования. По соглашению, классам присваиваются имена, которые заканчиваются на %:

(require racket/class
         racket/gui/base)
(define f (new frame% [label "Мое искусство"]
                      [width 300]
                      [height 300]
                      [alignment '(center center)]))
> (send f show #t)

Форма new создает экземпляр класса, где аргументы инициализации, такие как label и width, предоставляются по имени. Форма send вызывает метод объекта, такой как show, с аргументами после имени метода; аргумент #t в данном случае является логической константой «истина».

Картинки, сгенерированные с помощью slideshow, инкапсулируют функцию, которая использует команды рисования графического ящика для отображения картинки в контексте рисования, таком как холст в рамке. Функция make-pict-drawer из slideshow предоставляет функцию рисования картинки. Мы можем использовать make-pict-drawer в обратном вызове рисования холста, чтобы нарисовать картинку на холсте:

(define (add-drawing p)
  (let ([drawer (make-pict-drawer p)])
    (new canvas% [parent f]
                 [style '(border)]
                 [paint-callback (lambda (self dc)
                                   (drawer dc 0 0))])))
> (add-drawing (pict+code (circle 10)))
#(struct:object:canvas% ...)

> (add-drawing (colorize (filled-flash 50 30) "yellow"))
#(struct:object:canvas% ...)

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


12. Куда идти дальше

Этот вводный курс в Racket намеренно избегает многих традиционных способов представления и различия Lisp или Scheme: префиксной арифметической записи, символов, цитирования и квазицитирования списков, eval, первоклассных продолжений и идеи, что вся синтаксис на самом деле просто lambda в маскировке. Хотя все это является частью Racket, они не являются основными ингредиентами повседневной программирования в Racket.

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

Если вы новичок в программировании или если у вас есть терпение работать с учебником, мы рекомендуем прочитать «Как проектировать программы». Если вы уже прочитали его или если вы хотите увидеть, куда вас приведет книга, то перейдите к «Продолжить: веб-приложения в Racket».

Для опытных программистов, чтобы продолжить знакомство с Racket с системной точки зрения, а не с картинками, ваша следующая остановка — «Больше: системное программирование с Racket».

Чтобы начать изучать полный язык и инструменты Racket в глубину, перейдите к «Руководству по Racket».