Кафедра ИМО ПетрГУ | М. А. Крышень

Функциональное программирование

Курс для студентов 3 курса направления программная инженерия.

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

Чат-комната для дистанционных занятий и консультаций.

Задание 0

Знакомство с языком

  • Арифметические операции: преобразовать в префиксную нотацию и вычислить выражение:

    \[\frac{5 + 4 + (2 - (3 - (6 + 4/5)))}{3(6 - 2)(2 - 7)}\]

  • Библиотека 2htdp/image: изучить примеры из руководства.

    #lang racket
    
    (require 2htdp/image)
    

Задание 1

Рекурсия и процессы, порождаемые процедурами

Реализуйте процедуру, вычисляющую \(n\)-ю степень аргумента, где \(n\) — неотрицательное целое.

  1. Простой алгоритм, выполняющий \(n\) умножений в виде процедуры, порождающей a) линейно рекурсивный процесс, b) линейно итеративный процесс.
  2. Эффективный алгоритм с логарифмической степенью роста также в двух вариантах.

Задание 2

Процедуры высшего порядка

Дана абстрактная процедура вычисления суммы (см. SICP [1], глава 1.3.1 «Процедуры в качестве аргументов»):

(define (sum term a next b)
  (if (> a b)
      0
      (+ (term a)
         (sum term (next a) next b))))

Обратите внимание, что в качестве аргументов term и next принимаются процедуры. Как работает sum? Покажите пример использования.

Упражнение 1.31a из SICP.

Процедура sum — всего лишь простейшая из обширного множества подобных абстракций, которые можно выразить через процедуры высших порядков. Напишите аналогичную процедуру под названием product, которая вычисляет произведение значений функции в точках на указанном интервале. Покажите, как с помощью этой процедуры определить процедуру вычисления факториала.

Упражнение 1.32 из SICP.

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

(accumulate combiner null-value term a next b)

Процедура accumulate принимает в качестве аргументов те же описания термов и диапазона, что и sum с product, а еще процедуру combiner (двух аргументов), которая указывает, как нужно присоединить текущий терм к результату накопления предыдущих, и null-value, базовое значение, которое нужно использовать, когда термы закончатся. Напишите accumulate и покажите, как и sum, и product можно определить в виде простых вызовов accumulate.

Если Ваша процедура accumulate порождает рекурсивный процесс, перепишите ее так, чтобы она порождала итеративный. Если она порождает итеративный процесс, перепишите ее так, чтобы она порождала рекурсивный.

Задание 3

Процедуры как возвращаемые значения

Упражнение 1.42 из SICP.

Если \(f\) есть численная функция, а \(n\) — положительное целое число, то мы можем построить \(n\)-кратное применение \(f\), которое определяется как функция, значение которой в точке \(x\) равно \(f(f( \ldots (f(x)) \ldots ))\). Например, если \(f\) есть функция \(x \mapsto x + 1\), то \(n\)-кратным применением \(f\) будет функция \(x \mapsto x + n\). Если \(f\) есть операция возведения в квадрат, то \(n\)-кратное применение \(f\) есть функция, которая возводит свой аргумент в \(2^n\)-ю степень. Напишите процедуру, которая принимает в качестве ввода процедуру, вычисляющую \(f\), и положительное целое \(n\), и возвращает процедуру, вычисляющую \(n\)-кратное применение \(f\). Требуется, чтобы вашу процедуру можно было использовать в таких контекстах:

> ((repeated sqr 2) 5)
625

Подсказка: может оказаться удобно использовать compose.

Задание 4

Работа со списками

Реализуйте рекурсивную процедуру append, которая соединяет два списка:

> (append '(1 2 3) '(4 5 6))
'(1 2 3 4 5 6)

Доработайте append так, чтобы она работала для произвольного количества аргументов-списков:

> (append)
'()

