Как я уже упоминал в своем комментарии, этот вопрос состоит из двух частей:
- во-первых, как вы структурируете программы с использованием компонентов графического интерфейса или вообще;
- во-вторых, как Ракет может помочь вам с первым.
Я думаю, что первая часть - огромная тема: об этом написаны книги и проведены курсы. Я определенно не собираюсь отвечать на эту часть.
Вторая часть, ну, я тоже не совсем отвечаю на это вообще, так как я не очень специалист по ракеткам. Но, как никто другой, возможно, я могу помочь в простых случаях (и я думаю, что большинство случаев оказываются простыми).
Предупреждение: далее следует , безусловно, упрощенно и почти наверняка содержит ошибки. Я бы приветствовал исправление от любого, кто знает систему модулей Racket лучше меня. Он также слишком долго ждал ответа, который должен был быть простым: извините.
Модули в Racket
Основное, что нужно понять, это то, что все, что вы пишете в Racket, является частью определениямодуль, (в целом #lang ...
вещь является синтаксическим сахаром для определения модуля).
Модули решают, какие имена (?) они хотят экспортировать в другие модули, и некоторые вещи о том, как онихотите экспортировать их, а также какие другие модули они используют и некоторые сведения о том, как они на них полагаются.
Модули могут быть вложены в другие модули, в том числе в тот же файл. Но простой случай, когда каждый файл содержит один модуль, и это все, с чем я даже попытаюсь разобраться.
Модули введены здесь , а справочное руководство - здесь.
Определение имен: provide
Это делается с помощью provide
. Это немного синтаксиса, который говорит, какие имена экспортируются из модуля: все другие определения в модуле являются частными.
Существует много сложностей с provide
, но, скажем, у меня был какой-то файл, который определяеткакое-то понятие «делать что-то с фу». Он хочет определить функцию call-with-foo
и макрос with-foo
, которые связаны друг с другом обычным образом. Но в файле есть куча других вещей, связанных с реализацией foo
s, которые являются частными. Так что мой файл "foo.rkt"
может выглядеть так:
#lang racket
(provide call-with-foo
with-foo)
(define (call-with-foo fn)
...)
(define-syntax-rule (with-foo (f) form ...)
(call-with-foo (λ (f) form ...)))
(define (make-foo ...)
...)
(define (validate-foo foo)
...)
...
Так что это означает, что любой модуль, который хочет использовать этот, только увидит call-with-foo
и with-foo
: все остальные определенияinternal.
provide
может сделать гораздо больше, чем это: например, он может переименовывать определения по мере их экспорта. Это полезно, если вы переопределяете основные части языка. Например, если бы я определял язык немного похожий на Racket, но там, где define
был другим, я мог бы написать:
#lang racket
(provide (rename-out [new-define define]))
(define-syntax new-define
...)
;;; This is Racket's define, not ours
;;;
(define ...)
И вы можете сказать такие вещи, как «экспортировать все» (all-defined-out
),или «экспортировать все, кроме ...» (except-out
) и т. д. и т. д. Есть много вещей, которые вы можете сделать.
Указание модулей, от которых вы зависите
Таким образом, существует два способа, которыми модуль получает возможность импортировать имена из других модулей.
во-первых, через #lang ...
: что-то вроде
#lang racket
...
, я думаю, совпадает с
(module <name> racket ...)
Где <name>
происходит от имени файла, и это означает «запуск»используя все имя, которое экспортирует модуль racket
(но не забудьте переопределить их) '. Я думаю, что здесь есть нечто большее, потому что вы также можете переопределить основные аспекты синтаксиса остальной части файла здесь. В любом случае #lang
сообщает модулю, откуда он должен начинаться.
Другой способ - require
. Это даже более опасно, чем provide
, потому что он не только должен иметь возможность указывать такие вещи, как «Мне нужны только некоторые вещи из этого модуля» и «Мне нужны вещи из этого модуля под некоторыми другими именами», это также должен иметь возможность указать, что означает «этот модуль».
Наиболее распространенным случаем, который вы видите для указания 'thos module', является что-то вроде (require racket/tcp)
, что означает «мне нужен модуль "tcp"
из коллекции "racket"
» (и это тайно то же самое, что (require (lib "racket/tcp")
, которыйЯ думаю, что это легче понять на самом деле), где вся «коллекция» состоит из таинственных и сложных в том смысле, в каком он всегда есть в системах установки программного обеспечения (хотя, я думаю, это не понятно).
Но для модулей, которые вы определяете как часть программы, которую вы пишете, все гораздо проще: вы задаете «этот модуль» с помощью (строки, представляющей) имени его файла, которое интерпретируется относительно модуля, выполняющего require
ING. Если я хочу импортировать вещи из модуля "foo.rkt"
, описанного выше, я просто говорю:
(require "foo.rkt")
И теперь у меня есть все, что он готов дать мне (все в его форме или формах provide
).
Как и с provide
, я могу делать всякие хитрости, чтобы указать, что я хочу получить, а также переименовывать вещи и так далее. Простой случай, который будет работать с формой "foo.rkt"
provide
:
(require (only-in "foo.rkt" with-foo))
Что означает «просто дай мне with-foo
, мне больше все равно». Это полезно, потому что это означает, что вы можете быть очень точными в отношении того, какие имена вы хотите, и не загромождать свой модуль мусором.
Есть много других вещей, которые вы можете сделать с require
.
Модули и контракты
Одна очень полезная вещь, которую вы можете сделать, это указать контракты на границах модулей. Контракты вводятся здесь , а справочный материал - здесь .
Допустим, что для моего "foo.rkt"
модуля я знаю, что call-with-foo
ожидает процедуру как своюаргумент, и эта процедура получает один аргумент и может возвращать что угодно. Есть два способа сделать это: вы можете определить контракт для функции в "foo.rkt"
:
(define/contract (call-with-foo fn)
(-> (-> any/c any) any))
...)
Или вы можете указать контакт на уровне provide
:
(provide (contract-out
(call-with-foo
(-> (-> any/c any) any)))
with-foo)
Они в основном одинаковы для пользователей модуля. Первый случай выглядит лучше, потому что договор будет применяться даже внутри модуля. Но в первом случае, например, вы можете принудительно применять контракты на границах модуля, которые являются более строгими, чем контракты в модуле, что может быть полезно.
В любом случае контракты - довольно удобный инструмент для раннего обнаружения проблем. и они особенно полезны на границах модулей.
Большие модули
Одна вещь, которая почти неизбежно случается, состоит в том, что ваш маленький однофайловый модуль со временем становится слишком большим, поэтому вы хотите, чтобы он сталболее одного файла. Это легко сделать: вы можете просто сделать так, чтобы файл основного модуля воспроизводил вещи из модулей реализации. Так, например, "foo.rkt"
может стать:
#lang racket
(require "foo/main.rkt")
(provide (all-from-out "foo/main.rkt"))
, а "foo/main.rkt"
может, в свою очередь, быть:
#lang racket
(require "simple.rkt" "complicated.rkt")
(provide (all-from-out "simple.rkt" "complicated.rkt"))
и, наконец, "foo/simple.rkt"
может иметь реализацию частитеперь обширный модуль, дополненный provide
формами в зависимости от обстоятельств:
#lang racket
(provide (contract-out
(call-with-foo
(-> (-> any/c any) any))))
(define (call-with-foo fn)
...)
Все (require "x/y.rkt")
выглядит так, будто оно безнадежно * специфично для nix, но на самом деле все это не зависит от платформы: moduleспецификации на самом деле не являются путями, они просто переводятся в них, и этот перевод происходит соответствующим образом для платформы.
(Причина этого "main.rkt"
в том, что если это когда-нибудь превратится в библиотеку, то(require .../foo)
означает «ищите "main.rkt"
везде, куда .../foo
велел вам идти». По крайней мере, я так думаю.)