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))
Более типично, 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))
( )
Другая функция, которая работает со списками, — это 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».