> (append '(1 2 3))
'(1 2 3)

> (append '(1 2) '(3 4) '(5 6))
'(1 2 3 4 5 6)

Определите вариант append для двух и для произвольного числа списков с использованием функции свертки (foldl или foldr).

Задание 5

На основе упражнения 2.32 из SICP.

Множество можно представить как список его различных элементов, а множество его подмножеств как список списков. Например, если множество равно (1 2 3), то множество его подмножеств равно (() (3) (2) (2 3) (1) (1 3) (1 2) (1 2 3)). Разработайте процедуру, которая порождает множество подмножеств. Подсказка: рекурсивно определите множество подмножеств через множество подмножеств исходного множества без одного элемента. Используйте append и map.

Задание 6

Упражнение 2.42 из SICP.

В «задаче о восьми ферзях» спрашивается, как расставить на шахматной доске восемь ферзей так, чтобы ни один из них не бил другого (то есть никакие два ферзя не должны находиться на одной вертикали, горизонтали или диагонали). Один из способов решать эту задачу состоит в том, чтобы идти поперек доски, устанавливая по ферзю в каждой вертикали. После того, как \(k-1\) ферзя мы уже разместили, нужно разместить \(k\)-го в таком месте, где он не бьет ни одного из тех, которые уже находятся на доске. Этот подход можно сформулировать рекурсивно: предположим, что мы уже породили последовательность из всех возможных способов разместить \(k-1\) ферзей на первых \(k-1\) вертикалях доски. Для каждого из этих способов мы порождаем расширенный набор позиций, добавляя ферзя на каждую горизонталь \(k\)-й вертикали. Затем эти позиции нужно отфильтровать, оставляя только те, где ферзь на \(k\)-й вертикали не бьется ни одним из остальных. Продолжая этот процесс, мы породим не просто одно решение, а все решения этой задачи.

Это решение мы реализуем в процедуре queens, которая возвращает последовательность решений задачи размещения \(n\) ферзей на доске \(n \times n\). В процедуре queens есть внутренняя процедура queen-cols, которая возвращает последовательность всех способов разместить ферзей на первых \(k\) вертикалях доски.

(define (queens board-size)
  (define (queen-cols k)
    (if (= k 0)
        (list empty-board)
        (filter (λ (positions) (safe? k positions))
                (append-map
                 (λ (rest-of-queens)
                   (map (λ (new-row)
                          (adjoin-position new-row k rest-of-queens))
                        (range 1 (+ board-size 1))))
                 (queen-cols (- k 1))))))
  (queen-cols board-size))

В этой процедуре rest-of-queens есть способ размещения \(k-1\) ферзя на первых \(k-1\) вертикалях, а new-row — это горизонталь, на которую предлагается поместить ферзя с \(k\)-й вертикали. Завершите эту программу, реализовав представление множеств позиций ферзей на доске, включая процедуру adjoin-position, которая добавляет нового ферзя на определенных горизонтали и вертикали к заданному множеству позиций, и empty-board, которая представляет пустое множество позиций. Еще нужно написать процедуру safe?, которая для множества позиций определяет, находится ли ферзь с \(k\)-й вертикали в безопасности от остальных. (Заметим, что нам требуется проверять только то, находится ли в безопасности новый ферзь — для остальных ферзей безопасность друг от друга уже гарантирована.)

Задание 7

Разработайте процедуру для построения графического представления способов размещения ферзей, получаемых в задании 6. Используйте библиотеку 2htdp/image.

Пример визуализации одного из возможных способов размещения для доски 8 × 8 с использованием набора изображений Planet Cute (2htdp/planetcute):

queens.png

Задание 8

В задаче 6 количество способов размещения ферзей быстро растет с увеличением размера доски. Реализуйте вариант решения этой задачи с использованием потоков, так чтобы можно было получать нужное количество способов размещения, не вычисляя их все.

Комментарий к заданиям 6 и 8

Выражение (filter ...) в задаче 6 можно переписать, используя синтаксис for*/list, доступный в Racket:

(filter (λ (positions) (safe? k positions))
        (for*/list ([rest-of-queens (queen-cols (- k 1))]
                    [new-row (in-range 1 (+ board-size 1))])
          (adjoin-position new-row k rest-of-queens)))

Или так, используя for*/list и для фильтрации:

(for*/list ([rest-of-queens (queen-cols (- k 1))]
            [new-row (in-range 1 (+ board-size 1))]
            [positions (in-value (adjoin-position new-row k rest-of-queens))]
            #:when (safe? k positions))
  positions)

Настройка Emacs

Используемые пакеты:

  • Racket-mode — поддержка интерактивной разработки для языка Racket,
  • Company — автодополнение кода,
  • Smartparens — структурное редактирование S-выражений и обеспечение сбалансированности скобок, аналог Paredit.

Для самостоятельной установки можно использовать дистрибутив Емакса Prelude. После установки нужно раскомментировать в ~/.emacs.d/personal/prelude-modules.el подключение модуля prelude-racket.

Основные комбинации клавиш

  • Emacs
    • C-x C-f — открыть или создать файл,
    • C-x C-s — сохранить файл.
  • Racket-Mode
    • C-c C-k или F5 — выполнить редактируемый модуль,
    • C-c C-z — перейти в REPL,
    • C-j — перейти в REPL на следующую строку, не выполняя выражение,
    • C-x C-e — выполнить выражение перед курсором,
    • C-M-x — выполнить определение под курсором,
    • C-u C-M-x — то же, но при вычислении значения определения или соответствующей функции среда разработки перейдет в режим отладки,
    • C-c C-d — перейти к документации в браузере.
    • M-. — перейти к определению (M-, для возврата),
    • C-c C-. — показать описание идентификатора под курсором,
  • Smartparens/Paredit
    • M-( — поместить в скобки следующее выражение или выделение,
    • M-s — убрать скобки вокруг выражения,
    • C-right или C-) — втянуть в закрывающую скобку,
    • C-left или C-{ — вынести за закрывающую скобку,
    • C-M-left или C-( — втянуть в открывающую скобку,
    • C-M-right или C-{ — вынести за открывающую скобку,
    • M-S — разделить выражение,
    • M-j — объединить выражения,
    • M-x sp-cheat-sheet — подсказка по функциям Smartparens.

Обозначения: C — Control, M — Meta (Alt), дефис — одновременное нажатие, пробел — последовательное нажатие. Прописные буквы и скобки — использовать Shift.

Вопросы к экзамену

  1. Парадигмы программирования. Функциональное программирование.
  2. Лисп: история языка, основные диалекты, S-выражения.
  3. Scheme и Racket: основные типы значений, идентификаторы и связывание, процедуры, условные выражения и предикаты.
  4. Рекурсия. Линейно рекурсивный и линейно итеративный процессы, древовидная рекурсия.
  5. Лямбда-выражения и замыкания. Процедуры высших порядков.
  6. Основные понятия λ-исчисления: аппликация и абстракция, свободные и связанные переменные, каррирование.
  7. Понятие комбинатора. Комбинатор неподвижной точки, Y-комбинатор и рекурсия.
  8. Абстракция данных. Пары cons и их реализация с помощью процедур.
  9. Представление списков и деревьев. Кавычка и символьные данные.
  10. Основные функции для работы со списками (свертка, отображение, фильтрация).
  11. Сопоставление значений по шаблону.
  12. Оператор присваивания и изменяемые структуры данных, последствия их введения в язык программирования. Свойство ссылочной прозрачности.
  13. Отложенные вычисления и мемоизация.
  14. Потоки. Построение и обработка потоков.
  15. Функциональное представление интерактивных программ: реактивное программирование и «world programs».
  16. Метапрограммирование, синтаксические правила.
  17. Продолжения: программирование в стиле передачи продолжений, call-with-current-continuation, примеры использования.

Ссылки

  1. Х. Абельсон, Дж. Сассман. Структура и интерпретация компьютерных программ (книга на английском, перевод)
  2. Felleisen et al. How to Design Programs, Second Edition
  3. Язык программирования Racket
  4. Краткое руководство по Emacs
  5. Mike Vanier. The Y Combinator (Slight Return) — подробное объяснение принципа работы Y-комбинатора.

Примеры

  • ftw.scm — системное программирование на Guile и пример использования потоков.
  • game.rkt — пример использования библиотеки 2htdp/universe для создания игры (обновлено).

∗∗∗

xkcd-ru-297-v3.png

Рис. 1.: Круговорот Лиспа (xkcd.ru, оригинал на английском).

Автор: М. А. Крышень

Created: 2021-11-10 Ср 11:41

Emacs 27.2 (Org mode 9.5)

Validate