Racket: Строки и неизменяемость

В большинстве языков программирования высокого уровня строки неизменяемы. Это свойство уже практически стало стандартом. В Racket же строки по умолчанию изменяемые! Но неизменяемые строки также существуют. Разберёмся же, как с этой двойственностью работать.

Изменяемость как свойство

Изменяемые и неизменяемые строки не относятся к разным типам. Устойчивость к изменению — это свойство каждого конкретного строкового значения. Узнать, какая перед нами строка, можно с помощью предиката immutable?. Посмотрим его в действии:

(immutable? "a")                 ; #t
(immutable? (string #\a))        ; #f
(immutable? (make-string 1 #\a)) ; #f

(string? "a")          ;#t
(string? (string #\a)) ;#t

Видно, что строковые литералы неизменяемы, а строки, созданные с помощью функций таковыми не являются, но при этом и те, и другие являются строками!

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

Преобразование строк в неизменяемые и обратно

Любую изменяемую строку можно сделать неизменяемой, создав её неизменяемую копию с помощью функции string->immutable-string. Обратное преобразование не требует отдельной функции, вместо неё используется функция string-copy, которая создаёт изменяемую копию любой строки.

Неизменяемые строки без потери неизменяемости можно только конкатенировать, используя отдельную функцию string-append-immutable. Любые другие виды обработки потребуют работы с изменяемыми строками с преобразованием в неизменяемую строку результата.

Предназначение неизменяемых строк

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

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

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

Модификация изменяемой строки

Рассмотрим две функции, которые модифицируют содержимое изменяемой строки "по месту", не создавая изменённой копии. Это будут функции string-set! и string-copy!. Как уже было отмечено выше, восклицательный знак в имени означает оказание эффекта на изменяемый аргумент.

string-set! заменяет символ по указанному индексу на заданный:

(define s (make-copy "Cat")) ; изменяемая копия!
(string-set! s 0 #\B)
(string-set! s 1 #\o)
(displayln s) ; => Bot

string-copy! копирует одну строку в середину другой, размещая копию в некоторой позиции. Опционально можно указать, какой участок копируемой строки будет вставлен в модифицируемую. Вот так функция применяется:

(define names "Bob,Tom")

(define s (make-string 10 #\.))
(displayln s) ; => ..........

(string-copy! s 3 " VS ")
(displayln s) ; => ... VS ...

(string-copy! s 0 names 0 3)
(displayln s) ; => Bob VS ...

(string-copy! s 7 names 4 7)
(displayln s) ; => Bob VS Tom

Строку можно копировать и саму в себя:

(define s (string-copy ".Tod"))
(string-copy! s 0 s 1 4)
s ; "Todd"

Задание

Реализуйте функцию scroll-left!, которая должна "прокручивать" изменяемую строку-аргумент так, чтобы первый символ попадал в конец строки, а остальные символы, начиная со второго сдвигались на одну позицию влево. Пустую строку модифицировать не нужно. Возвращать изменённую строку тоже не следует — функция должна только модифицировать свой аргумент!

Примеры:

(define s (string-copy "abc"))
(scroll-left! s)
s ; "bca"
(scroll-left! s)
s ; "cab"

Нашли ошибку? Есть что добавить? Пулреквесты приветствуются https://github.com/hexlet-basics
Если вы столкнулись с трудностями и не знаете, что делать, задайте вопрос в нашем большом и дружном сообществе
Упражнение доступно только авторизованным пользователям.

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